线程死亡

  • 1、线程的状态
  • 2、使用标示符号结束线程
  • 3、关闭显示阻塞状态的线程
  • 4、关闭被IO所阻塞的线程
  • 5、关闭被等待锁而阻塞的线程
  • 6、检查中断状态

关于线程正确关闭的说法,一直都存在,最早最原始的使用stop方法,但是这种方式已经被禁止使用了,至于为什么禁止使用,原因大概是sun公司感觉使用stop方法来关闭线程太过于暴力,就是如果使用stop方法关闭线程,那么当前线程的状态不管处于什么状态都会进行关闭,并且也不会释放当前线程所获得的锁,这样的操作会导致整个线程的逻辑混乱无比。所以使用stop方法关闭线程不是一种正确关闭线程的方式,本篇讨论的就是如何正确的线程。

1、线程的状态

线程从创建出来到最后的消亡,一共会经历以下四个方面,分别是:

  • 1.1、new 当线程被创建出来的时候会短暂处于这一种状态,此时已经得到了相应的必需资源,并且执行了相应的初始化操作。
  • 1.2、runnable 这种状态下,只要调度器将时间片分配给当前的线程,线程就可以正常运行,这种状态有资格去竞争时间片。
  • 1.3、blocked 线程能够运行,但是由于某一种条件没满足,调度器不会考虑分配资源,调度它,直到条件满足以后回到runnable状态。
  • 1.4、dead 处于死亡或者即将死亡的线程,并且再也不会得到cpu时间片的分配,任务已经结束,但是需要注意的一点便是此时线程还可以被中断的。

2、使用标示符号结束线程

具体的意思就是,通过一个所有线程都可以看到的地方,比如共享资源类当中的一个boolean值来进行控制,让一个线程合理的终结掉。具体的实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class EventCheck implements Runnable{

private IntGenerator generator ;
private final int id ;

public EventCheck (IntGenerator generator, int ident) {
this.generator = generator ;
this.id = ident ;
}

@Override
public void run() {
while (!generator.isCancel()) {
int val = generator.next() ;
if(val % 2 != 0) {
System.out.println("var = " + val + ", id = " + id + ", not event");
//如果是奇数,则退出循环。
generator.cancel();
}
}
}

public static void test (IntGenerator generator, int count) {
System.out.println("press Control-C to exit");
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < count; i++) {
threadPool.execute(new EventCheck(generator, i));
}
threadPool.shutdown();
}

public static void test (IntGenerator generator) {
test(generator, 10);
}
}

使用这一种方式可以关闭大多数的线程,但是如果我们的一个线程处于阻塞状态,也就代表着,这个线程无法运行到循环开始的地方,因为当前线程阻塞,这种情况下的线程还会无法进行关闭的,所以以下讨论如何关闭阻塞的线程

3、关闭显示阻塞状态的线程

一般情况下一个任务进入阻塞状态有如下几种可能

  • sleep使当前的线程进入休眠的状态
  • wait使当前的线程挂起
  • 任务等待IO操作完成
  • 任务等待锁的到来。

以上的四种情况总结一下就是线程处于阻塞状态分类大体两种情况,第一种情况就是显示的调用让线程挂起,第二种情况就是被动的等待可以运行的条件。如果我们要显示的关闭这类阻塞的线程的话,那么我们就想着,如何让这个线程从run方法当中跳出来,我们必须强制这个任务跳出阻塞的状态,

如果一个线程是阻塞状态,那么我们通过什么办法打断这个线程呢?答案就是我们主动的打断这个线程,而我们只要在run方法当中使用try到相应的打断异常,然后通过catch语句做相应的清理工作即可,Thread类有一个interrupt方法,这个方法将设置这个线程的中断状态,如果一个线程是阻塞状态,或者正在进行阻塞操作,调用这个线程的interrupt方法会使这个线程抛出InterruptedException。当抛出异常或者该任务调用Thread.interrupted时,中断状态将被复位,也就是当我们一个线程处于阻塞状态,在其他的任务当中调用interrupted,如果这个线程当中定义抛出异常的语句,或则调用Thread.interrupted时,线程将被复位。

通过异常来控制程序的流程属于不恰当的用法,这里还是推荐使用Thread.interrupted。

