前言

Thread类想必都不陌生,第一次学习多线程的时候就一定会接触Thread类。本篇主要从Thread类的定义、使用、注意事项、源码等方面入手,全方位的讲解Thread类。

Thread

我们经常会被问到这样一个问题:

Java开启一个新线程有哪几种方法?

答案是两种:继承Thread类、实现Runnable接口。

说只有两种,有人可能就不服了,实现Callable接口为什么不算?线程池为什么不算?
Oracle官方说明如下:
https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html
其中已经写得很明白

There are two ways to create a new thread of execution.
One is to declare a class to be a subclass of Thread.This subclass should override the run method of class Thread.
The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method.

有两种方式创建一个新的执行线程
一种是定义Thread的子类,子类重写run方法
另一种是定义Runnable接口的实现类,实现run方法

至于为什么实现Callable接口和线程池不算,以后的博客会详细介绍。

一个小问题相信已经让大家回忆起了Thread类相关的知识,接下来就从源码的角度解析Thread

定义

Thread类从JDK1.0版本开始就有了,可谓是历史悠久。本篇以JDK1.8为例进行源码讲解,Thread类定义如下(只列出需要重点关注的成员变量、常量):

// Thread类实现了Runnable接口
public class Thread implements Runnable {// 是否是守护线程private boolean daemon = false;// 最小优先级public final static int MIN_PRIORITY = 1;// 默认优先级public final static int NORM_PRIORITY = 5;// 最大优先级public final static int MAX_PRIORITY = 10;// 线程名称private volatile char  name[];// 线程优先级private int priority;// 需要执行的单元private Runnable target;// 线程状态private volatile int threadStatus = 0;// 线程IDprivate long tid;
}

Thread类的定义可以看出,对于一个Thread,需要重点关注的有以下几点:

  • 实现了Runnable接口
  • 线程需要重点关注的四个属性:ID、Name、是否是守护线程、优先级
  • 线程的状态需要特别注意

接下来就从这三点分别进行详细讲解,因为线程的状态之前已经专门写过一篇博客:Java线程到底有几种状态。所以重点讲解其余的两点

实现Runnable接口

前文说到Java开启一个新线程的两种方式:继承Thread类,重写run方法;实现Runnable接口,实现run方法。接下来就来看一下Thread类中的run方法:

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

其中targetRunnable类型的引用,也可以看做线程的执行单元,结合下面一个小实例:

/*** @author sicimike*/
public class CreateThreadDemo {public static void main(String[] args) {new SicThread1().start();new Thread(new SicThread2()).start();}}class SicThread1 extends Thread {@Overridepublic void run() {System.out.println("extends Thread");}
}class SicThread2 implements Runnable {@Overridepublic void run() {System.out.println("implements Runnable");}
}

在代码

new SicThread1().start();

中调用的SicThread1start方法,间接调用重写的run方法。

SicThread2直接实现了Runnable接口,在代码

new Thread(new SicThread2()).start();

中调用的是构造方法public Thread(Runnable target) {...}
由于Thread类实现了Runnable接口,相当于SicThread1也实现了Runnable接口,所以也可以写new Thread(new SicThread1()).start();这样的代码来启动线程。

也就是说,不管是继承Thread类还是实现Runnable接口,都是利用Thread类的run方法。只是前者是重写了Thread类的run方法,后者是给Thread类传递一个Runnable target,调用targetrun方法。至于这两种方法本质上算不算同一种,这就“仁者见仁,智者见智”了。既然Oracle认为是两种,那还是以官方描述为准。

那这两种方式,哪一种更好
毫无疑问,实现Runnable接口更好,理由有三:

  • 解耦角度:Runnable接口只定义了一个抽象方法run,语义非常明确,就是线程需要执行的任务。而Thread类除了线程需要执行的任务,还需要维护线程的生命周期、状态转换等
  • 资源角度:继承Thread类的方式,如果想要执行一个任务,必须新建一个线程,执行完成后还要销毁,开销非常大;而实现Runnable接口只需要新建任务,可以做到同一个线程执行多个任务,大大减小了线程创建、销毁的资源浪费
  • 扩展角度:Java不支持多继承,一个类如果继承了Thread类就不能再继承别的类,不利于未来的扩展

