当前位置:   article > 正文

Java-Volatile线程访问共享变量(一)-XXOO_volatile 线程共享变量

volatile 线程共享变量

一、Volatile是什么?

volatile是轻量级的synchronized。如果一个变量使用volatile,则它比使用synchronized的成本更加低,因为它不会引起线程上下文的切换和调度。

Java语言规范对volatile的定义:

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。

通俗点讲就是说一个变量如果用volatile修饰了,则Java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。

二、内存模型相关概念

操作系统语义

计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存。 CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。 有了CPU高速缓存虽然解决了效率问题, 但是它会带来一个新的问题:数据一致性。

在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中, 在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中。

举一个简单的例子:i++

当线程运行这段代码时,首先会从主存中读取i( i = 1),然后复制一份到CPU高速缓存中, 然后CPU执行 + 1 (i = 2)的操作,然后将数据(i = 2)写入到缓存中,最后刷新到主存中。 其实这样做在单线程中是没有问题的,有问题的是在多线程中。

如下: 假如有两个线程A、B 都执行这个操作(i++),按照我们正常的逻辑思维主存中的i值应该=3, 但事实是这样么?

分析如下:

两个线程从主存中读取i的值(1)到各自的高速缓存中,然后线程A执行+1操作并将结果写入高速缓存中, 最后写入主存中,此时主存i==2,线程B做同样的操作,主存中的i仍然=2。所以最终结果为2并不是3。 这种现象就是缓存一致性问题。

解决缓存一致性方案有两种:

1.通过在总线加LOCK#锁的方式

2.通过缓存一致性协议

但是方案1存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够运行, 其他CPU都得阻塞,效率较为低下。

第二种方案,缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。

其核心思想如下:

当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的, 因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。

三、Java内存模型

在并发编程中我们一般都会遇到这三个基本概念:原子性、可见性、有序性。

1.原子性

即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

原子性就像数据库里面的事务一样,他们是一个团队,同生共死。

示例: (1)i=0; (2)j=i; (3)i++; (4)i=j+1; 这四个操作,那些是原子性?

(1)在Java中,对基本数据类型的变量和赋值操作都是原子性操作;

 (2)包含了两个操作:读取i,将i值赋值给j;

 (3)(4)包含了三个操作:读取i值、i + 1 、将+1结果赋值给i;

在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同, Java只保证了基本数据类型的变量和赋值操作才是原子性的 (注:在32位的JDK环境下,对64位数据的读取不是原子性操作,如long、double)。 要想在多线程环境下保证原子性,则可以通过锁、synchronized来确保。 注意: volatile是无法保证复合操作的原子性

2.可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。 Java提供了volatile来保证可见性。 当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中, 当其他线程读取共享变量时,它会直接从主内存中读取。当然,synchronize和锁都可以保证可见性。

3.有序性

即程序执行的顺序按照代码的先后顺序执行。

在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果, 但是对多线程会有影响。 Java提供volatile来保证一定的有序性。

最著名的例子就是单例模式里面的DCL(双重检查锁)。

四、Volatile原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。

在JVM底层volatile是采用“内存屏障”来实现的。

1.保证可见性、不保证原子性

2.禁止指令重排序

在执行程序时为了提高性能,编译器和处理器通常会对指令做重排序:

1.编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;

2.处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;

 指令重排序对单线程没有什么影响,他不会影响程序的运行结果,但是会影响多线程的正确性。

原则:happens-before

happen-before原则保证了程序的“有序性”,它规定如果两个操作的执行顺序无法从happens-before 原则中推导出来,那么他们就不能保证有序性,可以随意进行重排序。

 定义:

1.同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是, 在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说, 这一是规则无法保证编译重排和指令重排)。

 2.监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)

3.对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)

4.线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)

5.线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。

6.如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。

JVM是如何禁止重排序?

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时, 会多出一个lock前缀指令。 lock前缀指令其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。

volatile的底层就是通过内存屏障来实现的。

五、Volatile使用场景

1.对变量的写操作不依赖当前值;

2.该变量没有包含在具有其他变量的不变式中。

volatile经常用于两个两个场景:状态标记两、double check


