多线程(一)

1. 线程的生命周期

线程生命周期状态图:

 

1.1 线程的NEW状态

当我们用关键字new创建一个Thread对象时,此时它并不处于执行状态,因为没有调用start方法启动该线程,那么线程的状态为NEW状态,准确地说,它只是Thread对象的状态,因为在没有start之前,该线程根本不存在,与你用关键字new创建一个普通的Java对象没什么区别。
NEW状态通过start方法进入RUNNABLE状态。


1.2 线程的RUNNABLE状态

线程对象进入RUNNABLE状态必须调用start方法,那么此时才是真正地在JVM进程中创建了一个线程,线程一经启动就可以立即得到执行吗?答案是否定的,线程的运行与否和进程一样都要听令于CPU的调度,那么我们把这个中间状态称为可执行状态(RUNNABLE),也就是说它具备执行的资格,但是并没有真正地执行起来而是在等待CPU的调度。
由于存在Running状态,所以不会直接进入BLOCKED状态和TERMINATED状态,即使是在线程的执行逻辑中调用wait、sleep或者其他block的IO操作等,也必须先获得CPU的调度执行权才可以,严格来讲,RUNNABLE的线程只能意外终止或者进入RUNNING状态。


1.3 线程的RUNNING状态

一旦CPU通过轮询或者其他方式从任务可执行队列中选中了线程,那么此时它才能真正地执行自己的逻辑代码,需要说明的一点是一个正在RUNNING状态的线程事实上也是RUNNABLE的,但是反过来则不成立。
在该状态中,线程的状态可以发生如下的状态转换。
·直接进入TERMINATED状态,比如调用JDK已经不推荐使用的stop方法或者判断某个逻辑标识。
·进入BLOCKED状态,比如调用了sleep,或者wait方法而加入了waitSet中。
·进行某个阻塞的IO操作,比如因网络数据的读写而进入了BLOCKED状态。
·获取某个锁资源,从而加入到该锁的阻塞队列中而进入了BLOCKED状态。
·由于CPU的调度器轮询使该线程放弃执行,进入RUNNABLE状态。
·线程主动调用yield方法,放弃CPU执行权,进入RUNNABLE状态。


1.4 线程的BLOCKED状态

1.3节中已经列举了线程进入BLCOKED状态的原因,此处就不再赘述了,线程在BLOCKED状态中可以切换至如下几个状态。
·直接进入TERMINATED状态,比如调用JDK已经不推荐使用的stop方法或者意外死亡(JVM Crash)。
·线程阻塞的操作结束,比如读取了想要的数据字节进入到RUNNABLE状态。
·线程完成了指定时间的休眠,进入到了RUNNABLE状态。
·Wait中的线程被其他线程notify/notifyall唤醒,进入RUNNABLE状态。
·线程获取到了某个锁资源,进入RUNNABLE状态。
·线程在阻塞过程中被打断,比如其他线程调用了interrupt方法,进入RUNNABLE状态。


1.5 线程的TERMINATED状态

TERMINATED是一个线程的最终状态,在该状态中线程将不会切换到其他任何状态,线程进入TERMINATED状态,意味着该线程的整个生命周期都结束了,下列这些情况将会使线程进入TERMINATED状态。
·线程运行正常结束,结束生命周期。
·线程运行出错意外结束。
·JVM Crash,导致所有的线程都结束。

2. Thread API

Java Thread API 提供了一个强大的工具集,用于创建、管理、控制和操作线程。线程是 Java 中实现并发和多任务处理的核心。下面详细介绍 Java Thread API,包括常用方法、构造函数及其用法。

2.1  Thread 类的概述

Thread 类是 Java 中用于表示线程的核心类。通过 Thread 类,可以创建新的线程,并控制线程的执行。线程可以通过继承 Thread 类或实现 Runnable 接口来定义。

2.2  创建线程的方式

继承 Thread:直接继承 Thread 类,并重写 run() 方法。

实现 Runnable 接口:实现 Runnable 接口,然后将其传递给 Thread 构造函数。

实现 Callable 接口(通过 FutureTask):实现 Callable 接口,用于线程执行后返回结果。

2.3. 常用构造方法

// 构造函数示例
Thread()                       // 创建一个默认的新线程
Thread(Runnable target)        // 通过传入Runnable对象创建线程
Thread(String name)            // 创建具有指定名称的线程
Thread(Runnable target, String name)  // 通过传入Runnable对象和线程名称创建线程
Thread(ThreadGroup group, Runnable target, String name, long stackSize) // 指定线程组、Runnable对象、名称和栈大小创建线程

 

2.4  Thread 类的常用方法

以下是 Thread 类中的一些关键方法及其功能:

2.4.1. 启动与控制线程

void start(): 启动线程,调用 run() 方法,线程进入可运行状态。

void run(): 定义线程执行的任务逻辑,需重写以实现线程任务。

void interrupt(): 中断线程,设置线程的中断标志。

boolean isInterrupted(): 检查当前线程是否被中断,不会清除中断标志。

