上一篇我们简单的使用了synchoronized实现线程同步,接下来简单的使用volatile实现线程同步。
public class ThreadCount {
//如果去掉volatile就会出现错误
private static volatile int total;
public static void main(String[] args) {
Counter counter1 = new Counter(10);
Counter counter2 = new Counter(20);
Counter counter3 = new Counter(5);
Counter counter4 = new Counter(10);
Counter counter5 = new Counter(10);
Counter counter6 = new Counter(20);
counter1.start();
counter2.start();
counter3.start();
counter4.start();
counter5.start();
counter6.start();
}
static class Counter extends Thread {
private int i;
Counter(int i) {
this.i = i;
}
@Override
public void run() {
synchronized (this) {
total += i;
System.out.println("结果为: " + total);
}
}
}
}
//程序运行结果
结果为: 10
结果为: 30
结果为: 35
结果为: 45
结果为: 55
结果为: 75
//去掉volatile程序运行结果
结果为: 30
结果为: 45
结果为: 35
结果为: 30
结果为: 75
结果为: 55
一个关键字volatile可以让程序便可以上程序正确的执行,volatile有什么神通呢,接下来让我娓娓道来:
在jmm(java内存模型)中,线程共享主内存,其次每个线程有自己私有的缓存,正常情况下线程更新主内存数据的时候得先更新自己的缓存的数据,然后更新主内的数据,而使用volatile后,线程直接更新住内存数据,这样就保证了当前线程更新共享数据后对别的线程立即得知这个修改。
Java内存模型的抽象示意图如下:
对于i++下i被volatile修饰,多线程下能得到正确的结果吗?请看下面的例子:
public class VolatileTest {
public static volatile int race = 0;
public static void increase(){
race++;
}
private static final int THREAD_COUNT = 200;
public static void main(String[] args) {
Thread[] threads = new Thread[THREAD_COUNT];
for(int i = 0; i < THREAD_COUNT; i++){
threads[i] = new Thread(new Runnable() {
public void run() {
for(int i = 0; i < 2000; i++){
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
System.out.println(race);
}
}
}
//执行结果
393715
393715
393715
393715
393715
393715
你会发现执行结果小于我们预期400000,其实无论执行多少次都会小于400000,这个是为什么呢?反编译这段代码发现increase()方法产生四条指令,1:getstiatic 2:iconst_1 3: iadd 4:putstatic,当getstatic指令把race的值渠道操作栈顶时,volatile只保证了race在此时准确,但是执行iconst_1、iadd指令时可能其他的线程把race的值增大,而这个时候栈顶已经变为实效的数据,所以putstatic可能会被较小的race放会到主内存,其实还有编译出来只有一条字节码指令,也不意味着这条指令在机器执行时是一个原子操作。
所以说volatile只能保证可见行,不能保证原子性,我们仍然需要加锁来保证原子性 (使用synchoronized或java.util.concurrent中的原子类)
synchoronized如何保证原子性:
Java内存模型提供lock和unlock操作来保证原子性,尽管虚拟机没有把lock和unlock直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节吗指令反映到java代码中就是同步块synchoronized关键字。