线程:Thread类源码解析(上)

1.基本架构

Thread类涉及较多基本概念,理解基本概念是读源码的前提。

类注释

  1. 每个线程都有优先级,高优先级的线程可能会优先执行
  2. 父线程创建子线程后,子线程的优先级、是否为守护线程等属性与父线程是一致的
  3. JVM启动时,会将主线程执行,主线程是非守护线程。JVM在如下几种情况下才会停止,否则将持续运行
    1)类运行时调用exit方法,且安全管理器已允许进行退出操作
    2)全部非守护线程都已停止运行,无论是正常执行完成或是运行出错
  4. 每个线程都有名字,多个线程可以具有相同的名字。如果构造方法中没有传入指定名称,则会自动生成

任务状态

目前比较多的是如下两个版本,

  1. 版本1
    image
    注意在RUNNABLE和RUNNING之间是双向箭头,当线程切换时,原本正在执行的线程从RUNNING状态转移至RUNNABLE状态。

  2. 版本2(目前应用更多)
    image

任务状态说明,

1)NEW

线程对象刚创建完成,但是还没有运行。在new Thread()之后没有调用start方法之前的状态。

2)RUNNABLE

父线程调用子线程的start方法后,该线程对象才在JVM中挂号(JVM知道该线程要搞事)。版本1中的解释是此时处于可运行状态,但是还没有被CPU选中。版本2中则是调用start方法后,无论是否被CPU选中都称为RUNNABLE状态。

3)RUNNING

只出现在版本1中,一旦被CPU选中,才能执行线程的run方法,此时才算真正意义上的运行。版本2中RUNNING状态的转化

  1. 转入RUNNABLE状态
    当CPU切换到其他线程时,正在运行的线程状态变为RUNNABLE,因为run方法没有在被执行
  2. 转入BLOCKED状态
    正在执行的线程调用了wait或sleep方法时,就会进入BLOCKED状态。区别是wait方法会放下当前CPU使用权,而sleep方法继续占用CPU
  3. 转入TERMINATED状态
    run方法执行完毕或意外终止。或者正在被执行的线程被调用了stop方法

4)BLOCKED

阻塞,由RUNNABLE或RUNNING进入BLOCKED的原因有,

  • 线程等待获得 monitor lock 锁,比如在等待进入 synchronized 修饰的代码块或方法时,会从 RUNNABLE 变成 BLOCKED
  • 正在执行的线程调用了wait或sleep方法后就会进入BLOCKED状态(版本1中的解释)。在版本2中,执行了wait方法后,会进入到WAITING或TIMED_WAITING状态。

由BLOCKED进入RUNNABLE状态的时机,

  • 阻塞操作结束,等待CPU再次选中的这段过程
  • 阻塞过程被打断,如其他线程调用了interrupt方法
  • sleep方法设定的休眠时间结束后会回到RUNNABLE状态
  • 由于wait操作进入BLOCKED状态的线程,其他线程发出notify或notifyAll的信号时,会唤醒进入到RUNNABLE状态

线程在BLOCKED状态下如果被调用了stop方法时会转入TERMINATED状态。

5)TERMINATED

TERMINATED 状态意味着线程的生命周期已经走完。这是线程的终止状态。此状态的线程不会再转化为其它任何状态。

处于 RUNNING 或者 BLOCKED 状态的线程都有可能变为 TERMINATED 状态,但原因是类似的,如下:

  1. 线程运行正常结束
  2. 线程运行异常终止
  3. JVM意外停止

6)WAITING和TIMED_WAITING

版本2中,遇到Object#waitThread#joinLockSupport#park这些方法时,线程会等待另一个线程完成特定动作之后才结束等待。二者的区别是TIMED_WAITING有设定等待时间。

注意,这几种状态并不是任务所有的状态,只是在 Java 源码中列举出了几种状态, Java 线程的处理方法都是围绕这几种状态的。
在这里插入图片描述

优先级

优先级代表线程执行的机会的大小,优先级高的可能先执行,低的可能后执行,在 Java 源码中,优先级从低到高分别是 1 到 10,线程默认 new 出来的优先级都是 5,源码如下,

// 最低优先级
public final static int MIN_PRIORITY = 1;

// 普通优先级,也是默认的
public final static int NORM_PRIORITY = 5;

// 最大优先级
public final static int MAX_PRIORITY = 10;

请注意优先级大仅仅是概率会更大,并不意味着就一定能够先于优先级低的获取。和摇车牌号一个道理,即使现在中签概率是标准的数倍,但摇中依然摇摇无期。而身边却时不时的出现第一次摇号就中的人。如果在 CPU 比较空闲的时候,那么优先级就没有用了。

