Java的线程

科技资讯 投稿 10700 0 评论

Java的线程

介绍线程

每个线程有自己的程序计数器、栈(Stack)、寄存器(Register)、本地存储(Thread Local)等,但是会和进程内其他线程共享文件描述符、虚拟地址空间等。


守护线程(Daemon Thread)

在 Java 中将线程设置为守护线程,具体的实现代码如下所示:

public static void main(String[] args {
    Thread daemonThread = new Thread(;
    // 必须在线程启动之前设置
    daemonThread.setDaemon(true;
    daemonThread.start(;
}

通用的线程生命周期

在操作系统层面,线程有生命周期。

通用的线程生命周期基本上可以用下图这个 “五态模型” 来描述。这五态分别是:初始状态、可运行状态、运行状态、休眠状态和终止状态。


初始状态

初始状态属于编程语言特有的,这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有被创建。


可运行状态

在可运行状态下,真正的操作系统线程已经被创建。多个线程处于可运行状态时,操作系统会根据调度算法选择一个线程运行。


运行状态

在 Java 中,运行状态相当于是调用了 Thread#start( 方法,并且线程被分配 CPU 执行。


休眠状态

当等待的资源或条件满足后,线程就会从休眠状态转换到可运行状态,并等待 CPU 调度。


终止状态


这五种状态在不同编程语言里会有简化合并。例如:

    C 语言的 POSIX Threads 规范,就把初始状态和可运行状态合并了;
  • Java 程序设计语言把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而 Java 虚拟机层面不关心这两个状态,因为 Java 虚拟机把线程调度交给操作系统处理了。

Java 的线程生命周期

不同的程序设计语言对于操作系统线程进行了不同的封装,下面我们学习一下 Java 的线程生命周期。

    NEW(初始状态)
  1. RUNNABLE(可运行 / 运行状态)
  2. BLOCKED(阻塞状态)
  3. WAITING(无时限等待)
  4. TIMED_WAITING(有时限等待)
  5. TERMINATED(终止状态)

NEW(初始状态)、TERMINATED(终止状态)和通用的线程生命周期中的语义相同。

所以 Java 中的线程生命周期可以简化为下图:


    那具体是哪些情形会导致线程从 RUNNABLE 状态转换到这三种状态呢?
  • 而这三种状态又是何时转换回 RUNNABLE 的呢?
  • 以及 NEW、TERMINATED 和 RUNNABLE 状态是如何转换的?

下面我们详细讲解。

Java 的线程状态切换

从 NEW 到 RUNNABLE 状态

NEW 状态的线程,不会被操作系统调度,因此不会执行。Java 线程要执行,就必须转换到 RUNNABLE 状态。

public static void main(String[] args {
    Thread thread = new Thread(new Runnable( {
        @Override
        public void run( {
            System.out.println("hello";
        }
    };
    thread.start(;
}

从 RUNNABLE 到 TERMINATED 状态

线程执行完 Thrad#run( 方法后,会自动从 RUNNABLE 状态转换到 TERMINATED 状态。

1. RUNNABLE 与 BLOCKED 的状态转换

只有一种场景会触发 RUNNABLE 与 BLOCKED 的状态转换,就是线程等待 synchronized 的隐式锁。

    当使用 synchronized 申请加锁失败时,该线程的状态就会从 RUNNABLE 转换到 BLOCKED 状态。
  • 当等待的线程获得锁时,该线程的状态就会从 BLOCKED 状态转换到 RUNNABLE 状态。

Java 虚拟机层面并不关心操作系统调度相关的状态,因为在 Java 虚拟机看来,等待 CPU 的使用权(操作系统层面此时处于可执行状态)与等待 I/O(操作系统层面此时处于休眠状态)没有区别,都是在等待某个资源,所以都归入了 RUNNABLE 状态。

2. RUNNABLE 与 WAITING 的状态转换

总体来说,有三种场景会触发 RUNNABLE 与 WAITING 的状态转换。


第一种场景,获得 synchronized 隐式锁的线程,调用无参数的 Object#wait( 方法。

public void method( throws InterruptedException {
    synchronized (this {
        this.wait(;
    }
}
    当调用 wait( 方法时,调用方法的线程的状态从 RUNNABLE 状态转换到 WAITING 状态
  • 当调用 notify( 方法时,被唤醒的线程的状态从 WAITING 状态转换到 RUNNABLE 状态

第二种场景,调用无参数的 Thread#join( 方法。

    当调用 A.join( 方法时,执行这条语句的线程会等待 thread A 执行完,而等待中的这个线程,其状态会从 RUNNABLE 转换到 WAITING。
  • 当线程 thread A 执行完,原来等待它的线程又会从 WAITING 状态转换到 RUNNABLE。

Thread#join( 方法的实现基于 Object#wait(。


第三种场景,调用 LockSupport#park( 方法。

    当调用 LockSupport.park( 方法时,调用方法的线程的状态从 RUNNABLE 转换到 WAITING。
  • 当调用 LockSupport.unpark(Thread thread 方法时,被唤醒的线程的状态从 WAITING 状态转换到 RUNNABLE 状态

总结来说:Object#wait( 和 LockSupport#park( 方法使线程的状态转换到 WAITING。

3. RUNNABLE 与 TIMED_WAITING 的状态转换

    获得 synchronized 隐式锁的线程,调用带超时参数的 Object#wait(long timeout 方法;
  1. 调用带超时参数的 Thread#join(long millis 方法;(底层调用 Object#wait(long timeout )
  2. 调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline 方法;
  3. 调用带超时参数的 LockSupport.parkUntil(long deadline 方法。
  4. 调用带超时参数的 Thread.sleep(long millis 方法;

这里你会发现:

    TIMED_WAITING 和 WAITING 状态的区别,仅仅是触发条件多了超时参数。
  • 与 RUNNABLE 与 WAITING 的状态转换 相比,多了一个 Thread.sleep( 场景。

Java 线程 API 的使用

线程的创建

    继承 Thread 类,重写 run( 方法。
  1. 实现 Runnable 接口,实现其中的 run( 方法。将该实现类的对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象。
  2. 实现 Callable 接口,实现其中的 call( 方法。将该实现类的对象作为参数传递到 FutureTask 类的构造器中,创建FutureTask 类的对象。将 FutureTask 类的对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象。Callable 它解决了 Runnable 无法返回结果的困扰。

「实现 Runnable 接口」VS「继承 Thread 类」

    通过实现(implements)的方式没有类的单继承性的局限性
  • 实现的方式更适合处理多个线程有共享数据的情况

    call( 可以有返回值
  • call( 可以抛出异常被外面的操作捕获,获取异常的信息
  • 「实现 Callable 接口」支持泛型

// 自定义线程对象
class MyThread extends Thread {
    public void run( {
        // 线程需要执行的代码
        ......
    }
}

// 创建线程对象
MyThread myThread = new MyThread(;
// 实现Runnable接口
class Runner implements Runnable {
    @Override
    public void run( {
        // 线程需要执行的代码
        ......
    }
}

// 创建线程对象
Thread thread = new Thread(new Runner(;
public static void main(String[] args throws ExecutionException, InterruptedException {
    MyTask task = new MyTask(;
    // FutureTask 用于接收运算结果
    FutureTask futureTask = new FutureTask<>(task;
    Thread thread = new Thread(futureTask;

    thread.start(;
	// FutureTask 可用于线程间同步 (当前线程等待其他线程执行完成之后,当前线程才继续执行
    // get( 返回值即为 FutureTask 构造器参数 Callable 实现类实现的 call( 的返回值
    System.out.println(futureTask.get(;
}

public class MyTask implements Callable {
    @Override
    public String call( {
        // 若不需要返回值,可 return null;
        return "ok";
    }
}

线程的执行

创建好 Thread 类的对象后,通过调用 Thread#start( 方法创建线程执行任务。

    启动一个新的线程
  1. 新的线程调用 run( 方法

线程的停止

有时候我们需要强制中断 run( 方法的执行,例如 run( 方法访问一个很慢的网络,我们等不下去了,想终止怎么办呢?Java 的 Thread 类里面倒是有个 stop( 方法,不过已经标记为 @Deprecated,所以不建议使用了。正确的方式是调用 interrupt( 方法。Thread#interrupt( 配合合适的代码,即可优雅的实现线程的终止。

    stop( 方法会真的杀死线程,不给线程喘息的机会,如果线程持有 ReentrantLock 锁,被 stop( 的线程并不会自动调用 ReentrantLock 的 unlock( 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了。所以该方法就不建议使用了,类似的方法还有 suspend( 和 resume( 方法,这两个方法同样也都不建议使用了。
  • interrupt( 方法仅仅是通知线程,线程有机会执行一些后续操作,线程也可以无视这个通知。被 interrupt 的线程,是怎么收到通知的呢?一种是异常,另一种是主动检测。

异常

上面我们提到转换到 WAITING、TIMED_WAITING 状态的触发条件,都是调用了类似 wait(、join(、sleep( 这样的方法,我们看这些方法的签名,发现都会 throws InterruptedException 这个异常。这个异常的触发条件就是:其他的线程调用了该线程的 interrupt( 方法。

    当线程 A 处于 RUNNABLE 状态,并且阻塞在 java.nio.channels.InterruptibleChannel 上时,如果其他的线程调用线程 A 的 interrupt( 方法,线程 A 会触发 java.nio.channels.ClosedByInterruptException 这个异常;
  • 当线程 A 处于 RUNNABLE 状态,并且阻塞在 java.nio.channels.Selector 上时,如果其他的线程调用线程 A 的 interrupt( 方法,线程 A 的 java.nio.channels.Selector 会立即返回。

上面这两种情况属于被中断的线程通过异常的方式获得了通知。


主动检测

参考资料

第17讲 | 一个线程两次调用start(方法会出现什么情况?-极客时间 (geekbang.org

06 | 线程池基础:如何用线程池设计出更“优美”的代码? (geekbang.org

11 | 线程:如何让复杂的项目并行执行?-极客时间 (geekbang.org

编程笔记 » Java的线程

赞同 (50) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