四个属性

属性 用途/说明
ID(Long) 唯一标识不同的线程
Name(char[]) 线程名称,用于调试 、定位问题等
daemon(boolean) 是否是守护线程,true表示是守护线程,false表示非守护线程(用户线程)
priority(int) 用于告诉CPU哪些线程希望被更多的执行,哪些线程希望被更少的执行

线程ID

线程ID从1(主线程)开始自增,(程序)不能手动修改。现在看下Thread类中关于ID的部分:

// 线程ID
private long tid;public long getId() {return tid;
}// jdk1.8.0_101版本,第422行
// 设置线程ID
/* Set thread ID */
tid = nextThreadID();// 用于生成线程ID
private static long threadSeqNumber;// 加锁的自增操作
private static synchronized long nextThreadID() {return ++threadSeqNumber;
}

从源码可以看出ID的两个特点:

  • 从1开始自增
  • 不能手动修改

线程Name

看了线程ID相关的源码后,很容易就总结除了线程ID相关的特点。所以同样看下Thread类关于Name的重要操作:

// 不传入名字时,默认就是"Thread-" + 数字
public Thread(Runnable target) {init(null, target, "Thread-" + nextThreadNum(), 0);
}public Thread(Runnable target, String name) {
init(null, target, name, 0);
}// 用于匿名线程编号
private static int threadInitNumber;// 加锁的自增操作
private static synchronized int nextThreadNum() {return threadInitNumber++;
}// 可以动态设置线程name
public final synchronized void setName(String name) {// 确定当前线程有修改该线程的权限checkAccess();// 设置线程名字this.name = name.toCharArray();if (threadStatus != 0) {// 如果线程不是处于0(线程未启动)状态,则不能修改native层的namesetNativeName(name);}
}private native void setNativeName(String name);

至此,可以总结出关于线程Name的两个特点:

  • 默认线程名称是"Thread-" + 数字(从0开始),为了方便调试,应该给每个线程取一个有意义的名字
  • 实例化时如果没有设置线程Name,之后还可以通过setName的方式设置线程Name

守护线程

守护线程的主要作用是为了给用户线程提供一系列服务,守护线程有三个特点:

  • 线程类型默认继承自父线程:守护线程创建的线程默认就是守护线程;用户线程创建的线程默认就是用户线程,可以通过setDaemon方法修改这个属性
  • 守护线程一般由JVM启动
  • 守护线程不影响JVM的退出

守护线程和用户线程本质上没有多大区别,最大的区别就是守护线程不影响JVM的退出。

线程优先级

Java中定义的线程优先级有1-10(十个等级,数值越大,优先级越高),默认为5。虽然Thread类定义优先级这个功能,但是程序的设计不应该依赖于优先级。究其原因,主要有两点:

  • Thread类中定义的优先级不代表操作系统的优先级,不同的操作系统有不同的优先级定义
  • 优先级可能被操作系统改变

核心方法

了解了Thread的定义及核心属性后,再来看看Thread的核心方法startsleepjoinyield

start方法

启动一个线程的方式就是调用它的start()方法,而不是run()方法。有时也会被问到这样两个问题:

同一个线程两次(多次)调用start方法会怎样?
启动一个线程为什么不能调用run方法,而是start方法?

看完start方法的实现,能轻松回答这两个问题,下面是start方法的实现

public synchronized void start() {if (threadStatus != 0)// 如果线程状态不是“未启动”,会抛出IllegalThreadStateException异常// 这里就回答了上面的第一个问题throw new IllegalThreadStateException();// 加入线程组group.add(this);// 线程是否已经启动,启动后设置成trueboolean started = false;try {start0();started = true;} finally {try {if (!started) {// 启动失败,把线程从线程组中删除group.threadStartFailed(this);}} catch (Throwable ignore) {/* do nothing. If start0 threw a Throwable thenit will be passed up the call stack */}}
}// 真正的启动线程的方法(native方法)
private native void start0();

