Java 多线程(2)---- 线程的控制

前言

在上一篇文章中我们简单的认识了一下线程。包括线程的优先级、如何创建一个线程(通过继承 Thread 类或者通过新建 Runnable 对象并作为参数传入 Thread 的构造方法中)、线程的声明周期状态(新建状态、运行状态(就绪状态、正在运行状态)、等待状态、阻塞状态、结束状态),最后我们看了一下守护线程的概念和其特点。如果你对线程的一些概念还不熟悉,建议先从第一篇文章看起:Java 多线程(1)— 初识线程,当然,大神请无视这句话。
这篇文章我们来看一下 Java 多线程中对线程的控制。

线程控制

其实对一个线程的控制简单来说无非 3 种:开启线程、暂停线程、停止线程:
开启线程我们上篇文章已经使用过了,就是一个当线程对象调用start() 方法后(start() 方法只能被调用一次),这个线程就进入就绪状态了,正在等待线程调度器调度该线程,而一旦线程调度器调度了该线程之后,该线程便可获得 CPU 资源,进入正在运行状态。

如果我们需要暂停一个正在执行的线程时,我们可以通过调用该线程对象的 sleep(long millis) 方法来让该线程休眠指定的秒数,调用这个方法之后线程将会让出 CPU 进入休眠,休眠完成之后的线程并不会直接获得 CPU 资源,而是会进入就绪状态,等待着线程调度器的调度来获取 CPU 资源。

对于停止线程,可能有些小伙伴会通过调用线程对象的 stop() 来停止线程,但这个方法已经不被官方推荐使用了:
这里写图片描述
我们可以看看源码中关于这个方法的注释:

Because it is inherently unsafe. 
Stopping a thread causes it to unlock all the monitors that it has locked.
(The monitors are unlocked as the ThreadDeath exception propagates up the stack.) 
If any of the objects previously protected by these monitors were in an inconsistent state, 
other threads may now view these objects in an inconsistent state. 
Such objects are said to be damaged. When threads operate on damaged objects, 
arbitrary behavior can result. 
This behavior may be subtle and difficult to detect, or it may be pronounced. 
Unlike other unchecked exceptions, ThreadDeath kills threads silently;
thus, the user has no warning that his program may be corrupted.
The corruption can manifest itself at any time after the actual damage occurs, 
even hours or days in the future.

这个官方文档写的有点抽象啊。。。
大致意思是:Thread.stop 方法是不安全的,它会导致调用它的线程解锁所有它锁定的监视器(监视器解锁会导致 ThreadDeath 异常)。如果以前被这些监视器保护的对象处于不一致的状态,其他线程可能会在不一致的状态下查看这些对象。这些对象被已经被损坏。使用这些损坏的对象时,可能发生一些无法预计的行为。这种行为可能很容易被检测出来,也可能很难被检测出来。不像其他的未检查异常,ThreadDeath 默默地杀死线程;因此,用户没有收到警告,但是他的程序可能被损坏。错误可以发生在调用了 stop 方法之后的任意时间段。

那么用一句话来说就是通过 stop 方法结束线程是不安全的,它可能引发未知的错误。那么怎么样结束线程才是安全的呢?我们可以想一下,要安全的结束线程,归根结底来说就是安全的使得线程的 run 方法结束运行。对于这个,我们在写线程的 run 方法的时候可以采用一个简单的框架:

public void run() {
    boolean isFinish = false; // 记录线程任务是否完成
    while (!isFinish) {
        if(/*任务完成*/) {
            break; // 或者 isFinish = true;
        } else {
            // do something ...
        }
    }
}

其实就是利用一个 boolean 变量来标记任务是否完成,在线程任务完成之后直接退出循环或者修改这个标记变量。
这个方法其实也是官方推荐的做法,我们可以看看源码对 stop() 方法中的部分注释:

* Many uses of <code>stop</code> should be replaced by code that simply
* modifies some variable to indicate that the target thread should
* stop running.  The target thread should check this variable
* regularly, and return from its run method in an orderly fashion
* if the variable indicates that it is to stop running.

当然,我们还有可能看到 run 方法会是下面的实现框架:

public void run() {
    while (Thread.currentThread().isInterrupted() == false) {
        if (/*任务完成*/) {
            Thread.currentThread().interrupt();
        } else {
            // do something ...
        }
    }
}

我们看一下其中出现的一些方法:

Thread.currentThread() // 静态方法,返回执行当前代码的线程对象引用

Thread.isInterrupted() // 实例方法,返回调用这个方法的线程对象的中断标志(true / false)

Thread.interrupt() // 实例方法,将调用这个方法的线程对象的中断标志设置为 true,
                   // 请注意:线程的中断标志本身不会影响线程的执行

在上面的方法解释中引入了一个 中断标志 的概念,这个可以理解成线程内部的一个 boolean 类型的字段,其本身不会影响线程的执行,但是和其他方法混用时,就有可能影响线程的执行。

