Thinking in java 之并发其三:线程的状态
一、线程的四种状态
在 java 中,一个线程可以处于下列四种状态之一:
1)新建(new):当线程被创建时,它会短暂的处于这种状态。在这种状态下时,线程已经分配了必需的系统资源,并执行了初始化。此刻线程已经有资格获得 cpu 时间了,之后调度器将把这个线程转变为就绪或阻塞状态。
2)就绪(Runnable):在这种状态下,只要调度器把时间片分给线程,线程就可以运行。也就是说,在这种状态下,线程是可以运行也可以不运行的。只要调度器把时间片分给线程,线程立刻可以运行。这是就绪状态与阻塞或死亡状态的区别。
3)阻塞(Blocked):线程能够运行,但有某个条件阻止了它的运行。当线程进入阻塞状态时,调度器将忽略线程,不会将 cpu 时间分配给它。一个任务进入到阻塞状态,通常有以下几个原因:
- 通过调用 sleep() 使任务进入休眠状态;
- 通过调用 wait() 使线程挂起;
- 任务在等待某个输入/输出完成;
- 任务试图在某个对象上调用其同步控制方法。
4)死亡(dead):该状态下,线程不可能再被调度,并且再也不会得到 cpu 时间,它的任务已结束。任务死亡的方式是从 run() 方法返回。
二、终结任务
在一些情况下,我们会希望我们的线程能够在运行一段时间后终止。一种做法是,在 Runnable 里添加一个状态标识码,通过这个状态码来控制任务是否继续进行或者结束。下面就是这种方法的一个例子:
1 package ThreadTest.SycnSourceTest.concurrency; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 import java.util.Random; 6 import java.util.concurrent.ExecutorService; 7 import java.util.concurrent.Executors; 8 import java.util.concurrent.TimeUnit; 9 10 class Count{ 11 private int count=0; 12 private Random rand=new Random(47); 13 public synchronized int increment() { 14 int temp=count; 15 if(rand.nextBoolean()) Thread.yield(); 16 return (count = ++temp); 17 } 18 public synchronized int value() { 19 return count; 20 } 21 } 22 23 class Entrance implements Runnable{ 24 25 private static Count count = new Count(); 26 private static List<Entrance> entrances = new ArrayList<Entrance>(); 27 private int number = 0; 28 private final int id; 29 private static volatile boolean canceled = false; 30 public static void cancel() {canceled = true;} 31 public Entrance(int id) { 32 this.id = id; 33 entrances.add(this); 34 } 35 36 @Override 37 public void run() { 38 while(!canceled) { 39 synchronized(this) { 40 ++number; 41 } 42 System.out.println(this+" total: " + count.increment()); 43 try { 44 TimeUnit.MILLISECONDS.sleep(100); 45 } catch (InterruptedException e) { 46 // TODO Auto-generated catch block 47 e.printStackTrace(); 48 } 49 } 50 System.out.println("Stopping "+this); 51 } 52 53 public synchronized int getValue(){return number;} 54 public String toString() { 55 return "Entrances " + id +": " + getValue(); 56 } 57 public static int getTotalCount() { 58 return count.value(); 59 } 60 public static int sumEntrances() { 61 int sum=0; 62 for(Entrance entrance:entrances) { 63 sum+=entrance.getValue(); 64 } 65 return sum; 66 } 67 } 68 public class OrnametalGarden { 69 public static void main(String[] args) throws InterruptedException { 70 ExecutorService exec = Executors.newCachedThreadPool(); 71 for(int i=0;i<5;i++) { 72 exec.execute(new Entrance(i)); 73 } 74 TimeUnit.SECONDS.sleep(3); 75 Entrance.cancel(); 76 exec.shutdown(); 77 if(!exec.awaitTermination(250, TimeUnit.MILLISECONDS)) 78 System.out.println("Some task were not terminated"); 79 System.out.println("Total: "+Entrance.getTotalCount()); 80 System.out.println("Sum of Entrances: "+Entrance.sumEntrances()); 81 } 82 }
我们通过布尔变量 cannel 来控制任务是否应该终止,当 main 的线程进行到某一时刻时,我们将 cannel 置为 true (此处的 cannel 是volatile 的,所以它的改变会立刻被其他任务捕捉到),从而终止所有正在进行的任务。
有趣的时,我们从结果中不难发现,计数器并不是递增的,它会出现跳跃的情况。1 2 4 3 6 5... 这说明,虽然某个任务得以先进行,但未必会第一个完成。
java 的 concurrency 包也为我们提供了中断线程的方法。在第一篇线程文章里,我们使用了 Future 实现了让 run() 返回特定类型的信息。Future 也可以帮我们实现中断线程的操作。
如果我们在使用 Excutor 来启动线程时,不使用 executor() 而是使用 submit(),我们就可以获得一个 Future<?> 。这个 Future 是持有任务的上下文的,我们可以通过它的 cancel 方法来实现中断线程的操作。
1 package ThreadTest.ThreadStatus; 2 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.util.concurrent.ExecutorService; 6 import java.util.concurrent.Executors; 7 import java.util.concurrent.Future; 8 import java.util.concurrent.TimeUnit; 9 10 class SleepBlocked implements Runnable{ 11 public void run() { 12 try { 13 TimeUnit.SECONDS.sleep(100); 14 }catch(InterruptedException e) { 15 System.out.println("Catch InterruptedExcetpion"); 16 } 17 System.out.println("Exiting SleepBlocked run()"); 18 } 19 } 20 21 class IOBlocked implements Runnable{ 22 private InputStream in; 23 public IOBlocked(InputStream is) { 24 in = is; 25 } 26 public void run() { 27 try { 28 System.out.println("Waiting for read()"); 29 in.read(); 30 }catch(IOException e) { 31 if(Thread.currentThread().isInterrupted()) { 32 System.out.println("Interrupted from block I/O"); 33 }else { 34 throw new RuntimeException(e); 35 } 36 } 37 System.out.println("Exiting IOBlocked.run()"); 38 } 39 } 40 41 class SynchronizedBlocked implements Runnable{ 42 public synchronized void f() { 43 while(true) { 44 Thread.yield(); 45 } 46 } 47 public SynchronizedBlocked() { 48 new Thread() { 49 public void run() { 50 f(); 51 } 52 }.start(); 53 } 54 public void run() { 55 System.out.println("Trying to call f()"); 56 f(); 57 System.out.println("Exiting SynchronizedBlocked r()"); 58 } 59 } 60 public class Inturrupting { 61 62 public static ExecutorService exec = Executors.newCachedThreadPool(); 63 static void test(Runnable r) throws InterruptedException{ 64 Future<?> f = exec.submit(r); 65 TimeUnit.MILLISECONDS.sleep(100); 66 System.out.println("Interrupting "+r.getClass().getName()); 67 f.cancel(true); 68 System.out.println("Interrupt sent to "+r.getClass().getName()); 69 } 70 public static void main(String[] args) throws InterruptedException { 71 // TODO Auto-generated method stub 72 test(new SleepBlocked()); 73 test(new IOBlocked(System.in)); 74 test(new SynchronizedBlocked()); 75 TimeUnit.SECONDS.sleep(3); 76 System.out.println("Aborting with system.exit(0)"); 77 System.exit(0); 78 79 } 80 81 }
在这个示例中,我们一共对3中阻塞情况进行了中断任务操作。
对于 sleep() 引起的阻塞,在我们通过 Future 对其进行了中断操作之后,任务跑出了 InturruptedException 异常,证明了任务的确被中断。
另外两种情况(IO 阻塞和等待锁阻塞)我们并没有得到它们被中断的输出。这会导致一些问题,尤其是在创建 IO 的任务是,我们可能会被 IO 锁住多线程程序。
一个比较笨拙的解决方式是关闭任务在其上发生阻塞的底层资源。
(此处本该有示例,但是运行结果并没有符合预期,目前原因未知)
Java 的 IO 的 nio 类还为我们提供更加人性化 IO 中断操作。被阻塞的 nio 通道会自动的响应中断。
至于由于等待锁而造成的阻塞,Java 的 ReentrantLock 具备阻塞时中断的功能。
1 package ThreadTest.ThreadStatus; 2 3 import java.util.concurrent.TimeUnit; 4 import java.util.concurrent.locks.Lock; 5 import java.util.concurrent.locks.ReentrantLock; 6 7 class BlockedMutex{ 8 private Lock lock = new ReentrantLock(); 9 public BlockedMutex() { 10 lock.lock(); 11 } 12 13 public void f() { 14 try { 15 lock.lockInterruptibly(); 16 System.out.println("lock acquire in f()"); 17 }catch(InterruptedException e) { 18 System.out.println("Interrupted from lock acquisitiong in f()"); 19 } 20 } 21 } 22 23 class Blocked2 implements Runnable{ 24 BlockedMutex block = new BlockedMutex(); 25 public void run() { 26 System.out.println("wait for f() in BlockedMuex"); 27 block.f(); 28 System.out.println("Broken out of blocked call"); 29 } 30 } 31 public class Interrupting2 { 32 33 public static void main(String[] args) throws InterruptedException { 34 Thread t = new Thread(new Blocked2()); 35 t.start(); 36 TimeUnit.SECONDS.sleep(1); 37 System.out.println("Issuing t.interrupt()"); 38 t.interrupt(); 39 } 40 41 }
BlokedMutex 类的构造器会获取所创建对象上自身的 lock,并且我们没有在任何地方去释放这个锁。所以当其他任务想要调用 f() 时,将会因为Mutex不可获得而被阻塞。在Blcked2中,run() 方法总是在调用 f() 的地方停止。与 I/O 调用不同,interript() 可以打断被互斥锁阻塞的调用。
如果我们编写的程序有线程中断的可能,那么为了避免 run() 里面的循环能够检测到线程被中断并且正确退出(而不是通过抛出异常的方式退出)。检测的方式可以利用 Thread.interrupted() 实现:
1 package ThreadTest.ThreadStatus; 2 3 import java.util.concurrent.TimeUnit; 4 5 class NeedsCleanup{ 6 private final int id; 7 public NeedsCleanup(int ident) { 8 this.id = ident; 9 System.out.println("NeedsCleanUp: " + id); 10 } 11 public void cleanup(){ 12 System.out.println("cleaning up " + id); 13 } 14 15 } 16 17 class Blocked3 implements Runnable{ 18 private volatile double d = 0.0; 19 @Override 20 public void run() { 21 try { 22 while(!Thread.interrupted()) { 23 NeedsCleanup n1 = new NeedsCleanup(1); 24 try { 25 System.out.println("Sleeping"); 26 TimeUnit.SECONDS.sleep(1); 27 NeedsCleanup n2 = new NeedsCleanup(2); 28 try { 29 System.out.println("Calculation"); 30 for(int i=1;i<2500000;i++) { 31 d=d+(Math.PI+Math.E)/d; 32 } 33 System.out.println("Finished time-consuming operation"); 34 }finally { 35 n2.cleanup(); 36 } 37 }finally{ 38 n1.cleanup(); 39 } 40 } 41 System.out.println("Exiting via while() test"); 42 }catch(InterruptedException e) { 43 System.out.println("Exiting via InterruptedException"); 44 } 45 46 } 47 } 48 public class InterruptingIdiom { 49 50 private static int tm = 1002; 51 public static void main(String[] args) throws InterruptedException { 52 Thread t=new Thread(new Blocked3()); 53 t.start(); 54 TimeUnit.MILLISECONDS.sleep(tm); 55 t.interrupt(); 56 } 57 58 }
在这个示例中 NeedsCleanup 表示一个必须要做清理操作的类。我们使用 try-finally 来保证它的清理方法 cleanup 总是被调用。
通过调节 tm 的值,我们可以控制程序在 sleep 阶段或者在 calculation 阶段停止。当在 sleep 阶段停止时,任务会以抛出异常的方式退出,而在 calculation 阶段停止时,任务会在 while() 的判断处被中断。