专栏首页JavaEdgeThread 源码面试

Thread 源码面试

1 类注释

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

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

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

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

  • Runtime 类的 exit 方法已被调用,且安全管理器已允许执行退出操作(比如调用 Thread.interrupt 方法)
  • 不是守护线程的所有线程都已死亡,要么从对 run 方法的调用返回,要么抛出一个在 run 方法之外传播的异常

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

2 线程的基本概念

2.1 线程的状态

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

  • 线程的状态机

2.1.1 状态机说明

  • NEW 表示线程创建成功,但还没有运行,在 new Thread 后,没有 start 前,线程的状态都是 NEW
  • 当调用start(),进入RUNNABLE,当前线程sleep()结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入RUNNABLE
  • 当线程运行完成、被打断、被中止,状态都会从 RUNNABLE 变成 TERMINATED
  • 如果线程正好在等待获得 monitor lock 锁,比如在等待进入 synchronized 修饰的代码块或方法时,会从 RUNNABLE 转至 BLOCKED
  • WAITINGTIMED_WAITING 类似,都表示在遇到 Object#waitThread#joinLockSupport#park 这些方法时,线程就会等待另一个线程执行完特定的动作之后,才能结束等待,只不过 TIMED_WAITING 是带有等待时间的

2.2 线程的优先级

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

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

2.3 守护线程

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

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

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

3 线程的初始化的两种方式

无返回值的线程主要有两种初始化方式:

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。

4 线程的初始化

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

// 无参构造器,线程名字自动生成
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 线程其他操作

5.1 join

当前线程等待另一个线程执行死亡之后,才能继续操作。

    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

等待最多 millis 毫秒以使该线程消失。 0 超时时间意味着永远等待。

此实现使用以 this.isAlive 为条件的 this.wait 调用循环。当线程终止时,将调用this.notifyAll方法。 建议应用程序不要在线程实例上使用 wait,notify 或 notifyAll。

5.2 yield

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

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

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

让步不是绝不执行,重新竞争时,cpu 也有可能还重新选中自己。

5.3 sleep

根据系统计时器和调度器的精度和准确性,使当前执行的线程休眠(暂时停止执行)指定的毫秒数。但是注意,休眠期间线程并不会失去任何监视器的所有权。

毫秒的一个入参

native 方法

毫秒和纳秒的两个入参

表示当前线程会沉睡多久,沉睡时不会释放锁资源,所以沉睡时,其它线程是无法得到锁的。最终调用的其实还是单参数的 sleep 方法。

5.4 interrupt

中断这个线程。

除非当前线程是中断自身(这是始终允许的),否则将调用此线程的 checkAccess 方法,这可能导致抛 SecurityException。

如果这个线程被 Object 类的 wait(), wait(long), or wait(long, int) 方法或者 Thread 类的 join(), join(long), join(long, int), sleep(long), or sleep(long, int) 调用而阻塞,线程进入 WAITINGTIMED_WAITING状态,这时候打断这些线程,就会抛出 InterruptedException ,使线程的状态直接到 TERMINATED

如果这个线程在一个InterruptibleChannel的I/O操作中被阻塞,主动打断当前线程,那么这个通道将被关闭,线程的中断状态将被设置,线程将收到一个ClosedByInterruptException。

如果这个线程在 Selector 中被阻塞,那么这个线程的中断状态将被设置,并且它将从选择的操作立即返回,可能带有一个非零值,就像调用了选择器的 wakeup 方法一样。

如果前面的条件都不成立,那么这个线程的中断状态将被设置。

中断非活动的线程不会有任何影响。

    public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }
  • 最终调用的其实是该 native 方法

5.5 interrupted

测试当前线程是否已被中断。 通过此方法可以清除线程的中断状态。 换句话说,如果要连续两次调用此方法,则第二个调用将返回false(除非在第一个调用清除了其中断状态之后且在第二个调用对其进行检查之前,当前线程再次被中断)。

由于此方法返回false,因此将反映线程中断,因为该线程在中断时尚未处于活动状态而被忽略。

6 总结

本文主要介绍了线程的一些常用概念、状态、初始化方式和操作,这些知识是工作及面试中必备的,也是后面理解高级并发编程的基础。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java源码解析 - ThreadPoolExecutor 线程池

    1.Q群【Java开发技术交流】:https://jq.qq.com/?_wv=1027&k=5UB4P1T

    JavaEdge
  • 深入理解Java线程状态

    实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态

    JavaEdge
  • Java线程池-ThreadPoolExecutor源码解析(基于Java8)

    所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务.

    JavaEdge
  • Android多线程:你必须要了解的多线程基础知识汇总

    版权声明:本文为博主原创文章,未经博主允许不得转载,更多请继续关注Carson_Ho htt...

    Carson.Ho
  • 线程的生命周期

    线程的六种状态: NEW、RUNNABLE、BIOCKED、WAITING、TIME_WAITING、TERMINATED。

    用户7386338
  • 没想到,这么简单的线程池用法,深藏这么多坑!

    生产有个对账系统,每天需要从渠道端下载对账文件,然后开始日终对账。这个系统已经运行了很久,前两天突然收到短信预警,没有获取渠道端对账文件。

    andyxh
  • Java 线程池中的线程复用是如何实现的?

    那么就来和大家探讨下这个问题,在线程池中,线程会从 workQueue 中读取任务来执行,最小的执行单位就是 Worker,Worker 实现了 Runnabl...

    用户1516716
  • Java多线程:线程属性

    喜欢天文的pony站长
  • JUC线程池ThreadPoolExecutor源码分析

    很早之前就打算看一次JUC线程池ThreadPoolExecutor的源码实现,由于近段时间比较忙,一直没有时间整理出源码分析的文章。之前在分析扩展线程池实现可...

    Throwable
  • ReentrantLock 源码解读

    | 傻瓜源码-内容简介 |

    傻瓜源码

扫码关注云+社区

领取腾讯云代金券