- 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
|
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); } }
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"); } }
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"); } }
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
|
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
|
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
|
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
|
public class SyncBlocked { public static void main(String[] args) throws InterruptedException { 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 = new BlockedResource(); @Override public void run() { System.out.println("waiting for f() in 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{
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
|
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(); } }
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); } }
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
|