线程安全/不安全
线程安全性:
定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替进行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类时线程安全的.
线程安全性体现在三个方面:
1.原子性:互斥访问,同一个时刻只能有一个线程来对它进行操作, 如Atomic包,锁
2.可见性:一个线程对主内存的修改可以及时的被其他线程观察的到
3.有序性: 一个线程观察其他线程中指令执行顺序,由于指令重排序存在,观察结果一般杂乱无序
一.原子性:
提供了互斥访问,同一时刻只能有一个线程来对它进行操作.
方法1:atomic包
JDK里面提供了一个包,叫atomic包,这个包里面提供了很多AtomicXXX类,他们都是通过CAS来完成原子性的.如下图,获取结果后是线程安全的.
①.AtomicXXX
源码剖析:
这个方法里面用了一个unsafe的类.
点进去
注意上图的方法中的参数.
var 1是当前对象,也就是count对象.
var 2是当前值,var 4是增加量.(假如我们要做2+1的操作,那么第二个参数var2就是2,第三个参数var4就是1.)
var 5是我们通过调用底层方法getIntVolatile,得到的底层当前的值.也就是说,假如没有线程来处理我们count的时候,那么var 5 应该就是2.
这里面有一个compareAndSwapInt的方法,这个方法是java底层的方法,它要达到的效果就是:
对于count这个对象(var1),如果当前的值2(var2),和底层的值2(var5)相同的话,就把它更新为后面的值2+1=3(var5+var4);
否则的话,重新取出var5,var2相当于是从var1中取一次,也会变成3,继续判断,如果跟底层值相同的话,编程3+1=4;
通过do while语句不断循环,直到当前值和底层值完全相同的话,才更新为后面的值.
这个方法的核心,就是CAS的核心.
CAS的ABA问题,意思是变量从A变为B,又变为了A.解决办法就是让当前版本号+1.
②.AtomicLong和LongAdder
单独说一下AtomicLong这个类,在JDK8中,有一个跟它很像的类,叫LongAdder.
这两个类比较:
之前我们AtomicXX类,底层CAS是一个do while死循环,不断的尝试,直到修改成功.如果竞争不激烈的时候成功率很高,否则失败概率会提高.如果大量修改失败的话,会多次尝试,性能受到影响
对于普通类型的Long和Double变量,JVM会将64位的读写操作拆成2个32位的操作.这个LongAdder类的思想,是热点数据分离.LongAdder相当于在AtomicLong的基础上,将单点的更新压力分散到各个节点上,在低并发情况下通过对base的直接更新,可以很好的保证和Atomic的性能基本一致;而在高并发时候通过分散提高性能.
实际使用中,如果处理高并发计数,优先使用LongAdder;如果线程竞争很低的话,还是用AtomicLong就可以
③.AtomicReferemce类和AtomicReferemceFieldUpdater类
AtomicReferemce类:
AtomicReferemceFieldUpdater类:
方法2:锁
①.Synchronized
一个JAVA的关键字,依赖JVM实现锁.因此在这个关键字作用对象的作用范围内,都是同一时刻只能有一个线程可以进行操作的.
修饰代码块:修饰范围是大括号括起来的代码,作用于调用的对象.
修饰方法:修饰范围是整个方法,作用于调用的对象.
修饰静态方法:修饰范围是整个静态方法,作用于所有对象.
修饰类:修饰范围是括号括起来的部分,作用于所有对象.
②.Lock
JDK提供的一个代码层面的锁,依赖特殊的CPU指令,实现类里面,有代表性的类:Reentrantlock
简单对比:
synchronized:不可中断锁,适合竞争不激烈,可读性好.
Lock:可中断锁(调用Unlock方法就可以),多样化同步,竞争激烈时能维持常态.
Atomic:竞争激烈时也能维持常态,比Lock性能好,缺点是每次只能同步一个值.
二.可见性:
一个线程对主内存的修改可以及时的被其它线程观察到.
导致共享变量在线程间不可见的原因:
1.线程交叉执行
2.重排序结合线程交叉执行
3.共享变量更新后的值没有在工作内存与主存间及时更新
对于可见性,JVM提供了synchronized和volatile
①.synchronized:
JMM关于synchronized的两条规定:
1.线程解锁前,必须把共享变量的最新值刷新到主内存
2.线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁是同一把锁)
②.volatile:
通过加入内存屏障和禁止重排序优化来实现可见性
对volatile变量写操作时,会在操作后写入一条store屏障指令,将本地内存中共享变量的值刷新到主存.
对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量.
但是要注意,直接使用volatile进行+操作,是线程不安全的.
volatile不是原子性.不适合计数场景.通常来说,使用volatile特别适合作为状态标识量.标记啥的.如下图
还有一种场景叫 double check.
三.有序性:
一个线程观察其它线程中的执行执行顺序,由于指令重排的存在,该观察结果一般杂乱无序
有序性:heppens-before原则
1.程序次序原则: 一个线程内,按照代码顺序,书写在前面的操作先行发生与书写在后面的操作.
2.锁定规则: 一个unLock操作先行发生与后面对同一个锁的Lock操作.
3.volatile变量规则: 对一个变量的写操作先行发生于后面对这个变量的读操作.
4.传递规则: 如果操作A先行发生于操作B,操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
5.线程启动规则
6.线程中断规则
7.线程终结规则
8.对象终结规则
(前4个比较重要)