文章目录
一、线程常见方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新的线程运行 run 方法中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现IllegalThreadStateException | |
run() | 新线程启动后会调用的方法 | 如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待 n毫秒 | ||
getId() | 获取线程长整型的 id | id 唯一 | |
getName() | 获取线程名 | ||
setName(String) | 修改线程名 | ||
getPriority() | 获取线程优先级 | ||
setPriority(int) | 修改线程优先级 | java中规定线程优先级是1~10 的整数,较大的优先级能提高该线程被 CPU 调度的机率 ,默认优先级是5 | |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING,TIMED_WAITING, TERMINATED | |
isAlive() | 线程是否存活(还没有运行完毕) | ||
isInterrupted() | 判断是否被打断 | 不会清除 打断标记 | |
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置 打断标记;park 的线程被打断,也会设置打断标记 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除 打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时让出 cpu的时间片给其它线程 | |
yield() | static | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
不推荐的方法
- 这些方法已过时,容易破坏同步代码块,造成线程死锁
方法名 | 功能说明 |
---|---|
stop() | 停止线程运行 |
suspend() | 挂起(暂停)线程运行 |
resume() | 恢复线程运行 |
1. start 与 run
- 调用 run
- 程序仍在 main 线程运行,FileReader.read() 方法调用还是同步的
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
FileReader.read(Constants.PDF_FULL_PATH);
}
};
t1.run();
log.debug("do other things...");
}
10:14:02.886 c.Test4 [main] - running...
10:14:02.890 c.FileReader [main] - read [Java核心技术 卷2 高级特性 原书第10版.pdf] start ...
10:14:03.703 c.FileReader [main] - read [Java核心技术 卷2 高级特性 原书第10版.pdf] end ... cost: 813 ms
10:14:03.704 c.Test4 [main] - do other things...
- 调用 start
- 程序在 t1 线程运行,FileReader.read() 方法调用是异步的
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
FileReader.read(Constants.PDF_FULL_PATH);
}
};
t1.start();
log.debug("do other things...");
}
10:15:08.613 c.Test4 [main] - do other things...
10:15:08.613 c.Test4 [t1] - running...
10:15:08.618 c.FileReader [t1] - read [Java核心技术 卷2 高级特性 原书第10版.pdf] start ...
10:15:09.422 c.FileReader [t1] - read [Java核心技术 卷2 高级特性 原书第10版.pdf] end ... cost: 804 ms
小结
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
start 源码:
public synchronized void start() {
group.add(this);
boolean started = false;
try {
// 调的是start0()
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
// native 方法start0() 底层是C++实现的
private native void start0();
java语言本身底层是C++语言,只是在C++基础上进行了简化
可以通过OpenJDK查看底层源码
java线程是通过start方法启动执行的,主要内容在native方法start0中
OpenJDK与JNI一般是一一对应的,Thread.java对应的就是Thread.c
start0其实就是JVM_StartThread
在jvm.cpp中有JVM_StartThread的实现
在thread.cpp中有Thread::start的实现
最终由操作系统开启了一个线程
2. sleep 与 yield
- sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行,需要等到CPU分配时间片
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
log.debug("t1 state: {}", t1.getState());
try {
// 让main线程休眠是为了能获取到 t1 休眠后的状态
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state: {}", t1.getState());
}
10:42:19.292 c.Test6 [main] - t1 state: RUNNABLE
10:42:19.810 c.Test6 [main] - t1 state: TIMED_WAITING
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("enter sleep...");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
log.debug("wake up...");
e.printStackTrace();
}
}
};
t1.start();
TimeUnit.SECONDS.sleep(1);
log.debug("interrupt...");
t1.interrupt();
}
10:49:41.025 c.Test7 [t1] - enter sleep...
10:49:42.026 c.Test7 [main] - interrupt...
10:49:42.026 c.Test7 [t1] - wake up...
java.lang.InterruptedException: sleep interrupted
Disconnected from the target VM, address: '127.0.0.1:64497', transport: 'socket'
at java.lang.Thread.sleep(Native Method)
at cn.itcast.test.Test7$1.run(Test7.java:14)
- yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器,Runnable状态的线程仍然有机会被执行,所以yield不一定能让出时间片
3. 线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
4. 防止 CPU 占用100%
(1)sleep 实现
- 在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权给其他程序
while(true) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 可以用 wait 或 Lock + 条件变量 达到类似的效果
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
- sleep 适用于无需锁同步的场景
(2)wait 实现
synchronized(锁对象) {
while(条件不满足) {
try {
锁对象.wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
// do sth...
}
(3)Lock + 条件变量实现
lock.lock();
try {
while(条件不满足) {
try {
条件变量.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// do sth...
} finally {
lock.unlock();
}
5. join 同步等待线程运行结束
(1)同步等待一个结果
- 不加 join
@Slf4j(topic = "c.Test10")
public class Test10 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
},"t1");
t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}
}
11:26:10.757 c.Test10 [main] - 开始
11:26:10.887 c.Test10 [t1] - 开始
11:26:10.887 c.Test10 [main] - 结果为:0
11:26:10.890 c.Test10 [main] - 结束
11:26:11.899 c.Test10 [t1] - 结束
分析
- 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
- 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0
解决方法
-
用 join,加在 t1.start() 之后即可
-
加 join
@Slf4j(topic = "c.Test10")
public class Test10 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
},"t1");
t1.start();
t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
}
11:29:15.751 c.Test10 [main] - 开始
11:29:15.832 c.Test10 [t1] - 开始
11:29:16.843 c.Test10 [t1] - 结束
11:29:16.843 c.Test10 [main] - 结果为:10
11:29:16.846 c.Test10 [main] - 结束
图解
(2)同步等待多个结果
@Slf4j(topic = "c.TestJoin")
public class TestJoin {
static int r = 0;
static int r1 = 0;
static int r2 = 0;
public static void sleep(int i) {
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
Thread t2 = new Thread(() -> {
sleep(2);
r2 = 20;
});
t1.start();
t2.start();
long start = System.currentTimeMillis();
log.debug("join begin");
t1.join();
log.debug("t1 join end");
t2.join();
log.debug("t2 join end");
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}
11:44:53.981 c.TestJoin [main] - join begin
11:44:54.985 c.TestJoin [main] - t1 join end
11:44:55.987 c.TestJoin [main] - t2 join end
11:44:55.987 c.TestJoin [main] - r1: 10 r2: 20 cost: 2009
分析
- t1.join():等待 t1 时, t2 并没有停止, 也在运行
- t2.join():1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s
图解
(3)限定时间的等待
- 等够时间的join
@Slf4j(topic = "c.TestJoin")
public class TestJoin {
static int r = 0;
static int r1 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(2);
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
log.debug("join begin");
t1.join(3000);
long end = System.currentTimeMillis();
log.debug("r1: {} cost: {}", r1, end - start);
}
}
11:57:28.643 c.TestJoin [main] - join begin
11:57:30.652 c.TestJoin [main] - r1: 10 cost: 2011
- 没等够时间的join
@Slf4j(topic = "c.TestJoin")
public class TestJoin {
static int r = 0;
static int r1 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(2);
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
log.debug("join begin");
t1.join(1000);
long end = System.currentTimeMillis();
log.debug("r1: {} cost: {}", r1, end - start);
}
}
12:00:43.072 c.TestJoin [main] - join begin
12:00:44.088 c.TestJoin [main] - r1: 0 cost: 1019
6. interrupt
(1)打断 sleep,wait,join 的线程
- 这几个方法都会让线程进入阻塞状态
- 打断阻塞的线程, 会清空打断状态,以 sleep 为例
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
TimeUnit.SECONDS.sleep(3); // wait, join 同理
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
TimeUnit.SECONDS.sleep(1);
log.debug("interrupt");
t1.interrupt();
log.debug("打断标记:{}", t1.isInterrupted());
}
12:14:15.286 c.Test11 [t1] - sleep...
12:14:16.293 c.Test11 [main] - interrupt
12:14:16.293 c.Test11 [main] - 打断标记:false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at cn.itcast.test.Test11.lambda$main$0(Test11.java:14)
at java.lang.Thread.run(Thread.java:748)
(2)打断正常运行的线程
- 打断正常运行的线程, 不会清空打断状态
- 只是告诉被打断的线程,想要打断它,被打断的线程自己决定是否继续运行或中断
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("被打断了, 退出循环");
break;
}
}
}, "t1");
t1.start();
TimeUnit.SECONDS.sleep(1);
log.debug("interrupt");
t1.interrupt();
}
12:23:09.177 c.Test12 [main] - interrupt
12:23:09.181 c.Test12 [t1] - 被打断了, 退出循环
(3)打断 park 线程
- 打断 park 线程, 不会清空打断状态
- 打断park后,打断标记变为true,调用isInterrupted()方法判断打断状态后,再调用LockSupport.park()方法时,线程不会被阻塞
@Slf4j(topic = "c.Test14")
public class Test14 {
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park(); // 阻塞,被打断后,打断标记为true再往下执行
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
// 打断标记为true时,park不会阻塞
LockSupport.park();
log.debug("unpark...");
}, "t1");
t1.start();
sleep(1);
t1.interrupt();
}
public static void main(String[] args) throws InterruptedException {
test3();
}
}
14:52:24.612 c.Test14 [t1] - park...
14:52:25.621 c.Test14 [t1] - unpark...
14:52:25.621 c.Test14 [t1] - 打断状态:true
14:52:25.623 c.Test14 [t1] - unpark...
- 打断park后,调用Thread.interrupted()方法判断完打断状态后,接着会清除打断标记,再调用LockSupport.park()时,又会被阻塞
@Slf4j(topic = "c.Test14")
public class Test14 {
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
// log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
log.debug("打断状态:{}", Thread.interrupted());
log.debug("Thread.interrupted()会清除打断标记,打断标记为{}, park在此处阻塞", Thread.currentThread().isInterrupted());
LockSupport.park();
log.debug("unpark...");
}, "t1");
t1.start();
sleep(1);
t1.interrupt();
}
public static void main(String[] args) throws InterruptedException {
test3();
}
}
14:57:35.569 c.Test14 [t1] - park...
14:57:36.579 c.Test14 [t1] - unpark...
14:57:36.579 c.Test14 [t1] - 打断状态:true
14:57:36.581 c.Test14 [t1] - Thread.interrupted()会清除打断标记,打断标记为false, park在此处阻塞
7. 守护线程
- 默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
log.debug("结束");
}, "t1");
// 设置守护线程
t1.setDaemon(true);
t1.start();
// main线程结束,守护线程立刻结束
Thread.sleep(1000);
log.debug("结束");
}
15:11:38.839 c.Test15 [main] - 结束
注意
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
二、wait & notify
1. 小故事理解 wait & notify
-
由于条件不满足,小南不能继续进行计算
-
但小南如果一直占用着锁,其它人就得一直阻塞,效率太低
-
于是老王单开了一间休息室(调用 wait 方法),让小南到休息(WaitSet)等着去了,但这时锁释放开,其它人可以由老王随机安排进屋
-
直到小M将烟送来,大叫一声 [ 你的烟到了 ] (调用 notify 方法)
-
小南于是可以离开休息室,重新进入竞争锁的队列
2. wait & notify 原理
- Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
- BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
- BLOCKED 线程会在 Owner 线程释放锁时被唤醒
- WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时被唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
3. wait & notify API
- obj.wait() 让进入 object 监视器的线程到 waitSet 等待
- wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到被 notify 为止
- wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify 提前唤醒
- obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1被唤醒,执行其它代码....");
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t2被唤醒,执行其它代码....");
}
},"t2").start();
// 主线程0.5秒后执行
sleep(0.5);
log.debug("唤醒 obj 上所有线程");
synchronized (obj) {
// obj.notify(); // 唤醒obj上一个线程
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
14:44:13.682 c.TestWaitNotify [t1] - 执行....
14:44:13.684 c.TestWaitNotify [t2] - 执行....
14:44:14.194 c.TestWaitNotify [main] - 唤醒 obj 上所有线程
14:44:14.194 c.TestWaitNotify [t2] - t2被唤醒,执行其它代码....
14:44:14.194 c.TestWaitNotify [t1] - t1被唤醒,执行其它代码....
4. sleep 与 wait 区别
- (1)sleep 是 Thread 方法,而 wait 是 Object 的方法
- (2) sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用,并且需要先wait再notify
- (3) sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁
- (4)它们状态都是 TIMED_WAITING
5. wait & notify 使用套路
// wait 线程
new Thread(() -> {
synchronized(lock) {
// 用 while + wait,当条件不成立时,可以再次 wait
while(条件不成立) {
lock.wait();
}
// 被唤醒后再干活
}
}, "t1").start();
// notifyAll 线程
new Thread(() -> {
synchronized(lock) {
lock.notifyAll();
}
}, "t2").start();
案例
@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep5 {
static final Object room = new Object(); // 对象锁最好使用final修饰,防止改变
static boolean hasCigarette = false; // 是否有烟
static boolean hasTakeout = false; // 是否有外卖
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
}
20:14:17.024 c.TestCorrectPosture [小南] - 有烟没?[false]
20:14:17.042 c.TestCorrectPosture [小南] - 没烟,先歇会!
20:14:17.042 c.TestCorrectPosture [小女] - 外卖送到没?[false]
20:14:17.043 c.TestCorrectPosture [小女] - 没外卖,先歇会!
20:14:18.029 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
20:14:18.029 c.TestCorrectPosture [小女] - 外卖送到没?[true]
20:14:18.029 c.TestCorrectPosture [小女] - 可以开始干活了
20:14:18.029 c.TestCorrectPosture [小南] - 没烟,先歇会!
三、park & unpark
1. 基本使用
- 它们是 LockSupport 类中的方法,LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
- LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的是Unsafe中的native代码
- LockSupport不用加锁,程序性能好
- 可以调换先后顺序(先park后unpark 或者 先unpark后park)
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
案例一:先 park 再 unpark
@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
// 先 park 再 unpark, t1线程休眠时间小于main线程休眠时间
sleep(2);
log.debug("t1线程先park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(3);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
11:12:17.676 c.TestParkUnpark [t1] - start...
11:12:19.681 c.TestParkUnpark [t1] - t1线程先park...
11:12:20.676 c.TestParkUnpark [main] - unpark...
11:12:20.676 c.TestParkUnpark [t1] - resume...
案例二:先 unpark 再 park
- 先 unpark 后,就算再 park,也不会阻塞住
@Slf4j(topic = "c.TestParkUnpark")
public class TestParkUnpark {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
// 先 unpark 再 park , main线程休眠时间小于t1线程休眠时间
sleep(1);
log.debug("main线程先unpark...");
LockSupport.unpark(t1);
}
}
11:09:13.101 c.TestParkUnpark [t1] - start...
11:09:14.111 c.TestParkUnpark [main] - main线程先unpark...
11:09:15.113 c.TestParkUnpark [t1] - park...
11:09:15.113 c.TestParkUnpark [t1] - resume...
2. 特点
与 Object 的 wait & notify 相比
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不需要
- park & unpark 是以线程为单位来【阻塞】和【唤醒】线程(精确唤醒对应线程),而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
3. 原理
-
每个线程都有自己的一个 Parker 对象(类似许可证),由三部分组成 _counter (计数器), _cond(条件变量) 和 _mutex(互斥锁)。Parker对象是底层C语言实现的。
-
调用park时,首先会检查需不需要阻塞:
_counter = 1 运行,_counter = 0 阻塞。 -
调用unpark时,会让_counter = 1:
- 如果线程此时正在阻塞,则唤醒线程继续运行;
- 如果线程此时没有阻塞,则下次调用park时,发现_counter = 1,线程不会阻塞而是继续运行;
-
多次调用 unpark,_counter 仍为1,不会自增
-
与Semaphore不同的是,_counter 累加上限是1
(1)分析 先 park 再 unpark
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
- 线程进入 _cond 条件变量阻塞
- 设置 _counter = 0
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 唤醒 _cond 条件变量中的 Thread_0
- Thread_0 恢复运行
- 设置 _counter 为 0
(2)分析 先 unpark 再 park
- 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
- 当前线程调用 Unsafe.park() 方法
- 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
- 设置 _counter 为 0
①. 为什么可以先唤醒线程后阻塞线程?
- 因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞
②. 为什么唤醒两次后再阻塞两次,但最终结果还会阻塞线程?
- 因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行