【问题标题】:Synchronize on Integer does not lock correctly [duplicate]整数同步未正确锁定[重复]
【发布时间】:2021-10-23 05:37:53
【问题描述】:

我有一些代码使用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());
    }
}

【问题讨论】:

  • 您正在count 上同步。由于Integer 是不可变的,因此对于每个新值来说,它都是一个新对象。
  • 您锁定了一个非 final 变量,这只是在要求一场灾难。只锁定final 变量。创建一个专用的private static final Object lock = new Object(); 并在其上进行同步。
  • @Turing85:AtomicInteger 会更好,但如果使用正确,同步就足够了。
  • @JoachimSauer 确实如此。没有意识到对象发生了变化。

标签: java multithreading atomic synchronized


【解决方案1】:

问题描述

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

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

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


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

Person person = new Person("John");
...
synchronized (person) {
    ...
}

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

person = new Person("Jane");

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


对象锁定习语

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

private static final Object lock = new Object();

然后改为同步:

synchronized (lock) { ... }

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


资源

如果你喜欢书籍,请参考Effective JavaItem 82解释私有对象锁定成语

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

另见关于What is the use of “private final Object” locking in java multithreading?的SO线程

最后是Oracle Secure Coding Standard§Rule 09. Locking (LCK)#LCK00-J. 本身:

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

【讨论】:

    【解决方案2】:

    进行以下更改,它应该可以工作。

    static final private Object lock = new Object(); 
    private static void add10K() {
           long idx = 0;
           while (idx++ < 100_000_000) {
                    synchronized (lock){
                        count += 1;
                    }
           }
    }
    

    另外,如果你的方法不是静态的,你可以像这样在实例上同步。

    synchronized (this) {
            count += 1;
    }
    

    但是您的线程调用以及您对calc() 的调用都需要更改以引用实例方法。

    【讨论】:

    • 被否决:它在兜售一种反模式。删除关于锁定公共值的评论而不对其进行广泛记录(this,这往往是其他代码也可以访问的引用),这是一个很好的答案。 (其实那个锁对象应该是private,肯定是final)。
    • 它在兜售一种反模式。就在 Effective Java 3rd Ed 中,所以我的陪伴很好。
    • @rzwitserloot 否决:它在兜售一种反模式。 我可以否决评论吗?发布的代码执行发布的代码所做的事情。模式比训练轮更糟糕——它们强制将代码放入预定义的算法中,而这些算法并不适合实际需要完成的工作。诸如“兜售反模式”之类的短语被那些不理解这一点的人喋喋不休,并且会迫使方钉进入圆孔,因为其他人说这是个好主意。 “我需要做一个三角形。” “不,那是反模式!你必须做一个正方形!”
    • 一些很好的呼吁权威的谬论在这里四处流传。我将提出一个实际合理的论点:就像公共领域是一个坏主意一样(如果必须,最好广泛记录下来,并意识到对其进行的任何更改或类如何与它对使用该类的代码有影响)公共锁也不例外:如果一个锁是可访问的,那么其他代码可能会锁定它(就像:如果一个字段是可访问的,其他代码可能会读/写它),你应该记录这意味着什么。或者,就像字段一样:通常你只是将它们设为私有。
    • 我将访问限制更改为私有和最终。但我并没有尝试提供有关线程编程的深入教程。我只是提供一个解决方案来解决 OP 的直接问题。没有必要对每个问题的每个答案都写一本大部头,也没有必要涵盖所有可能的情况。
    猜你喜欢
    • 2016-07-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多