码迷,mamicode.com
首页 > 编程语言 > 详细

Java并发编程 - 逐级深入 看线程的中断

时间:2016-07-19 13:45:59      阅读:256      评论:0      收藏:0      [点我收藏+]

标签:

最近有足够的空闲时间 去东看看西看看,突然留意到在Java的并发编程中,有关线程中断的,以前初学时一直没弄清楚的一些小东西。
于是,刚好把收获简单的总结一下,通过此文来总结记录下来。

从源码看线程的状态

在开始分析线程的中断工作之前,我们肯定要先留意一个点,那就是肯定是有开启,才会有与之对应的中断工作出现。
开启一个线程的工作,相信每个Javaer都烂熟于心。它很简单,new一个thread对象,然后调用start方法开启线程。
那么,一个好玩的问题就出现了:既然开启一个线程的步骤如此简单明了,那么中断又能有什么值得说的呢?
是的,假设我们第一次接触到Thread类,我们已经发现start用于开启一个线程。那么通过查找该类的方法接口,我们一定会发现与之对应的stop()方法。
但事实上是该方法已经明确的被指定为过时了,即不赞成使用。具体原因我们可以参照API说明文档。但也正是因为这样:
即如此“显眼”的一个中断线程的方法被明确的声明为不赞成使用,所以我们才有必要专门去看看,中断一个线程,有哪些值得学习的东西。

但在了解中断线程的工作之前,我们肯定要先对线程这个东西本身的状态有一个了解才行。这个道理很简单:
我们脑子里一定有类似的电影场景:两个人发生冲突,一个人情绪失控之下,抓起手边的一个东西就砸在另一个人头上,另一个人“吧唧 ”,倒地不起。
在这里,我们为什么举这样一个狗血的场景呢?因为电影里接下来的剧情多半就是,这个大兄弟认为自己杀人了,仓皇失措,接着怎样怎样…..
这就联系上我们之前说的“状态”,出现这样的剧情正是因为分不清状态:当发现人流血倒地,下意识就会认为光荣了;但别人也有可能只是昏迷而已。
这对应到我们的并发编程来说也是一个道理:假设我们当前查看的时候,发现某个线程的工作内容已经没有执行了,那是不是线程就已经中断了呢?
答案当然是不一定。所以说,在了解如何中断一个线程之前,我们自然要先清楚线程的各种状态,才知道究竟要符合什么情况,才代表线程彻底中断。

通常一些资料或者大部分的书中,对于线程的状态都有类似如下的一个划分:

  • new - 创建状态:顾明思议,Java通过new创建了一个线程对象过后,该线程就处于该状态。
  • runnable- 可执行状态:也就是指在线程对象调用start()方法后进入的状态。但需要注意的是该状态是“可执行状态”而不是“执行状态”。也就是说,当一个线程对象调用start方法后,只是意味着它获取到了CPU的执行资格,并不代表马上就会被运行(CPU此时当然可能恰好切换在其它线程上做处理),只有具备了CPU当前执行权的线程才会被执行。
  • non Runnable- 不可执行/阻塞状态:也就是通过一些方法的控制,使该线程暂时释放掉了CPU的执行资格的状态。但此时该线程仍然是存在于内存中的。
  • done -退出状态:简单的说也就是当线程进入到退出状态,就意味着它消亡了,不存在了。

这种划分没有错,但这里我们不依照这个划分。而是通过Thread类的源码,来一探究竟。
如果我们查看Thread类的方法接口列表,我们会发现有一个方法很“扎眼”,即“getState”。
没错,我们发现对于该方法的描述正是:“返回线程的状态”。该方法的返回类型是State。
这是定义于Thread内部的一个枚举类型,这就好办了,我们看看该枚举的定义究竟是怎么样的:

 public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or 
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the 
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         * 
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.  
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call 
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on 
         * that object. A thread that has called <tt>Thread.join()</tt> 
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of 
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li> 
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

通过源码我们发现,Java为线程定义了6种状态,从而构成了Thread的整个生命周期。6种状态分别是:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。

在这里我们只分别的简要概括一下这六种状态代表什么,如果想要具体的了解这些状态,可以查看源码和注释或者查阅API说明文档。

  • NEW - 至今尚未启动的线程的状态。
    (该状态最好理解,就是一个Thread对象创建(new)出来之后,就进入这种状态。)
  • RUNNABLE - 可运行线程的线程状态。
    (有很多种方式进入该状态,如调用start,或者wait,sleep达到超时时间,亦或者wait后被notify等等)
  • WAITING - 某一等待线程的线程状态。
    (调用wait,join,LockSupport.park等方法就会进入该状态)
  • TIMED_WAITING - 具有指定等待时间的某一等待线程的线程状态。
    (与WAITING状态不同的就是,该状态具有等待超时。例如调用Thread.sleep以及带有超时值得wait、join等方法就会进入该状态)
  • BLOCKED - 受阻塞并且正在等待监视器锁的某一线程的线程状态
    (很明显,与等待线程状态不同之处就在于,该状态还要等待同步锁)
  • TERMINATED - 已终止线程的线程状态。线程已经结束执行。

