LockSupport
-
LockSupport是什么?
LockSupport是一种线程等待唤醒机制,用来创建锁和其他同步类的基本线程阻塞原语。LockSupport中的park()和 unpark()的作用分别是阻塞线程和解除阻塞线程。
-
让线程等待和唤醒的三种方式?
- 方式1:使用object中的wait()方法让线程等待,使用object中的notify()方法唤醒线程
- 方式2:使用uc包中condition的await()方法让线程等待,使用signal()方法唤醒线程
- 方式3: LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
3. wait,notify机制例子
代码:
输出结果:public class Demo01 { private static Object objectLock = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (objectLock) { System.out.println(Thread.currentThread().getName() + "---come in----"); try { objectLock.wait(); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "---被唤醒---"); } }, "t1").start(); new Thread(() -> { synchronized (objectLock) { objectLock.notify(); System.out.println(Thread.currentThread().getName() + "---com in---"); } }, "t2").start(); } }
那如果把代码改成下面的样子呢?
输出结果:public class Demo01 { private static Object objectLock = new Object(); public static void main(String[] args) { new Thread(() -> { // synchronized (objectLock) { System.out.println(Thread.currentThread().getName() + "---come in----"); try { objectLock.wait(); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "---被唤醒---"); // } }, "t1").start(); new Thread(() -> { // synchronized (objectLock) { objectLock.notify(); System.out.println(Thread.currentThread().getName() + "---com in---"); // } }, "t2").start(); } }
两个线程都去掉代码块会报错,也就是说,wait,notify机制是不能脱离synchronized的!!
我们也可以尝试将notify放在wait前面将导致线程永远不会唤醒!!!所以要先wait后notify!!!
-
Condition接口中的await后signal方法实现线程的等待和唤醒
代码:public class Demo02 { private static Lock lock = new ReentrantLock(); static Condition condition = lock.newCondition(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + " com in"); try { condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 被唤醒"); } finally { lock.unlock(); } }, "t1").start(); new Thread(() -> { lock.lock(); try { condition.signal(); System.out.println(Thread.currentThread().getName() + " 通知"); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } }, "t2").start(); } }
输出结果:
将两个线程的lock与unlock去掉,同样会报错:
-
LockSupport案例
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
代码:public class Demo03 { public static void main(String[] args) { Thread a = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in"); LockSupport.park(); System.out.println(Thread.currentThread().getName() + "\t 被唤醒"); }); a.start(); try { TimeUnit.SECONDS.sleep(3L); } catch (Exception e) { e.printStackTrace(); } Thread b = new Thread(() -> { LockSupport.unpark(a); System.out.println(Thread.currentThread().getName() + "\t 通知了"); }); b.start(); } }
输出结果:
那如果先unpark后park呢?输出结果:
LockSupport的重要说明:- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport是一个线程阻寨工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。 - LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit,也就是将1变成o,同时park立即返回。如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。 - 形象的理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法时,*如果有凭证,则会直接消耗掉这个凭证然后正常退出;*如果无凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
-
LockSupport的面试题
- 为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。 - 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次 unpark 效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。
- 为什么可以先唤醒线程后阻塞线程?
AQS
AQS是什么
是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态。抢到资源的线程直接使用处理业务逻辑,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
AQS在其他同步器组件中的应用
进一步理解锁和同步器的关系:
- 锁,面向锁的使用者
定义了程序员和锁交互的使用层APl,隐藏了实现细节,你调用即可。
- 同步器,面向锁的实现者
比如Java并发大神DougLee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等
而AQS则属于后者。
AQS内部体系结构
首先看一看AQS的类图(此类图只显示出相关的类和各类的变量,主要介绍红框里的变量):
AbstractQueuedSynchronizer类:
- head:双端队列的头指针
- tail:双端队列的尾指针
- state: 当前锁的持有状态(0表示空闲,1表示被占有)
注意:state是用voliate修饰的变量,目的是为了保证内存可见性,使得state变量被修改后,其他线程能够立即感知到。
Node类:
Node类是AbstractQueuedSynchronizer的一个内部类,如果有多个线程竞争锁,其中的一个线程获取到了锁,那么其他的线程会被封装成Node插入到双端队列。
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;
// 等待condition唤醒
static final int CONDITION = -2;
// 共享式同步状态获取将会无条件的传播下去
static final int PROPAGATE = -3;
// 初始为0,状态是上面的几种
volatile int waitStatus;
// 前置结点
volatile Node prev;
// 后继结点
volatile Node next;
volatile Thread thread;
}
AQS源码
以ReentrantLock为例,深入理解AQS源码。
从类的关系图中可以看到:
- ReentrantLock实现了Lock接口
- ReentrantLock内有NonfairLock、FairSync和Sync内部类,且NonfairLock、FairSync又继承了Sync类
- Sync类继承了AbstractQueuedSynchronizer抽象类
- AbstractQueuedSynchronizer内有Node和ConditionObject两个内部类
以下述代码为入口一步一步的进入源码,看一看AbstractQueuedSynchronizer内部是如何运行的:
public class Demo02 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
lock.lock();
try {
System.out.println("A获取了锁");
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "A").start();
new Thread(() -> {
lock.lock();
try {
System.out.println("B获取了锁");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "B").start();
new Thread(() -> {
lock.lock();
try {
System.out.println("C获取了锁");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "C").start();
}
}
代码执行流程分析
ReentrantLock无参构造函数
public ReentrantLock() {
sync = new NonfairSync();
}
ReentrantLock默认是非公平锁,以lock.lock();A线程获取锁为入口,看一下执行大致的流程图:
A线程获得锁
A线程获得锁,代码的执行流程为 1 =》 2
,对应的方法为lock() => compareAndSetState()
。
可以从图中看到,A获得锁的时候由于没有其他线程竞争,所以A线程通过自旋的方式将state设置为1,自己为锁的持有者。
B线程竞争锁(A线程未释放锁)
A线程获得锁后,由于执行任务需要占用锁2s,此时B线程过来竞争锁,对应的执行流程为1 =》 2 =》 3 =》 4 =》 5 =》 6 =》7 =》 8 =》 9 =》 10
,对应的方法为 lock() =》 compareAndSetState() =》 acquire() =》 tryAcquire() =》 addWaiter() =》 nonfairTryAcquire() =》 addWaiter() =》 enq() =》 acquireQueued() =》 tryAcquire() =》setHead()
执行lock()
方法尝试获取锁,要先调用compareAndSetState()
将state置为1,但是由于此时线程A已获得锁且state变量的值为1,所以自旋失败,执行acquire()
方法尝试以非公平方式去获取锁(也就是调用nonfairTryAcquire()
方法)。
以注释的形式描述nonfairTryAcquire()内部具体做了些什么:
final boolean nonfairTryAcquire(int acquires) {
// 获取当前请求锁的线程
final Thread current = Thread.currentThread();
// 获取锁的状态
int c = getState();
// 如果锁处于空闲状态
if (c == 0) {
// 自旋尝试设置state=1
if (compareAndSetState(0, acquires)) {
// 自旋成功,设置当前线程未持有锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
// 判断当前线程和已持有锁的线程是否为同一个线程
else if (current == getExclusiveOwnerThread()) {
// 如果是同一个线程,则state = state + 1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 重新设置state的值
setState(nextc);
return true;
}
return false;
}
由于线程A已占有锁,所以nonfairTryAcquire()方法执行结果为false,所以tryAcquire()
的结果为false,接着执行 addWaiter()
方法将B线程封装进一个Node对象并添加进双端队列中。我们可以看一看addWaiter()内部做了什么(addWaiter(Node.EXCLUSIVE)一个空结点 )。
addWaiter()源码:
private Node addWaiter(Node mode) {
// 创建一个新Node对象,
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) {
// 尾节点不是null,自旋设置新的结点为尾节点
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 入队
enq(node);
return node;
}
enq(node)源码:
private Node enq(final Node node) {
// 死循环
for (;;) {
// 获取尾节点
Node t = tail;
if (t == null) { // Must initialize
// 尾节点为NULL,自旋设置一个新的结点为头结点
if (compareAndSetHead(new Node()))
// 尾节点指向头结点
tail = head;
} else {
// 尾节点补位null,则将尾节点设置为新结点的前置结点,
node.prev = t;
if (compareAndSetTail(t, node)) {
// 新的结点成为尾节点
t.next = node;
return t;
}
}
}
}
在addWaiter(Node.EXCLUSIVE)方法中由于tail为null,所以直接调用了enq(Node)
方法,在enq()方法内部是一个死循环,第一次循环时由于尾节点为空,所以就新建一个结点使得head、tail指针都指向这个结点(这个结点被成为哨兵结点),执行完第一次循环队列图如下:
第一次循环结束后,多了个结点,但是此时此结点并不是我们传入的结点,内部的值是因为Node初始化(new Node())而得到的(Thread=null,waitStatus=0)。在第二次循环后,由于tail不为空,所以第二次循环后队列图为:
此时addWaiter(Node.EXCLUSIVE)方法执行完后,B线程已被封装进Node结点内放进双端队列中,接下来会调用acquireQueued()
去竞争锁,进到源码里看一看acquireQueued()是如何工作的。
acquireQueued():
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 死循环,直至获取到锁
for (;;) {
// 获取node的前驱结点
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);
}
}
从代码中我们可以知道Thread前驱结点为头结点,当tryAcquire(arg)返回为false时(获取锁失败)则再次尝试,在第一次获取失败后,代码则会执行到shouldParkAfterFailedAcquire()
方法。
shouldParkAfterFailedAcquire()源码:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire()执行完后,队列变成:
在第二次循环后,由于Thread又为获得锁,所以又执行到shouldParkAfterFailedAcquire()方法,而此方法由于第一次执行waitStatus=1会返回true,接下来会执行parkAndCheckInterrupt()
。
parkAndCheckInterrupt()源码:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
可以看到ThreadB将会被挂起,等待唤醒。
在ThreadA执行完毕后,调用unlock()释放锁后,ThreadB被唤醒尝试获取锁,获取锁成功后则将此结点从队列中删除,注意:并不是直接将ThreadB结点删除,而是将ThreadB结点置为新的哨兵结点
setHead()的源码:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
重置头结点后队列图: