这里是最全的线程安全性总结,如果觉得总结的还不错的话,不要吝啬你的小心心啊,哈哈。
一、安全性定义:
当多个线程访问某一个类的时候,不管运行时环境采取何种调度方式,或者这些线程将如何交替的执行,并且在主调代码中不需要任何额外的同步和协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
二、主要体现在三个方面:
1、原子性;2、可见性;3、 有序性;
2.1原子性:
原子性举个栗子,我们在做普通int变量++的操作的时候,会出现跟预期不同的结果。主要的原因是,看上去++是一个独立的操作,但是这个操作却不是原子性的,它实际的操作序列是“读取-修改-写入”。这个时候会出现两个线程获取到同样值,同时做++,后写入。这样就会导致有一次执行没有生效。
解决办法嘛,有两种,一种是加锁,这个很好理解,是显示的将一个++操作变成一个原子性操作。加了锁,不让别的线程(未获取到锁的线程)影响到目前正在执行的线程操作。保证了安全性的同时带来性能方面的问题。
还有一种是使用原子类 atomic定义的变量,来做计数操作。atomic包中的类都是用过CAS方式来保证原子性的。
2.1.1CAS:
atomic中使用了一个叫unsafe的类。类中有一个getAndAddInt的方法调用了一个叫 compareAndSwapInt的方法。而这个方法也是一个native的方法。主要有四个参数
CompareAndSwapvar1:需要改变的对象。var2:内存中的value。var5:期望的值。var5+var4更新后的值。var4:步长(++的话var4就是1)。
CAS没有通过锁机制(阻塞的方式)来保证原子性,是通过操作系统级别的指令来控制(对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。)
CAS这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。详细的CAS问题也可以看这篇文章 《java并发编程实战》-(2)-线程安全性-(CAS)。
2.1.1-1Atomiclong与Longadder的区别
我们刚才通过分析得出atomic类是通过cas不断循环来保证原子性,这样就会存在一个问题,是高并发的时候,可能要尝试(循环)很多次才能成功。
LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。
缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。
2.1.1-2 AtomicRefernce与AtomicIntegerFieldUpdater
AtomicIntegerFieldUpdater这个是为了解决对象的属性,而AtomicInteger只能解决基本类型
2.1.2锁:
synchronized:依赖JVM
Lock:依赖特殊的CPU指令,代码实现,ReentrantLock、
可以修饰代码块、修饰方法 (都是作用于调用的对象)、 修饰静态方法、修饰类(作用于所有对象)
要注意一个问题,父类的方法中加了synchronized修饰,子类不会继承这个修饰,要实现这个效果只能显示的定义一下。
总结:
synchronized:不可中断的同步锁,适合竞争不激烈,可读性好。
lock:可中断锁,多样化同步,竞争激烈时能维持常态
Atomic:竞争激烈时能维持常态,比lock性能好,但是只能同步一个值。
2.2可见性
首先要清楚导致共享变量在线程之间不可见的原因:
(1)线程交叉执行;
(2)重排序结合线程交叉执行;
(3)共享变量更新后的值没有在工作内存与主存之间及时更新;
2.2.1可见性-synchronized
JMM关于synchronized的两条规定:
线程解锁前,必须把共享变量的最新值刷新到主内存。
线程加锁时,将清空工作内存中共享变量的值,从而使共享变量时需要从主内存中重新读取最新的值(需要注意的一点是,加锁与解锁是同一把锁)
2.2.2可见性-volatile
通过加入内存屏障和禁止重排序优化来实现
内存屏障:
对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存。
对volatile变量读操作时,会在读操作之前加入一条load屏障指令,从主内存中读取共享变量。
volatile写 volatile读volatile保证了线程之间的可见性,但是不能保证线程之间的正确性(不具备原子性)
主要原因是在用volatile关键字修饰的变量上,做++操作的时候,其实是执行了三部,1获取主内存中的Count,2是+1 3是返回给主内存中,但是由于会出现,两个线程同时读到了主内存的值都是一样的,加一之后的值也是一样的,这样就是少加了一次,导致跟预期的值不同。
主要适用于 不同线程之间状态标志的一个变量
volatile boolean inited = false;
//线程1
context = loadContext();
inited = true;
//线程2
while(!inited){
sleep();
}
doSomethingWithConfig(context);
2.3有序性
java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile synchronized lock 都可以保证线程的有序性。
2.3.1happens-before原则
如果两个操作的秩序无法从happens-before原则推倒出来,jvm就可以随意的指令重排。
happens-before-1 happens-before-2 happens-before-3 happens-before-4
网友评论