文章目录
- 1 基本介绍
- 1.1 为什么要使用ReentrantLock
- 1.2 ReentrantLock的常用方法
- 2 源码梳理
- 2.1 构造方法
- 2.2 加锁
- 2.3 释放锁
- 参考
1 基本介绍
ReentrantLock是可重入独占锁,同时只能由一个线程锁持有,如果其他线程想要获取锁,就会被阻塞并放入该锁的AQS同步队列中。
1.1 为什么要使用ReentrantLock
在多线程编程中,如果要多资源进行加锁,最常用的方法是使用synchronized关键字,然而synchronized关键字缺有诸多不足之处,而ReentrantLock相对而言可以解决这些不足,如:
- synchronized无法知道线程有没有成功获取到锁,而ReentrantLock可以。
- synchronized的 wait方法只能有一个条件变量,而ReentrantLock可以配合Condition的await方法实现多个条件变量,更为灵活。
1.2 ReentrantLock的常用方法
下面就是介绍一下ReentrantLock的常用方法:
方法名 | 作用 |
---|---|
lock() | 获取锁,成功后变返回,如果失败了,就会被阻塞 |
lockInterruptibly() | 与lock()方法作用一样,不同的是该方法可以对响应,如果其他线程调用当前线程的interrupt() 方法,则当前线程会抛出InterruptedException异常并返回 |
tryLock() | 在不阻塞当前线程的情况下,尝试获取锁,成功则返回true,失败则返回false |
tryLock(long timeout, TimeUnit unit) | 如果在指定的时间内未能成功获取锁,则返回false,成功则返回true |
unlock() | 状态值减去1,如果减1后状态值为0则释放锁 |
newCondition() | 获取一个条件变量 |
2 源码梳理
这里还是一样的用的 Jdk11,和网上一些以 Jdk8的资料有一些差异,但是差异不大。
2.1 构造方法
public ReentrantLock() {sync = new NonfairSync();}
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}
由构造方法可知,ReentrantLock默认为非公平锁,因为这样获取锁时省略了一些检查操作,速度快一些。如果想要使用公平锁,可以在创建ReentrantLock时传入true即可。
2.2 加锁
方法入口如下:
public void lock() {sync.acquire(1);
}
sync是Sync对象,Sync继承了AbstractQueuedSynchronizer,所以sync.acquire(1)实际的调用了AbstractQueuedSynchronizer的acquire方法,下面进入acquire方法。
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
acquire方法中做了2个判断,第一个判断是尝试获取锁,如果失败了则进入第二个判断,将当前线程封装为类型为Node.EXCLUSIVE的Node节点,插入AQS同步队列的尾部,并不停的循环获取锁。
另外,AQS并不实现tryAcquire方法,而是由子类实现,所以我们进入FairSync的tryAcquire方法:
protected final boolean tryAcquire(int acquires) {// 具体实现方法是 nonfairTryAcquire。return nonfairTryAcquire(acquires);
}// 以非公平的方式获取锁
@ReservedStackAccess
final boolean nonfairTryAcquire(int acquires) {// 获取当前线程final Thread current = Thread.currentThread();// 获取 AQS状态值int c = getState();// 如果 AQS状态值为 0,则说明当前锁未被占用,则可以获取锁。if (c == 0) {// 这里主要是执行的 AQS的操作,以 CAS的方式将 AQS状态值设置为 1。// 非公平锁和公平锁的差异主要是体现在这里。if (compareAndSetState(0, acquires)) {// 如果成功将 AQS状态值,则将 AQS中锁的持有者设置为当前线程。然后返回。setExclusiveOwnerThread(current);return true;}}// 如果锁已经被占用了,那么就先判断当前线程是否是该锁的持有者。else if (current == getExclusiveOwnerThread()) {// 如果是,则将 AQS的状态值加 1int nextc = c + acquires;// int类型是有上限的,这里主要是防止可重入次数溢出。if (nextc < 0)throw new Error("Maximum lock count exceeded");// 重新设置 AQS状态值并返回。setState(nextc);return true;}return false;
}
以上是非公平锁的实现方式,另外试想一下,如果当前锁被线程A所持有,AQS同步队列中有一个线程B正在等待锁,正常的情况是A释放锁,然后B通过CAS的方式正常获取锁,但是如果B获取锁的时,恰好有一个线程C也通过CAS的方式获取锁,并且成功获取锁了,那么B将又被方法 AQS同步队列,继续挂起。这样对于B而y言是不公平的,因为它等待的时间很久,反而被C“插队”了。
接下来我们看一下公平锁是如何实现的:
// 以公平的方式获取锁
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {Thread current = Thread.currentThread();int c = this.getState();if (c == 0) {// 差异主要在这里,多了一个 hasQueuedPredecessors 判断。if (!this.hasQueuedPredecessors() && this.compareAndSetState(0, acquires)) {this.setExclusiveOwnerThread(current);return true;}} else if (current == this.getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0) {throw new Error("Maximum lock count exceeded");}this.setState(nextc);return true;}return false;
}
由此我们可以看出,公平锁与非公平锁的差异是在于公平锁多了一个 !this.hasQueuedPredecessors()判断,我们可以进入该判断看一下:
public final boolean hasQueuedPredecessors() {Node h, s;// 如过 AQS同步队列的前驱节点(head)不是空if ((h = head) != null) {// 根据前驱节点(head)获取下一个节点,也就是 AQS同步队列中的第一个节点,然后判断其是否为空。// 如果 h.next不是空,说明 AQS同步队列中已经有线程等待获取锁中。那么就接着判断该节点的线程是否调用了条件变量的 await方法将自己挂起。if ((s = h.next) == null || s.waitStatus > 0) {s = null;// 遍历 AQS同步队列,直到找到一个未调用条件变量的 await方法将自己挂起的线程。for (Node p = tail; p != h && p != null; p = p.prev) {if (p.waitStatus <= 0)s = p;}}// 如果 AQS同步队列中存在一个未调用过 await的线程,并且这个线程并非是当前线程,则返回true。if (s != null && s.thread != Thread.currentThread())return true;}return false;
}
由此方法便可以看出,如果 AQS同步队列中存在可以正常获取锁的线程,那么新来的线程就无法“插队”。
接下来我们便进入如果调用tryAcquire方法失败,而进入acquireQueued(addWaiter(Node.EXCLUSIVE), arg)的方法。进入addWaiter方法:
private Node addWaiter(Node mode) {//创建一个类型为 Node.EXCLUSIVE的Node节点Node node = new Node(mode);// 将其加入 AQS同步队列的尾部。for (;;) {Node oldTail = tail;if (oldTail != null) {node.setPrevRelaxed(oldTail);if (compareAndSetTail(oldTail, node)) {oldTail.next = node;return node;}} else {// 初始化同步队列,在这个方法中,是创建一个Node节点为哨兵节点,并设置为 AQS前驱节点(head)。initializeSyncQueue();}}
}
接下来我们便进入acquireQueued方法:
final boolean acquireQueued(final Node node, int arg) {boolean interrupted = false;try {// 循环的方式获取锁for (;;) {// 获取当前线程的前置节点final Node p = node.predecessor();// 如果当前线程的前一个(pre)节点就是 AQS的前驱节点(head),那么说明“轮到”这个线程去获取锁了// 然后调用 tryAcquire方法获取锁if (p == head && tryAcquire(arg)) {// 将 AQS的前驱节点(head)设置为当前节点,可以看出这是懒加载的形式。setHead(node);p.next = null;return interrupted;}if (shouldParkAfterFailedAcquire(p, node))interrupted |= parkAndCheckInterrupt();}} catch (Throwable t) {cancelAcquire(node);if (interrupted)selfInterrupt();throw t;}
}
由此,线程便成功获取锁了。
2.3 释放锁
释放锁就相对而言毕竟容易了,下面我们看一下代码:
public void unlock() {sync.release(1);
}public final boolean release(int arg) {// 具体逻辑由 AQS子类实现if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}@ReservedStackAccess
protected final boolean tryRelease(int releases) {// 将 AQS状态值减去 1(默认为1)int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())// 如果当期线程不是锁的拥有者,就会报错,所以在未获得锁之前,切勿调用锁的 unlock方法throw new IllegalMonitorStateException();boolean free = false;// 如果状态值减去1后为0,则可以释放锁。if (c == 0) {free = true;// 将当前锁的拥有者设置为空setExclusiveOwnerThread(null);}// 设置 AQS状态值setState(c);return free;
}
这里需要注意的是,因为是可重入锁,同一线程每获取锁一次状态值就会加1,所以解锁也应该解同样的次数,否则仅仅是状态值减1,不会释放锁。
参考
- 电子工业出版社,翟陆续,薛宾田著,《Java并发编程之美》。
- RedSpider社区,《深入浅出Java多线程》。