1、CAS
1.1 基本概念
CAS 是 compare and swap 的简写,即比较并交换。它是指一种操作机制,而不是某个具体的类或方法。在 Java 平台上对这种操作进行了包装。在 Unsafe 类中,调用代码如下
这里无法用Unsafe类看,我使用的是AtomicInteger查看的源码
这里是三个分别是内存位置 V,旧的预期值 A 和新的值 B。操作时,先从内存位置读取到值,然后和预期值A比较。如果相等,则将此内存位置的值改为新值 B,返回 true。如果不相等,说明和其他线程冲突了,则不做任何改变,返回 false。
这种机制在不阻塞其他线程的情况下避免了并发冲突,比独占锁的性能高很多。 CAS 在 Java 的原子类和并发包中有大量使用。
1.2底层实现
CAS 主要分三步,读取-比较-修改。其中比较是在检测是否有冲突,如果检测到没有冲突后,其他线程还能修改这个值,那么 CAS 还是无法保证正确性。所以最关键的是要保证比较-修改这两步操作的原子性。
CAS 底层是靠调用 CPU 指令集的 cmpxchg 完成的,它是 x86 和 Intel 架构中的 compare and exchange 指令。在多核的情况下,这个指令也不能保证原子性,需要在前面加上 lock 指令。lock 指令可以保证一个 CPU 核心在操作期间独占一片内存区域。那么 这又是如何实现的呢?
在处理器中,一般有两种方式来实现上述效果:
总线锁和缓存锁。
在多核处理器的结构中,CPU 核心并不能直接访问内存,而是统一通过一条总线访问。总线锁就是锁住这条总线,使其他核心无法访问内存。这种方式代价太大了,会导致其他核心停止工作。
而缓存锁并不锁定总线,只是锁定某部分内存区域。当一个 CPU 核心将内存区域的数据读取到自己的缓存区后,它会锁定缓存对应的内存区域。锁住期间,其他核心无法操作这块内存区域。
CAS 就是通过这种方式实现比较和交换操作的原子性的。值得注意的是, CAS 只是保证了操作的原子性,并不保证变量的可见性,因此变量需要加上 volatile 关键字。
1.3 ABA问题
上面提到,CAS 保证了比较和交换的原子性。但是从读取到开始比较这段期间,其他核心仍然是可以修改这个值的。如果核心将 A 修改为 B,CAS 可以判断出来。但是如果核心将 A 修改为 B 再修改回 A。那么 CAS 会认为这个值并没有被改变,从而继续操作。这是和实际情况不符的。解决方案是加一个版本号。
2.AQS
2.1基本概念
全称 AbstractQueuedSynchronizer。AQS 中有两个重要的成员:
- 成员变量 state。用于表示锁现在的状态,用 volatile 修饰,保证内存一致性。同时所用对 state 的操作都是使用 CAS 进行的。state 为0表示没有任何线程持有这个锁,线程持有该锁后将 state 加1,释放时减1。多次持有释放则多次加减。
- 还有一个双向链表,链表除了头结点外,每一个节点都记录了线程的信息,代表一个等待线程。这是一个 FIFO 的链表
3. ReentrantLock
3.1 基本概念
Lock
和 java.io.Serializable
接口,并提供了与synchronized
相同的互斥性和内存可见性
public class ReentrantLock implements Lock, java.io.Serializable {
.....
}
他是非公平(默认)也可以是非公平的,是支持重入的锁
3.2 底层实现
ReentrantLock与AQS的关系非常密切
abstract static class Sync extends AbstractQueuedSynchronizer {
。。。。
}
ReentrantLock的使用则是这样子
Lock lock = new ReentrantLock();
lock.lock();
try{
//更新对象状态
//捕获异常,并在必须时恢复不变性条件
}catch (Exception e){
e.printStackTrace();
} finally {
lock.unlock();
}
因为他继承了Lock的接口i,我们也要讲下Lock的方法
方法名称 | 方法描述 |
---|---|
lock | 用来获取锁,如果锁已被其他线程获取,则进行等待 |
tryLock | 表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待 |
tryLock(long time, TimeUnit unit) | 和tryLock()类似,区别在于它在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true |
lockInterruptibly | 获取锁,如果获取锁失败则进行等到,如果等待的线程被中断会相应中断信息 |
unlock | 释放锁的操作 |
newCondition | 获取Condition对象,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件wait()方法,而调用后,当前线程释放锁 |
ReentrantLock 也实现了上面接口的内容,同时 ReentrantLock 提供了 公平锁
和 非公平锁
两种模式,如果没有特别的去指定使用何种方式,那么 ReentrantLock 会默认为 非公平锁
,首先我们来看一下 ReentrantLock 的构造函数:
/**
* 无参的构造函数
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 有参构造函数
* 参数为布尔类型
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
我们会发现,在默认无参构造的是非公平锁,即对应了我上文所说的默认是非公平的锁。
我们这里就要说下非公平锁是怎么个东西了。
3.2.1 非公平锁NonfairSync.lock()
3.2.1.1 lock方法详解
当我们调用 ReentrantLock 的 lock() 方法的时候,实际上是调用了 NonfairSync 的 lock() 方法,代码如下:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
//这个方法先用CAS操作,去尝试抢占该锁
// 快速尝试将state从0设置成1,如果state=0代表当前没有任何一个线程获得了锁
if (compareAndSetState(0, 1))
//state设置成1代表获得锁成功
//如果成功,就把当前线程设置在这个锁上,表示抢占成功,在重入锁的时候需要
setExclusiveOwnerThread(Thread.currentThread());
else
//如果失败,则调用 AbstractQueuedSynchronizer.acquire() 模板方法,等待抢占。
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
调用 acquire(1) 实际上使用的是 AbstractQueuedSynchronizer 的 acquire() 方法,它是一套锁抢占的模板,acquire() 代码比较简单:
public final void acquire(int arg) {
//先去尝试获取锁,如果没有获取成功,就在CLH队列中增加一个当前线程的节点,表示等待抢占。
//然后进入CLH队列的抢占模式,进入的时候也会去执行一次获取锁的操作,如果还是获取不到,
//就调用LockSupport.park() 将当前线程挂起。那么当前线程什么时候会被唤醒呢?当
//持有锁的那个线程调用 unlock() 的时候,会将CLH队列的头节点的下一个节点上的线程
//唤醒,调用的是 LockSupport.unpark() 方法。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire() 会先调用 tryAcquire() 这个钩子方法去尝试获取锁,这个方法就是在 NonfairSync.tryAcquire()下的 **nonfairTryAcquire()**,源码如下:
//一个尝试插队的过程
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取state值
int c = getState();
//比较锁的状态是否为 0,如果是0,当前没有任何一个线程获取锁
if (c == 0) {
//则尝试去原子抢占这个锁(设置状态为1,然后把当前线程设置成独占线程)
if (compareAndSetState(0, acquires)) {
// 设置成功标识独占锁
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前锁的状态不是0 state!=0,就去比较当前线程和占用锁的线程是不是一个线程
else if (current == getExclusiveOwnerThread()) {
//如果是,增加状态变量的值,从这里看出可重入锁之所以可重入,就是同一个线程可以反复使用它占用的锁
int nextc = c + acquires;
//重入次数太多,大过Integer.MAX
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果以上两种情况都不通过,则返回失败false
return false;
}
tryAcquire() 一旦返回 false,就会则进入 acquireQueued() 流程,也就是基于CLH队列的抢占模式,在CLH锁队列尾部增加一个等待节点,这个节点保存了当前线程,通过调用 addWaiter() 实现,这里需要考虑初始化的情况,在第一个等待节点进入的时候,需要初始化一个头节点然后把当前节点加入到尾部,后续则直接在尾部加入节点。
//AbstractQueuedSynchronizer.addWaiter()
private Node addWaiter(Node mode) {
// 初始化一个节点,用于保存当前线程
Node node = new Node(Thread.currentThread(), mode);
// 当CLH队列不为空的视乎,直接在队列尾部插入一个节点
Node pred = tail;
if (pred != null) {
node.prev = pred;
//如果pred还是尾部(即没有被其他线程更新),则将尾部更新为node节点(即当前线程快速设置成了队尾)
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 当CLH队列为空的时候,调用enq方法初始化队列
enq(node);
return node;
}
private Node enq(final Node node) {
//在一个循环里不停的尝试将node节点插入到队尾里
for (;;) {
Node t = tail;
if (t == null) { // 初始化节点,头尾都指向一个空节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
将节点增加到CLH队列后,进入 acquireQueued() 方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//在一个循环里不断等待前驱节点执行完毕
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {// 通过tryAcquire获得锁,如果获取到锁,说明头节点已经释放了锁
setHead(node);//将当前节点设置成头节点
p.next = null; // help GC//将上一个节点的next变量被设置为null,在下次GC的时候会清理掉
failed = false;//将failed标记设置成false
return interrupted;
}
//中断
if (shouldParkAfterFailedAcquire(p, node) && // 是否需要阻塞
parkAndCheckInterrupt())// 阻塞,返回线程是否被中断
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
如果尝试获取锁失败,就会进入 shouldParkAfterFailedAcquire() 方法,会判断当前线程是否阻塞
/**
* 确保当前结点的前驱结点的状态为SIGNAL
* SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程
* 只有确保能够被唤醒,当前线程才能放心的阻塞。
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
//如果前驱节点状态为SIGNAL
//表明当前线程需要阻塞,因为前置节点承诺执行完之后会通知唤醒当前节点
return true;
if (ws > 0) {//ws > 0 代表前驱节点取消了
do {
node.prev = pred = pred.prev;//不断的把前驱取消了的节点移除队列
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//初始化状态,将前驱节点的状态设置成SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
进入阻塞阶段,会进入 parkAndCheckInterrupt() 方法,则会调用 LockSupport.park(this) 将当前线程挂起。代码如下:
// 从方法名可以看出这个方法做了两件事
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//挂起当前的线程
// 如果当前线程已经被中断了,返回true,否则返回false
// 有可能在挂起阶段被中断了
return Thread.interrupted();
}
3.2.1.2 NonfairSync.unlock()
- 调用 unlock() 方法,其实是直接调用 AbstractQueuedSynchronizer.release() 操作。
- 进入 release() 方法,内部先尝试 tryRelease() 操作,主要是去除锁的独占线程,然后将状态减一,这里减一主要是考虑到可重入锁可能自身会多次占用锁,只有当状态变成0,才表示完全释放了锁。
- 如果 tryRelease 成功,则将CHL队列的头节点的状态设置为0,然后唤醒下一个非取消的节点线程。
- 一旦下一个节点的线程被唤醒,被唤醒的线程就会进入 acquireQueued() 代码流程中,去获取锁。
-
public void unlock() { sync.release(1); } public final boolean release(int arg) { //尝试在当前锁的锁定计数(state)值上减1, if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0)//waitStatus!=0表明或者处于CANCEL状态,或者是SIGNAL表示下一个线程在等待其唤醒。也就是说waitStatus不为零表示它的后继在等待唤醒。 unparkSuccessor(h); //成功返回true return true; } //否则返回false return false; } private void unparkSuccessor(Node node) { int ws = node.waitStatus; //如果waitStatus < 0 则将当前节点清零 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); //若后续节点为空或已被cancel,则从尾部开始找到队列中第一个waitStatus<=0,即未被cancel的节点 Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }
当然在 release() 方法中不仅仅只是将 state - 1 这么简单,**-1** 之后还需要进行一番处理,如果 -1 之后的 新state = 0 ,则表示当前锁已经被线程释放了,同时会唤醒线程等待队列中的下一个线程。
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
//判断是否为当前线程在调用,不是抛出IllegalMonitorStateException异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//c == 0,释放该锁,同时将当前所持有线程设置为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//设置state
setState(c);
return free;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从后往前找到离head最近,而且waitStatus <= 0 的节点
// 其实在ReentrantLock中,waitStatus应该只能为0和-1,需要唤醒的都是-1(Node.SIGNAL)
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);// 唤醒挂起线程
}
重点:unlock最好放在finally中,因为如果没有使用finally来释放Lock,那么相当于启动了一个定时炸弹,如果发生错误,我们很难追踪到最初发生错误的位置,因为没有记录应该释放锁的位置和时间,这也就是 ReentrantLock 不能完全替代 synchronized 的原因,因为当程序执行控制离开被保护的代码块时,不会自动清除锁。
3.2.2 公平锁
FairSync相对来说就简单很多,只有重写的两个方法跟NonfairSync不同
inal void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&// 没有前驱节点了
compareAndSetState(0, acquires)) {// 而且没有锁
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
3.2.2、公平锁和非公平锁的区别
- 锁的公平性是相对于获取锁的顺序而言的。
- 如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO,线程获取锁的顺序和调用lock的顺序一样,能够保证老的线程排队使用锁,新线程仍然排队使用锁。
- 非公平锁只要CAS设置同步状态成功,则表示当前线程获取了锁,线程获取锁的顺序和调用lock的顺序无关,全凭运气,也就是老的线程排队使用锁,但是无法保证新线程抢占已经在排队的线程的锁。
- ReentrantLock默认使用非公平锁是基于性能考虑,公平锁为了保证线程规规矩矩地排队,需要增加阻塞和唤醒的时间开销。如果直接插队获取非公平锁,跳过了对队列的处理,速度会更快。