现在我们就对一个Thread整个生命周期内的各种可能出现的状态有了一个了解。接下来我们就来看如何让一个线程进入中断(TERMINATED )。

一步一步看各种线程中断

正常自然消亡

虽然我们在之前打下了不错的基础,但很明显,我们还需要一个切入点。幸运的是,我们注意到一点信息:
对于TERMINATED的状态的描述是这样的:已终止线程的线程状态。线程已经结束执行。那么,重点就在于线程已经结束执行。
由此我们推测,何为线程结束执行?是不是线程任务的内容已经执行完毕呢?我们可以通过尝试通过写一段代码来验证一下:

import java.lang.Thread.State;

public class Demo {

    private static Thread sThread;

    public static void main(String[] args) {
        sThread = new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("2==>" + sThread.getState());
            }
        });
        System.out.println("1==>" + sThread.getState());
        sThread.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("3==>" + sThread.getState());
    }
}

这段代码最终运行完成后,输入到控制台的信息如下:

1==>NEW
2==>RUNNABLE
3==>TERMINATED

通过输出信息我们验证了:

1、当我们new出一个Thread对象,该对象就进入了NEW状态。
2、当调用start方法之后,线程就进入了RUNNABLE状态。
3、当线程的工作任务,即run()方法中的内容执行完毕,线程就进入了TERMINATED状态。

也许有刚开始接触Java的朋友会好奇在调用sThread.start之后,为什么通过sleep让线程休眠了2秒。
这其实也是值得一说的,那就是一定要明白RUNNABLE只是代表可执行,而并非立马执行。
简单的说,也就是RUNNABLE状态下的线程仅仅是具备执行资格而已,到底轮不轮到它执行,还得看CPU大哥翻不翻你的牌子。
所以,我们也可以记住这样一点:即使当前对线程调用了start方法,也不要以为就会即刻开始该线程run()方法的执行。

不信我们可以试着将让线程sleep的代码删除,然后再次运行程序,会发现输出结果多半会变为:

1==>NEW
3==>RUNNABLE
2==>RUNNABLE

道理很简单,调用了sThread.start,该线程就进入了RUNNABLE状态。可惜的是CPU没有翻它的牌子,而是选择了继续执行“main thread”的内容。
于是我们看见先打印了标记为”3==>”的内容,这个时候主线程的内容执行完了,Cpu切换,才执行mThread,从而输出”2==>…”

通过标记控制中断

到了这个时候,我们发现,有个毛的趣啊?线程中断不是也很容易吗?让它执行完工作任务不就搞定了吗?
这样说没错,但基本上很多时候我们很明显不可能只在一个线程内执行如此简单的工作,而是会存在循环。
这个时候如何控制线程中断?没错实际上也很简单,就像如何退出一个普通的循环一样,我们也可以通过循环标记来中断一个线程。

我们来看如下的一段测试代码:

public class Demo {

    private static Thread sThread;
    private static boolean KEEP_RUNNING = true;
    private static int position;

