JAVA和Nginx 教程大全

网站首页 > 精选教程 正文

「超级详细」Java线程实现原理 java线程实现的三种方式

wys521 2024-10-29 16:59:47 精选教程 29 ℃ 0 评论

本章内容

基本概念

程序、进程和线程

程序、进程、线程之间的关系:

  • 程序指的是以文件形式存储在磁盘上的计算机指令的集合。
  • 进程指的是一个执行中的程序,每一个进程拥有独立的内存空间和系统资源。
  • 线程是CPU调度的最小单元,它运行在进程中,不能独立存在。

系统线程模型

操作系统中关键概念:

  • 内核线程KLT:内核级线程(Kemel-Level Threads)直接由操作系统内核支持,线程创建、销毁、切换开销较大。
  • 用户线程UT:用户线程(User Thread)建立在用户空间,系统内核不能感知用户线程的存在,线程创建、销毁、切换开销小。
  • 轻量级进程LWP:轻量级进程(Light weight process)是由操作系统提供给用户操作内核线程接口的实现,是用户级线程和内核级线程之间的中间层。

系统线程模型主要有三种:

  • 内核线程模型。
  • 用户线程模型。
  • 混合线程模型。

内核线程模型

内核线程模型依赖操作系统内核提供的内核线程(Kernel-Level Thread ,KLT)来实现多线程。在此模型下,线程的切换调度由系统内核完成,系统内核负责将多个线程执行的任务映射到各个CPU中去执行。

程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口:轻量级进程(Light Weight Process,LWP),轻量级进程即通常意义上的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型。

如图所示:

其中:

  • Process:进程。
  • LWP:轻量级进程(即:线程)。
  • KLT:内存线程。

对于Sun JDK来说,它的Windows版与Linux版都是使用一对一的线程模型实现的,一个Java线程映射一个轻量级进程之中,因为Windows和Linux系统提供的是一对一的线程模型。

用户线程模型

用户线程模型中多个用户线程对应一个内核线程(KLT)。从广义上来讲,一个线程只要不是内核线程,就可以认为是用户线程(User Thread),轻量级进程也属于用户线程,但轻量级进程的实现建立在内核之上,许多操作都要进行系统调用,效率会受到限制。

使用用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要用户程序自己处理(如:线程的创建、切换、调度等)。

如图所示:

其中:

  • Process:进程。
  • UT:用户线程。
  • KLT:内存线程。

混合线程模型

线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式。在这种混合实现下,既存在用户线程,也存在轻量级进程。

用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。

在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N:M的关系。

如图所示:

其中:

  • UT:用户线程。
  • LWP:轻量级进程(即:线程)。
  • KLT:内存线程。

线程调度策略

线程调度指的是系统为线程分配处理器使用权的过程。

线程调度方式有两种:

  • 协同式线程调度(Cooperative Threads-Scheduling):线程调度由其本身来控制,线程在自身工作执行完成后,主动通知系统切换到另一个线程执行。
  • 抢占式线程调度(Preemptive Threads-Scheduling)线程的调度由系统分配执行时间,线程的切换由系统决定。

JAVA使用的是抢占式线程调度策略。

线程生命周期

线程生命周期有五种状态:初始状态、可运行状态、运行状态、休眠状态和终止状态。

如图所示:

其中:

  • 初始状态:该状态下线程已经被创建,但是还不允许分配CPU执行。该状态属于编程语言特有状态。注意:此处的线程已经被创建指的是在编程语言层面被创建,在操作系统层面的线程还没有被创建。
  • 可运行状态:该状态下线程可以分配CPU执行。在这种状态下,真正的操作系统线程已经被创建,可以分配CPU执行。
  • 运行状态:当有空闲的CPU时,操作系统会将空闲的CPU分配给一个处于可运行状态的线程,被分配到CPU的线程的状态会由可运行状态转换成运行状态。
  • 休眠状态:运行状态的线程如果调用一个阻塞的API(如:以阻塞方式读文件)或者等待某个事件(如:条件变量),则线程的状态会由运行状态转换成休眠状态,同时释放CPU使用权,休眠状态的线程永远没有机会获得CPU使用权。当等待的事件出现,线程就会从休眠状态转换成可运行状态。
  • 终止状态:线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期已结束。

