每当在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


当前回答

简单的回答:您必须了解其中的区别,并根据代码做出选择。

Long answer: In general I would rather try to avoid synchronize(this) to reduce contention but private locks add complexity you have to be aware of. So use the right synchronization for the right job. If you are not so experienced with multi-threaded programming I would rather stick to instance locking and read up on this topic. (That said: just using synchronize(this) does not automatically make your class fully thread-safe.) This is a not an easy topic but once you get used to it, the answer whether to use synchronize(this) or not comes naturally.

其他回答

虽然我同意不要盲目地遵守教条规则,但“偷锁”的场景对你来说是不是很古怪?一个线程确实可以从你的对象“外部”获得锁(synchronized(theObject){…}),阻塞其他线程等待同步实例方法。

如果您不相信恶意代码,请考虑这些代码可能来自第三方(例如,如果您开发了某种应用程序服务器)。

“意外”版本似乎不太可能,但就像他们说的那样,“让一些东西不受白痴的影响,就会有人发明一个更好的白痴”。

所以我同意“这取决于这个班级做什么”的观点。


编辑以下eljenso的前3条评论:

我从来没有遇到过偷锁的问题,但这里有一个想象的场景:

假设您的系统是一个servlet容器,我们考虑的对象是ServletContext实现。它的getAttribute方法必须是线程安全的,因为上下文属性是共享数据;所以你声明它是同步的。让我们再想象一下,您提供了一个基于容器实现的公共托管服务。

我是您的客户,并在您的站点上部署我的“好”servlet。我的代码碰巧包含对getAttribute的调用。

黑客伪装成另一个客户,在您的站点上部署恶意servlet。它在init方法中包含以下代码:

synchronized (this.getServletConfig().getServletContext()) {
   while (true) {}
}

假设我们共享相同的servlet上下文(只要两个servlet位于同一个虚拟主机上,规范就允许),那么我对getAttribute的调用将永远锁定。黑客已经在我的servlet上实现了DoS。

如果getAttribute在私有锁上同步,则这种攻击是不可能的,因为第三方代码无法获得此锁。

我承认这个例子是人为设计的,对servlet容器如何工作的看法过于简单,但恕我直言,它证明了这一点。

因此,我将基于安全性考虑做出设计选择:我是否能够完全控制访问实例的代码?线程无限期地持有实例锁的后果是什么?

首先需要指出的是:

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

语义上等价于:

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

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

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

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

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

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


下面是详细的解释:

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

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

假设我们有一个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)

如果你已经决定:

你要做的就是锁定目标 当前对象;而且 你想要 锁定粒度小于 整体方法;

那么我就不认为synchronizezd是一个禁忌。

Some people deliberately use synchronized(this) (instead of marking the method synchronized) inside the whole contents of a method because they think it's "clearer to the reader" which object is actually being synchronized on. So long as people are making an informed choice (e.g. understand that by doing so they're actually inserting extra bytecodes into the method and this could have a knock-on effect on potential optimisations), I don't particularly see a problem with this. You should always document the concurrent behaviour of your program, so I don't see the "'synchronized' publishes the behaviour" argument as being so compelling.

至于应该使用哪个对象的锁的问题,我认为在当前对象上同步并没有什么错,如果这是你所做的逻辑所期望的,以及你的类通常是如何被使用的。例如,对于集合,逻辑上期望锁定的对象通常是集合本身。

当您使用synchronized(this)时,您正在使用类实例作为锁本身。这意味着当线程1获得锁时,线程2应该等待。

假设有以下代码:

public void method1() {
    // do something ...
    synchronized(this) {
        a ++;      
    }
    // ................
}


public void method2() {
    // do something ...
    synchronized(this) {
        b ++;      
    }
    // ................
}

方法一修改变量a,方法二修改变量b,应避免两个线程同时修改同一个变量。但是当thread1修改a和thread2修改b时,它可以在没有任何竞争条件的情况下执行。

不幸的是,上面的代码不允许这样做,因为我们对锁使用了相同的引用;这意味着线程即使没有处于竞争状态也应该等待,显然代码牺牲了程序的并发性。

解决方案是对两个不同的变量使用两个不同的锁:

public class Test {

    private Object lockA = new Object();
    private Object lockB = new Object();

    public void method1() {
        // do something ...
        synchronized(lockA) {
            a ++;      
        }
        // ................
    }


    public void method2() {
        // do something ...
        synchronized(lockB) {
            b ++;      
        }
        // ................
    }

}

上面的例子使用了更细粒度的锁(2个锁而不是一个锁(分别针对变量a和变量b的lockA和lockB),结果允许更好的并发性,另一方面,它变得比第一个例子更复杂……