前言
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;
// 线程ID
private 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 {
@Override
public void run() {
System.out.println("extends Thread");
}
}
class SicThread2 implements Runnable {
@Override
public 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层的name
setNativeName(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);
// 线程是否已经启动,启动后设置成true
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
// 启动失败,把线程从线程组中删除
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it 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
类。具体讲解的内容有线程的类定义、成员变量/常量、核心属性、核心方法、线程启动、线程终止等。
以上便是本篇的全部内容。