诡异并发三大恶人之有序性

开发 前端
正如大家所熟知那样,Java语言是运行在 Java 自带的 JVM(Java Virtual Machine) 环境中,在JVM环境中源代码(.class)的执行顺序与程序的执行顺序(runtime)不一致,或者程序执行顺序与编译器执行顺序不一致的情况下,我们就称程序执行过程中发生了重排序。

[[317250]]

 上一节阿粉我和大家一起打到了并发中的恶人可见性和原子性,这一节我们继续讨伐三恶之一的有序性。

序、有序性的阐述

有序性为什么要探讨?因为 Java 是面向对象编程的,关注的只是最终结果,很少去研究其具体执行过程?正如上一篇文章在介绍可见性时描述的一样,操作系统为了提升性能,将 Java 语言转换成机器语言的时候,吩咐编译器对语句的执行顺序进行了一定的修改,以促使系统性能达到最优。所以在很多情况下,访问一个程序变量(对象实例字段,类静态字段和数组元素)可能会使用不同的顺序执行,而不是程序语义所指定的顺序执行。

正如大家所熟知那样,Java语言是运行在 Java 自带的 JVM(Java Virtual Machine) 环境中,在JVM环境中源代码(.class)的执行顺序与程序的执行顺序(runtime)不一致,或者程序执行顺序与编译器执行顺序不一致的情况下,我们就称程序执行过程中发生了重排序。

而编译器的这种修改是自以为能保证最终运行结果!因为在单核时代完全没问题;但是随着多核时代的到来,多线程的环境下,这种优化碰上线程切换就大大的增加了事故的出现几率!

好心办了坏事!

 

 

 

[[317251]]

 

也就是说,有序性 指的是在代码顺序结构中,我们可以直观的指定代码的执行顺序, 即从上到下按序执行。但编译器和CPU处理器会根据自己的决策,对代码的执行顺序进行重新排序。优化指令的执行顺序,提升程序的性能和执行速度,使语句执行顺序发生改变,出现重排序,但最终结果看起来没什么变化(单核)。

有序性问题 指的是在多线程环境下(多核),由于执行语句重排序后,重排序的这一部分没有一起执行完,就切换到了其它线程,导致的结果与预期不符的问题。这就是编译器的编译优化给并发编程带来的程序有序性问题。

用图示就是:

 

 

 

 

阿粉小结:编译优化最终导致了有序性问题。

一、导致有序性的原因:

如果一个线程写入值到字段 a,然后写入值到字段 b ,而且b的值不依赖于 a 的值,那么,处理器就能够自由的调整它们的执行顺序,而且缓冲区能够在 a 之前刷新b的值到主内存。此时就可能会出现有序性问题。

例子:

 

  1. 1import java.time.LocalDateTime; 
  2.  2 
  3.  3/** 
  4.  4 * @author :mmzsblog 
  5.  5 * @description:并发中的有序性问题 
  6.  6 * @date :2020年2月26日 15:22:05 
  7.  7 */ 
  8.  8public class OrderlyDemo { 
  9.  9 
  10. 10    static int value = 1; 
  11. 11    private static boolean flag = false
  12. 12 
  13. 13    public static void main(String[] args) throws InterruptedException { 
  14. 14        for (int i = 0; i < 199; i++) { 
  15. 15            value = 1; 
  16. 16            flag = false
  17. 17            Thread thread1 = new DisplayThread(); 
  18. 18            Thread thread2 = new CountThread(); 
  19. 19            thread1.start(); 
  20. 20            thread2.start(); 
  21. 21            System.out.println("========================================================="); 
  22. 22            Thread.sleep(6000); 
  23. 23        } 
  24. 24    } 
  25. 25 
  26. 26    static class DisplayThread extends Thread { 
  27. 27        @Override 
  28. 28        public void run() { 
  29. 29            System.out.println(Thread.currentThread().getName() + " DisplayThread begin, time:" + LocalDateTime.now()); 
  30. 30            value = 1024; 
  31. 31            System.out.println(Thread.currentThread().getName() + " change flag, time:" + LocalDateTime.now()); 
  32. 32            flag = true
  33. 33            System.out.println(Thread.currentThread().getName() + " DisplayThread end, time:" + LocalDateTime.now()); 
  34. 34        } 
  35. 35    } 
  36. 36 
  37. 37    static class CountThread extends Thread { 
  38. 38        @Override 
  39. 39        public void run() { 
  40. 40            if (flag) { 
  41. 41                System.out.println(Thread.currentThread().getName() + " value的值是:" + value + ", time:" + LocalDateTime.now()); 
  42. 42                System.out.println(Thread.currentThread().getName() + " CountThread flag is true,  time:" + LocalDateTime.now()); 
  43. 43            } else { 
  44. 44                System.out.println(Thread.currentThread().getName() + " value的值是:" + value + ", time:" + LocalDateTime.now()); 
  45. 45                System.out.println(Thread.currentThread().getName() + " CountThread flag is false, time:" + LocalDateTime.now()); 
  46. 46            } 
  47. 47        } 
  48. 48    } 
  49. 49} 