    public static void main(String[] args) {
        sThread = new Thread(new Runnable() {

            @Override
            public void run() {
                while (KEEP_RUNNING) {
                    try {
                        if (++position < 5) {
                            Thread.sleep(2000);
                        } else {
                            KEEP_RUNNING = false;
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        sThread.start();

        while (sThread.getState() != State.TERMINATED) {
            System.out.println("sThead‘s state is ==>" + sThread.getState());
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        System.out.println(sThread.getState());
    }
}

运行该段代码的输出结果如下:

sThead’s state is ==>TIMED_WAITING
sThead’s state is ==>TIMED_WAITING
sThead’s state is ==>TIMED_WAITING
sThead’s state is ==>TIMED_WAITING
TERMINATED

我们来分析一下发生了什么:首先,sThread的run()方法中是一个while循环,控制循环的标记是一个布尔型的变量KEEP_RUNNING。
然后,我们每一次循环我们都会对一个int型的变量position进行自增并且判断,当小于5,我们就会让线程休眠2秒。否则就修改循环标记。
与此同时,我们在主线程当中也定义了一个while循环,判断的条件是只要sThread的状态不为TERMINATED,就在循环内输出sThread的状态。

我们需要明白这段程序运行的过程究竟是怎么样的,简单来说就是:
在postion小于5的时候,因为在main thread与sThead中都有调用sleep方法,所以cpu资格会来回切换。
也就是说,main thread和sThead会来回执行,每次position自增运算一次然后休眠,随之主线程输出一次sThead的状态。
由此我们同时验证了另外一点,就是调用带有超时参数的sleep方法,线程则会进入TIMED_WAITING状态。
当position自增运算到不再小于5,KEEP_RUNNING标记将被修改为false,这时sThread内的循环就将结束。
这其实也就意味着sThread的线程任务执行完毕,于是线程就完成了中断。

循环标记并非万能,interrupt来帮忙

到目前为止,我们已经掌握了一些让线程中断的操作。通常来说,它们足够使用了。但是,正如你所见,这仅仅指通常来说。
而编程的工作就是这么操蛋,往往就有很多不通常的情况出现。那么,我们接着来看这样一段代码:

public class Demo {

    private static Thread sThread;
    private static boolean KEEP_RUNNING = true;

    public static void main(String[] args) {
        sThread = new Thread(new Runnable() {

            @Override
            public void run() {
                while (KEEP_RUNNING) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (int i = 1; i <= 5; i++) {
                        System.out.println(i + "==>"
                                + "the thread is running...");
                    }

                }
            }
        });
        sThread.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        stopThread();

    }

    private static void stopThread() {
        System.out.println("we gonna stop sThread...");
        KEEP_RUNNING = false;
    }
}

输出结果如下:

we gonna stop sThread…
1==>the thread is running…
2==>the thread is running…
3==>the thread is running…
4==>the thread is running…
5==>the thread is running…

我们先不去分析这个结果出现的原因,至少从这个输出结果我们发现一件事情,那就是:
虽然我们已经在外部将循环标记KEEP_RUNNING设置为false了,却发现sThread在之后并未被中断,仍然执行了一次任务。

这个结果出现的原因实际上也不是那么难理解,说到底任然是cpu执行权切换造成的。
当sThread执行了start(),接着主线程首先sleep 2s。于是sThread开始执行,这时判断标记为true,进入循环,此时sThread则会sleep,修眠2秒。
那么这个时候cpu便切换到了主线程运行,于是stopThread()方法得以执行,输出对应信息,然后设置KEEP_RUNNING为false。
理想状态下,这个时候sThread就应该中断了才对。但遗憾的是恢复之后线程并不是从头开始循环判断,而是接着sThread休眠之前的位置开始执行。
那么就造成了sThread恢复之后,会跳过while循环,接着上次sleep的代码之后执行,于是for循环开始,输出对应信息。

于是我们发现了:如果想要操作的线程当前处于阻塞(冻结)的状态,那么通过标记的方式并不能够确保一定能让指定线程立刻中断。
所以很显然,我们需要另外一种方式来确保线程的中断了。这个时候如果去网上查资料或者自己查看方法列表,
多半就有这样一个方法出现在你的眼前,那就是”interrupt“,对于interrupt方法,API文档的说明是”中断线程“。

这个时候,似乎一切就变得很容易了。但实际上通过使用,你会发现并没有那么容易。
而更坑爹的是,网上大部分的资料对于该方法的使用说明,都是复制粘贴API文档中的内容。这就让我们这些菜鸟在初学的时候完全摸不着头脑。

我们看到这个方法的命名,会很容易有一种错觉,那就是:如果我们想在某个时候中断一个线程的执行,那么就在外部调用这个方法。
看上去十分的有理有据,但我们要做的,自然就是通过代码来验证一下是否真的行得通:

    public static void main(String[] args) {
        sThread = new Thread(new Runnable() {

            @Override
            public void run() {
                while(true){
                    System.out.println("the thread is running");
                    sThread.interrupt();
                }
            }
        });
        mThread.start();

    }

在以上代码中,按照我们的推断来说,程序应该在输出一次”the thread is running”后,则被中断。
但实际的情况,这并没有卵用,我们会发现程序仍然将无限的输出该信息。这是怎么回事呢?

那么,我们究竟应该怎么来使用interrupt方法呢?实际上我们只需要明白一个原理:
那就是该方法虽然名为“中断”,但实际上它做的工作并不是真的中断线程,而是去设置一种“中断状态”。
所以说,与其说interrupt是去中断线程,不如说它是在设置一个标识,告诉线程当前应该或者说可以被中断了。
这个时候我们发现,如果是这样说来的话,那么这与我们之前说的循环标记似乎没什么区别啊?
确实是这样的,因为通常我们都可以这样类似下面这样去使用它:

            sThread = new Thread(new Runnable() {

                @Override
                public void run() {
                    while (!sThread.isInterrupted()) {
                       //do something
                    }
                }
            });

与之前我们通过循环标记控制线程中断一样,不同在于之前我们将标记设为false,这里我们调用interrupt方法。

这个时候我们的疑问就在于,如果是这样,那么之前我们通过循环标记来控制中断可能遇到的问题,现在不是依然还会遇到吗?
看上去的确如此,但毕竟实践才能出真知。所以我们将之前“循环标记并非万能”一节中的用例中的代码修改如下:

public class Demo {

    private static Thread sThread;
    private static boolean KEEP_RUNNING = true;

    public static void main(String[] args) {
        sThread = new Thread(new Runnable() {

            @Override
            public void run() {
                while (KEEP_RUNNING) {
                    System.out.println("Thread is beginning");
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    for (int i = 1; i <= 5; i++) {
                        System.out.println(i + "==>"
                                + "the thread is running...");
                    }

                }
            }
        });
        sThread.start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        stopThread();

    }

    private static void stopThread() {
        System.out.println("we gonna stop sThread...");
        sThread.interrupt();
    }
}

截取部分程序的输出结果:

Thread is beginning
we gonna stop sThread…

java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at Demo1$1.run(Demo1.java:16)
at java.lang.Thread.run(Thread.java:619)

1==>the thread is running…
2==>the thread is running…
3==>the thread is running…

注意输出结果被标记为红色的部分,由此我们注意到:当我们对线程调用interrupt方法后,抛出了一个异常。
别急,我们现在回忆一下之前通过循环标记控制线程中断的时候情况如何?是的,正常情况下我们是可以实现目的的。
而出现意外的情况是怎么样的呢?没错,正是我们在修改循环标记的时候,目标线程正处于阻塞状态当中。
那么,什么时候,线程会进入到阻塞状态当中呢?通过之前对线程状态的分析我们知道情况很多,例如线程调用了sleep,wait,join等方法。
很显然,Java的设计者们自然考虑到了这一点,既然意外的情况就出现在线程为阻塞的情况下,那么针对意外的情况作出针对就行了。
于是当我们调用interrupt方法去中断线程,Java会判断该线程是否处于阻塞,如果是则会抛出InterruptedException,并且清除之前设置的中断状态。
这自然也就是为什么,我们在代码中使用sleep,wait,join等方法时,编译器必须要求我们处理编译时异常InterruptedException的原因了。

到了现在,我们要做的实际上就很简单了。既然通过标记控制中断,会在线程阻塞时出现意外;
但是,如果对处于阻塞状态的线程调用interrupt方法又会抛出异常,那么,我们只要在异常中作出处理让线程中断就搞定了。修改异常处理的代码:

                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        return;
                    }

再次运行程序,我们查看输出结果,就会发现线程已经正常中断了:

Thread is beginning
we gonna stop sThread…

于是,我们在这里总结一下interrupt最常见的两种使用方式:

  • 通过isInterrupted()作为while循环的条件,配合interrupt()可以控制线程的中断。
  • 而如果想要强行将某个处于阻塞状态的线程从阻塞状态中唤醒,实际上也可以通过interrupt()方法,不过记得处理异常。

如此,我们就对中断线程的常见方式都有了了解。得出结论:通过控制循环标记并合理配合interrupt方法的使用,能够最大程度保证线程的正常中断。
但是!这样做依然也并不能够保证线程绝对能够被正常中断。归根结底还是在于Java的多线程的原理是CPU的随机切换。
也就是说,也很可能会存在下面这样的情况:

            @Override
            public void run() {
                while (KEEP_RUNNING) {
                    System.out.println("1");
                    System.out.println("2");
                    System.out.println("3");
                    System.out.println("4");
                    System.out.println("5");
                    System.out.println("6");
                    System.out.println("7");
                    System.out.println("8");
                    System.out.println("9");
                }
            }

我们要知道在以上的代码中,我们没有使用任何可能让线程进入阻塞状态的代码。但问题在于,线程仍然可能在执行过程中被切换。
即可能出现当输出到“5”的时候,CPU突然进行了一次切换工作,开始另一个线程的执行。
那假设我们在另一个线程中将标记设置为false,那么回到该线程时仍然会执行完剩下的“6到9”的输出语句,并且显然这个时候interrupt也不能解决。
这个时候没有其它办法,我们自然还得通过并发的好兄弟同步锁等方式来解决这些问题。所以总的来说,并发编程还是很复杂和让人挠头的东西。
没有捷径可走,也只能通过不断的学习,不断的在实际使用中积累错误和教训,汲取别人使用的经验,才能慢慢提高。

Java并发编程 - 逐级深入 看线程的中断

标签:

原文地址:http://blog.csdn.net/ghost_programmer/article/details/51953346

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!