调用interrupt方法,我们必须持有相应的Thread,但是在JDK新的API当中,提倡的是使用Executors来执行所有的操作。如果我们调用shutdownNow,那么这个Executors将会给所有的线程调用interrupt方法。这里是所有的,如果我们只想关闭单独的线程,那么我们应该怎么做呢?那么我们只能通过submit一个线程,我们可以拿到一个Future<?>对象,如果想要结束submit的线程,我们只需要调用cancel方法即可,下面展示一个Demo,展示Interrupt的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
/**
* 基本的interrupt的使用方法,
* 主要用户结束线程。
* @author pengchengliu
*
*/
public class Interrupting {

private static ExecutorService exec = Executors.newCachedThreadPool() ;

static void test (Runnable task) throws InterruptedException {
Future<?> future = exec.submit(task);
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("Interrupting " + task.getClass().getName());
future.cancel(true) ;
System.out.println("Interrupt send to " + task.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("Aboring wilth System.exit(0)");
System.exit(-1);
}
}
/**
* 因为主动进入sleep,而让当前线程进入阻塞状态
* @author pengchengliu
*
*/
class SleepBlocked implements Runnable {
@Override
public void run() {
try{
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
System.out.println("SleepBlocked, InterruptedException");
}
System.out.println("SleepBlocked Exiting run");
}
}
/**
* 因为IO,让当前的线程进入阻塞状态
* @author pengchengliu
*
*/
class IoBlocked implements Runnable {
private InputStream input ;
public IoBlocked(InputStream in) {
this.input = in ;
}
@Override
public void run() {
try {
System.out.println("Waiting for read");
input.read() ;
} catch (IOException e) {
if(Thread.currentThread().isInterrupted()){
System.out.println("Interrupted from blocked I/O");
} else {
throw new RuntimeException(e);
}
}
System.out.println("IoBlocked Exiting run");
}
}
/**
* 因为得不到锁,让线程进入阻塞状态
* @author pengchengliu
*
*/
class SynchronizedBlocked implements Runnable {

public SynchronizedBlocked () {
new Thread(){
@Override
public void run() {
f() ;
}
}.start();
}

public synchronized void f () {
while(true){
Thread.yield();
}
}
@Override
public void run() {
System.out.println("try to call f()");
f();
System.out.println("SynchronizedBlocked Exiting run");
}

}
Interrupting com.suansuan.interrupt.SleepBlocked
Interrupt send to com.suansuan.interrupt.SleepBlocked
SleepBlocked, InterruptedException
SleepBlocked Exiting run
Waiting for read
Interrupting com.suansuan.interrupt.IoBlocked
Interrupt send to com.suansuan.interrupt.IoBlocked
try to call f()
Interrupting com.suansuan.interrupt.SynchronizedBlocked
Interrupt send to com.suansuan.interrupt.SynchronizedBlocked
Aboring wilth System.exit(0)

上述例子当中展示了三种常见的阻塞场景,分别是通过sleep进行阻塞,第二种方式就是通过IO上面的等待进行阻塞,第三种就是通过获取锁,而得不到锁进行阻塞,这三种方式,我们统一进行结束,我们发现只有sleep这种被打断,并且退出了,而其余的两种方式并没有结束。使用什么样的方式让其与的两种方式可以结束呢?

4、关闭被IO所阻塞的线程

通过上述的例子,我们看出了IO操作的线程,如果调用interrupt是不好使的,达不到我们的预期效果,那么如何才能有效的关闭被IO所阻塞的线程呢?这里有一种比较笨拙但实际很有效的方法,那就是关闭任务在其上发生阻塞的底层资源,如果一个任务因为inputStream所阻塞,那么我们只要关闭了底层资源,(inputStream.close)那么当前的intputStream也会被关闭。

下面我们通过一个例子来验证我们上述的说法是否正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 关闭被IO所阻塞的线程,
* 关闭任务所依赖的底册资源。
* @author pengchengliu
*
*/
public class CloseResource {
@SuppressWarnings("resource")
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();

ServerSocket serverSocket = new ServerSocket(8080);
InputStream inputStream = new Socket("localhost", 8080).getInputStream();

exec.execute(new IoBlocked(System.in));
exec.execute(new IoBlocked(inputStream));

TimeUnit.MILLISECONDS.sleep(100);
System.out.println("shutting down all threads");
exec.shutdownNow();
TimeUnit.SECONDS.sleep(1);
System.out.println("closeing " + inputStream.getClass().getName());
inputStream.close();
TimeUnit.SECONDS.sleep(1);
System.out.println("closeing " + System.in.getClass().getName());
System.in.close();
}
}
Waiting for read
Waiting for read
shutting down all threads
closeing java.net.SocketInputStream
Interrupted from blocked I/O
IoBlocked Exiting run
closeing java.io.BufferedInputStream
IoBlocked Exiting run

注意:某一些版本的JDK还提供了InterruptedIOExecption的支持,但是这些都只是部分的实现,而且只在某一些平台上面可用。所以不能太依赖只有一部分的东西。