运行结果:

 

 

 

 

从打印的可以看出:在 DisplayThread 线程执行的时候肯定是发生了重排序,导致先为 flag 赋值,然后切换到 CountThread 线程,这才出现了打印的 value 值是1,falg 值是 true 的情况,再为 value 赋值;不过出现这种情况的原因就是这两个赋值语句之间没有联系,所以编译器在进行代码编译的时候就可能进行指令重排序。

用图示,则为:

 

 

 

 

二、如何解决有序性

2.1、volatile

volatile 的底层是使用内存屏障来保证有序性的(让一个 CPU 缓存中的状态(变量)对其他 CPU 缓存可见的一种技术)。

volatile 变量有条规则是指对一个 volatile 变量的写操作, Happens-Before于后续对这个 volatile 变量的读操作。并且这个规则具有传递性,也就是说:

 

 

 

 

此时,我们定义变量 flag 时使用 volatile 关键字修饰,如:

 

  1. 1    private static volatile boolean flag = false

此时,变量的含义是这样子的:

 

 

 

 

也就是说,只要读取到 flag=true; 就能读取到 value=1024;否则就是读取到flag=false; 和 value=1 的还没被修改过的初始状态;

 

 

 

 

但也有可能会出现线程切换带来的原子性问题,就是读取到 flag=false; 而value=1024 的情况;看过上一篇讲述[原子性]()的文章的小伙伴,可能就立马明白了,这是线程切换导致的。

 

 

 

 

2.2、加锁

此处我们直接采用Java语言内置的关键字 synchronized,为可能会重排序的部分加锁,让其在宏观上或者说执行结果上看起来没有发生重排序。

代码修改也很简单,只需用 synchronized 关键字修饰 run 方法即可,代码如下:

 

  1. 1    public synchronized void run() { 
  2. 2        value = 1024; 
  3. 3        flag = true
  4. 4    } 

同理,既然是加锁,当然也可以使用 Lock 加锁,但 Lock 必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。这点在使用的时候一定要注意!

使用该种方式加锁也很简单,代码如下:

 

  1. 1    readWriteLock.writeLock().lock(); 
  2. 2    try { 
  3. 3        value = 1024; 
  4. 4        flag = true
  5. 5    } finally { 
  6. 6        readWriteLock.writeLock().unlock(); 
  7. 7    } 

好了,以上内容就是我对并发中的有序性的一点理解与总结了,通过这三篇文章我们也就大致掌握了并发中常见的可见性、有序性、原子性问题以及它们常见的解决方案。

最后

阿粉简单总结下三篇文章文章中使用的解决方案之间的区别:

 

 

 

 

References

[1]: https://juejin.im/post/5d52abd1e51d4561e6237124

[2]: https://juejin.im/post/5d89fd1bf265da03e71b3605

[3]: https://www.cnblogs.com/54chensongxia/p/12120117.html

[4]: http://ifeve.com/jmm-faq-reordering/

 

责任编辑:武晓燕 来源: Java极客技术
相关推荐

2021-01-12 07:39:48

线程线程安全

2021-05-16 17:14:30

线程安全性

2024-02-27 17:46:25

并发程序CPU

2021-05-06 19:20:05

Java内存模型

2022-12-04 09:19:25

JAVA并发有序性

2024-11-18 16:37:35

JMMJava内存模型

2020-02-28 14:48:51

结构系统程序

2016-09-19 21:53:30

Java并发编程解析volatile

2010-09-01 13:27:34

2010-07-19 15:07:23

SQL Server评

2022-07-02 08:40:00

并发编程

2020-08-27 08:17:05

缓存高并发系统

2024-03-11 15:13:22

数据库高并发

2019-08-19 15:36:55

SynchronizeVolatile性能

2018-08-07 16:01:32

synchronizevolatilefinal

2024-02-26 08:33:51

并发编程活跃性安全性

2021-12-13 10:43:45

HashMapJava集合容器

2022-11-27 08:12:11

RocketMQ源码工具类

2021-03-11 00:05:55

Java高并发编程

2021-03-04 07:24:24

JavaSemaphore高并发
点赞
收藏

51CTO技术栈公众号