写点什么

阿里 P8 对 Thread 核心源码讲解

发布于: 2020 年 11 月 06 日



程序中执行的线程。JVM允许应用程序拥有多个并发运行的执行线程。

每个线程都有一个优先级。优先级高的线程优先于优先级低的线程执行。每个线程可能被标记为守护线程,也可能不被标记为守护线程。

当在某个线程中运行的代码创建一个新 Thread 对象时,新线程的优先级最初设置为创建线程的优先级,并且只有在创建线程是一个守护线程时,新线程才是守护线程。

当JVM启动时,通常有一个非守护的线程(它通常调用某个指定类的main方法)。JVM 继续执行线程,直到发生以下任何一种情况时停止:

  • Runtime 类的 exit 方法已被调用,且安全管理器已允许执行退出操作(比如调用 Thread.interrupt 方法)

  • 不是守护线程的所有线程都已死亡,要么从对 run 方法的调用返回,要么抛出一个在 run 方法之外传播的异常

每个线程都有名字,多个线程可能具有相同的名字,Thread 有的构造器如果没有指定名字,会自动生成一个名字。



源码中一共枚举了六种线程状态





线程的状态机



  • NEW 表示线程创建成功,但还没有运行,在 new Thread 后,没有 start 前,线程的状态都是 NEW

  • 当调用start(),进入RUNNABLE,当前线程sleep()结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入RUNNABLE

  • 当线程运行完成、被打断、被中止,状态都会从 RUNNABLE 变成 TERMINATED

  • 如果线程正好在等待获得 monitor lock 锁,比如在等待进入 synchronized 修饰的代码块或方法时,会从 RUNNABLE 转至 BLOCKED

  • WAITING 和 TIMED_WAITING 类似,都表示在遇到 Object#waitThread#joinLockSupport#park 这些方法时,线程就会等待另一个线程执行完特定的动作之后,才能结束等待,只不过 TIMED_WAITING 是带有等待时间的

2.2 线程的优先级

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





分别为最低,普通(默认优先级),最大优先级



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

创建守护线程时,需要将 Thread 的 daemon 属性设置成 true





守护线程的优先级很低,当 JVM 退出时,是不关心有无守护线程的,即使还有很多守护线程,JVM 仍然会退出。

在工作中,我们可能会写一些工具做一些监控的工作,这时我们都是用守护线程去做,这样即使监控抛出异常,也不会影响到业务主线程,所以 JVM 也无需关注监控是否正在运行,该退出就退出,所以对业务不会产生任何影响。

3.1 继承 Thread









看下 start 方法的源码:

public synchronized void start() {
/**
* 对于由VM创建/设置的主方法线程或“系统”组线程,不调用此方法。
* 将来添加到此方法中的任何新功能可能也必须添加到VM中。
*
* 零状态值对应于状态“NEW”。
* 因此,如果没有初始化,直接抛异常
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();

/*
* 通知组此线程即将start,以便可以将其添加到组的线程列表中
* 并且可以减少组的unstarted线程的计数
*/
group.add(this);

// started 是个标识符,在处理一系列相关操作时,经常这么设计
// 操作执行前前标识符是 false,执行完成后变成 true
boolean started = false;
try {
// 创建一个新的线程,执行完成后,新的线程已经在运行了,即 target 的内容已在运行
start0();
// 这里执行的还是 main 线程
started = true;
} finally {
try {
// 若失败,将线程从线程组中移除
if (!started) {
group.threadStartFailed(this);
}
// Throwable 可以捕捉一些 Exception 捕捉不到的异常,比如子线程抛出的异常
} catch (Throwable ignore) {
/*
* 什么也不做。
* 如果start0抛出一个Throwable,那么它将被传递到调用堆栈
*/
}
}
}
// 开启新线程使用的是 native 方法
private native void start0();




注意上面提到的的threadStatus变量

用于工具的Java线程状态,初始化以指示线程“尚未启动”