上述程序的预期结果和我们的之前猜想的差不多,关闭底层资源确实可以有效的关闭当前被IO所阻塞的线程。还有一种情况便是Nio为我们提供了更加人性化的IO中断操作,被阻塞的Nio通道会自动的响应中断。下面我们通过一个Demo来了解一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 通过使用NIO,来让被阻塞的nio通道来自动的响应中断
* @author pengchengliu
*
*/
public class NIOBlocked {
@SuppressWarnings("resource")
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();

ServerSocket serverSocket = new ServerSocket(8080);
InetSocketAddress address = new InetSocketAddress("localhost", 8080);
SocketChannel channel1 = SocketChannel.open(address);
SocketChannel channel2 = SocketChannel.open(address);

Future<?> future = exec.submit(new NIOBlockedTask(channel1));
exec.execute(new NIOBlockedTask(channel2));
exec.shutdown();

TimeUnit.SECONDS.sleep(1);
future.cancel(true);

TimeUnit.SECONDS.sleep(1);
channel2.close();

}
}
class NIOBlockedTask implements Runnable {
private final SocketChannel sc ;

public NIOBlockedTask(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 e){
System.out.println("ClosedByInterruptException");
} catch (AsynchronousCloseException e){
System.out.println("AsynchronousCloseException");
} catch (IOException e) {
throw new RuntimeException(e) ;
}
System.out.println("Exiting NIOBlockedTask.run() " + this);
}
}
Waiting for read in com.suansuan.interrupt.NIOBlockedTask@3c03e5f5
Waiting for read in com.suansuan.interrupt.NIOBlockedTask@352ebfc0
ClosedByInterruptException
Exiting NIOBlockedTask.run() com.suansuan.interrupt.NIOBlockedTask@3c03e5f5
AsynchronousCloseException
Exiting NIOBlockedTask.run() com.suansuan.interrupt.NIOBlockedTask@352ebfc0

通过NIO可很容易的规避之前所出现的问题,我们可以选择使用NIO当中提供的ClosedByInterruptException来关闭我们线程,还可以通过AsynchronousCloseException来关闭线程,一个是通过调用interrupt,一个直接关闭底层的资源,而对于调用interrupt来说,必须使用Nio才OK,而直接关闭底层资源来关闭则适用于所有的IO。

5、关闭被等待锁而阻塞的线程

如果一个线程进入同步块的时候需要获取该对象上面的锁,方能进入,如果这个锁被别的线程所获取,那就必须等待,直到获取锁的线程释放掉锁,放可以进入。这样就造成了互斥情况,方可发生线程的阻塞,下面演示同一个互斥可以如何被同一个任务多次获得。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 同一个互斥可以如何能被同一个对象多次获得
* @author pengchengliu
*
*/
public class MultiLock {
public synchronized void f1 (int count) {
if(count-- > 0) {
System.out.println("f1() calling f2() with count " + count);
f2(count);
}
}
public synchronized void f2(int count) {
if(count-- > 0) {
System.out.println("f2() calling f1() with count " + count);
f1(count);
}
}

public static void main(String[] args) {
final MultiLock multiLock = new MultiLock();
new Thread(){
public void run() {
multiLock.f1(10);
};
}.start();
}
}
f1() calling f2() with count 9
f2() calling f1() with count 8
f1() calling f2() with count 7
f2() calling f1() with count 6
f1() calling f2() with count 5
f2() calling f1() with count 4
f1() calling f2() with count 3
f2() calling f1() with count 2
f1() calling f2() with count 1
f2() calling f1() with count 0

Synchronized内部维护着一个计数器,而一个任务获得锁后,该计数器加1,而后又获得这个对象的锁以后,计数器再加1,而后释放一次锁,则减1,最后直到0为止,代表着当亲锁对象被彻底释放,其他的任务便可以获取这个锁了。

