在Java关键部分中,我应该同步什么?

时间:2009-01-06 11:31:02

标签: java multithreading synchronized

在Java中,在代码中声明关键部分的惯用方法如下:

private void doSomething() {
  // thread-safe code
  synchronized(this) {
    // thread-unsafe code
  }
  // thread-safe code
}

几乎所有的块都在this上同步,但这有什么特别的原因吗?还有其他可能吗?是否有关于同步对象的最佳实践? (例如Object的私人实例?)

11 个答案:

答案 0 :(得分:64)

正如早先的回答者所指出的那样,最佳做法是在有限范围的对象上进行同步(换句话说,选择你可以使用的最严格的范围,并使用它。)特别是,在{{1上同步除非你打算允许你的类的用户获得锁定,否则这是一个坏主意。

但是,如果您选择在this上进行同步,则会出现一个特别难看的案例。字符串可以(实际上几乎总是)实习。这意味着每个相同内容的字符串 - 在整个JVM 中 - 变成幕后的同一个字符串。这意味着如果您在任何String上进行同步,另一个(完全不同的)代码部分也会锁定具有相同内容的String,实际上也会锁定您的代码。

我曾经在生产系统中对死锁进行故障排除,并且(非常痛苦地)跟踪两个完全不同的开源软件包的死锁,每个软件包都在一个内容为java.lang.String的String实例上同步。

答案 1 :(得分:46)

首先,请注意以下代码段是相同的。

public void foo() {
    synchronized (this) {
        // do something thread-safe
    }
}

public synchronized void foo() {
    // do something thread-safe
}

完全相同的事情。除了代码可读性和样式之外,没有偏好其中任何一个。

当你同步方法或代码块时,重要的是要知道为什么你正在做这样的事情,什么对象你正在锁定,以及<强烈的>目的。

另请注意,在某些情况下,您需要客户端同步代码块,其中您要求的监视器(即同步对象)不一定是{{1} },就像在这个例子中一样:

this

我建议您获得有关并发编程的更多知识,一旦您确切了解幕后发生的事情,它将为您提供很多帮助。你应该查看Concurrent Programming in Java,这是一本关于这个主题的好书。如果您想快速深入了解主题,请查看Java Concurrency @ Sun

答案 2 :(得分:39)

我尽量避免在this上进行同步,因为这会允许来自外部的所有参与该对象的人阻止我的同步。相反,我创建了一个本地同步对象:

public class Foo {
    private final Object syncObject = new Object();
    …
}

现在我可以使用该对象进行同步,而不必担心任何人“窃取”锁定。

答案 3 :(得分:6)

只是为了强调Java中还有ReadWriteLocks,可以找到java.util.concurrent.locks.ReadWriteLock。

在我的大部分用法中,我将锁定分为“阅读”和“更新”。如果您只使用synchronized关键字,则对同一方法/代码块的所有读取都将“排队”。一次只能有一个线程访问该块。

在大多数情况下,如果您只是在阅读,就不必担心并发问题。当你在写作时,你担心并发更新(导致数据丢失),或者在写入(部分更新)期间阅读,你必须担心。

因此,在多线程编程期间,读/写锁对我来说更有意义。

答案 4 :(得分:4)

您需要在可用作互斥锁的对象上进行同步。如果当前实例( this 引用)是合适的(例如,不是Singleton),您可以使用它,就像在Java中一样,任何Object都可以作为Mutex。

在其他情况下,如果这些类的实例都需要访问相同的资源,您可能希望在多个类之间共享互斥锁。

这在很大程度上取决于您所处的环境以及您正在构建的系统类型。在我见过的大多数Java EE应用程序中,实际上并不需要同步...

答案 5 :(得分:4)

就我个人而言,我认为坚持在this上同步永远或者很少正确的答案是错误的。我认为这取决于您的API。如果您的类是线程安全的实现,并且您记录它,那么您应该使用this。如果同步不是在调用它的公共方法时使每个类的实例作为一个整体线程安全,那么你应该使用私有内部对象。可重用的库组件通常属于前一类 - 在禁止用户将API包装在外部同步之前,您必须仔细考虑。

在前一种情况下,使用this允许以原子方式调用多个方法。一个例子是PrintWriter,您可能希望输出多行(比如堆栈跟踪到控制台/记录器)并保证它们一起出现 - 在这种情况下,它在内部隐藏同步对象这一事实真的很痛苦。另一个这样的例子是同步集合包装器 - 你必须在集合对象本身上进行同步以便迭代;由于迭代包含多个方法调用,因此无法完全在内部保护它。

在后一种情况下,我使用普通对象:

private Object mutex=new Object();

但是,看到许多JVM转储和堆栈跟踪表明锁是“java.lang.Object()的一个实例”,我不得不说使用内部类可能更有帮助,正如其他人所建议的那样。

无论如何,那是我的两位值。

编辑:另一件事,在this上进行同步时,我更喜欢同步方法,并保持方法非常精细。我认为它更清晰,更简洁。

答案 6 :(得分:2)

Java中的同步通常涉及在同一实例上同步操作。然后在this上进行同步是非常惯用的,因为this是一个共享引用,可以在类的不同实例方法(或部分)之间自动使用。

使用另一个专门用于锁定的引用,例如,通过声明和初始化私有字段Object lock = new Object(),这是我从未需要或使用的东西。我认为只有在对象内部的两个或多个非同步资源上需要外部同步时,它才有用,尽管我总是试图将这种情况重构为更简单的形式。

无论如何,在Java库中也使用了隐式(synchronized方法)或显式synchronized(this)。这是一个很好的习语,如果适用的话,应该永远是你的第一选择。

答案 7 :(得分:1)

关于同步的内容取决于可能与此方法调用冲突的其他线程可以同步。

如果this是仅由一个线程使用的对象,并且我们正在访问线程之间共享的可变对象,那么一个好的候选者就是在该对象上进行同步 - 在this上进行同步已经没有意义,因为修改该共享对象的另一个线程可能甚至不知道this,但确实知道该对象。

另一方面,如果许多线程同时调用此对象的方法,则this上的同步是有意义的,例如,如果我们是单个的话。

请注意,同步方法通常不是最佳选择,因为我们在方法运行的整个过程中保持锁定。如果它包含耗时但线程安全的部分,并且不是那么耗时的线程不安全的部分,则对该方法进行同步是非常错误的。

答案 8 :(得分:0)

  

几乎所有的块都会在此同步,但这有什么特别的原因吗?还有其他可能性吗?

此声明同步整个方法。

private synchronized void doSomething() {

此声明同步了代码块的一部分而不是整个方法。

private void doSomething() {
  // thread-safe code
  synchronized(this) {
    // thread-unsafe code
  }
  // thread-safe code
}

来自oracle文档page

使这些方法同步有两个影响:

首先,对同一对象的两个同步方法的调用不可能交错。当一个线程正在为对象执行同步方法时,所有其他线程都会调用同一对象的同步方法阻塞(暂停执行),直到第一个线程完成对象为止。

  

还有其他可能吗?是否有关于同步对象的最佳实践? (比如Object的私有实例?)

同步有很多种可能性和替代方案。您可以使用高级并发APIs(自JDK 1.5发布以来可用)来使代码线程安全

Lock objects
Executors
Concurrent collections
Atomic variables
ThreadLocalRandom

有关详细信息,请参阅以下SE问题:

Synchronization vs Lock

Avoid synchronized(this) in Java?

答案 9 :(得分:0)

最佳做法是创建一个仅提供锁的对象:

private final Object lock = new Object();

private void doSomething() {
  // thread-safe code
  synchronized(lock) {
    // thread-unsafe code
  }
  // thread-safe code
}

这样做很安全,没有任何调用代码可以通过无意的synchronized(yourObject)行使方法死锁。

@jared和@ yuval-adam的贷方,在上面进行了详细说明。

我的猜测是,在教程中使用this的流行来自早期的Sun Javadoc:https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html

答案 10 :(得分:0)

同步包括 3 个部分:原子性、可见性和排序

同步块是非常粗略的同步级别。它按照您的预期强制执行可见性和排序。但是对于原子性,它并没有提供太多的保护。原子性需要程序的全局知识,而不是局部知识。 (这使得多线程编程非常困难)

假设我们有一个具有方法 Accountdeposit 的类 withdraw。它们都是基于这样的私有锁同步的:

class Account {
    private Object lock = new Object();

    void withdraw(int amount) {
        synchronized(lock) {
            // ...
        }
    }

    void deposit(int amount) {
        synchronized(lock) {
            // ...
        }
    }
}

考虑到我们需要实现一个更高级别的类来处理传输,如下所示:

class AccountManager {
    void transfer(Account fromAcc, Account toAcc, int amount) {
        if (fromAcc.getBalance() > amount) {
            fromAcc.setBalance(fromAcc.getBalance() - amount);
            toAcc.setBalance(toAcc.getBalance + amount);
        }
    }
}

假设我们现在有 2 个帐户,

Account john;
Account marry;

如果 Account.deposit()Account.withdraw() 仅使用内部锁锁定。当我们有 2 个线程工作时,这会导致问题:

// Some thread
void threadA() {
    john.withdraw(500);
}

// Another thread
void threadB() {
    accountManager.transfer(john, marry, 100);
}

因为 threadAthreadB 可以同时运行。并且线程B完成条件检查,线程A退出,线程B再次退出。这意味着即使他的账户没有足够的钱,我们也可以从约翰那里提取 100 美元。这会破坏原子性。

您可能会建议:那为什么不将 withdraw()deposit() 添加到 AccountManager 中?但是在这个提议下,我们需要创建一个多线程安全的 Map 来从不同的账户映射到他们的锁。我们需要在执行后删除锁(否则会泄漏内存)。我们还需要确保没有其他人直接访问 Account.withdraw()。这将引入许多微妙的错误。

正确且最惯用的方法是在 Account 中公开锁。并让 AccountManager 使用锁。但在这种情况下,为什么不直接使用对象本身呢?

class Account {
    synchronized void withdraw(int amount) {
        // ...
    }

    synchronized void deposit(int amount) {
        // ...
    }
}

class AccountManager {
    void transfer(Account fromAcc, Account toAcc, int amount) {
        // Ensure locking order to prevent deadlock
        Account firstLock = fromAcc.hashCode() < toAcc.hashCode() ? fromAcc : toAcc;
        Account secondLock = fromAcc.hashCode() < toAcc.hashCode() ? toAcc : fromAcc;

        synchronized(firstLock) {
            synchronized(secondLock) {
                if (fromAcc.getBalance() > amount) {
                    fromAcc.setBalance(fromAcc.getBalance() - amount);
                    toAcc.setBalance(toAcc.getBalance + amount);
                }
            }
        }
    }
}

用简单的英语总结一下,私有锁不适用于稍微复杂的多线程程序。