示例

  1. package com.yl.springboottest.consurrency.jmm.volatileT;
  2. import java.util.concurrent.CountDownLatch;
  3. import java.util.concurrent.ExecutorService;
  4. import java.util.concurrent.Executors;
  5. import java.util.concurrent.Semaphore;
  6. /**
  7. * 描述: Volatile
  8. *
  9. * @author: yanglin
  10. * @Date: 2020-12-03-8:31
  11. * @Version: 1.0
  12. */
  13. public class VolatileT1 {
  14. // 线程并发数量
  15. private static int threadTotal = 200;
  16. // 请求总数
  17. private static int clientTotal = 5000;
  18. private static long count = 0;
  19. public static void add(){
  20. count++;
  21. }
  22. /**
  23. * test运行时,使用两独立的变量来保存时间,避免因使用同步而对t1,t2造成影响
  24. */
  25. long time1;
  26. long time2;
  27. volatile boolean boolValue = true;
  28. public static void main(String[] args) throws InterruptedException {
  29. /**
  30. * Java只保证了基本数据类型的变量和赋值操作才是原子性的
  31. *
  32. * 64位JVM只有server模式(server模式会进行更多的优化),
  33. * 32位JVM默认使用client模式,我将32位JVM设置为server模式后,问题同样出现
  34. */
  35. ExecutorService executorService = Executors.newCachedThreadPool();
  36. // 信号
  37. final Semaphore semaphore = new Semaphore(threadTotal);
  38. // 每次固定数量的线程获取许可
  39. final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
  40. for (int index = 0; index < clientTotal ; index++) {
  41. executorService.execute(new Runnable() {
  42. @Override
  43. public void run() {
  44. try {
  45. // 获取一个许可
  46. semaphore.acquire();
  47. add();
  48. // 释放一个许可
  49. semaphore.release();
  50. } catch (Exception e) {
  51. e.printStackTrace();
  52. }// 计数减一
  53. countDownLatch.countDown();
  54. }
  55. });
  56. }
  57. // 阻塞当前线程,直到计时器的值为0
  58. countDownLatch.await();
  59. executorService.shutdown();
  60. System.out.println("计数 num : " +count);
  61. System.out.println("----------------------------抢票场景中-------------");
  62. /** 创建线程 */
  63. TicketRunnable ticketRunnable=new TicketRunnable();
  64. // 第1个人
  65. Thread t1=new Thread(ticketRunnable,"张三");
  66. // 第2个人
  67. Thread t2=new Thread(ticketRunnable,"李四");
  68. // 第3个人
  69. Thread t3=new Thread(ticketRunnable,"王五");
  70. // 第4个人
  71. Thread t4=new Thread(ticketRunnable,"赵六");
  72. // 第5个人
  73. Thread t5=new Thread(ticketRunnable,"田七");
  74. /** 启动线程 */
  75. t1.start();
  76. t2.start();
  77. t3.start();
  78. t4.start();
  79. t5.start();
  80. /**
  81. * Java虚拟机JVM是64位JVM还是32位
  82. */
  83. System.out.println("Java虚拟机JVM是64位JVM还是32位::"+System.getProperty("sun.arch.data.model"));
  84. /**
  85. * boolValue 变量是我们要验证的 volatile 变量。一开始 boolValue 初始化为 true,
  86. * 其次启动 t2 线程让其进入死循环;接着,t1 线程启动,并且让 t1 线程先执行,
  87. * 将 boolValue 改为 false。理论上来讲,此时 t2 线程应该跳出死循环,但是实际上并没有。
  88. * 此时 t2 线程读到的 boolValue 的值仍然为 true。所以这段程序一直没有打印出结果。
  89. * 这便是多线程间的不可见性问题,官方话术为: 线程 t1 修改后的值对线程 t2 来说并不可见。
  90. */
  91. // 测试个数
  92. int size = 5000;
  93. VolatileT1 vs[] = new VolatileT1[size];
  94. long timeSum = 0;
  95. for(int n = 0; n < size; n++){
  96. (vs[n] = new VolatileT1()).test();
  97. }
  98. /**
  99. * 统计出,所有线程从boolValue变为false到while(boolValue)跳出所花时间的总和
  100. */
  101. for(int n = 0; n < size; n++){
  102. timeSum += vs[n].time2 - vs[n].time1;
  103. System.out.print(n+"\t"+vs[n].time2 +'\t' + vs[n].time1+'\t'+(vs[n].time2 - vs[n].time1)+'\n');
  104. }
  105. System.out.println("响应时间总和(毫微秒):"+timeSum);
  106. long time1,time2;
  107. time1 = System.nanoTime();
  108. time2 = System.nanoTime();
  109. // 顺序执行两条语句的时间间隔,供参考
  110. System.out.println(time2-time1);
  111. }
  112. /**
  113. * 假设在抢票场景中,我们一共只有10张火车票,在最后一刻,我们已经卖出了9张火车票,仅剩最后一张。
  114. * 这个时候,系统发来多个并发请求,这些并发请求都同时读取到火车票剩余数量为1,然后都通过了这一个
  115. * 余量判断,最终导致超发。也就是说,本来我们只卖10张火车票,最多只生成10个订单,但因为线程不安全,
  116. * 用户的并发请求,导致抢票成功的用户订单超过了10个,这就是线程不安全。
  117. */
  118. static class TicketRunnable implements Runnable {
  119. // 剩余的票数
  120. static volatile int count = 10;
  121. // 抢到第几张票
  122. static int num = 0;
  123. // 是否售完票
  124. boolean flag = false;
  125. @Override
  126. public void run() {
  127. // TODO Auto-generated method stub
  128. // 票没有售完的情况下,继续抢票
  129. while (!flag) {
  130. sale();
  131. }
  132. }
  133. /** 售票 */
  134. private synchronized void sale() {
  135. if(count <= 0){
  136. flag = true;
  137. return;
  138. }
  139. // 剩余的票数 减1
  140. count--;
  141. // 抢到第几张票 加1
  142. num++;
  143. System.out.println(Thread.currentThread().getName()+"抢到第"+num+"张票,剩余"+count+"张票。");
  144. }
  145. }
  146. public void test() throws InterruptedException{
  147. Thread t2 = new Thread(){
  148. @Override
  149. public void run(){
  150. while(boolValue)
  151. ;
  152. time2 = System.nanoTime();
  153. }
  154. };
  155. Thread t1 = new Thread(){
  156. @Override
  157. public void run(){
  158. time1 = System.nanoTime();
  159. boolValue=false;
  160. }
  161. };
  162. t2.start();
  163. Thread.yield();
  164. t1.start();
  165. /**
  166. * 保证一次只运行一个测试,以此减少其它线程的调度对 t2对boolValue的响应时间 的影响
  167. */
  168. t1.join();
  169. t2.join();
  170. }
  171. }

以上

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/266099?site
推荐阅读
相关标签
  

闽ICP备14008679号