为什么volatile加synchronized不起作用?

3

我正在尝试理解Java中的并发性。我知道synchronized可以在对象上创建监视器,这样其他线程就不能对该对象执行操作。Volatile与处理器缓存有关,如果使用它,则所有线程不会创建对象的副本。因此,在我的想法中,如果我运行此代码,我将获得正确的计数器值(40000)。但是实际上我得到的是错误的结果!

public class Main {

private static volatile Integer counter = 0;

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Counter();
    Thread thread2 = new Counter();
    thread.start();
    thread2.start();
    thread.join();
    thread2.join();
    System.out.println(counter);
}


public static class Counter extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20000; i++) {
            synchronized (counter){
                counter++;
            }
        }
    }
}
}

但是,如果我使用同步方法,我将获得正确的结果:

public class Main {

private static volatile Integer counter = 0;

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Counter();
    Thread thread2 = new Counter();
    thread.start();
    thread2.start();
    thread.join();
    thread2.join();
    System.out.println(counter);
}

public synchronized static void increment(){
    counter++;
}

public static class Counter extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20000; i++) {
            increment();

        }
    }
}
}

所以问题是 - 为什么在 Integer 对象上使用 synchronized 不起作用?

6
需要注意的关键是Integer是一个不可变类型,而counter++实际上是将一个引用分配给不同的Integer对象。然后duplink提供了答案。 - Stephen C
counter 也是 volatile 的事实对此没有影响。 - Stephen C
“重复”回答了一个完全不同的问题。这个重复谈到了一个常见的新手错误:新手编写synchronized(x),然后将不同的对象引用分配给x。但是,_这个_问题的作者在任何地方都没有这样做。 - Solomon Slow
2
@YuriMolodyko,volatile 不起作用是因为 counter++ 不是原子操作。counter++ 首先读取 volatile 计数器变量的值,然后计算新值,最后写回新值。volatile 无法防止两个线程同时读取相同的值,同时计算相同的新值,然后同时写入相同的新值。结果是,尽管两个不同的线程都增加了计数器,但计数器只增加了一次。 - Solomon Slow
2个回答

2
您有几个误解:
  1. volatile 与 CPU 缓存无关。所有现代处理器都采用多级 CPU 缓存,对应用程序完全透明,因此应用程序不必关心它们是从 L1、L2、L3、RAM 等获取的。这是通过 CPU 实现某些缓存一致性协议(例如 MESI 或其变体)来实现的。那么 volatile 是做什么的呢?它防止了某些编译器优化。例如,如果您只读取变量的值一次,没有使用 volatile,则编译器可能会优化掉对该变量的任何后续读取,因为它认为它不可能被更改。使用 volatile 将不会删除这些额外的读取。

  2. synchronized 关键字使用某个对象作为锁,但在您的情况下,您正在更改该对象。因此,假设线程 1 锁定 Integer(0),然后由于自动装箱,++ 操作将该对象更改为 Integer(1)。您的第二个线程可以自由地获取该锁,因为它没有被任何人持有。

  3. 在字符串(因为它们可以被 interned)、布尔值、整数等上锁/同步是非常糟糕的想法。例如,字符串对象可以被 interned,并且您可以有程序的几个部分尝试在同一实例上获取锁,尽管从它们的角度来看应该是不同的实例。布尔值 true/false 被缓存。从 -128 到 +127 的自动装箱整数被缓存,因此如果您可能会遇到与 interned 字符串相同的问题。

因此,对于同步,最好使用适当的锁并避免使用 synchronized 关键字。我甚至认为这是 Java 中的一个错误。


你的回答中包含了很多有用的信息。但我不同意 "synchronized" 是一个坏东西的说法。据我所知,锁(Lock)没有像锁合并(lock coalescing)、锁粗化(lock coarsening)、锁消除(lock elision)和自适应自旋锁(adaptive spin locking)这样的优化。而且,你也需要理解,在 "synchronized" 出现近 10 年之后,Java 才添加了锁的功能。 - pveentjer

1
您正在对一个非final字段使用同步块。当您使用counter++时,由于Integer是不可变的,新的引用将被分配给counter。请查看此答案以获取更多详细信息-Synchronization of non-final field 您可以使用ReentrantLock代替synchronized,但另一个问题是您没有在volatile字段上使用原子操作。您应该改用AtomicInteger。
import java.util.concurrent.atomic.AtomicInteger;

public class Main {

    private static final AtomicInteger counter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Counter();
        Thread thread2 = new Counter();
        thread.start();
        thread2.start();
        thread.join();
        thread2.join();
        System.out.println(counter);
    }


    public static class Counter extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 20000; i++) {
                counter.getAndIncrement();
            }
        }
    }
}

供参考,使用锁时不需要volatile:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Main {

    private static int counter = 0;
    private static final Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Counter();
        Thread thread2 = new Counter();
        thread.start();
        thread2.start();
        thread.join();
        thread2.join();
        System.out.println(counter);
    }


    public static class Counter extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 20000; i++) {
                lock.lock();
                counter++;
                lock.unlock();
            }
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接