LockSupport 是JUC中提供的一个工具类,主要是在 阻塞线程
(park)和唤醒线程
(unpark)时使用,提起线程阻塞和唤醒,我们可能第一时间想到的就是Object类提供的wait()
和notify()
方法,以及JUC中Condition接口提供的await()
和signal()
方法,在探究LockSupport之前,我们先简单回忆下这二者的使用。
1.Object的wait()/notify()
wait():让持有该对象锁的线程等待,会使线程暂停并让出CPU资源,同时释放持有的对象锁
notify():随机
唤醒一个处于WAITING状态的线程。
notifyAll():唤醒所有处于WAITING状态的线程。
注意:被notify()、notifyAll()唤醒后,线程不会立即执行,而是需要重新竞争对象锁,获得锁的线程可以从wait处继续向下执行
我们先看下代码:
public class ObjectWaitNotifyTest {
private static final Object object = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (object) {
try {
System.out.println(Thread.currentThread().getName() + " 线程进入 wait()方法");
/**
* 使线程暂停并让出CPU资源,同时释放持有的对象的锁
*/
object.wait();
System.out.println(Thread.currentThread().getName() + " 线程退出了 wait()方法, 被唤醒了....");
for (int i = 0; i < 5; i++) {
System.out.println("num: " + i);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (object) {
try {
System.out.println(Thread.currentThread().getName() + " 线程准备随机唤醒某个线程");
object.notify();
//object.notify(); 唤醒所有等待的线程
} catch (Exception e) {
e.printStackTrace();
}
}
}, "t2");
t2.start();
t1.start();
}
在使用上wait()和notify()的局限:
wait()
和notify()
必须要用在synchronized
同步代码块或者同步方法中,否则将抛出IllegalMonitorStateException
异常- 必须要确保先执行
wait()
, 后执行notify()
才可以唤醒等待的线程
2.Condition的await()/signal()
public class LockConditionTest {
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 线程进入 wait()方法");
//释放锁
condition.await();
System.out.println(Thread.currentThread().getName() + " 线程退出了 await()方法, 被唤醒了....");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1");
Thread t2 = new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "线程准备随机唤醒线程");
condition.signal();
//唤醒所有等待的线程
//condition.signalAll();
} finally {
lock.unlock();
}
}, "t2");
t1.start();
t2.start();
}
}
我们再来看下它的局限性:
Condition
必须搭配Lock
使用,否则将抛出IllegalMonitorStateException
异常(和Object
的wait()
,notify()
一样)- 必须要确保先执行
await()
,后执行signal()
才可以唤醒
从上面看,Object
的wait()/notify()
和 Condition
的 await()/signal()
都有局限性,必须要在同步块或者锁内使用,并且在使用上要保证顺序
,先阻塞后唤醒。为了打破这种局限性,JUC提供了一个全新的阻塞/唤醒工具类—— LockSupport.
3.LockSupport的park()/unpark()
我们先通过代码看下LockSupport是如何完成阻塞和唤醒的
public class LockSupportTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 线程进入 park()方法");
//进入阻塞,释放锁
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " 线程退出了 park()方法, 被唤醒了....");
} catch (Exception e) {
e.printStackTrace();
}
}, "t1");
Thread t2 = new Thread(() -> {
try {
//2s后唤醒t1线程
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 线程唤醒t1线程");
/**
* 指定唤醒哪个线程,这相比Object的notify()和Condition的signal()来说是精准唤醒了
*/
LockSupport.unpark(t1);
}, "t2");
t1.start();
t2.start();
}
}
不难发现,LockSupport不依赖于同步块或者锁,使用的时候直接调用静态方法即可,非常方便。
我们再看下先唤醒在阻塞是否可行?
public class LockSupportTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
try {
//目的是测试先唤醒,后阻塞
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 线程进入 park()方法");
//进入阻塞,释放锁
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " 线程退出了 park()方法, 被唤醒了....");
} catch (Exception e) {
e.printStackTrace();
}
}, "t1");
Thread t2 = new Thread(() -> {
try {
//2s后唤醒t1线程
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 线程唤醒t1线程");
/**
* 指定唤醒哪个线程,这相比Object的notify()和Condition的signal()来说是精准唤醒了
*/
LockSupport.unpark(t1);
}, "t2");
t1.start();
t2.start();
}
}
不难发现,先唤醒后阻塞也是没有问题的。
从使用上来说,LockSupport 没有额外的条件约束(对比
Object
的wait()/notify()
需要在synchronized
代码块内使用), 而且也没有顺序约束,不像其他的必须先阻塞后唤醒。
3.1 LockSupport 说明
LockSupport是JDK6中提供的一个工具类,用来创建锁和其他同步工具类的基本线程阻塞原语。LockSupport很类似于二元信号量Semaphore
(只有1个许可证可供使用),如果这个许可还没有被占用,当前线程获取许可并继续执行;如果许可已经被占用,当前线程阻塞,等待获取许可。
其实park/unpark的设计原理核心是“许可
”。park是等待一个许可。unpark是为某线程提供一个许可。如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。
park 直译过来就是停车,这就好比过高速经过收费站,如果你没有通行证(permit=0),那么你就要停下来等着,不允许过收费站
unpark 的意思就是说我给你提供一个通行证(permit=1),然后park的车拿到这个通行证就可以顺利出收费站了
注意:凭证(permit)不会叠加,它最多就只有一个(这就是前面说的它相当于二元信号量的原因)
就算多次调用unpark()它也只会有一个凭证,比如线程B连续调用了三次unpark函数,当线程A调用park函数就使用掉这个“许可”,如果线程A再次调用park,则进入等待状态。
当调用park()方法时:
- 如果有凭证,则直接消耗掉这个凭证然后正常退出阻塞
- 如果没有凭证,则必须阻塞直到有凭证可用
而当调用unpark()方法时,它会给予一个凭证,但是最多只能有一个,不能累加。
当先调用unpark()时,它就会给它一个凭证,然后调用park()时直接使用掉即可。还是以高速公路过收费站为例,当先执行unpark()时,就表示我已经有了通行证,等到了收费站检查时(park)直接把通行证交给工作人员,然后我就可以出高速了。
3.2 LockSupport源码分析
public class LockSupport {
private LockSupport() {} // Cannot be instantiated.
private static void setBlocker(Thread t, Object arg) {
// Even though volatile, hotspot doesn't need a write barrier here.
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
/**
* 唤醒指定的线程(精准唤醒)
* 注意:如果需要唤醒的线程尚未启动,则此操作不会有任何效果
*/
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
/**
* 使调用该方法的当前线程阻塞,永久阻塞,除非被唤醒或者被中断
*/
public static void park() {
UNSAFE.park(false, 0L);
}
/**
* 使调用该方法的当前线程阻塞
* blocker 参数相当于是一个阻塞原因,可以通过 getBlocker(Thread t) 获取
* 注意:一旦阻塞的线程被唤醒,blocker信息将被清空
*/
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
/**
* 可以指定阻塞多长时间
*/
public static void parkNanos(long nanos) {
if (nanos > 0)
UNSAFE.park(false, nanos);
}
/**
* 可以指定阻塞到什么时候,是一个绝对时间,毫秒值
*
*/
public static void parkUntil(long deadline) {
UNSAFE.park(true, deadline);
}
/**
* 可以指定阻塞多长时间, 同时指定阻塞原因
*/
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, nanos);
setBlocker(t, null);
}
}
/**
* 可以指定阻塞到什么时候,是一个绝对时间, 同时可以指定阻塞原因
*/
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(true, deadline);
setBlocker(t, null);
}
/**
* 获取blocker信息
*/
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}
//......
}
3.3 测试Demo
3.3.1 设置阻塞原因
/**
* @author qiuguan
* @date 2022/12/02 23:22:15 星期五
*/
public class LockSupportTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程累了,不想工作了......");
LockSupport.park("t1线程累了,摆烂了");
System.out.println(Thread.currentThread().getName() + "线程又爬起来打工了......");
}, "t1");
Thread t2 = new Thread(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object blocker = LockSupport.getBlocker(t1);
System.out.println(blocker);
System.out.println("t2线程准备打醒t1, 让它继续搬砖");
LockSupport.unpark(t1);
});
t1.start();
t2.start();
}
}
3.3.2 多次unpark效果演示
/**
* @author qiuguan
* @date 2022/12/02 23:22:15 星期五
*/
public class LockSupportTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "线程累了,不想工作了......");
//t2率先给了凭证,所以直接消耗掉凭证直接退出阻塞
LockSupport.park("t1线程累了,摆烂了");
System.out.println(Thread.currentThread().getName() + "线程又爬起来打工了......");
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("3s后t1线程又累了........");
LockSupport.park();
System.out.println("t1又起来搬砖了........");
}, "t1");
Thread t2 = new Thread(() -> {
/**
* 调用3次unpark(),但实际上凭证只有一个, t1第一次park直接消耗掉往下运行
*/
LockSupport.unpark(t1);
LockSupport.unpark(t1);
LockSupport.unpark(t1);
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.unpark(t1);
});
t1.start();
t2.start();
}
}
3.3.3 指定何时自动唤醒阻塞
/**
* @author qiuguan
* @date 2022/11/27 03:00:17 星期日
*
*/
public class LockSupportTest {
public static void main(String[] args) {
LocalDateTime localDateTime = LocalDateTime.now().plusSeconds(10L);
long end = localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli();
int x = 0;
for (;;) {
System.out.println("---------> " + (++x));
//10s后自动结束阻塞
LockSupport.parkUntil(end);
if (x == 5) {
System.out.println("退出for循环.......");
break;
}
}
System.out.println("end.........");
}
}
4.总结
- LockSupport 可以完成线程的阻塞和唤醒功能,其原理的核心就是许可
- LockSupport 可以先唤醒后阻塞,这是因为唤醒相当于分发凭证,而阻塞就是消耗凭证,所以可以先唤醒后阻塞,也就是先给通行证,这样遇到关卡就直接放行。
- 凭证不能累加,只有一个,多次调用unpark()也只会有一个凭证
和Object的wait()/notify()差异比较:
共同点:
LockSupport
中的park
方法和Object
中的wait
方法都可以使线程进入WAIT
或者TIMED_WAIT
状态LockSupport
中的unpark
方法和Object
中的notify
可以使线程脱离WAIT
或者TIMED_WAIT
状态- 二者都可以通过调用线程的
interrupt
方法终止等待状态
不同点:
Object
中的wait
方法必须在同步代码块中使用,否则会抛出IllegalMonitorException
;而LockSupport
无需加锁,直接调用静态方法park
就可以使当前线程进入阻塞状态。Object
中wait
和notify
方法必须要按顺序调用,如果因为线程调度问题导致线程A先调用notify
方法而线程B后调用wait
方法,那么会使线程A永远处于WAIT
状态。对于LockSupport
而言则没有这种限制,如果有线程A首先调用了unpark
方法并传入了线程B的引用,然后线程B再调用了park
方法,那么线程B是不会进入等待状态的。- 调用
Object
的wait
方法后,可以调用该线程的interrupt
方法脱离等待状态并捕获InterruptedException
。而LockSupport
的并不能捕获InterruptedException
。
关于中断我们演示下:
LockSupport 中断测试
:
/**
* @author qiuguan
* @date 2022/12/02 23:22:15 星期五
*/
public class LockSupportTest {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "线程累了,不想工作了......");
//t2率先给了凭证,所以直接消耗掉凭证直接退出阻塞
LockSupport.park("t1线程累了,摆烂了");
System.out.println(Thread.currentThread().getName() + "线程又爬起来打工了......");
}, "t1");
t1.start();
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
//t1线程中断
t1.interrupt();
}
}
中断后直接停止阻塞,继续往下执行,并且不会捕获中断异常。
Object的wait()中断演示
:
/**
* @author qiuguan
* @date 2022/12/03 02:59:41 星期六
*/
public class ObjectWaitNotifyTest {
private static Object object = new Object();
public static void main(String[] args) {
Thread t = new Thread(() -> {
synchronized (object) {
try {
System.out.println(Thread.currentThread().getName() + "线程累了,不想搬砖了......");
object.wait();
System.out.println(Thread.currentThread().getName() + "线程又爬起来搬砖了......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1");
t.start();
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
}
}
中断后停止阻塞,并捕获中断异常。
好了,关于LockSupport的介绍就到这里吧。
限于作者水平,文中难免有错误之处,欢迎指正,勿喷,感谢感谢