每当在SO上出现关于Java同步的问题时,有些人都急于指出应该避免同步(这一点)。相反,他们声称,对私有引用的锁定是首选的。

其中一些原因是:

一些邪恶的代码可能会偷你的锁(这个很流行,也有一个“意外”变体) 同一类中的所有同步方法都使用完全相同的锁,这降低了吞吐量 你(不必要地)暴露了太多信息

包括我在内的其他人认为,synchronized(this)是一个被大量使用的习语(在Java库中也是如此),是安全的,而且很容易理解。它不应该被避免,因为你有一个错误,你不知道你的多线程程序中发生了什么。换句话说:如果它是适用的,那么就使用它。

我感兴趣的是看到一些现实世界的例子(没有foobar的东西),避免锁定在这是可取的,当同步(这)也会做的工作。

因此:您应该总是避免synchronized(this),并将其替换为私有引用上的锁吗?


一些进一步的信息(随着答案的更新):

we are talking about instance synchronization both implicit (synchronized methods) and explicit form of synchronized(this) are considered if you quote Bloch or other authorities on the subject, don't leave out the parts you don't like (e.g. Effective Java, item on Thread Safety: Typically it is the lock on the instance itself, but there are exceptions.) if you need granularity in your locking other than synchronized(this) provides, then synchronized(this) is not applicable so that's not the issue


当前回答

让我先把结论说出来——对私有字段的锁定对于稍微复杂一点的多线程程序是不起作用的。这是因为多线程是一个全局问题。本地化同步是不可能的,除非你以一种非常防御的方式写(例如,复制所有传递给其他线程的内容)。


下面是详细的解释:

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

同步块是非常粗糙的同步级别。正如您所期望的那样,它加强了可见性和排序。但是对于原子性,它并不能提供太多的保护。原子性要求程序的全局知识,而不是局部知识。(这使得多线程编程非常困难)

假设我们有一个Account类,它有存取款方法。它们都是基于一个私有锁进行同步的,就像这样:

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);
        }
    }
}

假设我们现在有两个账户,

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);
}

因为线程a和线程b有可能同时运行。线程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);
                }
            }
        }
    }
}

简而言之,私有锁不适用于稍微复杂一点的多线程程序。

