在分析LinkedBlockingQueue源码的时候,里面用到了ReentrantLock,所以本篇我们分析下ReentrantLock的原理。
一、Lock方法介绍
Lock是一个接口,定义了如下接口,总体上,获取锁的方式分为阻塞、非阻塞两种。
- 阻塞:
- void lock() // 如果锁可用就获得锁,如果锁不可用就阻塞直到锁释放
- void unlock() // 释放锁
- void lockInterruptibly() // 和 lock()方法相似, 但阻塞的线程可中断,抛出java.lang.InterruptedException异常
- 非阻塞:
- boolean tryLock() // 非阻塞获取锁;尝试获取锁,如果成功返回true
- boolean tryLock(long timeout, TimeUnit timeUnit) //带有超时时间的获取锁方法
二、实现Lock的类介绍(ReentrantLock、ReentrantReadWriteLock)
常见的实现Lock接口的类有:ReentrantLock(重入锁) 、ReentrantReadWriteLock(重入读写锁)
- ReentrantLock:它是唯一一个实现了Lock接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数。
- ReentrantReadWriteLock:重入读写锁,它实现了ReadWriteLock接口,在这个类中维护了两个锁,一个是ReadLock,一个是WriteLock,他们都分别实现了Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是:读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。
1. ReentrantLock使用举例
下面是LinkedBlockingQueue构造函数的源码,这里把与ReentrantLock使用无关的部分都注释掉,只需要关注ReentrantLock的使用。这里采用阻塞模式,获取锁、释放锁。
public LinkedBlockingQueue(Collection<? extends E> c) {
//this(Integer.MAX_VALUE);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
/*int n = 0;
for (E e : c) {
if (e == null)
throw new NullPointerException();
if (n == capacity)
throw new IllegalStateException("Queue full");
enqueue(new Node<E>(e));
++n;
}
count.set(n);*/
} finally {
putLock.unlock();
}
}
2. AbstractQueuedSynchronizer原理分析
AbstractQueuedSynchronizer提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件
a). AbstractQueuedSynchronizer实现独占锁和共享锁
- 独占锁,每次只能有一个线程持有锁,比如前面给大家演示的ReentrantLock就是以独占方式实现的互斥锁
- 共享锁,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock
b). 内部维护的是一个FIFO的双向链表
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev; //前驱节点
volatile Node next; //后继节点
volatile Thread thread;//当前线程
Node nextWaiter;
//将线程构造成一个Node,添加到等待队列
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//这个方法会在Condition队列使用,后续单独写一篇文章分析condition
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
3. ReentrantLock.lock()方法源码
获取锁的入口调用了sync的lock方法
public void lock() {
sync.lock();
}
a). sync
sync是一个静态内部类,它继承了AQS这个抽象类,AQS是一个同步工具,主要用来实现同步控制。
abstract static class Sync extends AbstractQueuedSynchronizer
Sync有两种具体的实现,NofairSync(非公平锁)、FailSync(公平锁)
- 公平锁:表示所有线程严格按照FIFO来获取锁
- 非公平锁:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁
b). NonfairSync.lock
NonfairSync.lock的实现逻辑:
final void lock() {
if (compareAndSetState(0, 1)) //通过cas操作来修改state状态,表示争抢锁的操作
setExclusiveOwnerThread(Thread.currentThread());//设置当前获得锁状态的线程
else
acquire(1); //尝试去获取锁
}
c). compareAndSetState
compareAndSetState的逻辑是通过cas乐观锁的方式来做比较并替换。其中,
- 当state=0时,表示无锁状态
- 当state>0时,表示已经有线程获得了锁,也就是state=1,但是因为ReentrantLock允许重入,所以同一个线程多次获得同步锁的时候,state会递增,比如重入5次,那么state=5。 而在释放锁的时候,同样需要释放5次直到state=0其他线程才有资格获得锁
d). NonfairSync.acquire
acquire方法的源码如下,其实现的逻辑为:
- 通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false
- 如果tryAcquire失败,则会通过addWaiter方法将当前线程封装成Node添加到AQS队列尾部
- acquireQueued,将Node作为参数,通过自旋去尝试获取锁
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
e). NonfairSync.tryAcquire
这个方法的作用是尝试获取锁,如果成功返回true,不成功返回false,逻辑是:
- 获取当前线程,判断当前的锁的状态
- 如果state=0表示当前是无锁状态,通过cas更新state状态的值
- 如果当前线程是属于重入,则增加重入次数
final boolean nonfairTryAcquire(int acquires) {
//获得当前执行的线程
final Thread current = Thread.currentThread();
int c = getState(); //获得state的值
if (c == 0) { //state=0说明当前是无锁状态
//通过cas操作来替换state的值改为1
if (compareAndSetState(0, acquires)) {
//保存当前获得锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
//如果是同一个线程来获得锁,则直接增加重入次数
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; //增加重入次数
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
f). addWaiter
当tryAcquire方法获取锁失败以后,则会先调用addWaiter将当前线程封装成Node,然后添加到AQS队列
- 将当前线程封装成Node
- 判断当前链表中的tail节点是否为空,如果不为空,则通过cas操作把当前线程的node添加到AQS队列
- 如果为空或者cas失败,调用enq将节点添加到AQS队列
private Node addWaiter(Node mode) { //mode=Node.EXCLUSIVE
//将当前线程封装成Node,并且mode为独占锁
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// tail是AQS的中表示同步队列队尾的属性,刚开始为null,所以进行enq(node)方法
Node pred = tail;
if (pred != null) { //tail不为空的情况,说明队列中存在节点数据
node.prev = pred; //讲当前线程的Node的prev节点指向tail
if (compareAndSetTail(pred, node)) {//通过cas讲node添加到AQS队列
pred.next = node;//cas成功,把旧的tail的next指针指向新的tail
return node;
}
}
enq(node); //tail=null,将node添加到同步队列中
return node;
}
4. ReentrantLock.Condition
Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)
class BoundedBuffer {
final Lock lock = new ReentrantLock();//锁对象
final Condition notFull = lock.newCondition();//写线程条件
final Condition notEmpty = lock.newCondition();//读线程条件
final Object[] items = new Object[100];//缓存队列
int putptr/*写索引*/, takeptr/*读索引*/, count/*队列中存在的数据个数*/;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)//如果队列满了
notFull.await();//阻塞写线程
items[putptr] = x;//赋值
if (++putptr == items.length) putptr = 0;//如果写索引写到队列的最后一个位置了,那么置为0
++count;//个数++
notEmpty.signal();//唤醒读线程
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)//如果队列为空
notEmpty.await();//阻塞读线程
Object x = items[takeptr];//取值
if (++takeptr == items.length) takeptr = 0;//如果读索引读到队列的最后一个位置了,那么置为0
--count;//个数--
notFull.signal();//唤醒写线程
return x;
} finally {
lock.unlock();
}
}
}