守护线程

默认创建的线程都是守护线程。

创建守护线程时,需要将 Thread 的 daemon 属性设置成 true,守护线程的优先级很低,当 JVM 退出时,是不关心有无守护线程的,即使还有很多守护线程,JVM 仍然会退出。

守护线程类似于餐厅清洁员,一直在默默地做打扫卫生的工作。这个工作相对独立,不需要和别的角色有什么交互。而当其他所有人都不工作了,也就没有工作的必要了,因为不会有新的垃圾产生。那么可以下班,餐厅也就关门了。

ClassLoader

ClassLoader可以简单理解成类加载器,就是把类从文件、二进制数组、URL 等位置加载成可运行 Class。

2. 多线程实现的两种方式

无返回值的线程初始化方式有两种,

继承Thread类及start方法源码

class MyThread extends Thread{
  @Override
  public void run() {
    log.info(Thread.currentThread().getName());
  }
}
@Test
// 调用 start 方法即可,会自动调用到 run 方法的
public void extendThreadInit(){
  new MyThread().start();
}

start方法源码如下,

public synchronized void start() {
    // 如果没有初始化,抛异常
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);
    // started 是个标识符,动作发生之前标识符是 false,发生完成之后变成 true
    boolean started = false;
    try {
        // 这里会创建一个新的线程,执行完成之后,新的线程已经在运行了,既 target 的内容已经在运行了
        start0();
        // 这里执行的还是主线程
        started = true;
    } finally {
        try {
            // 如果失败,把线程从线程组中删除
            if (!started) {
                group.threadStartFailed(this);
            }
         // Throwable 可以捕捉一些 Exception 捕捉不到的异常,比如说子线程抛出的异常
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}
// 开启新线程使用的是 native 方法
private native void start0();

start方法主要逻辑如下,

  1. 检查线程状态,判断是否可启动
  2. 将子线程加入到线程组中
  3. 调用start0方法

start方法并不调用run方法,而是调用start0方法。start0方法是native方法,也称为JNI(Java Native Interface)方法。JNI方法是java和其它语言交互的方式。同样也是java代码和虚拟机交互的方式,虚拟机就是由C++和汇编所编写。

start0会进入JVM执行,run方法的执行在源码中无法找到,但是在start方法的注释中有这样一句话,

the Java Virtual Machine* calls the run method of this thread.

所以run方法的调用过程如下,
image

实现Runnable及run方法源码

Thread thread = new Thread(new Runnable() {
  @Override
  public void run() {
    log.info("{} begin run",Thread.currentThread().getName());
  }
});
// 开一个子线程去执行
thread.start();
// 不会新起线程,是在当前主线程上继续运行
thread.run();

这种就是实现 Runnable 的接口,并作为 Thread 构造器的入参。

调用时使用了两种方式,可以根据情况选择使用 start 或 run 方法,

  • 使用 start 会开启子线程来执行 run 里面的内容
  • 使用 run 方法执行的还是主线程

run方法源码如下,

public void run() {
    if (target != null) {
        target.run();
    }
}

将Runnable对象传入Thread类中,调用run方法的过程,
image

Thread和Runnable之间的关系

看若兄弟,实为父子。Thread类的定义如下,

public class Thread implements Runnable

Thread实现了Runnable接口。

Thread类的run方法上有@Override注解。所以继承thread类实现多线程,其实也相当于是实现runnable接口的run方法。此时,不需要再传入一个Runnable类去启动。它自己已具备了thread的功能,自己就可以运转起来。

Thread类也实现了Runnable接口,那么Thread子类对象也可以传入另外的Thread对象,让其执行自己的run方法。

3.初始化方法

源码关键部分如下,

// 无参构造器,线程名字自动生成
public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

// g 代表线程组,线程组可以对组内的线程进行批量的操作
// target 是Runnable对象
// name 线程的名字,自定义或自动生成
// stackSize 可以设置堆栈的大小
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对象的线程作为父线程
    Thread parent = currentThread();
    this.group = g;
    // 子线程会继承父线程的守护属性
    this.daemon = parent.isDaemon();
    // 子线程继承父线程的优先级属性
    this.priority = parent.getPriority();
    // classLoader
    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);
    // 当父线程的 inheritableThreadLocals 的属性值不为空时
    // 会把 inheritableThreadLocals 里面的值全部传递给子线程
    if (parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    this.stackSize = stackSize;
    /* Set thread ID */
    // 线程 id 自增
    tid = nextThreadID();
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章