Java并发编程之基础篇二——启动和终止线程

启动和终止线程

  • 1.1 构造线程
  • 1.2启动线程
  • 1.3理解中断
  • 1.4 过期的suspend()、resume()和stop()
  • 1.5 安全的终止线程
    • 结尾

通过上篇章节我们知道通过线程的stat()方法进行启动,对着run()方法的执行完毕,线程也随之终止。下面将详细介绍线程的启动和终止。

1.1 构造线程

在现场运行之前首先要构造一个线程对象,在构造的时候需要提供线程所需要的的属性,如线程所属的线程组、线程优先级、是否是Daemon线程等信息。我们来看下Thread中对象池进程初始化的源码。

private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) {
     
        if (name == null) {
     
            throw new NullPointerException("name cannot be null");
        }
        this.name = name.toCharArray();
        // 当前线程就是该线程的父线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
     
            if (security != null) {
     
                g = security.getThreadGroup();
            }
            if (g == null) {
     
                g = parent.getThreadGroup();
            }
        }
        g.checkAccess();
        if (security != null) {
     
            if (isCCLOverridden(getClass())) {
     
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
        g.addUnstarted();
        this.group = g;
        // 将daemon、priority属性设置为父线程的对应属性
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        // 将父线程的InheritableThreadLocal复制过来
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        this.stackSize = stackSize;
        // 分配一个线程ID
        tid = nextThreadID();
    }

一个新构造的线程对象时尤其parent线程来进行空间分配得,而child线程继承了parent是否为Deamon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程。至此,一个可以运行的线程对象就初始化好了,在堆内存中等待运行。

1.2启动线程

线程对象在初始化完毕之后,调用start()方法就可以启动。线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,立即启动调用start()方法的线程。

1.3理解中断

中断可以理解为线程的一个表示为属性,他表示一个运行中的线程是否被其他线程进行了中断操作。其他线程通过调用该线程interrupt()方法对其进行中断操作。

线程通过方法isInterrupt()来进行性判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进程复位。如果该线程为终止状态,即使该线程被中断过,在调用该线程对象的*isInterrupt()*方法时还是会返回false。

Java中有很多方法会抛出InterruptException(例如Thread.sleep(long millis)方法)异常,在这些方法抛出InterruptException一次之前,Java虚拟机会先将该线程的标识位清除,然后抛出InterruptException,此时再调用isInterrupt()方法返回值为false。

我们通过代码来观察下。

public class Interrupt {
     
    static class SleepRunner implements Runnable {
     

        @Override
        public void run() {
     
            while (true) {
     
                SleepUtils.second(10);
            }
        }
    }

    static class BusyRunner implements Runnable {
     

        @Override
        public void run() {
     
            while (true) {
     
            }
        }
    }

    public static void main(String[] args) throws Exception{
     
        // sleepThread不断的尝试睡眠
        Thread sleepThread = new Thread(new SleepRunner(), "sleepThread");
        /**
         * Daemon线程是一种支持线程,主要被用作程序中后台调度以及支持性工作。
         * 当一个Java虚拟机总不存在非Daemon时,Java虚拟机将会退出。
         */
        sleepThread.setDaemon(true);
        // busy线程不停地运行
        Thread busyThread = new Thread(new BusyRunner(), "busyThread");
        busyThread.setDaemon(true);
        sleepThread.start();
        busyThread.start();
        // 休眠5秒,让sleep线程和busy线程充分运行
        SleepUtils.second(5);
        sleepThread.interrupt();
        busyThread.interrupt();
        System.out.println("sleepThread interrupted is " + sleepThread.isInterrupted());
        System.out.println("busyThread interrupted is " + busyThread.isInterrupted());
        // 防止两个线程like退出
        SleepUtils.second(2);
    }
}

运行结果

sleepThread interrupted is false
busyThread interrupted is true

从结果看出,抛出InterruptException的线程是SleepThread,其中标识位被清除了,而一直工作的BusyThread线程中断标识位并没有被清除。

1.4 过期的suspend()、resume()和stop()

如果把一个线程的运行比作播放音乐的MP3的话,那么对音乐做出暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。

我们创建一个线程PrintThread,让它以一秒的频率进行打印,主线程对其进行暂停、恢复和停止操作。

public class Deprecated {
     
    static class Runner implements Runnable{
     

        @Override
        public void run() {
     
            DateFormat format = new SimpleDateFormat("HH:mm:ss");
            while (true) {
     
                System.out.println(Thread.currentThread().getName() + "run at " + format.format(new Date()));
                SleepUtils.second(1);
            }
        }
    }
    public static void main(String[] args) throws Exception {
     
        DateFormat format = new SimpleDateFormat("HH:mm:ss");
        Thread printThread = new Thread(new Runner(), "PrintThread");
        printThread.setDaemon(true);
        printThread.start();
        TimeUnit.SECONDS.sleep(3);
        // 将printThread进行暂停,输出内容工作停止
        printThread.suspend();
        System.out.println("主线程暂停打印线程:" + format.format(new Date()));
        TimeUnit.SECONDS.sleep(3);
        // 将printThread进行恢复,继续输出内容
        printThread.resume();
        System.out.println("主线程恢复打印线程:" + format.format(new Date()));
        TimeUnit.SECONDS.sleep(3);
        // 将printThread进行恢复,继续输出内容
        printThread.stop();
        System.out.println("主线程停止打印线程:" + format.format(new Date()));
        TimeUnit.SECONDS.sleep(3);
    }
}

运行结果

PrintThreadrun at 00:24:26
PrintThreadrun at 00:24:27
主线程暂停打印线程:00:24:28
主线程恢复打印线程:00:24:31
PrintThreadrun at 00:24:31
PrintThreadrun at 00:24:32
PrintThreadrun at 00:24:33
主线程停止打印线程:00:24:34

在执行过程中,PrintThread运行了3秒,随后被主线程暂停,3秒后恢复,最后运行3秒被终止。虽然这些API很“人性化”,但是这些这些API是过期的,也不建议使用。

不建议使用的原因有:以suspend()方法为例,在调用后线程不会释放已经占有的锁,而是占有者资源进入睡眠状态,这用容易引发死锁问题。同样stop()方法在终止一个线程时不会保证线程的资源是正常释放,一般是没有给线程释放资源的机会,因此会导致程序可能工作在不确定状态下。正因为这些方法带来的副作用,这些方法才会被标记为过时的,而暂停和恢复操作可以等待/通知机制来代替。

1.5 安全的终止线程

在1.3节中提到的中断状态是线程的一个标识位,而中断操作是一种简单的线程间交互方式,这种交互方式最适合用来取消或停止任务。而除了中断以外,还可以用一个boolean变量来控制是否需要停止任务或终止线程。

创建一个CountThread,让它不断的进行变量累加,主线程尝试对其进行中断和停止操作

public class CountThread {
     
    private static class Runner implements Runnable{
     
        private int i;
        private volatile boolean on = true;

        @Override
        public void run() {
     
            while (on && !Thread.currentThread().isInterrupted()) {
     
                i++;
            }
            System.out.println("i = " + i);
        }
        public void cancel() {
     
            on = false;
        }
    }

    public static void main(String[] args) {
     
        Runner one = new Runner();
        Thread countThread = new Thread(one, "CountThread");
        countThread.start();
        // 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
        SleepUtils.second(1);
        countThread.interrupt();

        Runner two = new Runner();
        countThread = new Thread(two, "CountThread");
        countThread.start();
        // 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束
        SleepUtils.second(1);
        two.cancel();
    }
}

运行结果

i = 86559055
i = 90464710

有实例代码可以看出,在执行过程中,mian线程通过中断操作和cancel()方法均可使CountThread终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断的去停止线程。

结尾

这一篇讲了线程的构造、启动、中断操作、一些过期的API以及安全地终止线程,重点是对线程中断的理解。欢迎大家积极留言评论,如果对你帮助的话不妨点个赞加个收藏。学习之长路漫漫,吾将上下而求索。共勉之。

你可能感兴趣的:(Java并发,多线程,java,thread)