目录
一. LockSupport 基础
- LockSupport是什么: 是用来创建锁和其它同步类的基本线程阻塞原语,LockSupport中有两个方法park() 和unpark()
park(): 阻塞线程
unpark(): 解除阻塞线程
- 简单来说: LockSupport 类,可以阻塞当前线程以及唤醒指定被阻塞的线程
二. 阻塞与唤醒线程的几种方式
wait() 与 notify() 方式
- wait() 与 notify() 方法讲解
- wati() 与 notify() 是 Object 中提供的方法,
- wati() 与 notify() 使用前提是在 synchronized 中
- wati() 表示休眠并释放锁资源, notify() 表示唤醒当前对象监视器上(同一锁下的)其它休眠线程
- wait() 与 join() 的区别: wait() 用在 synchronized 中, 被 wait() 的线程需要唤醒, 在一个方法中调用了 join(),表示当前线程进入阻塞状态,并且在其它线程执行完毕后才会自动唤醒执行,没有锁的概念
- wait() 与 sleep() 的区别: sleep 指定休眠时间,时间过后自动唤醒,并且sleep不会释放锁
- notify() 与 yield()区别: yield() 使当前线程重新回到可执行状态,但是并不会马上执行,没有锁的概念,notify() 是唤醒被 wait() 的线程,与 wait() 配合使用的
- 示例代码: testWaitNotify() 方法中存在两个线程t1与t2, t1执行时会阻塞, t2会唤醒同一个监视器上的其它阻塞线程
//wait与notify是监视器的(锁),声明锁对象
private Object syLock = new Object();
public void testWaitNotify() {
//1.t1线程中会阻塞
new Thread(() -> {
//3.注意点: t2线程中唤醒当前t1线程的阻塞,假设t2唤醒时t1线程还没阻塞,会出现什么情况
try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
synchronized (syLock) {
try {
System.out.println(Thread.currentThread().getName() + "线程阻塞执行");
syLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "线程被t2中唤醒");
}
}, "t1").start();
//2.t2.线程中会唤醒同一个监视器上的其它阻塞线程
new Thread(() -> {
synchronized (syLock) {
syLock.notify();
System.out.println(Thread.currentThread().getName() + "唤醒t1线程");
}
}, "t2").start();
}
public static void main(String[] args) {
LockSupportDemo demo = new LockSupportDemo();
demo.testWaitNotify();
}
- 根据示例代码总结: wait() 与 notify() 方式 中存在的问题
- wait() 与 notify() 是基于同一个监视器的也就是同一把锁,在唤醒时会唤醒同一个监视器上的其它被阻塞线程,所以如果使用该方式必须用synchronized
- wait() 阻塞线程, notify() 唤醒线程, 假设notify()在唤醒时,没有线程阻塞,这样可能就会造成,唤醒执行后,线程阻塞了,会一直阻塞, 也就是说: wait() 与 notify() 必须成对出现,并且阻塞唤醒顺序不能错,否则会阻塞
JUC包下 Condition 中的 await() 与 signal() 方式
- 使用api
await(): 表示让线程阻塞
signal(): 表示唤醒线程
- 示例代码: testCondition()方法中开启了两个线程t1与t2,在t1中会阻塞线程, t2中会唤醒t1线程
//基于lock, Condition 方式实现阻塞唤醒线程
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void testCondition() {
//1.t1线程中会阻塞
new Thread(() -> {
//3.t2线程中唤醒当前t1线程的阻塞,假设t2唤醒时t1线程还没阻塞,会出现什么情况
//try { TimeUnit.MILLISECONDS.sleep(3L); } catch (InterruptedException e) { e.printStackTrace(); }
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "线程阻塞执行");
condition.await();
System.out.println(Thread.currentThread().getName() + "线程被t2中唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}, "t1").start();
//2.t2.线程中会唤醒同一个监视器上的其它阻塞线程
new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "唤醒t1线程");
}finally {
lock.unlock();
}
}, "t2").start();
}
public static void main(String[] args) {
LockSupportDemo demo = new LockSupportDemo();
demo.testCondition();
}
- 根据上方使用await()与signal()阻塞唤醒线程示例了解可能存在的问题
- 必须有锁否则会报错
- 假设signal()唤醒时,其它线程还为执行await()阻塞,会出现问题,进而了解到await()与signal()要成对出现,并且是有顺序的
LockSupport 使用示例与底层原理
- 在上面了解到不管 wait() 与 notify() 方式还是await() 与 signal() 方式实现线程的阻塞与唤醒都存在两个问题
- 线程要先获取并持有锁,必须在锁(synchronized或Lock)中
- 必须要先等待后唤醒,线程才能正常唤醒,程序才能正常执行
- LockSupport 类使用一种名为Permit许可证方式来实现线程的阻塞唤醒功能,每个线程都有一个permit许可证, 可以把许可证permit看成是一种信号量Semaphore,但是与信号量不同的是许可的累加上限是1,每个许可证都只有两个值0和1,默认0(底层通过Unsafe实现)
- 源码分析: 底层通过调用UNSAFE的park()与unpark(Thrade thread)阻塞唤醒线程,默认permit许可证为0,所以第一次调park()方法时会阻塞线程,调unpark()传递指定线程会将传递线程的permit设置为1,阻塞线程被唤醒执行,并且阻塞线程会再次将permit设置为0
public static void park() {
UNSAFE.park(false, 0L);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
- 示例,在testLockSupport()方法中开启了两个线程t1, t1中会执行LockSupport.park()阻塞当前线程执行,t2中会执行LockSupport.unpark(t1) 传递t1的线程对象,唤醒t1线程
public void testLockSupport() {
//1.t1线程中会阻塞
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程阻塞执行");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "线程被t2中唤醒");
}, "t1");
t1.start();
//2.t2.线程中会唤醒同一个监视器上的其它阻塞线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程使用LockSupport唤醒t1线程");
LockSupport.unpark(t1);
}, "t2").start();
}
public static void main(String[] args) {
LockSupportDemo demo = new LockSupportDemo();
//demo.testWaitNotify();
//demo.testCondition();
demo.testLockSupport();
}
- 根据上面的示例代码,假设在t1线程执行阻塞前手动休眠5秒,先然t2线程执行,会不会出现因为阻塞唤醒的顺序不对造成程序一直阻塞?答案是不会,先执行t2,在t2中设置t1的permit为1,允许通行,然后t1执行发现当前的permit为1,已经被唤醒过了不在阻塞,并将permit设置回0,可以理解为两个抵消掉了
多次阻塞唤醒案例
- 上面讲到在使用 LockSupport 设置线程阻塞时,permit 上限是1,那么如果实际案例就是需要某个线程多次阻塞怎么办,需要多次阻塞的线程多次执行park(), 然后通过多个其它线程多次执行 unpark(Thread 需要唤醒的线程)
public void testLockSupport() {
//1.t1线程中会阻塞
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程阻塞执行");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "线程被唤醒");
System.out.println(Thread.currentThread().getName() + "线程再次阻塞");
LockSupport.park();
}, "t1");
t1.start();
//2.t2线程中会唤醒同一个监视器上的其它阻塞线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程使用LockSupport唤醒t1线程");
LockSupport.unpark(t1);
}, "t2").start();
//3.t3线程再次执行unpark()唤醒t1线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程使用LockSupport唤醒t1线程");
LockSupport.unpark(t1);
}, "t3").start();
}
三. 总结 LockSupport 的使用
- 如何阻塞唤醒线程
- wait() 与 notify()方式: Object中提供的方法: 需要在synchronized中使用, wati() 表示休眠并释放锁资源, notify() 表示唤醒当前此对象监视器上(同一锁下的)其它休眠线程
- JUC包下 Condition 中的 await() 与 signal() 方式
- 以上两种方式的缺点:
- wait() 与 notify()方式 需要使用synchronized,
- Condition 中的 await() 与 signal() 方式 需要使用 ReentrantLock
- 必须成对出现,并且要先阻塞后唤醒是有顺序的
- 还有几个基础问题
- wait() 与 join() 的区别: wait() 用在 synchronized 中, 被 wait() 的线程需要唤醒, 在一个方法中调用了 join(),表示当前线程进入阻塞状态,并且在其它线程执行完毕后才会自动唤醒执行,没有锁的概念
- wait() 与 sleep() 的区别: sleep 指定休眠时间,时间过后自动唤醒,并且sleep不会释放锁
- notify() 与 yield()区别: yield() 使当前线程重新回到可执行状态,但是并不会马上执行,没有锁的概念,notify() 是唤醒被 wait() 的线程,与 wait() 配合使用的
- 进而提出了阻塞唤醒线程第三种方式: LockSupport 中的 park() 与 unpark(Thread t), 以Permit许可证方式来实现线程的阻塞唤醒功能,底层基于UNSAFE的pack()与UNSAFE的unpark()实现,每个线程都有自己的permit标识,不用非要保证先执行阻塞,后执行唤醒的顺序,线程在执行时会自己进行判断,如果先执行的唤醒,会将被指定唤醒的线程中的permit设置为1,后执行阻塞时发现permit已经为1了,就不回再次阻塞,相当于两次抵消掉了,并且permit许可上限为1,多次累加不变