thinking-in-java(21)并发2

【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】死锁 

 

 

 

 

 

 

 

 

你可能感兴趣的:(ThinkinginJava)