前言
多线程编程时,为完成某个目的,线程和线程之间往往要进行一些沟通、协调来掌控各步骤的处理顺序,比如在处理异步任务回调时
针对线程之间的协调作业,有很多方式可以实现,本文就通过一些场景整体模拟下常用的线程协作方法,有遗漏再不断补充
与前文结合jvisualvm一次性看清线程状态内容大部分一致,本文重点关注方法使用而非线程状态,补充了一些方法
sleep
先来最简单常用的sleep方法,顾名思义,就是让线程睡一会并指定醒来时间,并且释放cpu资源
我们用sleep模拟一个孩子睡觉的过程
public class SleepTest {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "孩子");
thread.start();
TimeUnit.SECONDS.sleep(5);
System.out.println("线程的状态:" + thread.getState());
}
}
此时名为“孩子”的线程正在睡觉,对应的线程状态是TIMED_WAITING
使用sleep需要处理一个checked异常InterruptedException,这个接下来说
interrupt
interrupt字面意思上讲就是打断当前的干的事,只是修改了线程的一个中断标识(一个字段),换句话说,如果被打断的线程不理会,打断等于无效
那么interrupt有什么用呐?大部分情况是结合isInterrupted()或interrupted()使用,二者可以查看线程是否被打断,也就是说interrupt相当于发送一个打断信号给某线程,该线程通过isInterrupted可以获取打断信号,至于收到打断信号后如何处理线程己决定
我们用代码模拟一个家长喊在外面玩耍的孩子吃饭的故事
public class IsInterruptedTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("孩子正在玩...");
// 时刻关注自己是否被打断
while (!Thread.currentThread().isInterrupted()) {}
System.out.println("孩子回家吃饭");
}, "son");
thread.start();
TimeUnit.SECONDS.sleep(3);
System.out.println("孩子的状态:" + thread.getState());
System.out.println("家长喊孩子回家吃饭");
thread.interrupt();
}
}
输出:
孩子正在玩...
孩子的状态:RUNNABLE
家长喊孩子回家吃饭
孩子回家吃饭
在这里,主子线程完成了一个简单的协调
当然son线程也可以忽略interrupt信号继续的玩下去
以上代码使用的是isInterrupted()方法,同样也可以使用interrupted()方法,二者的区别是后者执行完后会擦拭掉中断标识
上面讲到的sleep()方法默认响应打断,而响应的方式就是抛出InterruptedException异常,这就是为什么我们使用sleep()一定要处理InterruptedException异常
我们用代码模拟一个家长叫睡觉的孩子起床的故事
public class InterruptedExceptionTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("孩子在睡觉...");
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
System.out.println("孩子起床吃饭: " + Thread.currentThread().isInterrupted());
}
}, "son");
thread.start();
TimeUnit.SECONDS.sleep(3);
System.out.println("家长喊孩子起床吃饭");
thread.interrupt();
while (true) {}
}
}
输出
孩子在睡觉...
家长喊孩子起床吃饭
孩子起床吃饭: false
最后一条出现false,说明sleep也会先擦除打断信息再抛出异常
join
join也是一种常用的线程协调方式,通过join某线程可以等待另一个线程执行结束
模拟一个家长等孩子起床一起吃饭的过程
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("孩子正睡觉...");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("孩子洗脸刷牙...");
int i = 0;
while (++i>0) {
new Object(); // 模拟刷牙过程
}
System.out.println("孩子起来了");
}, "son");
thread.start();
System.out.println("家长等孩子醒...");
thread.join();
System.out.println("家长孩子一起开始吃早饭");
}
}
输出
家长等孩子醒...
孩子正睡觉...
孩子洗脸刷牙...
孩子起来了
家长孩子一起开始早饭
join也响应interrupte,和sleep一样抛出异常InterruptedException
synchronized
这个作用就不多说了,模拟一下家长孩子早上抢着上厕所的场景
public class SyncTest {
public static void main(String[] args) throws InterruptedException {
Thread son = new Thread(() -> {
doRun();
}, "孩子");
son.start();
// 家长比孩子慢一秒开始抢厕所
Thread father = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
doRun();
}, "家长");
father.start();
}
public synchronized static void doRun() {
System.out.println(Thread.currentThread().getName() + "开始上厕所");
// 模拟蹲坑
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "上完");
}
}
输出
孩子开始上厕所
孩子上完
家长开始上厕所
家长上完
synchronized是不响应interrupte的
ReentrantLock
接下来看同样是锁工具的ReentrantLock,修改上面代码使用ReentrantLock
public class LockTest {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Thread son = new Thread(() -> {
lock.lock(); // 拿到锁不释放
doRun();
lock.unlock();
}, "孩子");
son.start();
Thread father = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
doRun();
lock.unlock();
}, "家长");
father.start();
}
public static void doRun() {
System.out.println(Thread.currentThread().getName() + "开始上厕所");
// 模拟蹲坑
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "上完");
}
}
输出
孩子开始上厕所
孩子上完
家长开始上厕所
家长上完
ReentrantLock的lock方法也不响应interrupt,但ReentrantLock很灵活,如果希望响应可以使用lockInterruptibly()方法
实际上ReentrantLock内部是使用Unsafe.park方法让线程进行等待的
Unsafe.park
这是一种让线程等待的方法,并需要其他线程通过unpark唤醒
依然模拟家长喊休息的孩子起来吃饭场景
public class ParkTest {
public static void main(String[] args) throws Exception {
// 获得unsafe
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
// 开始
Thread thread = new Thread(() -> {
System.out.println("孩子正在休息");
unsafe.park(false, 0);
System.out.println("孩子被唤醒: " + Thread.currentThread().isInterrupted());
}, "son");
thread.start();
TimeUnit.SECONDS.sleep(3);
System.out.println("孩子的状态:" + thread.getState());
System.out.println("家长喊孩子起来吃饭");
// thread.interrupt();
unsafe.unpark(thread);
}
}
输出
孩子正在休息
孩子的状态:WAITING
家长喊孩子起来吃饭
孩子被唤醒: false
把unsafe.unpark(thread)
改成thread.interrupt()
试试打断,输出
孩子正在休息
孩子的状态:WAITING
家长喊孩子起来吃饭
孩子被唤醒: true
可以看到unsafe.park响应interrupt,效果和unpark一样,且不清除打断标记
但问题来了为啥使用park的lock不响应interrupt?原因是lock的内部做了逻辑处理,被interrupt后继续调用unsafe.park
等待
wait¬ify
顾名思义,wait就是等待,notify就是通知,二者天生是配合使用的
wait和sleep、join都不一样,后二者是属于Thread对象的方法,而wait以及notify是任何对象都有的方法(即Object的方法)
wait 方法是释放对象锁并让当前线程等待在该对象上,直到另一个线程调用了该对象的 notify 或 notifyAll 方法之后,才能继续恢复执行
wait¬ify必须先获得对象锁才能调用(必须在synchronized下),也就是说只有获得对象的锁才有资格在对象上等待,也只有获得锁才有资格通知在对象上等待的线程
下面用代码模拟一个孩子等家长做蛋堡的场景
public class WaitTest {
public static Object o = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
synchronized (o) {
System.out.println("孩子正等待...");
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("孩子开始吃蛋堡");
}
}, "son");
thread.start();
TimeUnit.SECONDS.sleep(1);
synchronized (o) {
System.out.println("家长做蛋堡...");
TimeUnit.SECONDS.sleep(3);
System.out.println("孩子的状态:" + thread.getState());
System.out.println("家长做完喊孩子吃");
o.notifyAll();
// System.out.println("收拾桌子");
// TimeUnit.SECONDS.sleep(4);
// System.out.println("孩子的状态:" + thread.getState());
}
while (true) {}
}
}
输出
孩子正等待...
家长做蛋堡...
孩子的状态:WAITING
家长做完喊孩子吃
孩子开始吃蛋堡
以上代码o是一个空对象用来实现锁,它好比一个盘子,孩子和家长的一个约定做完蛋堡就放入盘子,首先孩子获得盘子,调用wait在盘子旁边等待,并交出盘子的控制权(调用wait会释放锁,收到notify时再重新尝试获得锁并继续执行代码)
这里有个小问题,wait¬ify必须在synchronized下执行,wait时会释放锁,那么执行notifyl也会立即释放锁吗,做个测试把上面代码的注释打开
synchronized (o) {
System.out.println("家长做蛋堡...");
TimeUnit.SECONDS.sleep(3);
System.out.println("孩子的状态:" + thread.getState());
System.out.println("家长做完喊孩子吃");
o.notifyAll();
System.out.println("收拾厨房");
TimeUnit.SECONDS.sleep(3);
System.out.println("孩子的状态:" + thread.getState());
}
输出
孩子正等待...
家长做蛋堡...
孩子的状态:WAITING
家长做完喊孩子吃
收拾厨房
孩子的状态:BLOCKED
孩子开始吃蛋堡
可以看到,家长做完喊孩子吃(调用notifyAll)后,孩子并没有马上开始吃,而是收拾家长完厨房后才开始吃,但孩子的状态由WAITING
变为BLOCKED
,证明收到notify消息是,孩子就已经不再wait了,而是尝试获取锁,也就是等在synchronized外,而最终家长的synchronized代码执行完毕才实际释放锁,孩子开始吃蛋堡
wait同样响应interrupte,响应方式和join,sleep一样是抛出InterruptedException异常
Condition
wait¬ify是在synchronized下使用的,而作更灵活的锁ReentrantLock却无法使用,所以在ReentrantLock下就有一个wait¬ify的替代品即:Condition
同样,我们使用Condition模拟wait¬ify一样的场景:一个孩子等家长做蛋堡的场景
public class ConditionTest {
// 模拟一个盘子
private static final Lock lock = new ReentrantLock();
// 食物准备好了放入盘子的条件
private static final Condition foodReadycondition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread son = new Thread(() -> {
// 获取盘子控制权
lock.lock();
System.out.println("孩子正等待...");
try {
// 等待食物就绪
foodReadycondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("孩子开始吃蛋堡");
// 释放盘子
lock.unlock();
}, "son");
son.start();
TimeUnit.SECONDS.sleep(1);
// 获取盘子控制权
lock.lock();
System.out.println("家长做蛋堡...");
TimeUnit.SECONDS.sleep(3);
System.out.println("孩子的状态:" + son.getState());
System.out.println("家长做完喊孩子吃");
// 发出盘子准备好食物的信号
foodReadycondition.signal();
// 交出盘子控制权
lock.unlock();
while (true) {}
}
}
可以看到,与wait¬ify的使用基本一致,相当于ReentrantLock下的wait¬ify(await&signal
)
和ReentrantLock对比synchronized一样,await&signal比wait¬ify功能更强更灵活,也体现了作者Doug Lea的强大
网友评论