【21.4.3】中断
1、Thread类包含 interrupt方法,可以终止被阻塞的任务。这个方法将设置线程的中断状态。 如果一个线程被阻塞,或者视图执行一个阻塞操作,那么设置这个线程的中断状态将抛出 InterruptedException异常。当抛出该异常或该任务调用了 Thread.interrupted() 方法时, 中断状态将被复位,设置为true;
2、如果调用 Executor上调用 shutdownNow方法,那么它将发送一个 interrupte方法调用给他启动的所有线程。
通过调用submit()而不是executor() 来启动任务,就可以持有该任务的上下文。submit()方法将返回一个泛型Future>, 持有这个Future的关键在于,你可以调用该对象的cancle() 方法, 并因此可以使用他来中断某个特定任务。如果把true传给 cancel方法,他就会拥有在该线程上调用 interrupt方法以停止这个线程的权限。
/**
* 中断由线程池管理的某个线程
*/
public class Interrupting {
private static ExecutorService exec = Executors.newCachedThreadPool();// 线程池
static void test(Runnable r) throws InterruptedException {
// 使用 ExecutorService.submit() 而不是 ExecutorService.execute()方法来启动任务,就可以持有该任务的上下文 ,submit() 方法返回 Future 对象
// exec.execute(r); 不用 execute() 方法
Future> f = exec.submit(r);
TimeUnit.MILLISECONDS.sleep(1000); // 睡眠1秒
System.out.println("interrupting " + r.getClass().getName()); // 正在中断某个线程
// 调用Future.cancel() 方法来中断某个特定任务
// 把true传给cancel() 方法,该方法就拥有在该线程上调用interrupt() 方法以停止这个线程的权限
// cancel 是一种中断由 Executor启动的单个线程的方式
f.cancel(true);
System.out.println("interrupt sent to " + r.getClass().getName()); // 中断信号发送给线程
System.out.println("====================================== seperate line ==================================== ");
}
public static void main(String[] args) throws Exception {
test(new SleepBlocked());
test(new IOBlocked(System.in));
test(new SynchronizedBlocked());
TimeUnit.SECONDS.sleep(3);
System.out.println("aborting with System.exit(0)");
System.exit(0);// 终止当前虚拟机进程,所以有部分打印信息无法没有正常输出
}
}
// 睡眠式阻塞线程, 可中断的阻塞
class SleepBlocked implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(3);// 睡眠3秒
} catch (InterruptedException e ) { // 捕获中断异常
System.out.println("interrupted exception in SleepBlocked ");
}
System.out.println("exiting SleepBlocked.run()");
}
}
// IO式阻塞线程 , 不可中断的阻塞
class IOBlocked implements Runnable {
private InputStream in;
public IOBlocked(InputStream is) {
in = is;
}
@Override
public void run() {
try {
System.out.println("waiting for read();");
in.read(); // 等待输入流输入数据
} catch (IOException e) { // IO 异常 , 但执行结果没有报 IO 异常
if (Thread.currentThread().isInterrupted()) {
System.out.println("interrupted from blocked IO");
} else {
throw new RuntimeException();
}
}
System.out.println("exiting IOBlocked.run()");
}
}
// 线程同步式阻塞,不可中断的阻塞
class SynchronizedBlocked implements Runnable {
public synchronized void f() {
while(true) {
Thread.yield(); // 让出cpu时间片
}
}
public SynchronizedBlocked() { // 构造器开启一个线程
new Thread() { // 匿名线程调用f() 方法,获取 SynchronizedBlocked 对象锁,且不释放;其他线程只能阻塞
public void run() {
f();// f() 为同步方法
}
}.start();
}
@Override
public void run() {
System.out.println("trying to call 同步f()");
f(); // 调用f() 同步方法 , 让出cpu时间片
System.out.println("exiting SynchronizedBlocked.run()"); // 这里永远不会执行
}
}
/*interrupting diy.chapter21.SleepBlocked
interrupt sent to diy.chapter21.SleepBlocked
====================================== seperate line ====================================
interrupted exception in SleepBlocked
exiting SleepBlocked.run()
waiting for read();
interrupting diy.chapter21.IOBlocked
interrupt sent to diy.chapter21.IOBlocked
====================================== seperate line ====================================
trying to call 同步f()
interrupting diy.chapter21.SynchronizedBlocked
interrupt sent to diy.chapter21.SynchronizedBlocked
====================================== seperate line ====================================
aborting with System.exit(0)
*/
小结:
序号 | 阻塞方式 | 是否可以中断 |
1 | sleep | 是 |
2 | IO | 否 |
3 | synchronized获取锁 | 否 |
所以,对于IO操作线程或synchronized操作的线程,其具有锁住多线程程序的潜在危险。
如何解决呢? 关闭任务在其上发生阻塞的底层资源;
/**
* 无法中断线程,但可以关闭任务阻塞所依赖的资源。
* 这里只能够中断 基于socket输入流的io线程,因为socket输入流可以关闭;
* 但无法中断基于系统输入流的io线程,因为系统输入流无法关闭;
*/
public class CloseResource {
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool(); // 线程池
ServerSocket server = new ServerSocket(8080); // 服务端套接字
InputStream socketInput = new Socket("localhost", 8080).getInputStream();
/* 启动线程 */
exec.execute(new IOBlocked(socketInput));
exec.execute(new IOBlocked(System.in));
TimeUnit.MILLISECONDS.sleep(1000); // 睡眠1秒
System.out.println("shutting down all threads");
exec.shutdownNow(); // 发送一个interrupte() 信号给exec启动的所有线程
TimeUnit.SECONDS.sleep(1); // 睡眠1秒
System.out.println("closing " + socketInput.getClass().getName());
socketInput.close(); // 关闭io线程依赖的资源
TimeUnit.SECONDS.sleep(1);
System.out.println("closing " + System.in.getClass().getName());
System.in.close(); // 关闭io线程依赖的资源
}
}
/**
waiting for read();
waiting for read();
shutting down all threads
closing java.net.SocketInputStream
interrupted from blocked IO
exiting IOBlocked.run()
closing java.io.BufferedInputStream
*/
3、nio类提供了更人性化的IO中断,被阻塞的nio通道会自动响应中断;
/**
* page 698
* nio中断
*/
public class NIOInterruption {
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool(); // 线程池
ServerSocket ss = new ServerSocket(8080); // 服务器套接字
// InetAddress:类的主要作用是封装IP及DNS,
// InetSocketAddress类主要作用是封装端口 他是在在InetAddress基础上加端口,但它是有构造器的。
InetSocketAddress isa = new InetSocketAddress("localhost", 8080);
SocketChannel sc1 = SocketChannel.open(isa); // 套接字通道
SocketChannel sc2 = SocketChannel.open(isa); // 套接字通道
// 使用 ExecutorService.submit() 而不是 ExecutorService.execute()方法来启动任务,就可以持有该任务的上下文 ,submit() 方法返回 Future 对象
Future> f = exec.submit(new NIOBlocked(sc1));// 以submit方式启动线程
exec.execute(new NIOBlocked(sc2)); // 以 execute方式启动线程
exec.shutdown(); // 关闭所有线程
TimeUnit.SECONDS.sleep(1); // 睡眠1秒
// 调用Future.cancel() 方法来中断某个特定任务
// 把true传给cancel() 方法,该方法就拥有在该线程上调用interrupt() 方法以停止这个线程的权限
// cancel 是一种中断由 Executor启动的单个线程的方式
f.cancel(true); //
sc2.close(); //
}
}
// NIO 新io式阻塞
class NIOBlocked implements Runnable {
private final SocketChannel sc;
public NIOBlocked(SocketChannel sc) {
this.sc = sc;
}
@Override
public void run() {
try {
System.out.println("waiting for read() in " + this);
sc.read(ByteBuffer.allocate(1));
} catch (ClosedByInterruptException e1) {
System.out.println("ClosedByInterruptException, this = " + this);
} catch (AsynchronousCloseException e2) {
System.out.println("AsynchronousCloseException, this = " + this);
} catch (IOException e3) {
throw new RuntimeException(e3);
}
System.out.println("exiting NIOBlocked.run() " + this);
}
}
/**
waiting for read() in diy.chapter21.NIOBlocked@3856c761
waiting for read() in diy.chapter21.NIOBlocked@55de2e48
ClosedByInterruptException, this = diy.chapter21.NIOBlocked@55de2e48
exiting NIOBlocked.run() diy.chapter21.NIOBlocked@55de2e48
AsynchronousCloseException, this = diy.chapter21.NIOBlocked@3856c761
exiting NIOBlocked.run() diy.chapter21.NIOBlocked@3856c761
*/
4、被互斥所阻塞: 一个任务能够调用在同一个对象中的其他的 synchronized 方法,而这个任务已经持有锁了 ;
/**
* 被互斥所阻塞
* 同步方法f1 和 f2 相互调用直到 count为0
* 一个任务应该能够调用在同一个对象中的其他 synchronized 方法,因为这个任务已经获取这个对象的锁
* 2020/04/16
*/
public class MultiLock {
public synchronized void f1(int count) { // 同步方法 f1
if(count-- > 0) {
System.out.println("f1() calling f2() with count = " + count);
f2(count); // 调用 f2
}
}
public synchronized void f2(int count) { // 同步方法f2
if(count-- > 0) {
System.out.println("f2() calling f1() with count = " + count);
f1(count); // 调用f1
}
}
public static void main(String[] args) {
final MultiLock multiLock = new MultiLock();
new Thread() {
public void run() {
multiLock.f1(5);
}
}.start();
}
}
/**
f1() calling f2() with count = 4
f2() calling f1() with count = 3
f1() calling f2() with count = 2
f2() calling f1() with count = 1
f1() calling f2() with count = 0
*/
5、java se5 并发类库中添加了一个特性,在 ReentrantLock 可重入锁上阻塞的任务具备可以被中断的能力;
/**
* 可重入锁的可中断式加锁
* page 700
*/
public class Interrupting2 {
public static void main(String[] args) throws Exception {
Thread t = new Thread(new Blocked2());
t.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("issuing t.interrupt()"); // 2
t.interrupt(); // 中断线程
}
}
/**
* 阻塞互斥量
*/
class BlockedMutex {
private Lock lock = new ReentrantLock(); // 可重入锁
public BlockedMutex() {
lock.lock(); // 构造器即加锁,且从不会释放锁
}
public void f() {
try {
lock.lockInterruptibly(); // 可中断式加锁
System.out.println("lock acquired in f()");
} catch(InterruptedException e) {
System.out.println("interrupted from lock acquisition in f()"); // 3 可中断阻塞,捕获中断异常
}
}
}
class Blocked2 implements Runnable {
BlockedMutex blocked = new BlockedMutex();
@Override
public void run() {
System.out.println("waiting for f() in Blocked Mutex"); // 1
blocked.f();
System.out.println("broken out of blocked call"); // 4
}
}
/**
* waiting for f() in Blocked Mutex
issuing t.interrupt()
interrupted from lock acquisition in f()
broken out of blocked call
*/
【21.4.4】检查中断
1、在线程上调用 interrupt方法去中断线程执行时,能够中断线程的前提是: 任务要进入到阻塞操作中,已经在阻塞操作内部;否则,调用 interrupt方法是无法中断线程的;需要通过其他方式;
其他方式是: 由中断状态来表示, 其状态可以通过调用 interrupt 来设置。通过 Thread.interrupted() 来检查中断 。
/**
* 通过 Thread.interrupted() 来检查中断
* page 701
*/
public class InterruptingIdiom {
public static void main(String[] args) throws Exception {
if(args.length != 1) {
System.out.println("InterruptingIdiom-傻瓜式中断");
}
Thread t = new Thread(new Blocked3());
t.start();
TimeUnit.SECONDS.sleep(3); // 睡眠
t.interrupt(); // 中断
}
}
class NeedsCleanup {
private final int id;
public NeedsCleanup(int id) {
this.id = id;
System.out.println("NeedsCleanup " + id);
}
public void cleanup() {
System.out.println("clean up " +id);
}
}
/**
* 在run()方法中创建的 NeedsCleanup 资源都必须在其后面紧跟 try-finally 子句,
* 以确保 清理资源方法被调用
*/
class Blocked3 implements Runnable {
private volatile double d = 0.0;
@Override
public void run() {
try {
int index = 1;
// interrupted方法来检查中断状态
while(!Thread.interrupted()) { // 只要当前线程没有中断
System.out.println("========== 第 " + index++ + " 次循环 ==========");
NeedsCleanup n1 = new NeedsCleanup(1);
try {
System.out.println("sleeping-睡眠一秒");
TimeUnit.SECONDS.sleep(1);
NeedsCleanup n2 = new NeedsCleanup(2);
try {
System.out.println("calculating-高强度计算");
for (int i=1; i<250000; i++) {
d = d + (Math.PI + Math.E) / d;
}
System.out.println("finished time-consuming operation 完成耗时操作.");
} finally {
n2.cleanup(); // 清理
}
} finally{
n1.cleanup(); // 清理
}
}
System.out.println("exiting via while() test-从while循环退出 "); // 从while循环退出
} catch (InterruptedException e) {
System.out.println("exiting via InterruptedException-从中断InterruptedException退出 "); // 从中断退出
}
}
}
/**
*
InterruptingIdiom-傻瓜式中断
========== 第 1 次循环 ==========
NeedsCleanup 1
sleeping-睡眠一秒
NeedsCleanup 2
calculating-高强度计算
finished time-consuming operation 完成耗时操作.
clean up 2
clean up 1
========== 第 2 次循环 ==========
NeedsCleanup 1
sleeping-睡眠一秒
NeedsCleanup 2
calculating-高强度计算
finished time-consuming operation 完成耗时操作.
clean up 2
clean up 1
========== 第 3 次循环 ==========
NeedsCleanup 1
sleeping-睡眠一秒
clean up 1
exiting via InterruptedException-从中断InterruptedException退出
*/
【21.5】线程间的协作
1、当任务协作时,关键问题是任务间的握手。握手可以通过 Object.wait() Object.notify() 方法来安全实现。当然了 java se5 的并发类库还提供了具有 await() 和 signal() 方法的Condition对象;
【21.5.1】wait()方法与notifyAll() 方法
1、wait() 方法会在等待外部世界产生变化的时候将任务挂起,并且只有在 nofity() 或notifyall() 发生时,即表示发生了某些感兴趣的事务,这个任务才会被唤醒去检查锁产生的变化。wait()方法提供了一种在任务之间对活动同步的方式。
还有,调用wait() 方法将释放锁,意味着另一个任务可以获得锁,所以该对象上的其他synchronized方法可以在线程A wait期间,被其他线程调用;
2、有两种形式的 wait() 调用
形式1: wait方法接收毫秒数作为参数,在wait()期间对象锁是释放的;通过 notify() notifyAll() 方法,或者时间到期后,从 wait() 恢复执行;
形式2:wait方法不接受任何参数,这种wait将无线等待下去,直到线程接收到 notify或 notifyAll方法;
补充1:wait方法,notify方法, notifyAll方法,都是基类Object的一部分,因为这些方法操作的锁也是对象的一部分,而所有对象都是OBject的子类;
补充2:实际上,只能在同步控制方法或同步控制块里调用 wait, notify, notifyAll方法(因为不操作锁,所有sleep方法可以在非同步控制方法里调用)。如果在非同步方法中调用 wait, notify, notifyAll方法, 编译可以通过,但运行就报 IllegalMonitorStateException 异常,异常意思是: 在调用wait, notify, notifyAll方法前,必须获取对象的锁;
(干货——只能在同步控制方法或同步控制块里调用 wait, notify, notifyAll方法)
【荔枝】涂蜡与抛光: 抛光任务在涂蜡完成之前,是不能执行其工作的;而涂蜡任务在涂另一层蜡之前,必须等待抛光任务完成;
抛光 WaxOn, WaxOff, 使用了wait和notifyAll方法来挂起和重启这些任务;
/**
* 汽车上蜡与抛光
* (抛光任务在涂蜡完成之前,是不能执行其工作的;而涂蜡任务在涂另一层蜡之前,必须等待抛光任务完成;)
* page 705
*/
public class WaxOMatic {
public static void main(String[] args) throws Exception {
Car car = new Car();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new WaxOff(car)); // 先上蜡
exec.execute(new WaxOn(car)); // 后抛光
TimeUnit.SECONDS.sleep(1); // 睡眠5秒
exec.shutdown(); // 线程池关闭
}
}
// 汽车
class Car {
private boolean waxOn = false; // 是否上蜡
// 已上蜡
/**
* notifyAll() 和 wait() 方法只能在 synchronized方法或synchronized块中执行,因为获取或释放锁
*/
public synchronized void waxed() { // 上蜡
waxOn = true;
notifyAll(); // 唤醒所有调用 wait() 方法锁阻塞的线程
// 为了使该任务从 wait() 中唤醒,线程必须重新获得之前进入wait()时释放的锁。
// 在这个锁变得可用之前,这个任务是不会被唤醒的。
}
public synchronized void buffed() { // 抛光
waxOn = false;
notifyAll();
}
public synchronized void waitForWaxing() throws InterruptedException { // 等待上蜡
while (waxOn == false) { // 若没有上蜡,则等待
wait(); // 线程被挂起, 当前线程持有的car对象锁被释放
}
}
public synchronized void waitForBuffing() throws InterruptedException { // 等待抛光
while(waxOn == true) { // 若已上蜡,则等待抛光
wait(); // 线程被挂起, 当前线程持有的car对象锁被释放
}
}
}
class WaxOn implements Runnable { // 上蜡线程(本线程先执行第1次上蜡,等待抛光,抛光线程第1次执行抛光后,本线程执行第2次上蜡......)
private Car car;
public WaxOn(Car c) {
this.car = c;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
System.out.println("wax on !");
TimeUnit.MILLISECONDS.sleep(200);
car.waxed(); // 先上蜡完成 (把waxOn设置为true),唤醒等待上蜡的线程
car.waitForBuffing(); // 再等待抛光,当waxOn为ture,则抛光线程一直等待
}
} catch (InterruptedException e ) {
System.out.println("exiting via interrupt");
}
System.out.println("ending wax on task");
}
}
class WaxOff implements Runnable { // 抛光线程(本线程先等待上蜡,上蜡线程第1次执行后,本线程立即执行第1次抛光,接着本线程等待第2次上蜡......)
private Car car;
public WaxOff(Car c) {
this.car = c;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
car.waitForWaxing(); // 先等待上蜡 , 当waxOn为false,则上蜡线程一直等待
System.out.println("wax off !"); //
TimeUnit.MILLISECONDS.sleep(200);
car.buffed(); // 抛光完成后,把waxOn设置为false,唤醒等待抛光的线程
}
} catch (InterruptedException e ) {
System.out.println("exiting via interrupt");
}
System.out.println("ending wax off task");
}
}
/**
* wax on !
wax off !
wax on !
wax off !
......
*/
补充:前面的实例强调必须用一个检查感兴趣的条件的while循环包围wait方法。这很重要,因为:(为啥要用while包裹wait呢)
前面的示例强调必须用一个检查感兴趣的条件的while循环包围wait()。这很重要,原因如下:
原因1:可能有多个任务出于相同的原因在等待同一个锁,而第一个唤醒任务可能会改变这种状况;如果属于这种情况,那么任务应该被再次挂起,直到其感兴趣的条件发生变化;
原因2:在本任务从其 wait()中被唤醒的时刻,有可能会有某个其他任务已经做出了改变,从而使得本任务在此时不能执行,或者执行其操作已显得无关紧要;此时,应该再次执行wait()将其重新挂起;
(个人理解——比如有2个任务A,B都在等待资源R可用而阻塞,当R可用时,任务A和B均被唤醒,但任务A被唤醒后立即拿到了临界资源或获取了锁,则任务B仍然需要再次阻塞,这就是while的作用)
原因3:有可能某些任务出于不同的原因在等待你的对象上的锁(必须使用notifyAll唤醒);在这种情况下,需要检查是否已经由正确的原因唤醒,如果不是,则再次调用wait方法;
用while 包围wait方法的本质:检查所有感兴趣的条件,并在条件不满足的情况下再次调用wait方法,让任务再次阻塞;
3、错失的信号:当两个线程使用 notify/wait() 或 notifyAll()/ wait() 方法进行协作时,有可能会错过某个信号;即 notify或 notifyAll发出的信号,带有wait的线程无法感知到。
荔枝:
// T1:
synchronized(sharedMonitor) {
sharedMonitor.notify() // 唤醒所有等待线程
}
// T2:
while(someCondition) {
// point 1
synchronized(sharedMonitor) {
sharedMonitor.wait(); // 当前线程阻塞
}
}
当T2 还没有调用 wait方法时,T1就发送了notify信号; 这个时候T2线程肯定接收不到这个信号;T1发送信号notify后,T2才调用wait方法,这时,T2将永久阻塞下去;因为他错过了T1的notify信号;
T2正确的写法如下:
// T2正确的写法如下:
synchronized(sharedMonitor) {
while(someCondition) {
sharedMonitor.wait(); // 当前线程阻塞
}
}
如果T1先执行后释放锁;此时T2获取锁且检测到 someCondition已经发生了变化,T2不会调用wait() 方法;
如果T2先执行且调用了wait()方法, 释放了锁; 这时T1后执行,然后调用notify()唤醒阻塞线程, 这时T2可以收到T1的 notify信号,从而被唤醒, 由T1修改了 someCondition的条件, 所以T2 不会进入while循环;
【21.5.2】notify与notifyAll方法
1、notify()方法:在使用 notify方法时,在众多等待同一个锁的任务中只有一个会被唤醒,如果你希望使用notify,就必须保证被唤醒的是恰当的任务。
2、notifyAll将唤醒所有正在等待的任务。这是否意味着在任何地方,任何处于wait状态中的任务都将被任何对notifyAll的调用唤醒呢。事实上,当notifyAll因某个特定锁而被调用时,只有等待这个锁的任务才会被唤醒;
/**
* page 707
* notify 与 notifyAll的对比,
* notify 唤醒单个阻塞线程,而notifyAll唤醒所有阻塞线程
*/
public class NotifyVsNotifyAll {
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();// 线程池
for (int i =0; i<5; i++) {
exec.execute(new Task()); // 运行5个任务, 只要task 任务一运行就会阻塞,除非被唤醒
}
exec.execute(new Task2()); // 运行第6个任务, 只要task2 任务一运行就会阻塞,除非被唤醒
Timer timer = new Timer(); // 定时器
// 定时调度, 延迟400毫秒开始执行,两次运行的时间间隔为500毫秒
timer.scheduleAtFixedRate(new TimerTask() {
boolean prod = true;
@Override
public void run() {
if (prod) {
System.out.println("\n notify() ");
Task.blocker.prod(); // 唤醒单个阻塞线程
prod = false ;
} else {
System.out.println("\n notifyAll()");
Task.blocker.prodAll(); // 唤醒所有阻塞线程
prod = true ;
}
}
}, 400, 500);
TimeUnit.SECONDS.sleep(5);
timer.cancel(); // 关闭定时器,关闭所有线程,正在运行的任务除外
System.out.println("timer canceled");
TimeUnit.MILLISECONDS.sleep(500); // 睡眠500毫秒
System.out.println("task2.blocker.prodAll()");
Task2.blocker.prodAll(); // task2 唤醒所有阻塞线程
TimeUnit.MILLISECONDS.sleep(500); // 睡眠500毫秒
System.out.println("\n shutting down");
exec.shutdownNow(); // 关闭线程池
}
}
// 阻塞器
class Blocker {
synchronized void waitingCall() {
try {
while(!Thread.interrupted()) {
wait(); // 期初所有线程均阻塞,等待 notify 或 notifyAll 来唤醒
System.out.println(Thread.currentThread() + " ");
}
} catch (InterruptedException e ) {
}
}
synchronized void prod() {
notify();// 唤醒单个阻塞线程
}
synchronized void prodAll() {
notifyAll(); // 唤醒所有阻塞线程
}
}
// 任务
class Task implements Runnable {
static Blocker blocker = new Blocker();// 阻塞器
@Override
public void run() {
blocker.waitingCall();// wait() 方法阻塞
}
}
// 任务2
class Task2 implements Runnable {
static Blocker blocker = new Blocker(); // 阻塞器
@Override
public void run() {
blocker.waitingCall(); // wait() 方法阻塞
}
}
/*
notify()
Thread[pool-1-thread-1,5,main]
notifyAll()
Thread[pool-1-thread-1,5,main]
Thread[pool-1-thread-5,5,main]
Thread[pool-1-thread-4,5,main]
Thread[pool-1-thread-3,5,main]
Thread[pool-1-thread-2,5,main]
notify()
Thread[pool-1-thread-1,5,main]
notifyAll()
Thread[pool-1-thread-1,5,main]
Thread[pool-1-thread-2,5,main]
Thread[pool-1-thread-3,5,main]
Thread[pool-1-thread-4,5,main]
Thread[pool-1-thread-5,5,main]
notify()
Thread[pool-1-thread-1,5,main]
notifyAll()
Thread[pool-1-thread-1,5,main]
Thread[pool-1-thread-5,5,main]
Thread[pool-1-thread-4,5,main]
Thread[pool-1-thread-3,5,main]
Thread[pool-1-thread-2,5,main]
notify()
Thread[pool-1-thread-1,5,main]
notifyAll()
Thread[pool-1-thread-1,5,main]
Thread[pool-1-thread-2,5,main]
Thread[pool-1-thread-3,5,main]
Thread[pool-1-thread-4,5,main]
Thread[pool-1-thread-5,5,main]
notify()
Thread[pool-1-thread-1,5,main]
notifyAll()
Thread[pool-1-thread-1,5,main]
Thread[pool-1-thread-5,5,main]
Thread[pool-1-thread-4,5,main]
Thread[pool-1-thread-3,5,main]
Thread[pool-1-thread-2,5,main]
timer canceled
task2.blocker.prodAll()
Thread[pool-1-thread-6,5,main]
shutting down
*/
补充:
// 阻塞器
class Blocker {
synchronized void waitingCall() {
try {
while(!Thread.interrupted()) {
wait(); // 期初所有线程均阻塞,等待 notify 或 notifyAll 来唤醒
System.out.println(Thread.currentThread() + " ");
}
} catch (InterruptedException e ) {
}
}
synchronized void prod() {
notify();// 唤醒单个阻塞线程
}
synchronized void prodAll() {
notifyAll(); // 唤醒所有阻塞线程
}
}
Blocker.waitingCall 方法中的while循环, 有两种方式可以离开这个循环:
方式1:发生异常而离开;
方式2:通过检查 interrupted标志离开;
【21.5.3】生产者与消费者
1、对于一个饭店,有一个厨师和服务员。服务员必须等待厨师准备好膳食。当厨师准备好时,他会通知服务员,之后服务员上菜,然后返回继续等待下一次上菜。
这是一个任务协作的荔枝,厨师代表生产者,服务员代表消费者。两个任务必须在膳食被生产和消费时进行握手,而系统必须以有序的方式关闭。
/**
* page 709
* 生产者(厨师chef)-与消费者(服务员WaitPerson)
*/
public class Restaurant { // 餐馆
Meal meal ;
ExecutorService exec = Executors.newCachedThreadPool(); // 线程池
WaitPerson waitPerson = new WaitPerson(this); // 服务员
Chef chef = new Chef(this); // 厨师
// 构造器中通过线程池 运行厨师和服务员的任务
public Restaurant() {
exec.execute(chef);
exec.execute(waitPerson);
}
public static void main(String[] args) {
new Restaurant();
}
}
class Meal { // 膳食
private final int orderNum; // 订单号
public Meal(int orderNum) {
this.orderNum = orderNum;
}
@Override
public String toString() {
return "meal " + orderNum;
}
}
class WaitPerson implements Runnable { // 服务员(消费者)
private Restaurant restaurant; // 餐馆
public WaitPerson(Restaurant restaurant) {
this.restaurant = restaurant;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
synchronized (this) {
while(restaurant.meal == null) { // 没有菜可以上,服务员等待
wait(); // 阻塞,直到 notify 或 notifyAll 唤醒
}
}
System.out.println("服务器取到餐=WaitPerson got " + restaurant.meal);
synchronized (restaurant.chef) { // 厨师
restaurant.meal = null;
restaurant.chef.notifyAll(); // 唤醒所有阻塞在 chef对象上的线程
}
}
} catch (InterruptedException e) {
System.out.println("WaitPerson interrupted(服务员线程中断)");
}
}
}
class Chef implements Runnable {// 厨师(生产者)
private Restaurant restaurant;
private int count = 0;
public Chef(Restaurant r) {
restaurant = r;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
synchronized (this) {
while(restaurant.meal != null) { // 菜没有被端走,厨师等待
wait(); // 阻塞,直到 notify 或 notifyAll 唤醒
}
}
if (++count == 10) { // 厨师只做10个菜
System.out.println("out of food, closing。厨师只做10个菜,关闭线程池");
restaurant.exec.shutdownNow(); // 关闭餐馆的线程池,该池运行着厨师和服务员任务
}
System.out.println("厨师说,上菜了,order up!");
synchronized (restaurant.waitPerson) {
restaurant.meal = new Meal(count); // 厨师生产一个菜
restaurant.waitPerson.notifyAll(); // 唤醒服务员端菜
}
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
System.out.println("chef interrupted");
}
}
}
/*
厨师说,上菜了,order up!
服务器取到餐=WaitPerson got meal 1
厨师说,上菜了,order up!
服务器取到餐=WaitPerson got meal 2
厨师说,上菜了,order up!
服务器取到餐=WaitPerson got meal 3
厨师说,上菜了,order up!
服务器取到餐=WaitPerson got meal 4
厨师说,上菜了,order up!
服务器取到餐=WaitPerson got meal 5
厨师说,上菜了,order up!
服务器取到餐=WaitPerson got meal 6
厨师说,上菜了,order up!
服务器取到餐=WaitPerson got meal 7
厨师说,上菜了,order up!
服务器取到餐=WaitPerson got meal 8
厨师说,上菜了,order up!
服务器取到餐=WaitPerson got meal 9
out of food, closing。厨师只做10个菜,关闭线程池
厨师说,上菜了,order up!
WaitPerson interrupted(服务员线程中断)
chef interrupted
*/
2、代码解说, Restraurant是 WaitPerson和Chef的焦点,作为连接两者的桥梁。他们知道在为哪个Restraurant工作,因为他们必须和这家饭店打交道,以便放置或拿取膳食。
2.1、(干货)再次提问:如果在等待一个订单,一旦你被唤醒,这个订单就必定是可以获得的吗?
答案不是的。因为在并发应用中,某个其他的任务可能会在WaitPerson被唤醒时,会突然插足并拿走订单,唯一安全的方式是使用下面这种惯用的wait() 方法,来保证在退出等待循环前,条件将得到满足。如果条件不满足,还可以确保你可以重返等待状态。
while(conditionIsNotMet) {
wait();
}
2.2、shutdownNow()将向所有由 ExecutorService启动的任务发送 interrupt信号。但是在Chef中,任务并没有在获得该interrupt信号后立即关闭,因为当任务试图进入一个可中断阻塞操作时, 这个中断只能抛出 InterruptException。然后当 Chef 试图调用sleep()时,抛出了 InterruptedException。如果移除对sleep()的调用,那么这个任务将回到run()循环的顶部,并由于Thread.interrupted() 测试而退出,同时并不抛出异常。
3、使用显式的Lock和 Condition 对象
使用互斥并允许任务挂起的基本类是 Condition,调用Condition的await() 可以挂起一个任务;调用signal() 可以唤醒一个任务;调用signalAll() 可以唤醒所有在这个Condition上被其自身挂起的任务。
(干货——与notifyAll()相比,signalAll()方法是更安全的方式)
/**
* page 711
* 使用显式的Lock 和 Condition对象
*/
public class WaxOMatic2 {
public static void main(String[] args) throws InterruptedException {
Car2 car = new Car2();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new WaxOff2(car)); // 抛光
executorService.execute(new WaxOn2(car)); // 打蜡
TimeUnit.SECONDS.sleep(1); // 睡眠5秒
executorService.shutdownNow();
}
}
class Car2 {
private Lock lock = new ReentrantLock(); // 可重入锁
private Condition condition = lock.newCondition(); // 获取锁的条件
private boolean waxOn = false; // 期初时,没有上蜡
public void waxed() { // 上蜡
lock.lock(); // 加锁
try {
waxOn = true; // 上蜡完成
condition.signalAll(); // 唤醒所有等待线程
} finally {
lock.unlock(); // 解锁
}
}
public void buffed() { // 抛光
lock.lock(); // 加锁
try {
waxOn = false; // 抛光完成,待上蜡
condition.signalAll();
} finally {
lock.unlock(); // 解锁
}
}
public void waitForWaxing() throws InterruptedException { // 等待上蜡
lock.lock();
try {
while(waxOn == false) { // 还未上蜡,等待上蜡
condition.await(); // 挂起
}
} finally {
lock.unlock();
}
}
public void waitForBuffing() throws InterruptedException { // 等待抛光
lock.lock();
try {
while(waxOn == true) { // 上蜡完成,等待抛光
condition.await(); // 挂起
}
} finally {
lock.unlock();
}
}
}
class WaxOn2 implements Runnable { // 打蜡任务
private Car2 car ;
public WaxOn2(Car2 c) {
this.car = c;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
System.out.println("wax on ");
TimeUnit.MILLISECONDS.sleep(200);
car.waxed(); // 打蜡完成
car.waitForBuffing(); // 等待抛光
}
} catch(InterruptedException e ) {
System.out.println("WaxOn2 exiting via interrupt");
}
System.out.println("WaxOn2 ending wax on task");
}
}
class WaxOff2 implements Runnable {// 打蜡结束,开始抛光任务
private Car2 car;
public WaxOff2(Car2 c) {
this.car = c;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
car.waitForWaxing(); // 等待打蜡
System.out.println("wax off");
TimeUnit.MILLISECONDS.sleep(200);
car.buffed(); // 抛光完成
}
} catch(InterruptedException e ) {
System.out.println("WaxOff2 exiting via interrupt");
}
System.out.println("WaxOff2 ending wax off task");
}
}
/*
wax on
wax off
wax on
wax off
wax on
WaxOff2 exiting via interrupt
WaxOff2 ending wax off task
WaxOn2 exiting via interrupt
WaxOn2 ending wax on task
*/
代码解说:每个对lock()的调用都必须紧跟一个try-finally子句,用来保证在所有情况下都可以释放锁。在使用内建版本时,任务在可以调用 await(), signal(), signalAll() 方法前,必须拥有这个锁。
(干货——不推荐使用Lock和Condition对象来控制并发)使用Lock和Condition对象来控制并发比较复杂,只有在更加困难的多线程问题中才使用他们;
【21.5.4】生产者与消费者队列
1、wait()和notifyAll() 是一种低级的方式来解决任务协作问题;也可以使用同步队列这种高级方式来解决,同步队列在任何时刻都只允许一个任务插入或移除元素。
2、同步队列 BlockingQueue,两个实现,LinkedBlockingQueue,无界队列, ArrayBlockingQueue-固定尺寸,放置有限数量的元素;
3、若消费者任务试图从队列中获取元素,而该队列为空时,队列可以挂起消费者任务让其阻塞;并且当有更多元素可用时,队列可以唤醒消费者任务。
阻塞队列可以解决非常多的问题,且比 wait()与notifyAll()简单得多。
【看个荔枝】
/**
* 阻塞队列
* page 714
*/
public class TestBlockingQueues {
static void getKey() {
try {
// 从控制台读入用户输入
new BufferedReader(new InputStreamReader(System.in)).readLine();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
static void getKey(String msg) {
System.out.println(msg);
getKey();
}
static void test(String msg, BlockingQueue queue) {
System.out.println(msg);
LiftOffRunner runner = new LiftOffRunner(queue);
Thread t = new Thread(runner);
t.start();
for (int i=0; i<3; i++) {
runner.add(new LiftOff(3)); // 添加5个发射任务到阻塞队列
}
getKey("press enter " + msg);
t.interrupt(); // 线程中断
System.out.println("finished " + msg + " test");
}
public static void main(String[] args) {
test("LinkedBlockingQueue", new LinkedBlockingQueue()); // 链表阻塞队列,无界
test("ArrayBlockingQueue", new ArrayBlockingQueue(3)); // 数组阻塞队列,固定长度
test("SynchronousQueue", new SynchronousQueue()); // 同步队列
}
}
// lift off 发射,起飞
class LiftOffRunner implements Runnable {
private BlockingQueue rockets; // 阻塞队列,火箭队列
public LiftOffRunner(BlockingQueue queue) {
this.rockets = queue;
}
public void add(LiftOff lo) { // LiftOff 发射起飞任务
try {
rockets.put(lo); // 往队列里面放入 发射起飞任务
} catch (InterruptedException e) {
System.out.println("interupted during put()");
}
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
LiftOff rocket = rockets.take(); // 从队列中取出任务,运行,没有任务,则阻塞
rocket.run();
}
} catch (InterruptedException e) {
System.out.println("waking from task()");
}
System.out.println("exiting LiftOffRunner");
}
}
/*
LinkedBlockingQueue
press enter LinkedBlockingQueue
#0(2),
#0(1),
#0(liftoff),
#1(2),
#1(1),
#1(liftoff),
#2(2),
#2(1),
#2(liftoff),
finished LinkedBlockingQueue test
waking from task()
exiting LiftOffRunner
ArrayBlockingQueue
press enter ArrayBlockingQueue
#3(2),
#3(1),
#3(liftoff),
#4(2),
#4(1),
#4(liftoff),
#5(2),
#5(1),
#5(liftoff),
finished ArrayBlockingQueue test
waking from task()
exiting LiftOffRunner
SynchronousQueue
#6(2),
#6(1),
#6(liftoff),
#7(2),
#7(1),
#7(liftoff),
#8(2),
press enter SynchronousQueue
#8(1),
#8(liftoff),
finished SynchronousQueue test
waking from task()
exiting LiftOffRunner
*/
【吐司BlockingQueue】
1、一台机器有3个任务: 一个制作吐司,一个给吐司抹黄油,另一个在抹过黄油的吐司上涂果酱;
/**
* 吐司制作程序-
* 一台机器有3个任务:第1制作吐司, 第2抹黄油,第3涂果酱,阻塞队列-LinkedBlockingQueue
* page 715
*/
public class ToastOMatic {
public static void main(String[] args) throws Exception {
ToastQueue dryQueue = new ToastQueue(); // 烘干的吐司队列
ToastQueue butterQueue = new ToastQueue(); // 涂黄油的吐司队列
ToastQueue finishQueue = new ToastQueue(); // 制作完成的吐司队列
/* 线程池 */
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new Toaster(dryQueue)); // 吐司
exec.execute(new Butterer(dryQueue, butterQueue)); // 黄油
exec.execute(new Jammer(butterQueue, finishQueue)); // 果酱
exec.execute(new Eater(finishQueue)); // 吃
TimeUnit.SECONDS.sleep(1);
exec.shutdownNow(); // 发出中断信号, 线程报 InterruptedException 中断异常 , 所有线程均结束
exec.shutdown();
}
}
class Toast { // 吐司类
public enum Status{DRY, BUTTERED, JAMMED}; // 枚举类 dry-烘干, butter-黄油,jam-果酱
private Status status = Status.DRY;
private final int id ;
public Toast(int id) { // 编号
this.id = id;
}
public void butter() { // 抹黄油结束
status = Status.BUTTERED;
}
public void jam() { // 涂果酱结束
status = Status.JAMMED;
}
public Status getStatus() {
return status;
}
public int getId() {
return id;
}
public String toString() {
return "toast " + id + " : " + status;
}
}
class ToastQueue extends LinkedBlockingQueue {} // 吐司队列
class Toaster implements Runnable { // 第1个工序: 做吐司
private ToastQueue toastQueue; // 吐司队列
private int count = 0; // 计数器
private Random rand = new Random(47);
public Toaster(ToastQueue toastQueue) {
this.toastQueue = toastQueue;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) { // 只要任务不中断
TimeUnit.MILLISECONDS.sleep(100+ rand.nextInt(500));
Toast t = new Toast(count++) ;
System.out.println(t);
toastQueue.put(t); // 往队列添加吐司
}
} catch (InterruptedException e) {
System.out.println("toaster interrupted");
}
System.out.println("toaster off"); // 吐司制作完成
}
}
class Butterer implements Runnable { // 第2个工序:黄油
private ToastQueue dryQueue, butterQueue;
public Butterer(ToastQueue dryQueue, ToastQueue butterQueue) { // 已烘干吐司队列, 已抹黄油的吐司队列
this.dryQueue = dryQueue;
this.butterQueue = butterQueue;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
Toast t = dryQueue.take(); // 获取烘干的吐司
t.butter(); // 抹黄油
System.out.println(t);
butterQueue.put(t); // 往黄油队列添加
}
} catch (InterruptedException e) {
System.out.println("butterer interrupted ");
}
System.out.println("butterer off");
}
}
class Jammer implements Runnable { // 第3个工序,涂果酱
private ToastQueue butterQueue, finishQueue;
public Jammer(ToastQueue butterQueue, ToastQueue finishQueue) {
this.butterQueue = butterQueue;
this.finishQueue = finishQueue;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
Toast t = butterQueue.take(); // 从抹黄油队列中获取吐司
t.jam(); // 涂果酱
System.out.println(t);
finishQueue.put(t); // 添加到完成队列
}
} catch (InterruptedException e ) {
System.out.println("jammer interrupted");
}
System.out.println("jammer off"); // 涂果酱完成
}
}
class Eater implements Runnable { // 消费者,吃吐司
private ToastQueue finishQueue;
private int counter = 0;
public Eater(ToastQueue finishQueue) {
this.finishQueue = finishQueue;
}
@Override
public void run() {
try {
while(!Thread.interrupted()) {
Toast t = finishQueue.take(); // 从吐司制作完成队列中获取吐司
if (t.getId() != counter++ || t.getStatus() != Toast.Status.JAMMED) {
System.out.println(">>>> Error: " + t);
System.exit(1);
} else {
System.out.println("chomp !" + t); // chomp-大声咀嚼,吃吐司
}
}
} catch (InterruptedException e) {
System.out.println("eater interrupted");
}
System.out.println("eat off"); // 吃饱回家
}
}
/*
toast 0 : DRY
toast 0 : BUTTERED
toast 0 : JAMMED
chomp !toast 0 : JAMMED
toast 1 : DRY
toast 1 : BUTTERED
toast 1 : JAMMED
chomp !toast 1 : JAMMED
toast 2 : DRY
toast 2 : BUTTERED
toast 2 : JAMMED
chomp !toast 2 : JAMMED
eater interrupted
eat off
toaster interrupted
toaster off
butterer interrupted
butterer off
jammer interrupted
jammer off
* */
【21.5.5】任务间使用管道进行输入输出
1、通过输入输出在线程间通信很常用。提供线程功能的类库以管道的形式对线程间的输入输出提供了支持,分别是PipedWriter和PipedReader类,分别允许任务向管道写和允许不同任务从同一个管道读取。
这种模式可以看做是 生产者-消费者问题的变体,管道就是一个封装好了的解决方案。管道可以看做是一个阻塞队列,存在于多个引入 BlockingQueue之间的java版本中。
/**
* 任务间使用管道进行输入输出
* page 718
*/
public class PipedIO {
public static void main(String[] args) throws Exception {
Sender sender = new Sender(); // 发送器
Receiver receiver = new Receiver(sender); // 接收器
ExecutorService exec = Executors.newCachedThreadPool(); // 线程池
exec.execute(sender); // 发送
exec.execute(receiver); // 接收
TimeUnit.SECONDS.sleep(3);
exec.shutdown(); // 关闭线程池
}
}
//发送者任务
class Sender implements Runnable {
private Random rand = new Random(47); // 随机数
private PipedWriter out = new PipedWriter(); // 管道输出对象
public PipedWriter getPipedWriter() {
return out;
}
@Override
public void run() {
try {
while(true) {
for (char c = 'A'; c <= 'z'; c++) {
out.write(c); // 把字符输出到管道
TimeUnit.MILLISECONDS.sleep(rand.nextInt(500));
}
}
} catch (IOException e) {
System.out.println("\n" + e + " sender write exception ");
} catch (InterruptedException e2) {
System.out.println("\n" + e2 + " sender sleep interrupted. ");
}
}
}
// 接收者任务
class Receiver implements Runnable {
private PipedReader in ;
public Receiver(Sender sender) throws IOException {
in = new PipedReader(sender.getPipedWriter()); // 管道输入对象
}
@Override
public void run() {
try {
while(true ) {
System.out.print("read:" + (char)in.read() + ", ");// 从管道读取数据
}
} catch (IOException e ) {
System.out.println("\n" + e + " receiver read exception.");
}
}
}
代码解说1: 当Receiver调用read() 方法时,如果没有更多的数据,管道将自动阻塞;
补充1:注意sender和receiver是在main()中启动的,即对象构造彻底完成以后。如果你启动一个没有构造完成的对象,在不同的平台上管道可能会产生不一致的行为。(BlockingQueue使用起来更加健壮且容易)(干货)
补充2:在shudownNow() 被调用时,PipedReader与普通IO之间的区别是:PipiedReader是可以中断的。 如果将 in.read() 修改为System.in.read(), 那么interrupt调用将不能打断read()调用。 (干货)
【21.6】死锁