同步整数不能正确锁定

pet*_*ang 0 java multithreading atomic synchronized

我有一些代码synchronized用来保护我递增的计数器count++

我希望我正确地保护了代码部分,因此得到2_0000_0000了结果,因为count在多次执行它之后,这将是正确的值,多线程。

但是,在运行代码时,我得到一个低于预期的值2_0000_0000,就好像我synchronized没有正确保护代码部分一样。

为什么会这样,我做错了什么?

public class Test {
    private static Integer count = 0;

    private static void add10K() {
        long idx = 0;
        while (idx++ < 1_0000_0000) {
            synchronized (count){
                count += 1;
            }
        }
    }

    public static long calc() {
        Thread th1 = new Thread(Test::add10K);
        Thread th2 = new Thread(Test::add10K);
        th1.start();
        th2.start();
        try {
            th1.join();
            th2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return count;
    }

    public static void main(String[] args) {
        System.out.println(calc());
    }
}
Run Code Online (Sandbox Code Playgroud)

Zab*_*uza 5

问题描述

Javassynchronized将锁存储在变量后面的实际对象中,而不是变量本身。

因此,当您将不同的对象分配给该变量时,您将拥有一个新的新锁。

现在,当您这样做时count++,这实际上不会修改Integer而是返回一个新Integer对象(该类是不可变的)。所以count变量被重新分配。


为了帮助解释我的观点,请考虑以下情况:

Person person = new Person("John");
...
synchronized (person) {
    ...
}
Run Code Online (Sandbox Code Playgroud)

锁存储在John 中,而不是存储在变量person本身中。所以当有人现在这样做时:

person = new Person("Jane");
Run Code Online (Sandbox Code Playgroud)

synchronized不再受保护,可以再次进入,因为Jane还没有锁定。


对象锁定习语

这就是为什么锁应该只放在final变量上,以避免这个问题。此外,您应该为此专门指定一个特定的对象。适合您情况的惯用方法是:

private static final Object lock = new Object();
Run Code Online (Sandbox Code Playgroud)

然后改为同步:

synchronized (lock) { ... }
Run Code Online (Sandbox Code Playgroud)

其他非常常见的选择是锁定类或this(对于非静态情况)。所以例如synchronized (Test.class)。不过,拥有一个专门的对象有一些优势。


资源

如果您喜欢书籍,请参阅Effective Java第 82 条解释私有对象锁习惯用法

请注意,锁定字段被声明为最终的。这可以防止您无意中更改其内容,这可能会导致灾难性的非同步访问(条目 78)。我们通过最小化锁定字段的可变性来应用条款 17 的建议。锁定字段应始终声明为 final。

另请参阅 SO 线程关于Java 多线程中“私有最终对象”锁定的用途是什么?

最后是Oracle 安全编码标准§Rule 09. Locking (LCK)#LCK00-J。本身:

防止此漏洞的一种技术是私有锁对象习语 [Bloch 2001]。这个习惯用法使用与在类中声明的私有 final java.lang.Object 的实例关联的内在锁,而不是对象本身的内在锁。这个习惯用法需要在类的方法中使用同步块,而不是使用同步方法。类的方法和敌对类的方法之间的锁争用变得不可能,因为敌对类无法访问私有的最终锁对象。