Java并发-常见关键字

本文最后更新于:2 年前

synchronized

见《线程安全的机制》

CAS

CAS 是英文单词 CompareAndSwap 的缩写,中文意思是:比较并替换。CAS 需要有 3 个操作数:内存地址 V,旧的预期值 A,即将要更新的目标值 B。

CAS 指令执行时,当且仅当内存地址 V 的值与预期值 A 相等时,将内存地址 V 的值修改为 B,否则就什么都不做。整个比较并替换的操作是一个原子操作。

CAS 虽然很高效的解决了原子操作问题,但是 CAS 仍然存在三大问题。

循环时间长开销很大。
只能保证一个共享变量的原子操作。
ABA 问题。

如果 CAS 失败,会一直进行尝试。如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销。

当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

如果值曾经被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。这个漏洞称为 CAS 操作的“ABA”问题。

Java 并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证 CAS 的正确性。因此,在使用 CAS 前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。

AQS-抽象队列同步器

image-20210803192200462

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。

image-20210805140155989

AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

节点状态

nt waitStatus:

1、CANCELLED,值为 1 。场景:当该线程等待超时或者被中断,需要从同步队列中取消等待,则该线程被置 1,即被取消(这里该线程在取消之前是等待状态)。节点进入了取消状态则不再变化;

2、SIGNAL,值为-1。场景:后继的节点处于等待状态,当前节点的线程如果释放了同步状态或者被取消(当前节点状态置为-1),将会通知后继节点,使后继节点的线程得以运行;

3、CONDITION,值为-2。场景:节点处于等待队列中,节点线程等待在 Condition 上,当其他线程对 Condition 调用了 signal()方法后,该节点从等待队列中转移到同步队列中,加入到对同步状态的获取中;

4、PROPAGATE,值为-3。场景:表示下一次的共享状态会被无条件的传播下去

5、INITIAL,值为 0,初始状态。

AQS 对资源的共享方式

AQS 定义两种资源共享方式

Exclusive(独占)

只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁,下面以 ReentrantLock 对这两种锁的定义做介绍:

公平锁 :按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁 :当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。

ReentrantLock 默认采用非公平锁因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁)。

非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

Share(共享)

多个线程可同时执行,如 Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 。

ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在上层已经帮我们实现好了。

Java 实现锁有两种语法,一种是 synchronized 语句,另外一种是 reentrantlock 关键字

公平锁/非公平锁

公平锁指多个线程按照申请锁的顺序获得锁。

非公平锁指多个线程获得锁的顺序不按照申请顺序。

Java reentranthlock 通过构造函数来指定锁是公平还是非公平,默认是非公平锁,对于 synchronized 而言,也是一种非公平锁。

非公平锁优点在于吞吐量比公平锁大。

可重入锁

可重入锁又叫递归锁,是指同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

Synchronized 也是一个可重入锁。

可重入锁的优点是可以一定程度避免死锁。

独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有,共享锁可以被多个线程所持有。

Java reentrantlock 是一个独享锁,但是对于 lock 的另一个实现 readwritelock,其读锁是一个共享锁,写锁是一个独享锁。

对于 synchronized 是一个独享锁。

互斥锁/读写锁

互斥锁在 Java 中具体实现就是 reentrantlock。

读写锁在 Java 中的具体实现就是 readwritelock。

乐观锁/悲观锁

乐观锁和悲观锁不是指具体的锁类型,而是对于看待并发编程中加锁问题的角度。

悲观锁认为,对于一个数据的并发操作,一定会改变数据,即使实际上数据没被改变,但是也悲观的认为被改变的可能性比较大,一定要加锁,不加锁早晚要出问题。

乐观锁认为,对于一个数据的并发操作,是不会改变数据的,不加锁也不会出问题。

乐观锁指 java 中的无所编程,适合读操作非常多的场景。

悲观锁就是指 java 中,适合并发下写非常多的场景。

自旋锁

在 java 中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当循环条件被其他线程改变时,才能进入临界区。这样的好处是减少线程上下文切换的消耗,缺点是会消耗 CPU。

由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用 CPU 时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。

偏向锁/轻量级锁/重量级锁

这三种锁,就是指锁的状态,针对 synchronized。

偏向锁是指一段代码一直被一个线程所访问,那么理论上,这个线程会自动获取这个锁,并一直拥有这个锁,这样就降低了获取锁的代价。

轻量级锁是指当偏向锁的状态下,被另一个线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋形式尝试获取锁,不会阻塞,提高效率。

重量级锁是指在轻量级锁的状态下,另一个线程虽然自旋,但不会一直持续下去,当自旋一定次数的时候还没有获取到锁的话,就会进入阻塞,该锁就会膨胀为重量级锁,重量级锁会让其他申请的线程陷入阻塞,降低性能。