3.2 实现 Runnable 接口





这是实现 Runnable 的接口,并作为 Thread 构造器的入参,调用时我们使用了两种方式,可以根据实际情况择一而终

  • 使用 start 会开启子线程来执行 run 里面的内容

  • 使用 run 方法执行的还是主线程。

我们来看下 run 方法的源码:

不会新起线程,target 是 Runnable





源码中的 target 就是在 new Thread 时,赋值的 Runnable。



线程初始化的源码有点长,我们只看比较重要的代码 (不重要的被我删掉了),如下:

// 无参构造器,线程名字自动生成
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
// g 代表线程组,线程组可以对组内的线程进行批量的操作,比如批量的打断 interrupt
// target 是我们要运行的对象
// name 我们可以自己传,如果不传默认是 "Thread-" + nextThreadNum(),nextThreadNum 方法返回的是自增的数字
// 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 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();
}


从初始化源码中可以看到,很多属性,子线程都是直接继承父线程的,包括优先性、守护线程、inheritableThreadLocals 里面的值等等。

5.1 join



join 的意思就是当前线程等待另一个线程执行完成之后,才能继续操作,我们写了一个 demo,如下:

@Test
public void join() throws Exception {
Thread main = Thread.currentThread();
log.info("{} is run。",main.getName());
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log.info("{} begin run",Thread.currentThread().getName());
try {
Thread.sleep(30000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("{} end run",Thread.currentThread().getName());
}
});
// 开一个子线程去执行
thread.start();
// 当前主线程等待子线程执行完成之后再执行
thread.join();
log.info("{} is end", Thread.currentThread());
}



执行的结果,就是主线程在执行 thread.join (); 代码后会停住,会等待子线程沉睡 30 秒后再执行,这里的 join 的作用就是让主线程等待子线程执行完成,我们画一个图示意一下:



  • yield 是个 native 方法,源码如下:





令当前线程做出让步,放弃当前 cpu,让 cpu 重新选择线程,避免线程长时占用 cpu。

在写 while 死循环时,预计短时间内 while 死循环可结束的话,可在其中使用 yield 方法,防止 cpu 一直被占用。

sleep 也是 native 方法,可以接受毫秒的一个入参





也可以接受毫秒和纳秒的两个入参





表示当前线程会沉睡多久,沉睡时不会释放锁资源,所以沉睡时,其它线程是无法得到锁的。



表示中断当前运行的线程,比如:

Object#wait ()、Thread#join ()、Thread#sleep (long) 这些方法运行后,线程的状态是 WAITING 或 TIMED_WAITING,这时候打断这些线程,就会抛出 InterruptedException 异常,使线程的状态直接到 TERMINATED

如果 I/O 操作被阻塞了,我们主动打断当前线程,连接会被关闭,并抛ClosedByInterruptException



来说明如何打断 WAITING 的线程,代码如下:

@Test
public void testInterrupt() throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log.info("{} begin run",Thread.currentThread().getName());
try {
log.info("子线程开始沉睡 30 s");
Thread.sleep(30000L);
} catch (InterruptedException e) {
log.info("子线程被打断");
e.printStackTrace();
}
log.info("{} end run",Thread.currentThread().getName());
}
});
// 开一个子线程去执行
thread.start();
Thread.sleep(1000L);
log.info("主线程等待 1s 后,发现子线程还没有运行成功,打断子线程");
thread.interrupt();
}



例子主要说的是,主线程会等待子线程执行 1s,如果 1s 内子线程还没有执行完,就会打断子线程,子线程被打断后,会抛出 InterruptedException 异常,执行结束。

看完三件事❤️

========

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

关注公众号 『 Java斗帝 』,不定期分享原创知识。

同时可以期待后续文章ing🚀







用户头像

还未添加个人签名 2020.09.07 加入

还未添加个人简介

评论

发布
暂无评论
阿里P8对Thread核心源码讲解