[原创]Java并发编程(3):终止任务
使用共享变量来终止任务
比较简单的终止任务的方式是在任务执行时,判断一个标志位,如果满足一定条件,任务就终止。下面这个例子演示了这种简单的情形:
例子:使用共享变量来终止任务

import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; class Count { private /*volatile*/ int count = 0; private Random rand = new Random(47); // Remove the synchronized keyword to see counting fail: public synchronized int increment() { int temp = count; if (rand.nextBoolean()) // Yield half the time Thread.yield(); return (count = ++temp); } public synchronized int value() { return count; } } class Entrance implements Runnable { private static Count count = new Count(); private static List<Entrance> entrances = new ArrayList<Entrance>(); private int number = 0; // Doesn’t need synchronization to read: private final int id; private static volatile boolean canceled = false; // Atomic operation on a volatile field: public static void cancel() { canceled = true; } public Entrance(int id) { this.id = id; // Keep this task in a list. Also prevents // garbage collection of dead tasks: entrances.add(this); } public void run() { while (!canceled) { synchronized (this) { ++number; } System.out.println(this + " Total: " + count.increment()); try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { System.out.println("sleep interrupted"); } } System.out.println("Stopping " + this); } public synchronized int getValue() { return number; } public String toString() { return "Entrance " + id + ": " + getValue(); } public static int getTotalCount() { return count.value(); } public static int sumEntrances() { int sum = 0; for (Entrance entrance : entrances) sum += entrance.getValue(); return sum; } } public class OrnamentalGarden { public static void main(String[] args) throws Exception { ExecutorService exec = Executors.newCachedThreadPool(); for (int i = 0; i < 5; i++) exec.execute(new Entrance(i)); // Run for a while, then stop and collect the data: TimeUnit.MILLISECONDS.sleep(100); Entrance.cancel(); exec.shutdown(); if (!exec.awaitTermination(250, TimeUnit.MILLISECONDS)) System.out.println("Some tasks were not terminated!"); System.out.println("Total: " + Entrance.getTotalCount()); System.out.println("Sum of Entrances: " + Entrance.sumEntrances()); } }
输出:

Entrance 1: 1 Total: 1 Entrance 3: 1 Total: 4 Entrance 0: 1 Total: 2 Entrance 4: 1 Total: 5 Entrance 2: 1 Total: 3 Entrance 1: 2 Total: 6 Entrance 4: 2 Total: 8 Entrance 0: 2 Total: 7 Entrance 3: 2 Total: 9 Stopping Entrance 2: 1 Stopping Entrance 0: 2 Stopping Entrance 4: 2 Stopping Entrance 1: 2 Stopping Entrance 3: 2 Total: 9 Sum of Entrances: 9
说明:
例子中模拟了为一个观赏植物公园计算进入园内游客数量的场景。公园有多个入口,每个入口都有一个计数器,另外有一个公共的计数器,每一个入口为自己的计数器加1的时候,同时也把公共计数器加1。最后判断各个入口计数器之和与公共计数器的数字是否一致。这个例子除了演示任务终止,同时也掩饰了共享资源的同步访问,以及volatile关键字的使用。
Count类用于模拟公共计数器,它有一个count成员,通过increment函数来对count执行递增操作。increment函数被synchronized关键字修饰,是受锁保护的:
public synchronized int increment() { int temp = count; if (rand.nextBoolean()) // Yield half the time Thread.yield(); return (count = ++temp); }
Entrance模拟公园的入口。它有一个static类型的类变量count,用于存储公共计数器。还有一个static类型的类变量List<Entrance>,用于把每一个入口实例都存起来,便于后续对每个入口的统计数字求和。number成员就是各个入口用于记录本入口统计数据的实例变量。还有一个static类型的并且被volatile修饰的类成员canceled,它就是共享变量,各个任务会判断这个变量来确定是否终止任务,不妨再看一下这个变量的定义:
private static volatile boolean canceled = false;
对canceled变量的修改是不依赖于其它值的(不管是它自己的旧值还是其它变量的值),也就是修改操作的过程中没有读操作,这是volatile修饰符能起到保持可见性和数据一致性的前提。Entrance类的run()函数中会判断canceled变量的值,如果改值为true,就退出任务:
public void run() { while (!canceled) { synchronized (this) { ++number; } System.out.println(this + " Total: " + count.increment()); try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { System.out.println("sleep interrupted"); } } System.out.println("Stopping " + this); }
在这个例子中还需要重复几点说明:
- canceled成员是多个任务共享的,必须用volatile修饰符来修饰它。并且对它的写操作不依赖与对其它变量(包括自己的旧值)的读操作,所以volatile修饰符是可以起到作用的。使用同步机制来控制对canceled成员的访问也是一种好办法。
- count成员也是多任务共享的,而且它也是基本类型,但是对它的写操作必须使用synchronized来保护,而不能使用volatile变量。因为对count成员的写操作依赖于它的旧值。在increment()方法里面是先都去了count的值,存储到临时变量temp中,再对temp的值加1,然后赋值给count。
线程的四种状态
线程可以处于下面四种状态之一:
- new
一个线程只会瞬间处于该状态。系统已经为该新生线程分配了必要的资源,并且完成了所有的初始化工作。线程调度器将会把这个线程转换为Runnable或者Blocked状态。
- Runnable
这个状态意味着,线程处于可运行的状态,只要线程调度器为该线程分配了CPU时间,该线程就能够立即执行。处于该状态的线程可能正在运行,也可能正在等待分配CPU时间。
- Blocked
线程因为某种原因(I/O操作,sleep)被阻塞了。线程调度器不会为该线程分配CPU时间,直到线程重新进入Runnable状态。
- Dead
线程终止了,它不会再被调度,也就是不会再被分配CPU时间。终止的线程不可能再进入Runnable状态。线程终止的原因可以是从run()方法中返回,或者是线程被中断(interrupted)。
下面的图形描述了各状态之间的转换方式:
一个线程在下面四种情况下会进入Blocked状态:
- 对线程调用sleep(milliseconds),线程会睡眠一段时间;
- 调用wait(),线程会进入Bolocked状态,直到notify()或notifyAll()的通知消息。
- 任务(线程)在等待I/O操作的完成
- 线程试图调用synchronized方法,而此时锁被其它线程持有。线程进入Blocked状态,直到获取锁。
备注:
过去曾经使用过的suspend(),resume()和stop()方法都被废弃了。stop()方法不会释放线程持有的锁,容易导致其它线程被锁死。不要使用这些方法。
中断
终止一个任务,就是给该任务发一个终止的信号。不同状态中的任务对于信号的处理是不同的,有下面三种情况:
- 线程处于Runnable状态,需要自己判断到这个信号,然后平稳的自行退出;
- 线程处于Blocked状态,并且这种阻塞是可中断阻塞,则会立即抛出一个InterruptedException,线程可以捕获到这个异常,做一些清理工作,而后退出;
- 线程处于Blocked状态,并且这种阻塞是不可中断阻塞,那么线程会对终止信号置之不理。也就是无法让处于不可中断阻塞状态的线程收到一个终止信号而退出。
可中断的阻塞包括:
- sleep()调用
- wait()调用
不可中断的阻塞
- I/O操作
- 因为synchronized造成的阻塞
可以调用Executor的shutdownNow()方法来终止该Executor中的所有任务,实际上它是在Executor中所有线程上调用了Interrupt()方法。要想只终止Executor中的某一个线程,需要在提交任务的时候,调用execute.submit(),它会返回一个Future<?>对象,可以在这个对象上调用cancel()方法来终止这个任务。下面的例子演示了三种阻塞:I/O阻塞,sleep()阻塞,和synchronized阻塞:
例子:可中断阻塞和不可中断阻塞:

import java.io.IOException; import java.io.InputStream; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; class SleepBlocked implements Runnable { public void run() { try { TimeUnit.SECONDS.sleep(100); } catch (InterruptedException e) { System.out.println(new Date() + ": " + "InterruptedException"); } System.out.println(new Date() + ": " + "Exiting SleepBlocked.run()"); } } class IOBlocked implements Runnable { private InputStream in; public IOBlocked(InputStream is) { in = is; } public void run() { try { System.out.println(new Date() + ": " + "Waiting for read():"); in.read(); } catch (IOException e) { if (Thread.currentThread().isInterrupted()) { System.out.println(new Date() + ": " + "Interrupted from blocked I/O"); } else { throw new RuntimeException(e); } } System.out.println(new Date() + ": " + "Exiting IOBlocked.run()"); } } class SynchronizedBlocked implements Runnable { public synchronized void f() { while (true) // Never releases lock Thread.yield(); } public SynchronizedBlocked() { new Thread() { public void run() { f(); // Lock acquired by this thread } }.start(); } public void run() { System.out.println(new Date() + ": " + "Trying to call f()"); f(); System.out.println(new Date() + ": " + "Exiting SynchronizedBlocked.run()"); } } public class Interrupting { private static ExecutorService exec = Executors.newCachedThreadPool(); static void test(Runnable r) throws InterruptedException { Future<?> f = exec.submit(r); TimeUnit.MILLISECONDS.sleep(100); System.out.println(new Date() + ": " + "Interrupting " + r.getClass().getName()); f.cancel(true); // Interrupts if running System.out.println(new Date() + ": " + "Interrupt sent to " + r.getClass().getName()); } 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(new Date() + ": " + "Aborting with System.exit(0)"); System.exit(0); // ... since last 2 interrupts failed } }
以此分别放开main()方法中的三条test()语句,对比一下不同的执行结果。执行test(new SleepBlocked());的结果是:

Wed Feb 20 08:29:30 CST 2013: Interrupting com.quanquan.concurrency.interrupt.SleepBlocked Wed Feb 20 08:29:30 CST 2013: Interrupt sent to com.quanquan.concurrency.interrupt.SleepBlocked Wed Feb 20 08:29:30 CST 2013: InterruptedException Wed Feb 20 08:29:30 CST 2013: Exiting SleepBlocked.run() Wed Feb 20 08:29:33 CST 2013: Aborting with System.exit(0)
可以看到sleep()线程在收到Interrupt信号之后,立即退出了。执行test(new IOBlocked(System.in))的结果是:

Wed Feb 20 08:33:21 CST 2013: Waiting for read(): Wed Feb 20 08:33:21 CST 2013: Interrupting com.quanquan.concurrency.interrupt.IOBlocked Wed Feb 20 08:33:21 CST 2013: Interrupt sent to com.quanquan.concurrency.interrupt.IOBlocked Wed Feb 20 08:33:24 CST 2013: Aborting with System.exit(0)
可以看到 I/O阻塞的线程收到Interrupt信号之后,没有任何响应,直到整个程序退出。执行test(new SynchronizedBlocked());语句的结果是:

Wed Feb 20 08:34:38 CST 2013: Trying to call f() Wed Feb 20 08:34:38 CST 2013: Interrupting com.quanquan.concurrency.interrupt.SynchronizedBlocked Wed Feb 20 08:34:38 CST 2013: Interrupt sent to com.quanquan.concurrency.interrupt.SynchronizedBlocked Wed Feb 20 08:34:41 CST 2013: Aborting with System.exit(0)
打破I/O阻塞
这个结果和I/O阻塞的结果是一样的,也没有对Interrupt信号做任何响应。
从上面的例子可以看出,应用程序可能会被I/O操作阻塞住。面对这种情况,可以通过关闭I/O资源来打破阻塞,因为关闭资源的时候,会导致I/O操作抛出IOException异常,如下面的例子:

import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; 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)); TimeUnit.MILLISECONDS.sleep(100); System.out.println("Shutting down all threads"); exec.shutdownNow(); TimeUnit.SECONDS.sleep(1); System.out.println("Closing " + socketInput.getClass().getName()); socketInput.close(); // Releases blocked thread TimeUnit.SECONDS.sleep(1); } }
输出:

Wed Feb 20 08:54:46 CST 2013: Waiting for read(): Shutting down all threads Closing java.net.SocketInputStream Wed Feb 20 08:54:47 CST 2013: Interrupted from blocked I/O Wed Feb 20 08:54:47 CST 2013: Exiting IOBlocked.run()
可以看到在关闭socket连接之后,阻塞的I/O线程抛出了IOException异常,I/O线程的代码是这样的:
try { System.out.println(new Date() + ": " + "Waiting for read():"); in.read(); } catch (IOException e) { if (Thread.currentThread().isInterrupted()) { System.out.println(new Date() + ": " + "Interrupted from blocked I/O"); } else { throw new RuntimeException(e); } }
可中断的“互斥量”阻塞(ReentrantLock锁)
Java SE5的coccurrency库中新增了ReentrantLock,它的特别之处在于被这种锁阻塞的任务是可以中断的。请看下面的例子:
例子:ReentrantLock锁

import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class BlockedMutex { private Lock lock = new ReentrantLock(); public BlockedMutex() { // Acquire it right away, to demonstrate interruption // of a task blocked on a ReentrantLock: lock.lock(); } public void f() { try { // This will never be available to a second task lock.lockInterruptibly(); // Special call System.out.println("lock acquired in f()"); } catch (InterruptedException e) { System.out.println("Interrupted from lock acquisition in f()"); } } } class Blocked2 implements Runnable { BlockedMutex blocked = new BlockedMutex(); public void run() { System.out.println("Waiting for f() in BlockedMutex"); blocked.f(); System.out.println("Broken out of blocked call"); } } 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()"); t.interrupt(); } }
输出:

Waiting for f() in BlockedMutex Issuing t.interrupt() Interrupted from lock acquisition in f() Broken out of blocked call
说明:
例子中,BlockedMutex对象在构造函数里获取了ReentrantLock锁,所以f()函数会被阻塞,f()函数调用的是lock.lockInterruptibly();这个函数在收到Interrupt信号之后,会退出阻塞状态,并抛出InterruptedException异常。
检查中断状态
一个不会处于阻塞状态的任务,在收到interrupt信号之后,不会抛出InterruptedException异常,但是其中断状态被设置,可以通过调用Thread.interrupted( ) 来判断中断状态,以便在收到interrupt信号之后退出任务。下面的例子演示了这一点。
例子:判断中断状态

import java.util.concurrent.TimeUnit; class NeedsCleanup { private final int id; public NeedsCleanup(int ident) { id = ident; System.out.println("NeedsCleanup " + id); } public void cleanup() { System.out.println("Cleaning up " + id); } } class Blocked3 implements Runnable { private volatile double d = 0.0; public void run() { try { while (!Thread.interrupted()) { // point1 NeedsCleanup n1 = new NeedsCleanup(1); // Start try-finally immediately after definition // of n1, to guarantee proper cleanup of n1: try { System.out.println("Sleeping"); TimeUnit.MILLISECONDS.sleep(900); // point2 NeedsCleanup n2 = new NeedsCleanup(2); // Guarantee proper cleanup of n2: try { System.out.println("Calculating"); // A time-consuming, non-blocking operation: for (int i = 1; i < 2500000; 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"); } catch (InterruptedException e) { System.out.println("Exiting via InterruptedException"); } finally { ; } } } public class InterruptingIdiom { public static void main(String[] args) throws Exception { if (args.length != 1) { System.out.println("usage: java InterruptingIdiom delay-in-mS"); System.exit(1); } Thread t = new Thread(new Blocked3()); t.start(); TimeUnit.MILLISECONDS.sleep(new Integer(args[0])); t.interrupt(); } }
输出:

NeedsCleanup 1 Sleeping NeedsCleanup 2 Calculating Finished time-consuming operation Cleaning up 2 Cleaning up 1 NeedsCleanup 1 Sleeping Cleaning up 1 Exiting via InterruptedException
说明:
Blocked3是一个任务,在run()函数里面,while循环在每次循环开始的时候,调用Thread.interrupted()来判断线程的中断状态是否为true,如果没有被中断,就初始化一个NeedsCleanup对象,并立即在一个try{sleep()}finally{}的finally子句中调用n1.cleanup()函数,做必要的清理工作,以保证sleep()调用被中断的时候,可以完成清理工作。从代码中注释的point2位置开始,都是不会中断的代码,所以n2的清理工作总能够完成。这个例子清楚的演示了,如何让任务在合适的时机终止,并做好最后的清理工作。
posted on 2013-03-02 00:59 seeker2012 阅读(358) 评论(0) 收藏 举报