static boolean interrupted(): 检查并清除当前线程的中断标志。

void join(): 等待线程执行完毕,调用方阻塞直到目标线程完成。

void join(long millis): 最多等待指定的时间后线程返回。

void sleep(long millis): 让当前线程暂停执行指定的毫秒数。

2.4.2  线程状态与信息

Thread.State getState(): 获取当前线程的状态,如 NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。

long getId(): 获取线程的唯一标识 ID。

String getName(): 获取线程名称。

void setName(String name): 设置线程名称。

int getPriority(): 获取线程的优先级。

void setPriority(int newPriority): 设置线程的优先级(1 至 10)。

 

2.4.3. 线程调度与同步

static void yield(): 暂停当前线程的执行,给其他相同或更高优先级的线程运行机会。

void setDaemon(boolean on): 将线程设置为守护线程。守护线程在所有用户线程结束后自动终止。

boolean isDaemon(): 检查线程是否为守护线程。

 

2.5  Thread API 的应用场景

并行任务处理:将耗时任务分配给不同线程执行,提高程序效率。

后台服务:守护线程用于执行后台任务,如垃圾回收、监控等。

异步事件处理:响应用户事件或定时任务时使用线程处理。

多线程同步:利用线程间的协调机制,如锁和等待,管理共享资源访问。

 2.6 代码示例

2.6.1 interrupt()
2.6.1.1 可中断方法

可中断方法(Interruptible Methods)是指在 Java 中,当线程正在执行这些方法时,如果线程接收到中断请求(调用 interrupt() 方法),这些方法会立即响应中断,通常通过抛出 InterruptedException 异常来终止阻塞状态。这些方法允许线程在被阻塞的状态下及时响应中断请求,从而避免线程长时间停留在不可控的状态。

可中断方法的特点

1. 响应中断:当线程正在执行可中断方法时,如果被其他线程调用 interrupt(),该方法会捕获中断信号并抛出 InterruptedException。

2. 中断标志的清除:当抛出 InterruptedException 异常时,线程的中断标志会被清除。开发者需要根据逻辑决定是否重新设置中断标志或者进行其他操作。

3. 主要用途:可中断方法用于处理需要等待或阻塞的操作,如等待 I/O 操作完成、等待线程结束、等待条件满足等。在这些操作中使用可中断方法可以使线程更灵活、响应更迅速。

常见的可中断方法

以下是 Java 中一些常见的可中断方法:

1. Thread.sleep(long millis):使当前线程休眠指定的时间。如果线程在休眠期间被中断,会抛出 InterruptedException。

2. Object.wait()Object.wait(long timeout):让线程等待某个条件的发生。如果等待期间被中断,则抛出 InterruptedException。

3. Thread.join()Thread.join(long millis):等待另一个线程完成执行。如果等待期间被中断,会抛出 InterruptedException。

4. I/O 操作相关方法(java.nio.channels 包)

• Selector.select()

• SelectableChannel 中的阻塞方法,例如 SocketChannel.read()

这些方法在被中断时会抛出 ClosedByInterruptException 或 AsynchronousCloseException。

5. 并发工具类中的方法(java.util.concurrent 包)

• LockSupport.park()

• Condition.await()

• BlockingQueue.take()

• CountDownLatch.await()

• Semaphore.acquire()

• CyclicBarrier.await()

2.6.1.2 代码示例

当可中断方法使得当前线程进入阻塞状态时,若另外的一个线程调用被阻塞线程的interrupt方法,则会打断这种阻塞,记住,打断一个线程并不等于该线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。

1. interrupt() 方法的基本原理

interrupt() 方法用于请求中断一个线程。当一个线程被调用 interrupt() 方法时,线程的 中断标志(interrupt flag) 会被设置为 true。这个标志是线程内部的一个状态,表示线程被请求中断。

2. 中断标志的设置

• 每个线程都有一个内部的中断标志,默认是 false。

• 当调用 interrupt() 方法时,这个标志被设置为 true。

• 中断标志本身不会直接终止线程,它只是一个信号,需要线程自己检查这个标志并决定如何响应(例如停止运行或执行其他逻辑)。

3. 可中断方法和中断标志清除

当线程正在执行某些 可中断方法 时(例如 Thread.sleep()、Object.wait()、Thread.join() 等),如果线程被中断,系统会做以下操作:

触发中断:线程正在执行的可中断方法会抛出 InterruptedException 异常。

清除标志:当 InterruptedException 被抛出时,JVM 会自动清除线程的中断标志,将其重置为 false。

影响线程行为:异常的抛出让线程可以捕获并处理这个中断请求,如退出阻塞状态或执行特定的中断处理逻辑。

这种机制的设计是为了让线程在处理异常时有机会决定接下来的动作:是否需要重新设置中断标志,或者采取其他操作。

示例代码解释

以下代码展示了 interrupt() 的实际效果和中断标志的清除:

