【高并发系列】4.线程的核心原理和基本操作

本文介绍了Java中线程的核心原理,包括线程调度模型(分时与抢占式),线程优先级,线程的六种生命周期状态(新建、可运行、阻塞、无限期等待、限时等待、死亡)以及状态转换。此外,还讨论了守护线程的概念,线程的基本操作如设置/获取线程名称、线程休眠、中断、合并和让步。最后提到了线程中断的优雅方式和join方法的使用。
摘要由CSDN通过智能技术生成

今天总结一下Java中线程的核心原理及基本操作。

一、线程的核心原理

由于现代操作系统提供了强大的线程管理能力,Java 就将线程调度工作委托给了操作系统的调度进程处理,而不需要进行独立的线程管理和调度。

1、线程调度模型

首先,要搞清楚线程调度与 CPU 时间片的关系。

什么是 CPU 时间片呢?因为 CPU 计算频率很高,甚至可达每秒计算10亿次,如果我们将 CPU 的时间从毫秒维度进行分段,那么每一小段就是一个 CPU 时间片。

由于时间片非常短,在各个线程之间快速的切换,因此看上去很多个线程在"同时执行"或"并发执行"一样。目前操作系统主流的线程调度方式,就是基于 CPU 时间片的。也就是说,线程只有得到 CPU 时间片才能执行指令(执行状态),没有获取到 CPU 时间片的线程,则会等待系统分配下一个 CPU 时间片(就绪状态)。

线程调度模型目前主要有2种,即分时调度和抢占式调度。

分时调度模型,指的是操作系统会平均分配 CPU 时间片,所有线程轮流占有 CPU,有点"众生平等"的意思。

抢占式调度模型,指的是操作系统按照线程优先级分配 CPU 时间片。优先级高的线程优先分配 CPU 时间片,如果所有就绪状态的线程的优先级相同,那么会随机选择一个线程。

目前大部分操作系统采用的是抢占式调度模型,因为 Java 的线程管理和调度是委托给操作系统完成,所以 Java 的线程调度也是抢占式调度模型。

2、线程优先级

在 Java 线程 Thread 类存在一个实例属性(private int priority;)和两个实例方法(getPriority(),setPriority()),用于线程优先级的有关操作。

private int            priority;

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;

public final void setPriority(int newPriority) {
    ......
}

public final int getPriority() {
    return priority;
}

线程优先级最小值为1,最大值为10,Thread 实例的priority属性默认是级别5,对应的类常量是NORM_PRIORITY。

由于 Java 使用的是抢占式调度模型进行线程调度,当线程实例 priority 属性优先级越高,获取 CPU 时间片的概率就越大!

3、线程的生命周期

Java 中线程的生命周期有六种状态,上面提及的执行状态和就绪状态就包含其中。

在 Thread 类中有一个实例属性(private int threadStatus;)和一个实例方法(public Thread.State getState(); )就是用于保存和获取线程的状态。其中,Thread.State 是一个内部枚举类,通过定义六个枚举常量来表示 Java 线程的六种状态,如下所示:

public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

接着,Java 线程的这6种状态及各种状态进入的条件,详细介绍下。

3.1 新建状态(NWE)

线程创建之后,但未通过start()方法启动线程,此时线程处于新建状态。

前两篇文章介绍的四种创建线程方式,但前三种本质都是通过 new Thread() 创建的,是创建了不同的 target 执行目标实例(比如 Runnable 实例)。

3.2 可运行状态(RUNABLE)

Java 中把就绪状态(READY)和执行状态(RUNNING),统称为可运行状态。调用了线程的 start() 后,线程就进入了就绪状态。如果线程获取到 CPU 时间片后,则会执行 run() 里的业务逻辑,进入了执行状态。

请注意,就绪状态只能表示线程具备运行的资格,至于什么时候进入执行状态,要看操作系统的调度情况。

那么,哪些情况可以使线程进入就绪状态呢?

  • 1. 调用线程的start()方法后。
  • 2. 当前线程分配的CPU时间片用完了。
  • 3. 线程休眠操作结束。
  • 4. 线程合并操作结束。
  • 5. 当前线程让出CPU执行权。
  • 6. 等待用户输入结束。
  • 7. 线程争抢到对象锁。

3.3 阻塞状态(BLOCKED)

线程处于阻塞状态,不会占用CPU,以下情况会使线程进入阻塞状态:

  • 等待获取锁。
  • io阻塞(磁盘io,网络io等)。

线程等待获取锁,而该锁被其他线程持有,则该线程进入阻塞状态,只有当其他线程释放了该锁,并且线程调度器允许该线程持有该锁时,该线程退出阻塞状态。

至于io阻塞,线程进行一个阻塞式io操作后,如果不具备io操作条件,则会进入阻塞状态。一个简单的例子就是,线程等待用户输入完成后才继续执行。

3.4 无限期等待(WAITING)

线程处于无限期等待状态,需要被其他线程显式的唤醒,才能进入就行状态。

