Java并发学习笔记(4)-线程的挂起/恢复、加入、谦让

在这篇博文中,我们将讲解 Java 线程基础知识的最后三个知识点。我们首先讨论线程的挂起和恢复,然后介绍两个常用的操作,线程的加入和线程的谦让。之后,我们会开始介绍 java.util.concurrent 包下的内容。

一、线程的挂起和恢复

在上一篇博文中,我们讲解了线程的等待和唤醒机制,当线程进入等待状态时会释放锁,将执行权让渡给别的线程进行竞争,当线程被唤醒时将从上次等待的地方开始继续执行。下面,我们将介绍与等待唤醒机制类似的一组操作,线程的挂起和恢复。挂起操作由方法 suspend() 来实现,恢复操作由方法 resume() 来实现。

线程的挂起和线程的等待类似,他们都是停止继续执行当前的线程任务,但是线程被挂起后不会释放锁,也就是说线程将等待在挂起处并持有锁,直到线程被的挂起状态被恢复。另外,线程的挂起和恢复是由线程自身控制的,这和线程的等待唤醒基于同一把锁来控制的机制不同。我们来看一个线程挂起和恢复的实例,示例代码如下:

/**
 * suspend()方法 将线程挂起,但是并不会释放占用的临界资源(锁)
 * resume()方法 将挂起的线程解除挂起状态,继续执行
 * @author leongan
 */
public class BadSuspend {

    public static Object u = new Object();
    static SuspendThread t1 = new SuspendThread("t1");
    static SuspendThread t2 = new SuspendThread("t2");
    
    public static class SuspendThread extends Thread {
        public SuspendThread(String name) {
            super.setName(name);
        }
        
        @Override
        public void run() {
            synchronized(u) {
                System.out.println("in " + getName());
                Thread.currentThread().suspend();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        t1.start();
        Thread.sleep(100);
        t2.start();
        t1.resume();
        t2.resume();
        t1.join();
        t2.join();
    }
}

我们相继启动两个同样的线程,他们会在线程任务中挂起自己,然后在主线程中又恢复自己。我们运行示例可以看到,程序永远不会退出,这是为什么呢?其实这是线程挂起和恢复机制的一个弊端:当 resume() 操作发生在 suspend() 操作之前且之后不会再有 resume() 操作时,线程将永远被挂起。而我们知道当线程被挂起时将在原地等待且不会释放锁,这就导致了程序将永远无法退出。

我们来分析整个流程,验证上述的这个弊端。程序启动,我们先启动了 t1 线程,t1 在线程任务中将自己挂起,线程持有锁在原地等待,接着主线程睡眠 0.1s 后启动 t2 线程,这时 t1 还持有锁在挂起状态,t2 无法执行线程任务。接着主线程中解除了 t1 线程的挂起状态,t1 线程执行完线程任务释放锁,t2 紧接着获得锁开始执行线程任务,在线程任务中将自己挂起。在 t1 被解除挂起的之后,主线程中也立即对 t2 线程进行了解除挂起操作,但是这时 t2 还没走到线程任务中的挂起操作这一步,也就是说,解除挂起操作在挂起操作之前发生了,而之后也没有了更多的解除挂起操作,所以导致 t2 线程永远挂起,程序无法结束。

通过上述的分析,我们可以看到,基于 suspend()resume() 两个操作来完成线程的挂起和解除挂起是没有安全保障的,任何意外都可能导致程序无法正常退出,所以我们在日常开发中应当尽量避免这两个操作的使用。在后面的博文中,我们将介绍基于 java.util.concurrent.locks.LockSupport 工具类的挂起和解除挂起实现。

二、线程的加入

我们在前面的讲解中经常看到 join() 这个方法,其实这个就是线程的加入操作。当 线程A 执行了 join() 操作后,那么 线程A 所在的线程环境,即 线程A 外面一层的线程将停止继续执行,必须等待 线程A 执行完自己的线程任务后再继续执行。也就是说,由于 线程A 加入了当前线程,所以当前线程要等待 线程A 执行完毕。我们看一个图解(不会画图,见谅):

Java并发学习笔记(4)-线程的挂起/恢复、加入、谦让_第1张图片

三、线程的谦让

相信大家在示例中看到的 yield() 方法的调用也很多次了吧,这其实就是线程的谦让操作。我们知道,线程执行的先决条件是拿到 CPU 的执行权,而 CPU 的执行权在同一时刻只能由一个线程持有,所以当一个线程觉得自己接下来的工作并不那么重要,可以先让别的线程执行重要任务时,这个线程就可以执行谦让操作,让出 CPU 的执行权,但是这并不意味着这个让出 CPU 执行权的线程就不再执行了,在其让出执行权后,也会立即参与 CPU 执行权的竞争,这一点是线程的谦让里最值得关注的一点。

四、小结

花费了4篇博文的篇幅,我们讲解完了线程的基础操作,接下来我们将开始讲解线程的高级知识。这里附上一张线程基础部分的思维导图:

Java并发学习笔记(4)-线程的挂起/恢复、加入、谦让_第2张图片

你可能感兴趣的:(Java并发学习笔记(4)-线程的挂起/恢复、加入、谦让)