以上五种状态在不同编程语言中会进行简化合并。如:

  • C语言的POSIX Threads规范,就将初始状态和可运行状态进行了合并。
  • Java语言中将可运行状态和运行状态进行了合并,可运行状态和运行状态在操作系统调度层面有用,而在JVM层面无需关心这两个状态,因为JVM将线程调度交给操作系统进行处理。

除了简化合并,以上五种状态也有可能被细化,如:Java语言中细化了休眠状态。

Java线程

Java线程分类

Java线程一般分为两类:

  • 用户线程:主要负责业务逻辑的处理和控制。
  • 守护线程:主要支持用户线程的工作。只有在用户线程存在的情况下才能存在,JVM会在所有的用户线程结束后自动退出。

Java线程生命周期

Java线程有六种状态:

  • NEW(初始化):线程创建后还未调用start()方法启动线程。
  • RUNNABLE(可运行):线程调用start()方法启动线程,等待CPU调度。RUNNABLE分为RUNNING(运行中)和READY(就绪)状态。处在RUNNING状态的线程可以调用yield()方法让出CPU使用权,并与其他处于READY状态的线程一起等待被调度。
  • WAITING(等待):处于RUNNABLE状态的线程调用wait()方法后,线程就处于等待状态,需要其他线程显示地唤醒。
  • TIMED_WAITING(超时等待):处于RUNNABLE状态的线程调用wait(long)方法后,线程就处于等待状态,无需其他线程显示地唤醒,等待指定时间后由系统自动唤醒。
  • BLOCKED(阻塞):等待进入synchronized()方法或代码块,处于阻塞状态。
  • TERMINATED(终止):线程已经执行结束。

如图所示:

其中,BLOCKED、WAITING、TIMED_WAITING可以理解为线程导致休眠状态的三种原因。

状态转换

1)RUNNABLE与BLOCKED的状态转换

只有一种场景会触发这种转换,就是线程等待synchronized隐式锁。synchronized修饰的代码块、方法同一时刻只能有一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从RUNNABLE转换到BLOCKED状态。 而当等待的线程获取到了synchronized隐式锁时,又会从BLOCKED转换到RUNNABLE状态。

2)RUNNABLE与WAITING的状态转换

有三种场景会触发RUNNABLE到WAITING的状态转换:

  • 获得synchronized隐式锁的线程,调用无参数的Object.wait()方法。
  • 调用无参数的Thread.join()方法。
  • 调用LockSupport.park()方法。其中LockSupport对象是Java并发包中的锁,调用LockSupport.park()方法,当前线程会阻塞,线程的状态会从RUNNABLE转换到WAITING。调用LockSupport.unpark(Thread thread)可唤醒目标线程,目标线程的状态又会从WAITING状态转换到RUNNABLE。

3)RUNNABLE与TIMED_WAITING的状态转换

有五种场景会触发RUNNABLE到TIMED_WAITING的状态转换:

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

4)从NEW到RUNNABLE状态

调用线程对象的start()方法。

5)从RUNNABLE到TERMINATED状态

线程执行完run()方法后,会自动转换到TERMINATED状态,如果执行run()方法时抛出异常,也会导致线程终止。如果需要强制中断run()方法的执行,可调用interrupt()方法。

Java多线程实现

Java多线程实现方式有五种:

  • 继承Thread类。
  • 实现Runnable接口。
  • 实现Callable接口。
  • JDK8以上版本使用CompletableFuture进行异步调用。
  • 使用线程池。

继承Thread类

@Slf4j
public class ThreadImpl extends Thread{
    @Override
    public void run() {
        log.info("继承Thread类");
    }
}

实现Runnable接口

@Slf4j
public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        log.info("实现Runnable接口");
    }
}

实现Callable接口

