0%

线程安全性

线程安全性

概念

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

特性

线程安全性主要体现在三个方面:

  • 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作
  • 可见性:一个线程对主内存的修改可以及时的被其他线程观察到
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

原子性

JAVA中的原子性,一般涉及两个机制:

  • JDK中已经提供好的Atomic包,他们均使用了CAS完成线程的原子性操作
  • 使用锁的机制来处理线程之间的原子性。锁包括:synchronized、Lock

Atomic包

image-20210504002340565

AtomicInteger与CAS

我们从最简单的AtomicInteger类来了解什么是CAS

1
2
3
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
1
2
3
4
5
6
7
8
9

public final int getAndAddInt(Object var1, long var2, int v ar4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}
1
2
3
4
5
6
7
8
9
10
/**
* 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
*
* @param obj 需要更新的对象
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值
* @return 如果field的值被更改返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);

getIntVolatile方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。

compareAndSwap是一个本地方法,在AtomicInteger类中,它的参数具体含义是:

  • var1 AtomicInteger对象本身。
  • var2 该对象值的引用地址。
  • var4 需要变动的数量
  • var5 是用过var1var2 找出的主内存中真实的值

用该对象当前的值与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屏障指令,将本地内存中的共享变量值刷新到主内存

    image-20210504003303719

  • 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量

    image-20210504003330856

  • volatile的屏障操作都是cpu级别的

  • volatile关键字不具有原子性,不适合累加值。volatile适合状态验证,在修饰状态标记量时,要保证对:

    • 变量的写操作不依赖于当前值
    • 该变量没有包含在具有其他变量的式子中

有序性

Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

java提供了 volatile、synchronized、Lock可以用来保证有序性。另外,java内存模型具备一些先天的有序性,即不需要任何手段就能得到保证的有序性。通常被我们成为happens-before原则(先行发生原则)。如果两个线程的执行顺序无法从happens-before原则推导出来,那么就不能保证它们的有序性,虚拟机就可以对它们进行重排序。

并发之atomicInteger与CAS机制 - 枫飘雪落 - 博客园 (cnblogs.com)

深入理解CAS(乐观锁) - 简书 (jianshu.com)

深入剖析LongAdder是咋干活的 (juejin.cn)

从 LongAdder 中窥见并发组件的设计思路 | 犀利豆的博客 (xilidou.com)