无限期等待及被唤醒的方式有三种:

  • 调用了线程实例方法 join(),对应的唤醒方式为:被合并的线程执行完毕。
  • Object.wait() 方法,对应的唤醒方式为:调用 Object.notify() 或 Object.notifyAll()。
  • LockSupport.park() 方法,对应的唤醒方式为调用 LockSupport.unpark(…)。

3.5 限时等待(TIMED_WAITING)

在指定时间内没有被唤醒,处于限时等待的线程会被系统自动唤醒,随后进入就绪状态。

限时等待及被唤醒的方式也有三种:

  • 调用了线程 sleep(time) 方法,对应的唤醒方式为:睡眠时间结束。
  • Object.wait(time) 方法,对应的唤醒方式为:调用 Object.notify() 或 Object.notifyAll() 主动唤醒,或者限时结束。
  • LockSupport.parkNanos(time) 或 parkUntil(time) 方法,对应唤醒方式为:线程调用配套的 LockSupport.unpark(Thread) 方法结束,或者线程停止时限结束。

3.6 死亡状态(TERMINATED)

线程执行任务结束,正常进入死亡状态。或者执行任务过程中发生了异常,但未处理异常,也会导致线程死亡。

4、守护线程

守护线程也叫做后台线程,指的是在程序进程运行过程中,在后台提供某种通用服务的线程。比如,每启动一个 JVM 进程,都会在后台运行一系列的 GC(垃圾回收)线程,这些 GC 线程就是守护线程,在后台提供垃圾回收服务。

在 Java Thread 类中提供了一个实例属性(daemon)和两个实例方法(setDaemon,isDaemon)对守护线程进行操作。实例属性 daemon,保存了一个线程实例的守护状态,默认为 false,表示线程默认为用户线程。set方法可设置守护线程(true)或用户线程(false),而is方法则是用于判定线程类型。

从守护线程角度,Java 线程可分为用户线程和守护线程,二者本质区别在于,与 Java 虚拟机进程终止的方向不同。

用户线程和 JVM 进程是主动关系,如果用户线程全部终止,JVM 虚拟机进程也会随之终止;守护线程和JVM进程是被动关系,如果JVM进程终止,所有的守护线程也随之终止。如下图所示:

为了更好的理解,可以换个角度解释,守护线程可看作是服务提供者,用户线程可看作是消费者。

只有用户线程全部终止了,也就是没有了消费者,守护线程提供的服务就没有任何意义了,也可以全部终止了。或者说,只要还有一个用户线程存在,守护线程还是有存在的必要!

请注意:

  • 如果线程为守护线程,就必须在线程实例的start() 方法调用之前调用线程实例的 setDaemon(true),设置其 daemon 实例属性值为 true,否则 JVM 会抛出 InterruptedException 异常。
  • 守护线程有被 JVM 终止的风险,要避免使用守护线程去访问系统资源(数据库连接,文件句柄等),否则可能造成系统资源无法挽回的损坏。
  • 在守护线程中创建线程,新线程也是守护线程。创建之后,如果通过调用 setDaemon(false) 将新线程显式地设置为用户线程,新线程可以调整成用户线程。

二、线程的基本操作

Java 线程的常用操作基本都在 Thread 类中,包括一些静态方法和线程实例方法等。

1、set/get线程名称

设置和获取线程名称是 Java 线程最基本的操作了,我们可以通过 Thread 构造器初始化设置线程名称,也可以调用实例方法 setName(…) 设置,而获取线程名称则通过 getName() 方法即可。

//入参带线程名称的 Thread 构造器
public Thread(String name){......}
public Thread(Runnable target, String name) {......}
public Thread(ThreadGroup group, String name) {......}
public Thread(ThreadGroup group, Runnable target, String name) {......}
public Thread(ThreadGroup group, Runnable target, String name, long stackSize) {......}

public final synchronized void setName(String name) {
    ......
}

public final String getName() {
    return name;
}

实际中,关于线程名称,有一些规范需要知道:

  • 应避免给不同的线程对象设置相同的线程名称;
  • 如果不设置线程名称,系统会自动设置线程名称,通常格式为 Thread-编号;
  • 如果调用 Thread.currentThread() 静态方法获取当前正在执行的线程名称,该线程是当前线程,表示当前正在执行代码逻辑的Java线程;
  • 创建线程或线程池时,需要命名有指定含义的线程名称,方便报错排查问题!

2、线程休眠

线程休眠,指的是让当前正在执行任务的线程休眠,让CPU去执行其他的任务。从线程状态角度看,就是线程由执行状态变成了阻塞状态。

在 Java Thread 类提供了一组线程休眠的静态方法,如下所示:

public static native void sleep(long millis) throws InterruptedException;

public static void sleep(long millis, int nanos)
    throws InterruptedException {
    ......
}

请注意:

  • 使用 sleep() 方法需要捕获中断异常,入参的时间单位是毫秒或纳秒。
  • 线程休眠时间到后,线程不一定会立即执行,因为CPU可能正在执行其他任务,此时的线程会进入就绪状态,等待下一次分配CPU时间片。

3、线程中断

线程中断,指的是将正在执行任务的线程打断,终止运行。