这种线程阻塞的情况,我们调用线程的interrupt来中断当前的阻塞状态不是OK的,例子上述也有,那么我们如何通过合理的方式,让当前的线程结束掉呢?使用JDK5以后的并发类库当中提供的ReentrantLock锁去同步我们的任务,这个锁可以使在其上阻塞的任务具备可以被中断的能力。下面通过一个示例来学习一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* 关闭被等待锁而阻塞的线程
* @author pengchengliu
*
*/
public class SyncBlocked {
public static void main(String[] args) throws InterruptedException {
//在这里初始化对象,并且初始化BlockedSyncTask的成员变量,当前线程还未开启
Thread thread = new Thread(new BlockedSyncTask());
thread.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("Issuing t.interrupt");
thread.interrupt();
}
}
class BlockedSyncTask implements Runnable {
//这个是在主线程当中调用的,初始化BlockedResource,并且在BlockedResource的构造器当中锁住的是main线程,
BlockedResource blockedResource = new BlockedResource();
@Override
public void run() {
System.out.println("waiting for f() in BlockedResource");
//main线程已经锁住了相应的blockedResource对象,当该线程调用的时候发生阻塞
blockedResource.f();
System.out.println("broken out of blocked call");
}
}
class BlockedResource{
private Lock lock = new ReentrantLock();
public BlockedResource () {
//创建自身的时候,锁住自身
lock.lock();
}

public void f() {
try{
/*
* 1)如果当前线程未被中断,则获取锁。
* 2)如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。
* 3)如果当前线程已经保持此锁,则将保持计数加 1,并且该方法立即返回。
* 4)如果锁被另一个线程保持,则出于线程调度目的,禁用当前线程,并且在发生以下两种情况之一以前,该线程将一直处于休眠状态:
* 1)锁由当前线程获得;或者
* 2)其他某个线程中断当前线程。
* 5)如果当前线程获得该锁,则将锁保持计数设置为 1。
*
* 如果当前线程:
* 1)在进入此方法时已经设置了该线程的中断状态;或者
* 2)在等待获取锁的同时被中断。
* 则抛出 InterruptedException,并且清除当前线程的已中断状态。
* 6)在此实现中,因为此方法是一个显式中断点,所以要优先考虑响应中断,而不是响应锁的普通获取或重入获取
*/
lock.lockInterruptibly();
System.out.println("lock acquired in f()");
} catch (InterruptedException e){
System.out.println("interrupted from lock acquisition in f()");
}
}
}
waiting for f() in BlockedResource
Issuing t.interrupt
interrupted from lock acquisition in f()
broken out of blocked call

上述的例子当中比较抽象,抽象的点和lock.lockInterruptibly相应的说明,已经在上述例子当中通过注释所说明。

6、检查中断状态

当我们调用一个thread.interrupt方法的时候,thread也许此时是在阻塞状态,同时也有可能是在运行状态,如果是阻塞状态的话,会抛出InterruptedExecption异常,我们只要把握好catch字句就OK,那么如果是运行状态呢? 所以我们需要检查当前线程的中断状态,好判断是否继续运行,一般在run方法的开头这么做,例如下面的例子当中的那样。

在线程当中维护这一个中断状态,其状态可以通过调用线程的interrupt来进行设置。我们可以通过调用interrupted来检查中断状态,这不仅仅可以知道interrupt是否被调用过,而且还可以清除中断状态。清除中断状态可以确保并发结构不会就某个任务被中断而通知你两次,你可以经由单一的InterruptedExecption或者单一的成功的Thread.interrupted来测试得到这种通知,如果想要再次检查以了解是否被中断,则可以通过调用Thread.interrupted时将结果保存起来。下面通过一个例子了解一下,检查线程的中断状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* 检查线程的中断状态
* @author pengchengliu
*
*/
public class Interrupted {
public static void main(String[] args) throws Exception {
Thread thread = new Thread(new BlockedTask());
thread.start();

TimeUnit.MILLISECONDS.sleep(1000);

System.out.println("close interrupts");
thread.interrupt();
}
}
/**
* 清理资源
* @author pengchengliu
*
*/
class NeedsCleanup {
private final int id ;
public NeedsCleanup (int ident) {
this.id = ident ;
System.out.println("NeedsCleanup " + this.id);
}
public void cleanup () {
System.out.println("Cleaning up" + this.id);
}
}
/**
*
* @author pengchengliu
*
*/
class BlockedTask implements Runnable {
private volatile double d = 0.0 ;
@Override
public void run() {
try{
while(! Thread.interrupted()) {
NeedsCleanup needsCleanup1 = new NeedsCleanup(1);
try{
System.out.println("Sleeping ");
TimeUnit.SECONDS.sleep(1);

NeedsCleanup needsCleanup2 = new NeedsCleanup(2);
try{
System.out.println("Calculating");
for(int i = 1; i < 250000; i++){
d = d + (Math.PI + Math.E) / d ;
}
} finally {
needsCleanup2.cleanup();
}
} finally {
needsCleanup1.cleanup();
}
}
System.out.println("Exiting via while() test");
} catch (InterruptedException e) {
System.out.println("Exeting via InterruptedException");
}
}
}
NeedsCleanup 1
Sleeping
NeedsCleanup 2
Calculating
close interrupts
Cleaning up2
Cleaning up1
Exiting via while() test