面试——ReentrantLock
AQS
AQS(AbstractQueuedSynchronizer)定义了一套多线程访问共享资源的同步器框架,是一个依赖状态的同步器。AQS定义了很多并发中的行为,比如:
- 阻塞等待队列
- 共享/独占
- 公平/非公平
- 可重入
- 允许中断
ReentrantLock介绍
ReentrantLock是基于AQS框架实现的锁,它类似于Synchronized互斥锁,可以保证线程安全。基于AQS强大的并发特性和处理多线程的能力,ReentrantLock相比Synchronized,拥有更多的特性,比如支持手动加锁、解锁,支持公平锁等。
先来看一个例子:
package com.qf.lock;
import java.util.concurrent.locks.LockSupport;
public class MyLockSupportDemo {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+":开始执行。");
for(;;){//自旋
System.out.println(thread.getName()+":即将park当前线程");
LockSupport.park();//用于阻塞住线程
System.out.println(thread.getName()+":当前线程已被唤醒");
}
}
},"thread-1");
t1.start();
try {
Thread.sleep(5000);
System.out.println("准备唤醒"+t1.getName()+"线程");
LockSupport.unpark(t1);//唤醒阻塞的线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
显示结果:
thread-1:开始执行。
thread-1:即将park当前线程
准备唤醒thread-1线程
thread-1:当前线程已被唤醒
thread-1:即将park当前线程
从这个例子可以推导出,ReentrantLock的核心是这么一个逻辑:
LockSupport上锁->自旋->队列
Reentrantlock上锁的例子
接下来看下Reentrantlock上锁的例子
package com.qf.lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyReentrantLockDemo {
public static ReentrantLock lock = new ReentrantLock(true);
public static void reentrantLock(){
lock.lock();
System.out.println(Thread.currentThread().getName()+":,第一次加锁");
lock.lock();
System.out.println(Thread.currentThread().getName()+":,第二次加锁");
lock.unlock();
System.out.println(Thread.currentThread().getName()+":,第一次解锁");
lock.unlock();
System.out.println(Thread.currentThread().getName()+":,第二次解锁");
}
public static void main(String[] args) {
Thread t0 = new Thread(new Runnable() {
@Override
public void run() {
reentrantLock();
}
},"t0");
t0.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
reentrantLock();
}
},"t1");
t1.start();
}
}
运行结果:
t0:,第一次加锁
t0:,第二次加锁
t0:,第一次解锁
t0:,第二次解锁
t1:,第一次加锁
t1:,第二次加锁
t1:,第一次解锁
t1:,第二次解锁
我们发现一定得是在t0线程完全释放锁后,t1线程才能获得锁。
ReentrantLock的实现逻辑
公平锁和非公平锁
在ReentrantLock内部定义了Sync类,Sync类继承自AbstractQueuedSynchronizer类。我们发现AbstractQueuedSynchronizer是多个AQS关键类中的基类。这个类涉及到上锁的核心逻辑
那ReentrantLock是如何实现公平锁和非公平锁呢?ReentrantLock默认使⽤⾮公平锁,也可以通过构造器来显示的指定使⽤公平锁。在ReentrantLock中还有两个类继承自Sync:
- NonfairSync
- FairSync
他们实现公平和非公平的逻辑非常简单
——
我们先看一下公平锁,在获锁之前,通过!hasQueuedPredecessors()
先看下是否有人排队,如果没有排队则尝试获锁,如果有排队,则进入排队队列。
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;
}
而非公平锁止是没有这个判断的。也就是说,非公平锁的情况下,相对较晚来的线程,在尝试上锁的时候,即使之前已经有等待锁的线程存在,它也是有可能上锁成功的。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
但公平锁则是先等待的,先获得锁,后来的后获得锁。这是hasQueuedPredecessors()
方法的逻辑
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
AbstractQueuedSynchronizer类的关键属性
ReentrantLock如何获得锁呢?先来看下AbstractQueuedSynchronizer类的结构。
-
state:同步器状态变量,值为0时表示当前可以被加锁。值为1 时表示有线程占⽤,其他线程需要进⼊到同步队列等待,同步队列是⼀个双向链表。
-
exclusiveOwnerThread:当前获取锁的线程
-
head:指向基于Node类构造的队列的队头,同步队列是⼀个双向链表。
-
tail:指向基于Node类构造的队列的队尾,同步队列是⼀个双向链表。
-
Thread:表示当前线程的引用,比如需要唤醒的线程。
上锁逻辑
public final void acquire(int arg) {//1
if (!tryAcquire(arg) &&//尝试加锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//没有加锁成功,尝试入队列
selfInterrupt();
}
以公平锁上锁为例,当使用lock()上锁,会传入1作为cas对state状态量的预计值进行修改,前提是查看同步队列中是否没有其他线程等待。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&//查看同步队列
compareAndSetState(0, acquires)) {//CAS设置state值,期望旧值为0,期望新值为1
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//如果上锁失败,查看是否是自己持有锁,如果是,state+1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
如果上锁失败,查看是否是自己持有锁,如果是则state++。
如果没有加锁成功,则尝试进入队列。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
队列的细节
虽然说当前线程已经入队列了,但线程还没有阻塞,接下来线程要做阻塞。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
/*
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
*/
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {//阻塞之前,再抢一次锁,如果锁成功,头节点出队列
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//进行阻塞
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
什么时候被唤醒呢?在lock.unlock()中唤醒
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//state-1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);//所属线程置空
}
setState(c);
return free;
}
唤醒线程:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);//唤醒线程
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/65621.html