java多线程4:volatile关键字
上文说到了 synchronized,那么就不得不说下 volatile关键字了,它们两者经常协同处理多线程的安全问题。
volatile保证可见性
那么volatile的作用是什么呢?
在jvm运行时刻内存的分配中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,
线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,
然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,
在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
注意这些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,
不会产生对应的变化,所以计算出来的结果会和预期不一样,对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
如何解决缓存不一致的问题?
在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。
因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),
从而使得只能有一个CPU能使用这个变量的内存。比如一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,
那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。
这样就解决了缓存不一致的问题。但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。
所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。
它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,
因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
看一个例子:
public class MyThread15 extends Thread { private boolean isRunning = true; public boolean isRunning() { return isRunning; } public void setRunning(boolean isRunning) { this.isRunning = isRunning; } public void run() { System.out.println("进入run了"); while (isRunning == true){} System.out.println("线程被停止了"); } }
@Test public void test15() throws InterruptedException { try{ MyThread15 a = new MyThread15(); a.start(); Thread.sleep(1000); a.setRunning(false); System.out.println("已赋值为false"); } catch (InterruptedException e){ e.printStackTrace(); } a.join(); }
执行结果:
进入run了 已赋值为false
可以看待,明明已经将 isRunning设置为false了,但是“线程被停止了”还是没有被执行。
当我们给isRunning加上关键字volatile后,执行结果:
进入run了 已赋值为false 线程被停止了
可见volatile关键字可以保证共享变量在多线程之间的可见性。
volatile不保证原子性
下面看个demo,定义一个线程类,将count值++ 10000次。
public class MyDomain16 { private volatile int count = 0; public MyDomain16() { } public void count() { for (int i=0; i<10000; i++) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } count++; } System.out.println(Thread.currentThread().getName() + ": count=" + count); } }
public class Mythread16 extends Thread { private MyDomain16 myDomain16; public Mythread16(MyDomain16 myDomain16) { this.myDomain16 = myDomain16; } public void run() { myDomain16.count(); } } @Test public void test16() throws InterruptedException { MyDomain16 myDomain16 = new MyDomain16(); Mythread16 a = new Mythread16(myDomain16); Mythread16 b = new Mythread16(myDomain16); a.start(); b.start(); a.join(); b.join(); }
执行结果:
Thread-1: count=17913 Thread-0: count=17919
可以看出两个线程总共应该增加2万次,但是count值并没有等于20000,说明自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
当我们给count方法加上synchronized之后
public class MyDomain16 { private volatile int count = 0; public MyDomain16() { } public synchronized void count() { for (int i=0; i<10000; i++) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } count++; } System.out.println(Thread.currentThread().getName() + ": count=" + count); } }
执行结果:
Thread-0: count=10000 Thread-1: count=20000
总结:
关键字synchronized和volatile进行一下比较:
1)关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法,以及代码块。
随着JDK新版本的发布,synchronized关键字在执行效率上得到很大提升,在开发中使用synchronized关键字的比率还是比较大的。
2)多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
3)volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
4)关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。
线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。
参考文献
1:《Java并发编程的艺术》
2:《Java多线程编程核心技术》
==================================================================
勇气是,尽管你感到害怕,但仍能迎难而上。
尽管你感觉痛苦,但仍能直接面对。
向前一步,也许一切都会不同。
==================================================================