为什么同步字段变量并在同步块内增加它会导致打印乱序?

问题描述

我有一个简单的代码片段

public class Itemmanager {

    private Integer itemCount = 0;

    public void incrementAndPrint() {
        synchronized(this) { 
            System.out.println(++itemCount + " ");
        }
    }

    public static void main(String[] args) {
        Itemmanager manager = new Itemmanager();
        ExecutorService executor = Executors.newFixedThreadPool(20);

        for (int i = 0; i < 10; i++) {
            executor.submit(manager::incrementAndPrint); 
        }
        executor.shutdown();
    }
}

按预期产生 1 2 3 4 5 6 7 8 9 10 。我还可以使用 Object 实例创建另一个字段并锁定它

    private Integer itemCount = 0;
    private Object lock = new Object();

    public void incrementAndPrint() {
        synchronized(lock) {
            System.out.println(++itemCount + " ");
        }
    }

它也会如预期的那样产生 1 2 3 4 5 6 7 8 9 10。

但是,如果我尝试锁定要递增和打印的同一个对象

    private Integer itemCount = 0;

    public void incrementAndPrint() {
        synchronized(itemCount) {
            System.out.println(++itemCount + " ");
        }
    }

操作将保持原子性,但结果是乱序的:2 1 3 4 5 6 7 8 9 10。

我知道 synchronized(this) 或同步整个方法解决我的所有问题。我只是不明白为什么我可以锁定一个字段 (Object lock),但不能锁定另一个字段 (Integer itemCount)?不应该正确锁定 synchronized 块内的所有内容,不管这个对象是什么,只要它是所有线程之间共享的单个对象?

解决方法

Java 中的

Integer 是不可变的。当您调用 ++itemCount 时,实际上是在执行三个操作:首先,将 Integer 拆箱为 int,其值为 Integer。然后,这个原语 int 递增,最后递增的 int 被自动装箱回 Integer。所以实际上,您最终会拥有不同 Integer 实例。因为你在不同的实例上同步,所以同步没有意义,你会看到乱序打印。

,

因为当你做++itemCount时,它等价于:

int old = itemCount.intValue();
itemCount = Integer.valueOf(old + 1);

这改变了锁正在使用的对象,因此同步不再正确。对于 IntegerLong 等原始包装类会发生这种情况。

如果将 Integer 替换为可以递增相同对象的 AtomicInteger,您可以看到不同之处:

private AtomicInteger itemCount = new AtomicInteger();

public void incrementAndPrint() {
    synchronized(itemCount) { 
        System.out.println(itemCount.getAndIncrement() + " ");
    }
}