线程安全性
概念
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
特性
线程安全性主要体现在三个方面:
- 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作
- 可见性:一个线程对主内存的修改可以及时的被其他线程观察到
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
原子性
JAVA中的原子性,一般涉及两个机制:
- JDK中已经提供好的Atomic包,他们均使用了CAS完成线程的原子性操作
- 使用锁的机制来处理线程之间的原子性。锁包括:synchronized、Lock
Atomic包
AtomicInteger与CAS
我们从最简单的AtomicInteger类来了解什么是CAS
1 | public final int incrementAndGet() { |
1 |
|
1 | /** |
getIntVolatile
方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。
compareAndSwap
是一个本地方法,在AtomicInteger类中,它的参数具体含义是:
var1
AtomicInteger对象本身。var2
该对象值的引用地址。var4
需要变动的数量var5
是用过var1
、var2
找出的主内存中真实的值
用该对象当前的值与var5
比较,如果相同,更新var5+var4
并且返回true;如果不相同继续取值然后在比较,直到更新完成。
简单来说,CAS (CompareAndSwap) 比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止。
CAS优缺点
优点:非阻塞的轻量级的乐观锁,通过CPU指令实现,在资源竞争不激烈的情况下性能高,相比synchronized重量锁,synchronized会进行比较复杂的加锁、解锁和唤醒操作。
缺点:
ABA
问题: 线程C、D;线程D将A修改为B后又修改为A,此时C线程以为A没有改变过。java
的原子类AtomicStampedReference
,通过控制变量值的版本号来保证CAS的正确性。具体解决思路就是在变量前追加上版本号,每次变量更新的时候把版本号加一,那么A - B - A
就会变成1A - 2B - 3A
。- 自旋时间过长,消耗CPU资源,如果资源竞争激烈,多线程自旋长时间消耗资源。
LongAdder
AtomicLong 的缺陷
AtomicLong 的 Add() 是依赖自旋不断的 CAS 去累加一个 Long 值。如果在竞争激烈的情况下,CAS 操作不断的失败,就会有大量的线程不断的自旋尝试 CAS 会造成 CPU 的极大的消耗。
LongAdder
LongAdder 功能类似 AtomicLong ,在低并发情况下二者表现差不多,在高并发情况下 LongAdder 的表现就会好很多。
LongAdder先尝试一次CAS更新,如果失败会转而通过Cell[]
的方式更新值,如果计算index的方式足够散列,那么在并发量大的情况下,多个线程定位到同一个cell的概率也就越低,这有点类似于分段锁的意思。
当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用LongAdder(例如网站访问人数计数)。 当需要保证线程安全,可允许一些性能损耗,要求高精度时,需要使用AtomicLong(例如自增id)。
其他
AtomicBoolean
常用于控制某一件事只让一个线程执行,并仅能执行一次。
AtomicIntegerFieldUpdater
这个类的核心作用是要更新一个指定的类的某一个字段的值。并且这个字段一定要用volatile修饰同时还不能是static的。
AtomicLongArray
这个类实际上维护了一个Array数组,我们在对数值进行更新的时候,会多一个索引值让我们更新。
synchronized
synchronized是java中的一个关键字,是一种 同步锁。在synchronized关键字作用范围内,同一时刻只能有一个线程对其进行操作的。
它可以修饰的对象主要有四种:
- 修饰代码块:大括号括起来的代码,作用于调用的对象
- 修饰方法:整个方法,作用于调用的对象
- 修饰静态方法:整个静态方法,作用于所有对象
- 修饰类:括号括起来的部分,作用于所有对象
原子性操作各方法间的对比
- synchronized:不可中断锁,适合竞争不激烈,可读性好
- Lock:可中断锁,多样化同步,竞争激烈时能维持常态
- Atomic:竞争激烈时能维持常态,比Lock性能好,每次只能同步一个值
可见性
导致可见性失效的原因:
- 线程交叉执行
- 重排序结合线程交叉执行
- 共享变量更新后的值没有在工作内存与主存间及时更新
JVM对于可见性的实现,提供了synchronized和volatile。
synchronized
JMM关于synchronized的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁是同一把锁)
volatile
volatile通过加入内存屏障和禁止重排序优化来实现
对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存
对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量
volatile的屏障操作都是cpu级别的
volatile关键字不具有原子性,不适合累加值。volatile适合状态验证,在修饰状态标记量时,要保证对:
- 变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的式子中
有序性
Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
java提供了 volatile、synchronized、Lock可以用来保证有序性。另外,java内存模型具备一些先天的有序性,即不需要任何手段就能得到保证的有序性。通常被我们成为happens-before原则(先行发生原则)。如果两个线程的执行顺序无法从happens-before原则推导出来,那么就不能保证它们的有序性,虚拟机就可以对它们进行重排序。
并发之atomicInteger与CAS机制 - 枫飘雪落 - 博客园 (cnblogs.com)
深入理解CAS(乐观锁) - 简书 (jianshu.com)