一、概述
锁的思维导图:
二、synchronized
- synchronized 是可重入锁,Lock的实现类都是可重入锁;
- synchronized 是不可中断锁,而Lock的实现类都是可中断锁;
- synchronized 是一种非公平锁,Lock有FairSync(公平锁)和NonFairSync(非公平锁);
- synchronized 属于互斥锁,任何时候只允许一个线程的读写操作,其他线程必须等待;而Lock中的ReadWriteLock允许多个线程获得读锁。
使用:
// 可锁方法,也可锁类
private static synchronized void addCount() {
count++;
}
三、CAS(比较和交换)
CAS, 英文直译为 compare and swap,即比较和交换。乐观锁其实就是一种比较与交换的过程。
简单描述一下就是:读取到一个值为 A ,在要将这个值更新为B 之前,检查是否等于 A (比较),如果是则将 A 更新为 B(交换) ,否则什么都不做。
通过这种方式,可以实现不必使用加锁的方式,就能保证资源在多线程之间的同步,显然,不阻塞线程,可以大大提高吞吐量。方式虽好,但是也存在问题。
- ABA 问题,即如果一个值从 A 变为 B 再变回 A 时,这样 CAS 就会认为值没有发生变化。对于这个问题,已经有了使用版本号的解决方式,即每次变量更新的时候变量的版本号都 +1,即由 A->B->A 就变成了 1A->2B->3A 。
- 循环时间长开销大,如果锁的竞争比较激烈,就会导致 CAS 不断的重复执行,一直循环,耗费 CPU 资源。
- 只能保证一个变量的同步,显然,由于其特性,CAS 只能保证一个共享变量的原子操作。
JDK 中对 CAS 的实现在 java.util.concurrent.atomic 包中:
可以用原子方式更新其值。
四、AQS
AQS,全名 AbstractQueuedSynchronizer,直译为抽象队列同步器,是构建锁或者其他同步组件的基础框架,可以解决大部分同步问题。实现原理可以简单理解为:同步状态( state ) + FIFO 线程等待队列 。
- 资源 state
AQS使用了一个 int 类型的成员变量 state 来表示同步状态,使用了 volatile 关键字来保证线程间的可见性,当 state > 0 时表示已经获取了锁,当 state = 0 时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,确保对state的操作是安全的。
❀ 而对于不同的锁,state 也有不同的值:
- 独享锁中 state =0 代表释放了锁,state = 1 代表获取了锁。
- 共享锁中 state 即持有锁的数量。
- 可重入锁 state 即代表重入的次数。
- 读写锁比较特殊,因 state 是 int 类型的变量,为 32 位,所以采取了中间切割的方式,高 16 位标识读锁的数量 ,低 16 位标识写锁的数量 。
- FIFO 线程等待队列
实现队列的方式无外乎两种,一是使用数组,二是使用 Node 。AQS 使用了 Node 的方式实现队列。
FIFO 线程等待队列的结构如下图:
列举两个方法使用(还有更多其他方法):
// 独占模式获取锁,忽略中断。
acquire(int arg)
// 以独占模式释放对象。
release(int arg)
五、Lock
Java中的Lock:
- ReentrantLock:可重入锁
- ReentrantReadWriteLock :读写锁,允许多个线程获得读锁,但只允许一个线程获得写锁,效率相对较高。(读读✔,读写×,写读×,写写×)
- StampedLock:对ReentrantReadWriteLock锁的增强方法,解决ReentrantReadWriteLock在读写分离时的线程饥饿问题。当ReentrantReadWriteLock对其写锁想要获取的话,就必须没有任何其他读写锁存在才可以,这实现了悲观读取。如果读操作很多,写很少的情况下,线程有可能就会遭遇饥饿问题;(乐观读时,写✔)
StampedLock的三种模式
1、写入(Writing):writeLock是一个独占锁,也是一个悲观锁。
2、读取(Reading):readLock这时候是一个悲观锁。
3、乐观读取(Optimistic Reading):提供了tryOptimisticRead方法返回一个非0的stamp,只有当前同步状态没有被写模式所占有是才能获取到。他是在获取stamp值后对数据进行读取操作,最后验证该stamp值是否发生变化,如果发生变化则读取无效,代表有数据写入。这种方式能够降低竞争和提高吞吐量。
StampedLock使用示例:
public static void read() {
//获取乐观锁,并返回stamp值,该方法不会使writeLock阻塞
long stamp = lock.tryOptimisticRead();
//读取数据
System.out.println("normal R-> " + stamp + "==" + list.size());
//判断stamp值是否发生变化
if (!lock.validate(stamp)) {
//内容被修改,重新获取
try {
stamp = lock.readLock();
System.out.println("data change R-> " + stamp + "==" + list.size());
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlockRead(stamp);
}
}
}
锁的实现原理可看:https://segmentfault.com/a/1190000023735772