在 Java Thread 类中提供了 stop() 方法(该方法已标记为过时)用于终止正在运行的线程,为什么不推荐使用呢?

因为 stop() 方法是简单粗暴且危险的操作。举个例子,比如,家里的洗衣机正在洗衣,热水器正在烧水,或者其他电器正在工作中,突然电闸被拉掉了,所有电器会被强行终止当前工作,是不是很不开心!而如果换一种更优雅的停电方式岂不更好,比如,我要半小时后停电,此时只是发出一个通知或者叫标记即将停电状态,家里的电器都有了完成既定工作后做好停电准备的时间。

从程序上看,也是不能随随便便终止线程运行的,因为我们不知道正在运行的线程都正在干啥。这个线程有可能在执行操作数据库的任务,强行终止会造成数据不一致的问题;这个线程也有可能持有锁,强行终止会导致锁不能释放的问题。正是由于调用 stop() 方法强行终止线程会造成不可预知的结果,所以不推荐使用!

那么,该如何优雅的终止正在运行的线程呢?其实,原理和停电例子说明的一样,通过标记中断状态而不是直接中断。

在 Java Thread 类就提供了 interrupt() 方法,可用于更优雅的终止线程。该方法使用场景有两个:

  • 如果线程被阻塞(比如,调用了 wait(),sleep(),join() 等方法造成阻塞),此时调用interrupt() 方法就会立即退出阻塞状态,并抛出 InterruptedException 异常,线程就可以通过捕获该异常来做一定的处理,然后让线程退出。
  • 如果线程正在运行,线程不会受到任何影响,只是线程的中断标记字段被设置成了true。我们可以在适当的位置通过调用 isInterrupted() 方法来查看自己是否被中断,并执行退出操作。
public void interrupt() {
    ......
    synchronized(){
        ......
    }
    ......
}

public boolean isInterrupted() {
    ......
}

因此,interrupt() 方法只是改变中断状态,不会中断一个正在运行的线程。

至于线程是否停止执行,需要程序去监视线程的 isInterrupted() 状态,并进行相应的处理,演示如下:

    @Test
    public void testInterrupted() throws InterruptedException {
        Thread thread = new Thread() {
            @SneakyThrows
            public void run() {
                log.info("线程启动了...");
                //一直循环
                while (true) {
                    log.info(String.valueOf(isInterrupted()));
                    Thread.sleep(1000);
                    //如果线程被中断,就退出死循环
                    if (isInterrupted()) {
                        log.info("线程结束了...");
                        return;
                    }
                }
            }
        };
        thread.start();
        Thread.sleep(2000); //等待2秒
        thread.interrupt(); //中断线程
    }

输出结果:

4、线程合并

假设有两个线程A和B,线程A运行过程中需要依赖线程B的执行逻辑,这就是线程合并。为了更好理解线程A合并线程B的流程,画图如下:

在 Java Thread 类中提供了一组实例方法做合并操作,这里 join() 方法有三个重载版本,如下:

public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis)
    throws InterruptedException {
    ......
}

public final synchronized void join(long millis, int nanos)
    throws InterruptedException {
    ......
}

需要注意:

  • 线程A合并线程B,需要使用线程B的实例去调用 join() 方法,此时线程A会让出 CPU 时间片,并进入等待状态。
  • 线程A在等待状态时,如果被中断会抛出 InterruptedException 异常。
  • 线程A可以处于无限等待状态,或限时等待状态,取决于 join() 方法入参是否使用限时参数。
  • 线程合并,优点是操作简单,缺点是不知道线程B的执行结果。

我们知道,当一个线程调用 Object 的 wait() 方法后,如果没有唤醒操作,该线程会一直处于等待状态。而无参的 join() 也是这种情况,如果线程B一直在执行,那么线程A则会一直等待线程B执行完成,才会结束线程A的等待状态。当然,有限时参数的 join() 方法,即处于限时等待的线程A,无论线程B有没有执行完逻辑,都会结束等待状态,转为可运行状态。

请注意,线程结束等待状态不一定会立即被分配到 CPU 时间片,要看操作系统的具体调度情况,毕竟话事人是操作系统~

5、线程让步

线程让步,指的是当前正在执行任务的线程放弃正在执行的操作,让出 CPU 使用权,使得 CPU 去执行其他的线程。从线程状态角度看,当前正在执行任务的线程由执行状态变成了就绪状态。

在 Java Thread 类中提供了一个静态方法 yield(),表示当前正在执行的线程放弃 CPU 时间片使用,让线程暂停,但不会阻塞该线程,只是转为就绪状态而已。

public static native void yield();

至于该线程什么时候重占 CPU 时间片是不确定的,要看系统的调度情况(优先级等),有可能刚放弃又被分配了 CPU 时间片,也有可能放弃之后很长时间才被分配。

最后~

这里,主要梳理和总结了 Java 线程的调度模型,优先级,生命周期,六种线程状态和进入条件,守护线程与用户线程,以及线程的基本操作,比如线程休眠,中断,合并,让步等内容。Java 多线程是基础而重要的内容,可以说是开发必知必备的,有必要学习和掌握。

【不积硅步无以至千里,共同进步吧~】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值