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