@Slf4j
public class CallableImpl implements Callable {
    @Override
    public Object call() throws Exception {
        return "实现Callable接口";
    }

    @SneakyThrows
    public static void main(String[] args) {
        FutureTask task = new FutureTask(new CallableImpl());
        Thread thread = new Thread(task);
        thread.start();
        log.info(String.valueOf(task.get()));
    }
}

使用CompletableFuture进行异步调用

@Slf4j
public class CompletableFutureImpl {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        CompletableFuture<String> futureTask = CompletableFuture.supplyAsync(new Supplier<String>() {
            @Override
            public String get() {
                log.info("任务开始");
                try {
                    Thread.sleep(5000);
                } catch (Exception e) {
                    e.printStackTrace();
                    return "执行异常";
                }
                log.info("任务结束");
                return "执行成功";
            }
        }, threadPool);
        // 异步获取FutureTask执行结果
        futureTask.thenAccept(e-> log.info("执行结果:{}",e));
    }
}

使用线程池

@Slf4j
public class ThreadPoolExecutorImpl {
    public static void main(String[] args) {
        ExecutorService pool = new ThreadPoolExecutor(
                1,
                2,
                0,
                TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
        for(int i = 0;i < 3;i++){
            pool.execute(new ThreadTask());
        }
    }
    static class ThreadTask implements Runnable {
        @Override
        public void run() {
            log.info(Thread.currentThread().getName());
        }
    }
}

Java线程通信

Java线程间的通信方式有以下几种:

  • 共享变量:多个线程共享同一个变量,通过对变量的读写操作来实现线程间的通信。可以使用synchronized关键字来保证线程安全。
  • wait()和notify():使用Object类的wait()和notify()方法来实现线程间的通信。wait()方法使线程进入等待状态,直到其他线程调用notify()方法唤醒它。notify()方法用于唤醒等待中的线程。
  • Condition:使用Condition接口来实现线程间的通信。Condition接口提供了await()和signal()方法,类似于wait()和notify()方法,但更加灵活。
  • 管道(Pipe):使用管道来实现线程间的通信。一个线程将数据写入管道,另一个线程从管道中读取数据。
  • 阻塞队列(BlockingQueue):使用阻塞队列来实现线程间的通信。一个线程将数据放入队列,另一个线程从队列中取出数据。阻塞队列提供了put()和take()方法,当队列满时,put()方法会阻塞线程,直到队列有空闲位置;当队列空时,take()方法会阻塞线程,直到队列有数据。
  • 信号量(Semaphore):使用信号量来实现线程间的通信。信号量可以控制同时访问某个资源的线程数量。

Java线程中断

Java中的线程中断是一种协作机制,调用线程对象的interrupt()方法并不会立即中断正在运行的线程,而是向线程发出中断请求,线程在合适的时机自行中断。

线程中断主要方法

线程中断涉及的主要方法:

  • interrupt()方法:该方法为实例方法,在线程内外都可发起调用。主要作用是中断此线程(此线程不一定是当前线程,而是指调用该方法的Thread实例所代表的线程),实际上只是给线程设置一个中断标识,线程仍会继续运行:
    • 作用于正常执行线程时会将中断标记设置为true。
    • 作用于阻塞线程时会将中断标志重置为false(中断标识默认初始值为false)。
  • interrupted()方法:该方法为Thread类的静态方法,只能在线程内部调用。主要作用是测试当前线程是否被中断(检查中断标识),返回一个boolean并重置中断状态,第二次再调用时中断状态已经被重置,将返回一个false。
  • isInterrupted()方法:该方法的主要作用是只检测此线程是否被中断 ,不修改中断状态。

线程中断场景分析

线程中断应用场景:

