Thread类、Runnable接口详解
前言
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();}
}
其中target
是Runnable
类型的引用,也可以看做线程的执行单元,结合下面一个小实例:
/*** @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();
中调用的SicThread1
的start
方法,间接调用重写的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
,调用target
的run
方法。至于这两种方法本质上算不算同一种,这就“仁者见仁,智者见智”了。既然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
的核心方法start
、sleep
、join
、yield
。
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启动一个新线程 - 如果启动失败,从线程组中删除线程
再来回顾下Thread
中run
方法的实现:
@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封装好的并发工具CountDownLatch
和CyclicBarrier
代替:并发工具三巨头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接口详解相关推荐
- Java Thread类源码详解
概述 Java所有多线程的实现,均通过封装Thread类实现,所以深入Thread类,对深入理解java多线程很有必要 构造函数: Thread的构造函数,采用缺省的方式实现: //传入Runnabl ...
- Thread 类部分常用方法详解
currentThread() currentThread() 方法用来返回代码段正在被哪个线程调用,它是 Thread 类提供的一个 native 方法,返回一个 Thread 类的实例对象,源码如 ...
- python:threading.Thread类的使用详解
参考: https://blog.csdn.net/drdairen/article/details/60962439 http://www.cnblogs.com/429512065qhq/p/87 ...
- Thread 与Runnable区别详解
//使用Thread实现线程不能实现资源共享 class MyThread extends Thread { private int ticket=5; private String name; pu ...
- Python多线程编程(一):threading 模块 Thread 类的用法详解
我们进行程序开发的时候,肯定避免不了要处理并发的情况. 一般并发的手段有采用多进程和多线程. 但线程比进程更轻量化,系统开销一般也更低,所以大家更倾向于用多线程的方式处理并发的情况. Python 提 ...
- JAVA8学习7-Collector接口详解以及实现类
7 Collector 接口详解(collect 收集器.Collectors)***************** collect: 收集器 Collector 作为 collect 方法的参数 Co ...
- java 重启线程_java 可重启线程及线程池类的设计(详解)
了解JAVA多线程编程的人都知道,要产生一个线程有两种方法,一是类直接继承Thread类并实现其run()方法:二是类实现Runnable接口并实现其run()方法,然后新建一个以该类为构造方法参数的 ...
- 【java8新特性】——lambda表达式与函数式接口详解(一)
一.简介 java8于2014年发布,相比于java7,java8新增了非常多的特性,如lambda表达式.函数式接口.方法引用.默认方法.新工具(编译工具).Stream API.Date Time ...
- Callable接口详解
Callable接口详解 Callable: 返回结果并且可能抛出异常的任务. 优点: 可以获得任务执行返回值: 通过与Future的结合,可以实现利用Future来跟踪异步计算的结果. Runnab ...
最新文章
- 学生科技周的讲座-2021-内容准备
- linux下source insight安装以及打开
- C++ Primer 5th笔记(chap 16 模板和泛型编程)类型无关和模板编译
- 傻瓜式制作的U盘winpe(支持4G以上U盘)--速度超快
- VTK:交叉点 PolyData 过滤器用法实战
- 高压断路器故障诊断的相关方法
- 超级好用 将html字符串,转化为纯文本
- java如何限制输入值_[限制input输入类型]常用限制input方法
- Windows下第三方库安装Nuget与Vcpkg
- SuperRuntimeLibrary.TextVoice 发布,支持文本到语音 文本到.wav
- java读取nfc数据_JAVA有关NFC读卡器读取数据
- 闲聊人工智能产品经理(AIPM)—人工智能产品经理工作流程
- smb协议讲解_SMB/CIFS协议解析一概述
- 探测器类的电路设计流程框图
- 减少域名DNS解析时间将网页加载速度提升新层次
- vs2012程序打包部署下载InstallShield2015LimitedEdition的下载及安装打包整套教程
- 苹果手机又刷屏啦!!它是如何做到的?
- 图特摩斯三世厚积薄发
- 安卓 11 文件储存
- html国庆节代码,小程序10行代码实现微信头像挂红旗,国庆节个性化头像