根据start方法的实现可以总结出start做了哪些逻辑:

  • 检查线程状态
  • 加入线程组
  • 调用native方法start0通知JVM启动一个新线程
  • 如果启动失败,从线程组中删除线程

再来回顾下Threadrun方法的实现:

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

对比这两个方法就可看出,start方法为线程的启动做了一系列准备,再去通知JVM启动一个新线程;而run方法仅仅是一个普通方法,所以不能启动一个新线程。

sleep方法

sleep(long millis)方法的作用是让线程休眠指定的时间,在指定时间内不占用CPU资源。sleep方法的特点有以下几点:

  • 线程处于TIMED_WAITING状态
  • sleep期间不占用CPU资源
  • sleep期间不释放锁(Synchronized锁和ReentrantLock都不释放)
  • sleep方法能响应中断,检测到中断后抛出InterruptedException然后清除中断状态

join方法

join的作用是阻塞当前线程等待加入的线程执行完成后再继续执行。使用这个方法一定要清楚是哪个线程被阻塞,举个例子:

/*** 使用join方法* @author sicimike*/
public class ThreadJoinDemo {public static void main(String[] args) {Thread thread = new Thread(()->{// 可以在此适当的休眠,使结果更清晰System.out.println("sub thread");});thread.start();try {// join方法thread.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main thread");}
}

执行结果

sub thread
main thread

在主线程中调用thread.join(),结果子线程先输出,主线程后输出。这个结果是确定的,不存在随机性。这就是join方法的作用,主线程中调用子线程的join方法,阻塞的主线程,等待子线程执行完成后,主线程继续执行。
日常编码中应该尽量避免使用join方法,而是使用JDK封装好的并发工具CountDownLatchCyclicBarrier代替:并发工具三巨头CountDownLatch、CyclicBarrier、Semaphore使用

接下来看下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()) {// 线程处于可用状态(既不是NEW,也不是TERMINATED)// 永久阻塞wait(0);}} else {while (isAlive()) {// 线程处于可用状态(既不是NEW,也不是TERMINATED)// 计算剩余的阻塞时间long delay = millis - now;if (delay <= 0) {// 阻塞时间已经到了break;}// 阻塞时间未到,阻塞指定时间wait(delay);now = System.currentTimeMillis() - base;}}
}

通过源码可以看出,Thread类中的阻塞是通过wait方法实现的。
值得注意的是:整个方法执行结束也没有执行notify或者notifyAll方法。因为Thread类的run执行结束后,会自动执行notifyAll方法。这也是Thread类不适合作为锁对象的原因。

join方法的特点有以下几点:

  • 线程处于WAITING或者TIMED_WAITING状态
  • 底层调用的wait方法
  • 能响应中断,检测到中断后抛出InterruptedException然后清除中断状态

yield方法

yield方法的作用是释放CPU时间片,然后重新竞争。该方法不会释放锁,也不会改变线程状态,线程始终处于RUNNABLE状态。

停止线程

如何停止线程是一个比较大的话题,之前特意单独拿出来写过: 如何优雅的中断线程 ,此处就不再赘述。

总结

本篇主要深入源码,结合实例较为完整的讲解了JDK中的线程Thread类。具体讲解的内容有线程的类定义、成员变量/常量、核心属性、核心方法、线程启动、线程终止等。

以上便是本篇的全部内容。

Thread类、Runnable接口详解相关推荐

  1. Java Thread类源码详解

    概述 Java所有多线程的实现,均通过封装Thread类实现,所以深入Thread类,对深入理解java多线程很有必要 构造函数: Thread的构造函数,采用缺省的方式实现: //传入Runnabl ...

  2. Thread 类部分常用方法详解

