前言

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. javascript定时器
  2. 无法安装ie8,因为其他程序或更新正在等待重启您的计算机,ie8 因为其他程序或更新正在等待重新启...
  3. PHP实用小程序(四)
  4. linux(fedora) 下dvwa 建筑环境
  5. 几道偏序问题(数据结构)
  6. 可下拉选项可模糊查询的文本输入框
  7. 聚类 6 Affinity Propagation
  8. stm32—光敏电阻传感器的初步使用
  9. JS判断数组是否包含某个元素
  10. 什么是王道?什么是王道中的王道?
  11. CPU性能排行与评分
  12. selenium+python爬取京东评论最多的计算机配置信息
  13. Mac双系统Win10系统安装MySQL的坑
  14. VS Code 常用快捷键代码大全
  15. Docker 使用--link出现Cannot link to /xxx, as it does not belong to异常
  16. Spring源码分析(二)BeanFactoryPostProcessor之ConfigurationClassPostProcessor的调用过程
  17. 剑指 Offer II 076. 数组中的第 k 大的数字
  18. 软件架构设计 大型网站技术架构与业务架构融合之道
  19. 三轮DES差分攻击(免费完整代码)
  20. 算法笔记--极大极小搜索及alpha-beta剪枝

热门文章

  1. 小方制药冲刺A股上市:毛利率走低,方之光、鲁爱萍夫妇为实控人
  2. VBA POST 调用网页API 格式化SQL语句
  3. 微众银行重视用户体验 完善消费者权益保护体系
  4. 简明教程 | 用 PPT 做卡通热图 - eFP Graph?!
  5. 解决ENSP路由器启动之后一直出现#问题
  6. 算法:递归启蒙-汉诺塔
  7. css文字超出省略号代替
  8. 蓝桥杯_穿越雷区 java
  9. 测试架构师: 软件测试架构师应该做和不该做的事情
  10. day01-Web自动化测试进阶