[关闭]
@dodola 2015-02-25T17:54:37.000000Z 字数 3506 阅读 2202

volatile 关键字

Java


  1. 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
  2.   而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
  3. 一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
  4.   1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  5.   2)禁止进行指令重排序。
  6. //先看一段代码,假如线程1先执行,线程2后执行:
  7. //线程1
  8. boolean stop = false;
  9. while(!stop){
  10. doSomething();
  11. }
  12. //线程2
  13. stop = true;
  14. 这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。
  15. 下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。
  16. 那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2stop变量的更改,因此还会一直循环下去。
  17. 但是用volatile修饰之后就变得不一样了:
  18. 第一:使用volatile关键字会强制将修改的值立即写入主存;
  19. 第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPUL1或者L2缓存中对应的缓存行无效);
  20. 第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
  21. 那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。
  22. 那么线程1读取到的就是最新的正确的值。
  23. //volatile保证原子性吗?
  24. 从上面知道volatile关键字保证了操作的可见性,但是volatile能保证对变量的操作是原子性吗?
  25. 下面看一个例子:
  26. public class Test {
  27. public volatile int inc = 0;
  28. public void increase() {
  29. inc++;
  30. }
  31. public static void main(String[] args) {
  32. final Test test = new Test();
  33. for(int i=0;i<10;i++){
  34. new Thread(){
  35. public void run() {
  36. for(int j=0;j<1000;j++)
  37. test.increase();
  38. };
  39. }.start();
  40. }
  41. while(Thread.activeCount()>1) //保证前面的线程都执行完
  42. Thread.yield();
  43. System.out.println(test.inc);
  44. }
  45. }
  46.   大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。
  47.   可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000
  48.   这里面就有一个误区了,volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。
  49.   在前面已经提到过,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:
  50.   假如某个时刻变量inc的值为10
  51.   线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;
  52.   然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
  53.   然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
  54.   那么两个线程分别进行了一次自增操作后,inc只增加了1
  55.   解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?然后其他线程去读就会读到新的值,对,这个没错。这个就是上面的happens-before规则中的volatile变量规则,但是要注意,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。
  56.   根源就在这里,自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
  57.   把上面的代码改成以下任何一种都可以达到效果:
  58. //采用synchronized
  59. public class Test {
  60. public int inc = 0;
  61. public synchronized void increase() {
  62. inc++;
  63. }
  64. public static void main(String[] args) {
  65. final Test test = new Test();
  66. for(int i=0;i<10;i++){
  67. new Thread(){
  68. public void run() {
  69. for(int j=0;j<1000;j++)
  70. test.increase();
  71. };
  72. }.start();
  73. }
  74. while(Thread.activeCount()>1) //保证前面的线程都执行完
  75. Thread.yield();
  76. System.out.println(test.inc);
  77. }
  78. }
  79. //采用Lock
  80. public class Test {
  81. public int inc = 0;
  82. Lock lock = new ReentrantLock();
  83. public void increase() {
  84. lock.lock();
  85. try {
  86. inc++;
  87. } finally{
  88. lock.unlock();
  89. }
  90. }
  91. public static void main(String[] args) {
  92. final Test test = new Test();
  93. for(int i=0;i<10;i++){
  94. new Thread(){
  95. public void run() {
  96. for(int j=0;j<1000;j++)
  97. test.increase();
  98. };
  99. }.start();
  100. }
  101. while(Thread.activeCount()>1) //保证前面的线程都执行完
  102. Thread.yield();
  103. System.out.println(test.inc);
  104. }
  105. }
  106. //采用AtomicInteger
  107. public class Test {
  108. public AtomicInteger inc = new AtomicInteger();
  109. public void increase() {
  110. inc.getAndIncrement();
  111. }
  112. public static void main(String[] args) {
  113. final Test test = new Test();
  114. for(int i=0;i<10;i++){
  115. new Thread(){
  116. public void run() {
  117. for(int j=0;j<1000;j++)
  118. test.increase();
  119. };
  120. }.start();
  121. }
  122. while(Thread.activeCount()>1) //保证前面的线程都执行完
  123. Thread.yield();
  124. System.out.println(test.inc);
  125. }
  126. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注