    currentThread() currentThread() 方法用来返回代码段正在被哪个线程调用,它是 Thread 类提供的一个 native 方法,返回一个 Thread 类的实例对象,源码如 ...

  3. python:threading.Thread类的使用详解

    参考: https://blog.csdn.net/drdairen/article/details/60962439 http://www.cnblogs.com/429512065qhq/p/87 ...

  4. Thread 与Runnable区别详解

    //使用Thread实现线程不能实现资源共享 class MyThread extends Thread { private int ticket=5; private String name; pu ...

  5. Python多线程编程(一):threading 模块 Thread 类的用法详解

    我们进行程序开发的时候,肯定避免不了要处理并发的情况. 一般并发的手段有采用多进程和多线程. 但线程比进程更轻量化,系统开销一般也更低,所以大家更倾向于用多线程的方式处理并发的情况. Python 提 ...

  6. JAVA8学习7-Collector接口详解以及实现类

    7 Collector 接口详解(collect 收集器.Collectors)***************** collect: 收集器 Collector 作为 collect 方法的参数 Co ...

  7. java 重启线程_java 可重启线程及线程池类的设计(详解)

    了解JAVA多线程编程的人都知道,要产生一个线程有两种方法,一是类直接继承Thread类并实现其run()方法:二是类实现Runnable接口并实现其run()方法,然后新建一个以该类为构造方法参数的 ...

  8. 【java8新特性】——lambda表达式与函数式接口详解(一)

    一.简介 java8于2014年发布,相比于java7,java8新增了非常多的特性,如lambda表达式.函数式接口.方法引用.默认方法.新工具(编译工具).Stream API.Date Time ...

  9. Callable接口详解

    Callable接口详解 Callable: 返回结果并且可能抛出异常的任务. 优点: 可以获得任务执行返回值: 通过与Future的结合,可以实现利用Future来跟踪异步计算的结果. Runnab ...

最新文章

  1. 学生科技周的讲座-2021-内容准备
  2. linux下source insight安装以及打开
  3. C++ Primer 5th笔记(chap 16 模板和泛型编程)类型无关和模板编译
  4. 傻瓜式制作的U盘winpe(支持4G以上U盘)--速度超快
  5. VTK:交叉点 PolyData 过滤器用法实战
  6. 高压断路器故障诊断的相关方法
  7. 超级好用 将html字符串,转化为纯文本
  8. java如何限制输入值_[限制input输入类型]常用限制input方法
  9. Windows下第三方库安装Nuget与Vcpkg
  10. SuperRuntimeLibrary.TextVoice 发布,支持文本到语音 文本到.wav
  11. java读取nfc数据_JAVA有关NFC读卡器读取数据
  12. 闲聊人工智能产品经理(AIPM)—人工智能产品经理工作流程
  13. smb协议讲解_SMB/CIFS协议解析一概述
  14. 探测器类的电路设计流程框图
  15. 减少域名DNS解析时间将网页加载速度提升新层次
  16. vs2012程序打包部署下载InstallShield2015LimitedEdition的下载及安装打包整套教程
  17. 苹果手机又刷屏啦!!它是如何做到的?
  18. 图特摩斯三世厚积薄发
  19. 安卓 11 文件储存
  20. html国庆节代码,小程序10行代码实现微信头像挂红旗,国庆节个性化头像

热门文章

  1. 使用队列解决约瑟夫环问题
  2. 利用docker部署深度学习模型的一个最佳实践
  3. 让万圣节更可怕,玩乐一晚的恐怖电影派对主题
  4. Python基础(5)-Pandas
  5. 微信支付 postman_微信分付开通入口在这里!教你顺利申请额度!包教包会!
  6. react项目的搭建与启动
  7. 前端开发语言有哪些?需要掌握什么?
  8. vue3之 element-plus的循环图标
  9. 信息安全数学基础-素数模高次同余方程 2021-10-09
  10. 一道有趣的大厂测试面试题,你能用 Python or Shell 解答吗?