多线程二之线程安全
线程安全主要分二个问题。一是可见性,二是原子性
线程安全:只有当多个线程 访问并更新共享资源时,会发生资源竞争,导致错误的结果
如果保证共享资源的正确访问?
1、栈封闭时,不会在线程之间共享的变量,都是线程安全的
2、局部对象引用本身不共享,但是引用对象存储在共享堆中。如果方法创建的对象,只是在方法中传递,并且其他线程不可用,也是线程安全的。
3、不可变的共享对象来保证对象在线程共享时不被修改,实现线程安全
4、使用ThreadLocal,相当不同线程访问不同资源,不存在线程安全
一、可见性
volatile:保证数据的可见性,不保证原子性。原子性需要同步锁。直接存取原始内存地址。
可见性问题描述:
1、 不同线程加载内存中的变量,a线程去写,b线程去读。数据经过a线程的工作区间–高速缓存–内存中。b线程从内存中读,在到高速缓存中,最后读取到cpu的线程的工作区间中。不做任何处理时候,由于高速缓存的存在,b线程从内存中读取的时候,将变量值放在缓存中,在到线程的工作区间中b线程每次读取的时候,发现缓存中已经存在了了,就直接读取这值。a线程写到内存的值,b线程没有读取到。
虽然cpu高速缓存速度很快,不过极短的时间内看不见的,不过也是存在的。
2、CPU指令重排序
在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序。
JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier (内存屏障)指令重排序。
当一个变量使用volatile修饰的时候:
- 将当前处理器缓存行的数据写回到系统内存
- 写会内存的操作会使其他cpu缓存了该内存的数据失效
为了提高处理器处理速度,处理器不直接和内存进行通信,是将内存里的数据读取到内部缓存(L1,L2等)后进行操作,但是操作后不知道什么时候将内存的数据写入内存。当时对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送指令,将这个变量所在缓存行的数据写回到系统内存。为了保证各个处理器的缓存是一致的,实现了缓存一致性协议(MESI)。
首字母缩略词MESI中的字母表示可以标记高速缓存行的四种独占状态
1、修改(M)
高速缓存行仅存在于当前高速缓存中,并且是脏的 - 它已从主存储器中的值修改(M状态)。在允许对(不再有效)主存储器状态的任何其他读取之前,需要高速缓存在将来的某个时间将数据写回主存储器。回写将该行更改为共享状态(S)。
2、独家(E)
缓存行仅存在于当前缓存中,但是干净 - 它与主内存匹配。它可以随时更改为共享状态,以响应读取请求。或者,可以在写入时将其改变为修改状态。
3、共享(S)
表示此高速缓存行可能存储在计算机的其他高速缓存中并且是干净的 - 它与主存储器匹配。可以随时丢弃该行(更改为无效状态)。
4、无效(I)
表示此缓存行无效(未使用)。
对于任何给定的高速缓存对,给定高速缓存行的允许状态如图1。
当块标记为M(已修改)时,其他高速缓存中块的副本将标记为I(无效)
volatile 变量通过这样的机制就使得每个线程都能获得该变量的最新值。
1、同步的规则,遵循Happens-before先行发送原则
对volatitle变量的写入,与所有的后续对v的读同步(可见)
对监视器m的解锁和后续操作m的加锁同步
1)线程m的加锁,里面的所有值的操作,对m的值都是可见的
2)防止指令重拍
二、原子性
原子性的问题产生原因?
一个程序针对一个数进行加法,6个线程,每个线程执行1万次,最终结果会小于6万。
原因是:
单线程在进行加法的时候,程序编译为.class,加入jvm中.jvm运行时内存区域对.class文件不同值存入不同区域
堆内存存类的实例和数组。线程也分配独享的工作内存,将进行计算。先把堆内存的i=0,取出来放到操作数栈中,
然后+1,然后再放到内存中。当多个线程在跑的时候,堆内存的i值,可能同时被线程读取到各自的独享工作内存中,分别+1;
然后在放入到共享的堆内存中。这个是只+1次。
这是因为运算的时候,+1这个动作,被程序分解成多个步骤,导致每个步骤无法保证原子性。多线程的运行的时候会造成数据不对,线 程安全问题。
原子操作可以是一个步骤或多个步骤,但是顺序不可以被打乱,也不可以被切割,只执行了一部分(不可中断)将整个资源做一个整体,资源在该操作中保持一致,这是原子性的核心特征。
原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作,通常是通过互斥锁的方式实现。
怎么实现原子性呢?
1、 可以通过同步锁synchronized 来实现上面的需求.同步:统一时刻只有一个线程在跑,保证资源的唯一性。synchronized 是轻量级锁,内置cas自旋机制,当资源争抢的时候会变为重量级锁,就不自旋了。
2、Reentrantlock和condition来实现互斥锁的功能
3、原子操作类Atomic开头的JUC类。
Reentrantlock() 是jdk提供的;synchronized 是jvm提供的
简要说明下cas自旋机制:
compare and swap 比较和交互。属于硬件同步语言,处理器提供了基本内存的原子性保障
CAS操作需要输入两个数值,一个旧值A和一个新值,在操作前选对旧值进行比较,如没有发生变化,才交换新值,发送变化则不换。
类似源码如下:
/**
* Atomically sets the element at position {@code i} to the given
* updated value if the current value {@code ==} the expected value.
*
* @param i the index
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int i, int expect, int update) {
return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
CAS问题:
1、循环+cas,自旋的实现让所有线程都处于高频运行,争夺cpu的执行时间的状态。如果长时间不操作,会打来大量的cpu的资源消耗
2、仅针对单个变量的,不能多个变量实现原子操作
3、会产生ABA问题。cas,线程处于runnable状态,线程不会阻塞和等待,cpu高度运行
综上可将volatile和synchronized的对比
a.关键字volatile是线程同步的轻量级实现,所以volatile性能比synchronized要好,并且volatile只能修饰变量,而synchronized可以修饰方法,以及代码块。
b.多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
c.volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步。
参考文章:
https://www.jianshu.com/p/ccfe24b63d87