(转载自https://stackoverflow.com/a/67877650/474197)

其他回答

我对2019年的看法,尽管这个问题本可以已经解决了。

如果你知道你在做什么,锁定'this'并不坏,但在幕后锁定'this'是(不幸的是synchronized关键字在方法定义中允许)。

如果你真的希望你的类的用户能够“窃取”你的锁(即阻止其他线程处理它),你实际上希望所有同步方法在另一个同步方法运行时等待,以此类推。 它应该是有意的、经过深思熟虑的(因此有文档来帮助用户理解它)。

更详细地说,反过来,你必须知道如果你锁定了一个不可访问的锁(没有人可以“偷”你的锁,你完全控制等等),你会“获得”(或“失去”)什么。

对我来说,问题是方法定义签名中的synchronized关键字使程序员很容易不考虑锁定什么,如果你不想在多线程程序中遇到问题,这是一个非常重要的事情。

人们不能争辩说,“通常”你不希望你的类的用户能够做这些事情,或者“通常”你想要……这取决于你编写的是什么功能。因为你不能预测所有的用例,所以你不能制定一个经验法则。

例如,prinwriter使用了一个内部锁,但是如果人们不想让他们的输出相互交错,他们就很难从多个线程中使用它。

锁是否可以在类外部访问是程序员根据类的功能来决定的。它是api的一部分。例如,你不能从synchronized(this)移到synchronized(provateObjet)而不冒破坏使用它的代码更改的风险。

注1:我知道你可以实现任何同步(这个)'实现'通过使用显式锁对象和暴露它,但我认为这是不必要的,如果你的行为是很好的记录,你实际上知道什么锁定'this'的意思。

注2:我不同意这样的观点:如果一些代码不小心偷了你的锁,那就是一个bug,你必须解决它。这在某种程度上等同于说我可以让我所有的方法公开,即使它们本不应该是公开的。如果有人“意外”调用我的意图是私人方法,这是一个bug。为什么会发生这样的事故!!如果能偷你的锁对你的类来说是一个问题,那就不要允许它。就这么简单。

首先需要指出的是:

public void blah() {
  synchronized (this) {
    // do stuff
  }
}

语义上等价于:

public synchronized void blah() {
  // do stuff
}

这是不使用synchronized(this)的一个原因。您可能会说,您可以围绕synchronized(this)块做一些事情。通常的原因是试图避免必须进行同步检查,这将导致各种并发问题,特别是双重检查锁定问题,这表明要使一个相对简单的检查具有线程安全性是多么困难。

私有锁是一种防御机制,这从来都不是一个坏主意。

另外,正如您所提到的,私有锁可以控制粒度。一个对象上的一组操作可能与另一组完全无关,但同步(这)将相互排除对所有这些操作的访问。

同步(这个)真的不能给你任何东西。

I think there is a good explanation on why each of these are vital techniques under your belt in a book called Java Concurrency In Practice by Brian Goetz. He makes one point very clear - you must use the same lock "EVERYWHERE" to protect the state of your object. Synchronised method and synchronising on an object often go hand in hand. E.g. Vector synchronises all its methods. If you have a handle to a vector object and are going to do "put if absent" then merely Vector synchronising its own individual methods isn't going to protect you from corruption of state. You need to synchronise using synchronised (vectorHandle). This will result in the SAME lock being acquired by every thread which has a handle to the vector and will protect overall state of the vector. This is called client side locking. We do know as a matter of fact vector does synchronised (this) / synchronises all its methods and hence synchronising on the object vectorHandle will result in proper synchronisation of vector objects state. Its foolish to believe that you are thread safe just because you are using a thread safe collection. This is precisely the reason ConcurrentHashMap explicitly introduced putIfAbsent method - to make such operations atomic.

总之

Synchronising at method level allows client side locking. If you have a private lock object - it makes client side locking impossible. This is fine if you know that your class doesn't have "put if absent" type of functionality. If you are designing a library - then synchronising on this or synchronising the method is often wiser. Because you are rarely in a position to decide how your class is going to be used. Had Vector used a private lock object - it would have been impossible to get "put if absent" right. The client code will never gain a handle to the private lock thus breaking the fundamental rule of using the EXACT SAME LOCK to protect its state. Synchronising on this or synchronised methods do have a problem as others have pointed out - someone could get a lock and never release it. All other threads would keep waiting for the lock to be released. So know what you are doing and adopt the one that's correct. Someone argued that having a private lock object gives you better granularity - e.g. if two operations are unrelated - they could be guarded by different locks resulting in better throughput. But this i think is design smell and not code smell - if two operations are completely unrelated why are they part of the SAME class? Why should a class club unrelated functionalities at all? May be a utility class? Hmmmm - some util providing string manipulation and calendar date formatting through the same instance?? ... doesn't make any sense to me at least!!

这实际上只是对其他答案的补充,但如果你对使用私有对象进行锁定的主要反对意见是,它会使你的类与与业务逻辑无关的字段混乱,那么Project Lombok有@Synchronized在编译时生成样板:

@Synchronized
public int foo() {
    return 0;
}

编译,

private final Object $lock = new Object[0];

public int foo() {
    synchronized($lock) {
        return 0;
    }
}

如果可能的话,让你的数据不可变(最终变量) 如果你不能避免跨多个线程共享数据的突变,使用高级编程结构[例如,粒度锁API]

Lock提供对共享资源的独占访问:一次只有一个线程可以获得锁,并且对共享资源的所有访问都要求首先获得锁。

使用ReentrantLock实现Lock接口的示例代码

 class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     lock.lock();  // block until condition holds
     try {
       // ... method body
     } finally {
       lock.unlock()
     }
   }
 }

锁定相对于同步的优势

The use of synchronized methods or statements forces all lock acquisition and release to occur in a block-structured way. Lock implementations provide additional functionality over the use of synchronized methods and statements by providing A non-blocking attempt to acquire a lock (tryLock()) An attempt to acquire the lock that can be interrupted (lockInterruptibly()) An attempt to acquire the lock that can timeout (tryLock(long, TimeUnit)). A Lock class can also provide behavior and semantics that is quite different from that of the implicit monitor lock, such as guaranteed ordering non-re entrant usage Deadlock detection

看看这个关于各种锁的SE问题:

同步vs锁定

您可以通过使用高级并发API而不是synchronized块来实现线程安全。该文档页提供了实现线程安全的良好编程结构。

锁对象支持简化许多并发应用程序的锁定习惯用法。

executor为启动和管理线程定义了高级API。concurrent提供的执行器实现提供了适合大型应用程序的线程池管理。

并发集合使管理大型数据集合变得更容易,并且可以大大减少同步的需要。

原子变量具有最小化同步和帮助避免内存一致性错误的特性。

ThreadLocalRandom(在JDK 7中)提供了从多个线程有效生成伪随机数的功能。

其他编程结构也可以参考java.util.concurrent和java.util.concurrent.atomic包。