线程的创建

  1. private void init(ThreadGroup g, Runnable target, String name,
  2. long stackSize, AccessControlContext acc,
  3. boolean inheritThreadLocals) {
  4. // 必定要传一个线程名字
  5. if (name == null) {
  6. throw new NullPointerException("name cannot be null");
  7. }
  8. this.name = name;
  9. // 获取创建该线程的线程,调用此方法的线程作为parent
  10. Thread parent = currentThread();
  11. // 安全检查
  12. ...
  13. // 记录为启动的线程数量
  14. g.addUnstarted();
  15. // 设置线程组
  16. this.group = g;
  17. // 设置是否为守护线程
  18. this.daemon = parent.isDaemon();
  19. // 保存线程优先级
  20. this.priority = parent.getPriority();
  21. // isCCLOverridden()是Java的安全检查机制
  22. if (security == null || isCCLOverridden(parent.getClass()))
  23. this.contextClassLoader = parent.getContextClassLoader();
  24. else
  25. this.contextClassLoader = parent.contextClassLoader;
  26. // Java的安全检查机制
  27. this.inheritedAccessControlContext =
  28. acc != null ? acc : AccessController.getContext();
  29. // Runnable,即待运行的程序
  30. this.target = target;
  31. // 设置线程优先级
  32. setPriority(priority);
  33. // 是否从父线程那里继承ThreadLocals && 父线是否程持有inheritableThreadLocals
  34. // 如果为true,就设置inheritableThreadLocals
  35. if (inheritThreadLocals && parent.inheritableThreadLocals != null)
  36. this.inheritableThreadLocals =
  37. ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  38. // 设置栈大小
  39. this.stackSize = stackSize;
  40. // 设置线程ID(单纯的递增了一下)
  41. tid = nextThreadID();
  42. }

这里有几个点可以着重关注一下:

  1. 线程的 parent 是创建该线程的线程
  2. 父线程可以通过 inheritableThreadLocalsThreadLocal 共享给子线程。简单来说, ThreadLocal 是作为 Key 绑定到当前线程的,如果有一些必要的值想要传给子线程,通常要暴力传入使用了。但是通过 InheritableThreadLocal 可以很自然的将要继承的值在创建子线程时设置给子线程

Tips 在使用线程的时候,最好给每个线程都标注好名称,方便到时候根据日志排错时能更方便地定位到异常!

线程启动

当调用 Thread#start() 时,线程就被创建出来了。本质是让 JVM 创建一个操作系统级的线程,然后映射到虚拟机线程上~
QQ图片20201015221331.gif
具体的,等我学玩操作系统,看完 JVM 源码,过来补充~

线程中止

个人觉得 Java 最正统的结束就是让其执行完毕,正常结束,想要达到这个目的一般有两种手段:

  1. 中断( interrupt )
  2. 设置标志位

这两者本质上是一样的,都是借助于某一个标识位来决定程序的进程走向。如果一定要说区别,第一种方式是借用 Thread 内置的中断标识位, Thread 本身提供了一些处理机制;第二种主要依赖共享对象的线程间通信来完成的。

中断

中断是一个动作,即设置线程自带的一个标识位,线程会检查自身是否被中断来响应这个标识位。目前和中断有关的方法有下面三个:

方法名 作用
ThreadObj.interrupt() 设置指定线程的中断状态
ThreadObj.isInterrupted() 判断当前线程是否被中断。若线程已结束但处于中断状态,依然返回false;
Thread.interrupted() 当前所在线程的中断标识位进行复位

当我们对可以抛出InterruptedException 异常的方法执行中断时,这些方法在抛出 InterruptedException 之前JVM 会清除该线程的中断标识位然后抛出 InterruptedException ,此时 ThreadObj.isInterrupted() 会返回 FALSE

suspend()、resume()、stop()这仨方法已经废弃了~

  • suspend():在调用该方法后,线程不会释放已经占有的资源(比如锁),而是持有着锁进入睡眠状态,这样就容易发生死锁;
  • stop():在调用该方法后,不会保证线程的资源正确释放,通常是没有给予线程完成资源释放工作的机会,因此会导致程序可能工作在不确定状态下;

这部分样例、细节可以看《Java并发 - 为什么Thread#suspend被弃用?》

设置标志位

public static void main(String[] args) throws Exception {
    // ===================第一种方式=======================
    Runner one = new Runner();
    Thread countThread = new Thread(one, "CountThread");
    countThread.start();
    // 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
    TimeUnit.SECONDS.sleep(1);
    countThread.interrupt();
    // ===================第二种方式=======================
    Runner two = new Runner();
    countThread = new Thread(two, "CountThread");
    countThread.start();
    // 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束
    TimeUnit.SECONDS.sleep(1);
    two.cancel();
}
private static class Runner implements Runnable {
    private long i;
    private volatile boolean on = true;
    @Override
    public void run() {
        while (on && !Thread.currentThread().isInterrupted()){
            i++;
        }
        System.out.println("Count i = " + i);
    }
    public void cancel() {
        on = false;
    }
}

main 线程可以通过中断操作和 cancel() 方法使得 CountThread 线程终止。通过中断、设置标志位这样的方式可以让线程结束前有机会清理资源,将资源释放工作交给开发人员,避免 Thread#stop() 的悲剧。