为什么易失性加同步不起作用?



我正在努力理解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();
}
}
}
}

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

您误解了几件事:

  1. volatile与CPU缓存无关。所有现代处理器都采用了对应用程序完全透明的多级CPU缓存,因此应用程序不必关心它们是否从L1、L2、L3、RAM等中提取。这是通过CPU实现一些缓存一致性协议来实现的,例如MESI或其变体。那么volatile会做什么呢?它阻止某些编译器优化。例如,如果您读取一次变量的值,而没有volatile,编译器可能会优化该变量的任何后续读取,因为它假设它不可能发生更改。对于volatile,它不会删除那些额外的读取。

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

  3. 锁定/同步字符串(因为它们可以是interned(、Boolean或Integer等,这是一个非常糟糕的主意。例如,字符串对象可以被interned,你可以让程序的几个部分尝试在同一个实例上获取锁,尽管从它们的角度来看,它应该是一个不同的实例。缓存布尔值true/false。-128到+127之间的自动装箱整数是缓存的,所以如果您可能遇到与中间字符串相同的问题。

因此,对于同步,最好使用适当的锁并避免使用synchronized字。我甚至会更进一步地说,这是java中的一个错误

您在非最终字段上使用synchronized块。当您使用counter++时,由于Integer是不可变的,那么新引用将被分配给counter。请检查此答案以了解更多详细信息-非最终字段的同步

您可以使用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();
}
}
}
}

最新更新