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;

	// 线程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();
    }
}

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

/**
 * @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();

中调用的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层的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的核心方法startsleepjoinyield

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启动一个新线程
  • 如果启动失败,从线程组中删除线程

再来回顾下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类。具体讲解的内容有线程的类定义、成员变量/常量、核心属性、核心方法、线程启动、线程终止等。

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

发布了52 篇原创文章 · 获赞 107 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Baisitao_/article/details/103689603