知道了这几个方法的作用,我们也就能理解上面的第二个 run 方法的实现原理了:其实本质都是通过设置 / 读取 某个标志的状态来控制线程的结束,只不过第一个 run 方法的实现框架是通过我们自定义的标志来控制,而第二个 run 方法的实现框架是通过线程内部已经有的 中断标志 来控制。
那么可能有小伙伴要说了,既然两种方法的原理都一样,那么我直接用下面那种就好了,何必用第一种呢?还省的我自定义一个 boolean 类型的标志变量。
对于这个问题,我们先来看一下我们所熟悉的 sleep(long millis) 方法,我们都知道这个方法是使得调用它的线程休眠参数指定的毫秒数,我们来看一下源码中这个方法的部分注释:
这里写图片描述
注意看红色矩形框包裹的注释,大致意思是如果当前线程已经中断了(中断标志true),那么在抛出 InterruptedException 异常的同时会清除当前线程的中断标志 (即将 中断标志 设置为 false)。
也就是说如果当我们调用 sleep(long millis) 方法时,如果调用这个方法的线程的 中断标志true ,那么会抛出一个 InterruptedException 异常。同时清除当前线程的 中断标志 。我们可以看一个小例子:

public static void stopThreadTest() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; Thread.currentThread().isInterrupted() == false; i++) {
                // 如果 i 大于 5 则设置当前线程中断标志为 true,
                // 在此之后 Thread.currentThread().isInterrupted() 方法返回 true
                if (i > 5) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("i: " + i);
                try {
                    Thread.sleep(1000);
                // 在抛出异常的时候会设置当前线程中断标志为 false,
                // 在此之后 Thread.currentThread().isInterrupted() 方法返回 false
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return ; // 防止死循环,在捕货异常时直接返回结束 run 方法
                }
            }
        }
    }).start();
}

public static void main(String[] args) {
    stopThreadTest();
}

注意在上面的代码中,我在捕获异常并打印异常信息之后直接执行了 return ; 方法,否则的话就会造成死循环(sleep(long millis) 方法在抛出异常的时候会清除当前线程的中断标志(设置其为 false))。
我们看一下运行结果:
这里写图片描述
当 i 大于 5 的时候,线程的 中断标志interrupt() 设置为 true ,而 sleep(long millis) 方法恰好当线程 中断标志true 时会抛出异常,于是这个结果很自然的就产生了。
从上面的例子我们知道,当线程中 interrupt() 方法和 sleep(long millis) 方法需要同时使用的时候,我们需要仔细的处理两者的调用关系,因为 interrupt() 方法是有可能导致 sleep(long millis) 方法抛出异常的。

其他API

好了,到这里我们已经把如何开启一个线程、暂停一个线程和如何安全的结束一个线程介绍完了。这里再补充两个控制线程执行的方法:

Thread.join() // 实例方法,分为有参数版本和无参数版本,
              // 调用这个方法的线程会让出 CPU 资源进行等待参数指定的时间(毫秒),如果没有指定参数,
              // 那么会直到这个方法所属的线程对象执行完成后,陷入等待的线程会恢复就绪态,等待 CPU 资源

Thread.yield() // 静态方法,提示线程调度器当前调用这个方法的线程(当前持有 CPU 资源的线程)已经完成任务,
               // 可以让出 CPU 资源了,当然,这只是一种提示,线程调度器可以忽略这种提示,
               // 所以 CPU 资源是否让出并不是一定的,是有一定概率的。

可能 Thread.join() 方法说的有点抽象,我们用个例子看一下:

/**
 * Thread.join 方法的测试
 */
public static void joinTest() {
    System.out.println("主线程开始");

    Thread thread = new Thread("线程1") {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(getName() + "打印: " + i);
            }
        }
    };
    thread.start(); // 开启子线程

    try {
        // 在子线程执行完成之前,主线程让出 CPU 资源并陷入等待
        thread.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
        return ;
    }
    System.out.println("主线程结束");
}

public static void main(String[] args) {
    joinTest();
}

来看看结果:
这里写图片描述
如果我们把 thread.join(); 这段代码去掉,结果是:
这里写图片描述
对比两个结果,我相信你已经知道 Thread.join() 方法的作用了。

有一点需要注意的是,我们发现在调用 join() 方法的时候也需要用 catch 语句捕获 InterruptedException 异常。其实和 Thread.sleep(long millis) 方法一样:当调用 Thread.join() 方法的线程的 中断标志true 时,join() 方法也会抛出一个 InterruptedException 异常。有兴趣的小伙伴可以自己看一下源码中对这个方法的注释说明,这里就不贴了。

再来看看 Thread.yield() 方法的例子:

/**
  * Thread.yield 静态方法测试
  */
public static void threadYieldTest() {
    new Thread("线程1 ") {
        @Override
        public void run() {
            while (true) {
                System.out.println(getName() + "正在占用 CPU 执行");
                // 请求线程调度器让出当前线程的 CPU 资源
                Thread.yield();
            }
        }
    }.start();
    while (true) {
        System.out.println("主线程正在占用 CPU 执行");
    }
}

public static void main(String[] args) {
    threadYieldTest();
}

运行结果:
这里写图片描述
当然这个结果具有偶然性,但是不管怎么说,一般情况下在子线程调用了 Thread.yield() 方法之后主线程的到 CPU 资源的次数会大于子线程中没有调用 Thread.yield() 方法的次数。

好了,对于线程的控制就介绍到这里了,如果博客中有什么不正确的地方,请多多指点。如果文章对您有帮助,请不要吝啬您的赞。欢迎继续关注本专栏。

谢谢观看。。。

猜你喜欢

转载自blog.csdn.net/Hacker_ZhiDian/article/details/79522137
今日推荐