一:线程等待唤醒的实现方法
方式一:使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程
必须都在synchronized同步代码块内使用,调用wait,notify是锁定的对象;
notify必须在wait后执行才能唤醒;
public class LockSupportDemo1 {
public static void main(String[] args) {
Object objectLock = new Object();
/**
* t1 -----------come in
* t2 -----------发出通知
* t1 -------被唤醒
*/
new Thread(() -> {
synchronized (objectLock) {
System.out.println(Thread.currentThread().getName() + "\t -----------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t -------被唤醒");
}
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
synchronized (objectLock) {
objectLock.notify();
System.out.println(Thread.currentThread().getName() + "\t -----------发出通知");
}
}, "t2").start();
}
}
方式二:使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程
必须在lock同步代码块内使用;
signal必须在await后执行才能唤醒;
public class LockSupportDemo2 {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t -----------come in");
condition.await();
System.out.println(Thread.currentThread().getName() + "\t -----------被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "\t -----------发出通知");
} finally {
lock.unlock();
}
}, "t2").start();
}
}
方式三:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
不需要锁块;
unpark()可以在park()前唤醒;
public class LockSupportDemo {
public static void main(String[] args) {
/**
* t1 -----------come in
* t2 ----------发出通知
* t1 ----------被唤醒
*/
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t -----------come in");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t ----------被唤醒");
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
LockSupport.unpark(t1);//指定需要唤醒的线程,可以先给t1发放许可证,t1再被锁定,此时t1可以立马被唤醒
System.out.println(Thread.currentThread().getName() + "\t ----------发出通知");
}, "t2").start();
}
}
二: 介绍一下LockSupport
LockSupport是什么: LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(Permit),许可证只有两个值,1和0;默认是0,许可证不能超过1;
park()方法:调用park方法,当前线程会阻塞,直到别的线程给当前线程发放peimit,park方法才会被唤醒。
unpack(thread)方法: 调用unpack方法,就会将thread线程的许可证peimit发放,唤醒处于阻塞状态的指定线程。
面试题:
1:LockSupport为什么可以先唤醒线程后阻塞线程但不会阻塞?
答:因为unpark()方法获得了一个许可证,许可证值为1,再调用park()方法,就可消费这个许可证,所以不会阻塞;
2:为什么唤醒两次后阻塞两次,最终还是会阻塞?
答:如果线程A调用两遍park(),线程B调用两边unpark(),那么只会解锁一个park(),因为许可证最多只能为1,不能累加;
三:AQS是什么
AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列完成资源获取排队工作,将每条要去抢占资源的线程封装成一个NODE节点来实现锁的分配,通过CAS完成对State值的修改,
AQS的本质是一个双向队列加一个状态为state
五:公平锁与非公平锁的区别
公平锁: 多个线程按照线程调用lock()的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
非公平锁: 多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
六: 非公平锁加锁的源码分析
tryAcquire(arg):尝试获取锁
addWaiter(Node.EXCLUSIVE):添加到同步队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)):队列里自旋等待获取等
第一步、tryAquire
先获取当前AQS 的state的值,判断是否为0,如果为0表示没有人抢占,此刻他抢占,返回true,抢占锁后就完事了;
// 这里调用进入非公平锁的tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// 具体代码在这里
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 若当前无其他线程抢占锁,则抢占;
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果已获取锁的线程再调用lock()则state值+1,这里就是可重入的原理
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 都不是则返回false
return false;
}
第二步、addWaiter
创建队列的节点,首先就先new出来一个节点,由于刚开始当前队列没有节点,因此进入enq()方法;
enq方法插入节点是死循环:
第一次循环,由于tail为空,他先创建一个空的node节点,作为头节点,此时waitStatus=0,然后将head指向该头节点,并将tail指针也指向head;
第二次循环,他将待插入node节点(线程B)的前置指针指向tail指向的节点(头节点),然后CAS将tail指向当前待插入节点(线程B),再让原来的tail指向的节点(头节点)的next域指向当前节点,这样就完成了节点(线程B)插入队尾,完成链式结构,跳出循环;
private Node addWaiter(Node mode) {
// 创建一个节点 mode
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 刚开始tail是null,如果tail有值了就将node插入队尾;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 若队列为空,则插入节点
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) { // 死循环
Node t = tail;
if (t == null) { // 初始下tail为null,因此创建一个头节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;// 第二次循环,队列不为空,就将该节点插入队尾
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
第三步:aquireQueued
这个方法依旧是死循环。
第一次循环:首先predecessor()取出的就是前置节点,p就是链表中的头节点,然后进入判断,当前确实是头节点,然后再次尝试tryAcquire(),由于线程A并没有释放锁,因此,只能进入shouldParkAfterFailedAcquire()方法;
第二次循环,再次进入shouldParkAfterFailedAcquire(),这一次由于ws=-1,因此返回true,并进入parkAndCheckInterrupt()方法;这里会调用LockSupport.park()将线程挂起,此刻线程B就阻塞再这里了。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//获取节点的前置节点,线程B获取到的是头节点
if (p == head && tryAcquire(arg)) {//由于线程A占用,尝试获取失败
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())// 线程B会进入这里
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;// 头节点的waitStatus=0
if (ws == Node.SIGNAL)// -1
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);// 将头节点的waitStatus设置成-1
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
七:非公平锁解锁的具体流程
线程A就先去获取AQS的state,并对应减去1个,并设置当前占有线程为null,然后找到头节点去调用unparkSuccessor(head),他将头节点的状态从-1设置为0,然后唤醒线程B;
// 执行ReentrantLock.unlock()
public void unlock() {
sync.release(1);
}
// AQS.release()
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 执行ReentrantLock.tryRelease()
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
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) {
int ws = node.waitStatus;// 头节点是-1
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);// 头节点设置为0
Node s = node.next;// 线程B
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);//唤醒线程B
}
线程B还锁在parkAndCheckInterrupt()方法中,解锁后开始第三次循环,第三次循环发现前置节点是头,且可以占用锁,因此线程B获取到锁并进入第一个if;然后重新设置头节点,将头指向线程B,将原头节点剔除队列,然后将线程B设置成头节点。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//获取节点的前置节点,线程B获取到的是头节点
if (p == head && tryAcquire(arg)) {//目前锁无占用,进入此处
setHead(node); // 重新设置头节点
p.next = null; // help GC
failed = false;
return interrupted; // 被改为true
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())// 线程B从这里唤醒
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 修改头节点
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}