死磕Java多线程(二)--- 多线程必须了解的三大特性

引言:

我们都知道并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

下面让我们来看看原子性、可见性以及有序性到底是什么?

可见性

可见性指的是一个线程对变量的写操作对其他线程后续的读操作可见。由于现代CPU都有多级缓存,CPU的操作都是基于高速缓存的,而线程通信是基于内存的,这中间有一个Gap, 可见性的关键还是在对变量的写操作之后能够在某个时间点显示地写回到主内存,这样其他线程就能从主内存中看到最新的写的值。volatile,synchronized, 显式锁,原子变量这些同步手段都可以保证可见性。

可见性底层的实现是通过加内存屏障实现的:

  1. 写变量后加写屏障,保证CPU写缓冲区的值强制刷新回主内存
  2. 读变量之前加读屏障,使缓存失效,从而强制从主内存读取变量最新值
    写volatile变量 = 进入锁
    读volatile变量 = 释放锁

有序性

有序性指的是数据不相关的变量在并发的情况下,实际执行的结果和单线程的执行结果是一样的,不会因为重排序的问题导致结果不可预知。volatile, final, synchronized,显式锁都可以保证有序性。

有序性的语意有几层

  1. 最常见的就是保证多线程执行的串行顺序
  2. 防止重排序引起的问题
  3. 程序执行的先后顺序,比如JMM定义的一些Happens-before规则

原子性

原子性由Java内存模型来直接保证的原子性变量包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的。如果应用场景需要一个更大方位的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反应到Java代码中就是同步块–synchronized关键字,因此在synchronized块之间的操作也具备原子性。

有个专有名词竞态条件来描述原子性的问题。----竞态条件

竞态条件(racing condition) 是指某个操作由于不同的执行时序而出现不同的结果,比如先检查后操作。

volatile变量只保证了可见性,不保证原子性, 比如a++这种操作在编译后实际是多条语句,比如先读a的值,再加1操作,再写操作,执行了3个原子操作,如果并发情况下,另外一个线程很有可能读到了中间状态,从而导致程序语意上的不正确。所以a++实际是一个复合操作。加锁可以保证复合语句的原子性,sychronized可以保证多条语句在synchronized块中语意上是原子的。显式锁保证临界区的原子性。原子变量也封装了对变量的原子操作。非阻塞容器也提供了原子操作的接口,比如putIfAbsent。

发布了45 篇原创文章 · 获赞 3 · 访问量 2329

猜你喜欢

转载自blog.csdn.net/weixin_44046437/article/details/99091800