引导语

从本章开始我们开始学习线程的知识,线程是非常有趣的一个章节,大多数同学对于线程 API,属于不用就忘,到用时需要百度的情况,希望通过本小节的源码阅读,能够加深对线程的印象。

本小节主要三章,本章主要说线程的基本概念、使用姿势、Thread 和 Runnable 的源码;Future、ExecutorService 源码解析章节主要说异步线程执行;押宝线程源码面试题章节主要说说常遇到的源码面试题。

由于线程的概念很多,所以本章会先介绍很多线程的基本概念,说清楚后再解析源码,不然有些同学会看不懂,大家见谅。

1 类注释

1.1 Thread

  1. 每个线程都有优先级,高优先级的线程可能会优先执行;

  2. 父线程创建子线程后,优先级、是否是守护线程等属性父子线程是一致的;

  3. JVM 启动时,通常都启动 MAIN 非守护线程,以下任意一个情况发生时,线程就会停止:
    退出方法被调用,并且安全机制允许这么做(比如调用 Thread.interrupt 方法);
    所有非守护线程都消亡,或者从运行的方法正常返回,或者运行的方法抛出了异常;

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

2 线程的基本概念

我们接下来介绍一下线程的基本概念:

2.1 线程的状态

网上有各种介绍线程状态的文章,我们这里说线程的状态是从源码的角度,源码中一共列举了六种状态,如下图:

我们解析一下这个图:

  1. NEW 表示线程创建成功,但没有运行,在 new Thread 之后,没有 start 之前,线程的状态都是 NEW;
  2. 当我们运行 strat 方法,子线程被创建成功之后,子线程的状态变成 RUNNABLE,RUNNABLE 表示线程正在运行中;
  3. 子线程运行完成、被打断、被中止,状态都会从 RUNNABLE 变成 TERMINATED,TERMINATED 表示线程已经运行结束了;
  4. 如果线程正好在等待获得 monitor lock 锁,比如在等待进入 synchronized 修饰的代码块或方法时,会从 RUNNABLE 变成 BLOCKED,BLOCKED 表示阻塞的意思;
  5. WAITING 和 TIMED_WAITING 类似,都表示在遇到 Object#wait、Thread#join、LockSupport#park 这些方法时,线程就会等待另一个线程执行完特定的动作之后,才能结束等待,只不过 TIMED_WAITING 是带有等待时间的(可以看下面的 join 方法的 demo)。

再次重申,这 6 种状态并不是线程所有的状态,只是在 Java 源码中列举出的 6 种状态, Java 线程的处理方法都是围绕这 6 种状态的。

2.2 优先级

优先级代表线程执行的机会的大小,优先级高的可能先执行,低的可能后执行,在 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;

2.3 守护线程

我们默认创建的线程都是非守护线程。创建守护线程时,需要将 Thread 的 daemon 属性设置成 true,守护线程的优先级很低,当 JVM 退出时,是不关心有无守护线程的,即使还有很多守护线程,JVM 仍然会退出,我们在工作中,可能会写一些工具做一些监控的工作,这时我们都是用守护子线程去做,这样即使监控抛出异常,但因为是子线程,所以也不会影响到业务主线程,因为是守护线程,所以 JVM 也无需关注监控是否正在运行,该退出就退出,所以对业务不会产生任何影响。

2.4 ClassLoader

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

3 线程两种初始化方式

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

3.1 继承 Thread,成为 Thread 的子类

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

上述代码打印出的线程名称是:Thread-0,而主线程的名字是:Thread [main,5,main],由此可见,的确是开了一个子线程来执行打印的操作。

我们一起来看下 start 的底层源码:

// 该方法可以创建一个新的线程出来
public synchronized void start() {// 如果没有初始化,抛异常if (threadStatus != 0)throw new IllegalThreadStateException();group.add(this);// started 是个标识符,我们在做一些事情的时候,经常这么写// 动作发生之前标识符是 false,发生完成之后变成 trueboolean 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 thenit will be passed up the call stack */}}
}
// 开启新线程使用的是 native 方法
private native void start0();

3.2 实现 Runnable 接口,作为 Thread 的入参

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

这种就是实现 Runnable 的接口,并作为 Thread 构造器的入参,我们调用时使用了两种方式,可以根据情况选择使用 start 或 run 方法,使用 start 会开启子线程来执行 run 里面的内容,使用 run 方法执行的还是主线程。

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

// 简单的运行,不会新起线程,target 是 Runnable
public void run() {if (target != null) {target.run();}
}

源码中的 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();// classLoaderif (security == null || isCCLOverridden(parent.getClass()))this.contextClassLoader = parent.getContextClassLoader();elsethis.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

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