  • 中断处于阻塞状态的线程:
    • 当线程A处于WAITING、TIMED_WAITING状态(如:线程调用sleep()、join()、wait()等方法)时,如果其他线程调用线程A的interrupt()方法,会使线程A返回到RUNNABLE状态,同时线程A的代码会触发InterruptedException异常。
    • 当线程A处于RUNNABLE状态,并且阻塞在java.nio.channels.InterruptibleChannel上时,如果其他线程调用线程A的interrupt()方法,线程A会触发java.nio.channels.ClosedByInterruptException异常;而阻塞在java.nio.channels.Selector上时,如果其他线程调用线程A的interrupt()方法,线程A的java.nio.channels.Selector会立即返回。
  • 中断正常执行的线程:需要在目标线程内部检测中断位,并由用户手动终止,来做出响应。如:中断计算圆周率的线程A,此时需要依赖线程A主动检测中断状态。如果其他线程调用线程A的interrupt()方法,线程A就可以通过isInterrupted()方法,检测是不是自己是否被中断。

注意:

  • 1)synchronized和reentrantLock.lock()在获锁的过程中不能被中断,即:如果产生了死锁,则不可能被中断。但是如果调用带超时的方法reentrantLock.tryLock(long timeout, TimeUnit unit),线程在等待时被中断,则会抛出一个InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁。也可以调用reentrantLock.lockInterruptibly()方法,它就相当于一个超时设为无限的tryLock()方法。
  • 2)当需要中断线程时,最佳实践就是利用线程的中断位,而不是自定义中断状态,因为当线程被阻塞时,原生中断位仍然会被监听,而自定义的则不能。

线程中断编写范式

while循环在try块中编写范式

@Override
public void run() {
    try {
        ...
        /**
         * 理论上调用sleep()、join()、wait()等方法时发生异常会退出循环,但可能存在线程调用了阻塞方法而没有进入阻塞的情况,
         * 因此,需要判断当前线程是否被中断
         */
        while (!Thread.currentThread().isInterrupted()&& more work to do) {
            do more work;
        }
    } catch (InterruptedException e) {
        // 线程阻塞期间(如:调用sleep()、join()、wait()等方法)被中断
    } finally {
        // 线程结束前做善后处理(如:资源清理等)
    }
}

try块在while循环中编写范式

@Override
public void run() {
    while (!Thread.currentThread().isInterrupted()&& more work to do) {
        try {
            ...
            sleep(delay);
        } catch (InterruptedException e) {
            // 重新设置中断标识
            Thread.currentThread().interrupt();
        }
    }
}

线程中断代码示例

/**
 * @author 南秋同学
 * 线程中断代码示例
 */
@Slf4j
public class InterruptExample {
    @SneakyThrows
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    // 异常被捕获后,阻塞被打破、中断位被重置为false,因此,此处永远输出false
                    log.warn("线程是否被中断:{}", Thread.currentThread().isInterrupted());
                    log.info("回收资源");
                    e.printStackTrace();
                    // 注意:这里需要再次将中断位设为true,否则外面的while循环不会退出
                    Thread.currentThread().interrupt();
                    log.info("重新设置中断标识:{}", Thread.currentThread().isInterrupted());
                }
                log.info("子线程逻辑");
            }
        });
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.interrupt();
        log.info("中断子线程:{}", thread.isInterrupted());
        TimeUnit.SECONDS.sleep(5);
    }
}

stop()和interrupt()区别

stop()和interrupt()方法中断线程的主要区别:

  • stop()方法会真的杀死线程,如果线程持有ReentrantLock锁,调用stop()方法中断线程不会自动调用ReentrantLock的unlock()方法释放锁,那么其他线程就再也没机会获得ReentrantLock锁。注意:stop()方法已经被标注为@Deprecated。
  • interrupt()方法不会立刻杀死线程,它仅仅是通知线程,线程有机会执行一些后续操作,同时也可以忽略这个通知。

【阅读推荐】

更多精彩内容,如:

  • Redis系列
  • 数据结构与算法系列
  • Nacos系列
  • MySQL系列
  • JVM系列
  • Kafka系列
  • 并发编程系列

请移步【南秋同学】个人主页进行查阅。内容持续更新中......

【作者简介】

一枚热爱技术和生活的老贝比,专注于Java领域,关注【南秋同学】带你一起学习成长~

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表