public class InterruptExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                System.out.println("Thread going to sleep...");
                // 线程进入睡眠状态,这是一个可中断方法
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                // 捕获中断异常
                System.out.println("Thread was interrupted during sleep!");
                // 输出当前中断标志状态
                System.out.println("Interrupt flag after exception: " + Thread.currentThread().isInterrupted());
            }
        });

        thread.start();

        // 主线程稍作等待后中断子线程
        try {
            Thread.sleep(2000); // 让子线程先运行一会
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        // 中断子线程
        thread.interrupt();
    }
}

运行结果分析

1. 线程启动并进入睡眠:线程执行 Thread.sleep(5000),开始睡眠 5 秒。

2. 主线程等待 2 秒并中断子线程:主线程调用 thread.interrupt(),这会设置子线程的中断标志为 true。

3. 子线程抛出 InterruptedException 异常:由于子线程正在执行 sleep(),这是一个可中断方法,它检测到中断请求并抛出 InterruptedException。

4. 中断标志被清除:异常抛出时,JVM 会自动清除中断标志,将其设置为 false。

5. 输出结果:子线程捕获异常后,输出中断状态。此时,Thread.currentThread().isInterrupted() 返回 false,显示中断标志已被清除。

总结

• interrupt() 设置线程的中断标志为 true,用来提示线程应该中断。

• 如果线程在执行 可中断方法 时被中断,该方法会抛出 InterruptedException,并且中断标志会被自动清除。

• 线程需要捕获异常并根据需要重新设置中断标志或进行其他处理。

这段机制使得线程可以优雅地处理中断请求,而不是在不确定状态下强制停止。

2.6.2 join

join() 方法用于让一个线程等待另一个线程完成执行。这是 Java 中用于线程协调和同步的一种机制,确保一个线程在另一个线程执行完毕后再继续执行。join() 方法是 Thread 类中的一种同步机制,确保线程之间的协调。

join() 方法的定义

join() 方法有两个主要重载版本:

1. void join():让当前线程等待调用 join() 的线程完成。

2. void join(long millis):让当前线程等待调用 join() 的线程完成,最多等待指定的时间(以毫秒为单位)。

使用场景

1. 线程依赖:当一个线程需要依赖另一个线程的结果时,可以使用 join() 方法等待另一个线程完成。

2. 确保顺序执行:如果需要确保线程按照特定顺序执行,可以使用 join() 方法来实现线程间的顺序执行。

方法的工作原理

join() 方法:当前线程调用 join() 方法后,会进入等待状态,直到目标线程完成执行。此时,当前线程会被阻塞,直到目标线程的 run() 方法结束。

join(long millis) 方法:当前线程会等待最多 millis 毫秒的时间。如果在指定时间内目标线程完成,当前线程会被唤醒。如果时间到达而目标线程仍未完成,当前线程会在时间到达后继续执行。

在B线程中有代码A.join() ,称为jion某个线程A,会使当前线程B进入等待,直到线程A结束生命周期,或者到达给定的时间,那么在此期间B线程是处于BLOCKED的,下面就来通过一个简单的实例解释一下join方法的基本用法:

package com.wangwenjun.concurrent.chapter03;

import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

import static java.util.stream.Collectors.toList;

public class ThreadJoin
{

    public static void main(String[] args) throws InterruptedException
    {
        //① 定义两个线程,并保存在threads中
        List<Thread> threads = IntStream.range(1, 3)
                .mapToObj(ThreadJoin::create).collect(toList());

        //② 启动这两个线程
        threads.forEach(Thread::start);


        //③ 执行这两个线程的join方法
        for (Thread thread : threads)
        {
            thread.join();
        }

        //④ main线程循环输出
        for (int i = 0; i < 10; i++)
        {
            System.out.println(Thread.currentThread().getName() + "#" + i);
            shortSleep();
        }
    }
    //构造一个简单的线程,每个线程只是简单的循环输出
    private static Thread create(int seq)
    {
        return new Thread(() ->
        {
            for (int i = 0; i < 10; i++)
            {
                System.out.println(Thread.currentThread().getName() + "#" + i);
                shortSleep();
            }
        }, String.valueOf(seq));
    }

    private static void shortSleep()
    {
        try
        {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

上面的代码结合Java 8的语法,创建了两个线程,分别启动,并且调用了每个线程的join方法(注意:join方法是被主线程调用的,因此在第一个线程还没有结束生命周期的时候,第二个线程的join不会得到执行(注意是jion不执行,不是run任务不执行),但是此时,第二个线程也已经启动了,run也开始执行了),运行上面的程序,你会发现线程一和线程二会交替地输出直到它们结束生命周期,main线程的循环才会开始运行,程序输出如下:

...
2#8
1#8
2#9
1#9
main#0
main#1
main#2
main#3
...

总结

• 通过 join() 方法,主线程可以等待其他线程的执行结果,确保线程间的同步与顺序执行。

• 使用 shortSleep() 让线程输出节奏变得清晰,便于观察线程的执行顺序。

• 该程序演示了如何使用 join() 来协调多个线程的执行顺序,非常适合用作线程同步的学习示例。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值