@Test
public void join() throws Exception {Thread main = Thread.currentThread();log.info("{} is run。",main.getName());Thread thread = new Thread(new Runnable() {@Overridepublic 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 的作用就是让主线程等待子线程执行完成,我们画一个图示意一下:

从图中可以看出,主线程一直等待子线程沉睡 30s 后才继续执行,在等待期间,主线程的状态也是 TIMED_WAITING。

5.2 yield

yield 是个 native 方法,底层代码如下:

public static native void yield();

意思是当前线程做出让步,放弃当前 cpu,让 cpu 重新选择线程,避免线程过度使用 cpu,我们在写 while 死循环的时候,预计短时间内 while 死循环可以结束的话,可以在循环里面使用 yield 方法,防止 cpu 一直被 while 死循环霸占。

有点需要说明的是,让步不是绝不执行,重新竞争时,cpu 也有可能重新选中自己。

5.3 sleep

sleep 也是 native 方法,可以接受毫秒的一个入参,也可以接受毫秒和纳秒的两个入参,意思是当前线程会沉睡多久,沉睡时不会释放锁资源,所以沉睡时,其它线程是无法得到锁的。

接受毫秒和纳秒两个入参时,如果给定纳秒大于等于 0.5 毫秒,算一个毫秒,否则不算。

5.4 interrupt

interrupt 中文是打断的意思,意思是可以打断中止正在运行的线程,比如:

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() {@Overridepublic 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 异常,执行结束,运行的结果如下图:

6 总结

本章主要介绍了线程的基本概念、状态、无返回值线程的初始化方式和线程的常用操作,这些知识也是工作中常用的,也是大家都必须了解的,为后面的学习打下基础。

老猿说说-Thread相关推荐

  1. 老猿学5G专栏完结说明

    老猿学5G是因为工作原因促成的,主要目的是为了研究5G的计费架构相关内容,到今天为止,基本上达成目标,因此这个专栏基本上告一段落了. 回想这2个多月的日子,从一个对5G相关知识完全不熟悉的小白,到现在 ...

  2. 为什么用python写爬虫_老猿为什么写Python爬虫教程

    对于"爬虫", 或许你只是听说过,或许已经有所了解.无论怎样,你可能有过这样的困惑: + 学了爬虫不知道怎么挣钱? + 技术不知道如何进阶? + 遇到问题不知道找谁交流? 十多年前 ...

  3. 70后.net老猿,尚能饭否?

    相关阅读: 阿里P7/P8学习路线图--技术封神之路 MySQL大表优化方案,单表优化.读写分离.缓存.分区表--都在这里了 互联网技术(java框架.分布式.集群)干货视频大全,不看后悔!(免费下载 ...

  4. Blender基础建模 | 大帅老猿threejs特训

    最近接触到了web3d相关的知识,特别感觉"大帅老猿"和"胖达老师"带我入门! 今天来和大家一起讲讲我建模的故事!顺便教大家一起实现建模自由! 一.首先下载一个 ...

  5. 老猿学5G扫盲贴:中国移动网络侧CHF主要功能及计费处理的主要过程

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt+moviepy音视频剪辑实战 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 一. ...

  6. 《老猿Python精品文章》专栏文章目录

    ☞ ░ 前往老猿Python博客 https://blog.csdn.net/LaoYuanPython ░ 一.专栏简介 <老猿Python精品文章>专栏收集的是老猿2021年开始写的一 ...

  7. 老猿学5G扫盲贴:N6接口用户平面协议栈对应的网络分层模型

    在网络通信模型中,都对应有分层的网络结构,如开放式系统互联(OSI)的七层模型(物理层.数据链路层.网络层.传输层.会话层.表示层和应用层)以及TCP/IP四层(网络接口层.网络层.传输层和应用层)模 ...

  8. 老猿学5G扫盲贴:移动边缘计算(Mobile Edge Computing, MEC)

    版权声明:本文为CSDN博主「魏晓蕾」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明. 原文链接:https://blog.csdn.net/gongxifaca ...

  9. 老猿学5G扫盲贴:与用户和终端相关的名词UE、SUPI、GPSI、PEI

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt+moviepy音视频剪辑实战 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 和4 ...

最新文章

  1. 双边滤波算法的原理、流程、实现及效果
  2. 正则判断 手机邮箱的正确格式
  3. android水平滚动条,Android使用HorizontalScrollView实现水平滚动
  4. Oracle 实例崩溃恢复原理剖析 -- 检查点队列的作用与意义
  5. login.html id=9453,SN100C Sublizenzierung und Aufnahme in die ISO 9453
  6. SQLSTATE[HY000]: General error: 1030 Got error 28 from storage engine
  7. 知乎App有哪些非常“贴心”的UI设计
  8. Android 自定义View(二)绘制一个封闭多边形
  9. mingw,cygwin,gnuwin32,msys,msys2 的区别
  10. MySQL查询优化和索引优化学习笔记
  11. 富士胶片滤镜人像调色预设PS插件
  12. 操作系统课程设计 —— 模拟磁盘文件系统实现 (Java)
  13. Cobalt Strike之网站克隆
  14. 方根法公式_Excel怎么对一个数开n次方根 Excel开方方法汇总
  15. 解决“微信与此IPAD不兼容
  16. Python 爬取微信公众号文章和评论 (有源码)
  17. 作者已死?AI正用艺术征服人类
  18. pands 画图 调整大小_关于cad画图比例,图纸比例,打印比例问题详解
  19. EASEUS Partition Master 调整Windows 7分区
  20. 怎么检查计算机启动程序,如何查看电脑开机启动项

热门文章

  1. 如何避免期货程序化交易中的过度拟合现象
  2. MATLAB Simulink中Scope数据导入到工作区
  3. QQ语录(随时更新)
  4. 工厂设计模式(java版本、spring源码中使用的工厂模式)
  5. openmp matlab,openmp和mex问题
  6. 【Bug】No validator... javax.validation.constraints.NotBlank validating type java.lang.Integer
  7. 东华研究生复试------22 骑士斗恶龙
  8. jquery给p标签赋值
  9. 初识Android framework层
  10. win10 开机慢优化方法