[Java]线程生命周期与线程通信

简介: 本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。

【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://developer.aliyun.com/article/1631772
出自【进步*于辰的博客

线程生命周期与进程有诸多相似,所以我们很容易将两者关联理解并混淆,一些细节之处确有许多不同,因为线程调度与进程调度虽都由CPU完成,但两者并不相同。
特意耗费一些时间,系统地对线程生命周期与线程通信进行梳理、整理。
参考笔记三,P62、P63.1。

1、线程生命周期

1.1 JDK1.8版本

启发博文:《线程的生命周期及五种基本状态》(转发)。

引用其中一张线程生命周期图:

作者:代码の羁绊

在启发博文中,博主对线程五大状态和生命周期进行了很详细的说明,大家可以先行查阅。

这张图对线程生命周期总结得比较全面,我一一梳理、核对后觉得稍有不妥之处,略作修改后作如下图:

在这里插入图片描述

在此我先简述一下我对线程五个状态的理解:

  1. new(新建):在线程创建后、启动(start())之前所处的状态。
  2. Runnable(就绪):线程新建后并不是直接开始运行,而是被加入到等待队列,等待线程调度(等待CPU),此时就处于就绪状态。因此,这两种情况将进入就绪状态:(1)调用start();(2)因某种原因(如:线程通信、等待IO)进入阻塞状态后重新等待运行。
  3. Running(运行):线程正在运行时的状态。
  4. Blocked(阻塞):线程因某种原因(如:线程通信、等待IO)而停止运行后进入的状态。
  5. Dead(死亡):线程正常结束或异常终止后所处的状态。

相信大家在阅读完以上简述后,对线程的五大状态已经有了一个初步的认识,那状态间是如何转换的?又怎么理解呢?对于这两个问题,由于涉及到各个方法的业务和底层逻辑,本篇文章不便一一详述。如果大家想要进一步了解,可移步 → 《Thread类源码解析》。

其中,Blocked状态可能不太好理解,那位博主将其划分为三种情况:等待阻塞、同步阻塞和其他阻塞。我赞同,大家可移步启发博文查阅详述,在此不赘述,仅稍作说明:

三种阻塞情况的变动主要因“线程通信”引起,变化仅是阻塞情况的变化,状态不变,仍是Blocked

点出两个问题:
1:为什么调用notify()/notifyAll(),线程由等待Blocked变为锁定Blocked
文章排版考虑,在下文【使用Object类的wait()notify()】中说明。

2:interrupt()可中断线程,那么可中断正在阻塞的线程吗?
本质上说,可以,但会抛出异常(即不可以,故我未将其写入上图),在上文我给出的《Thread类源码解析》文章中有具体说明。

1.2 早期版本(JDK1.2之前)

相信能坚持阅读到这的博友,大部分是站在Java门槛上或刚入门不久的Java小白,你们现在了解和学习线程生命周期,获得的是已更新、迭代后的知识。个人认为,大家不需要掌握已过时的知识,但不能不了解,我先抛出两个问题:

  1. “挂起”状态是什么?怎么不在线程五大状态之列?
  2. 相信大家在一些资料中,可能见到过suspend()、resume()、stop()destroy()这4个方法,怎么上图中没有?为什么不用了?

当然是有的,只是过时了,所以没放上去,完整的图是这样:

在这里插入图片描述

OK,现在回答那两个问题。

“挂起”状态是一种类似Runnable(就绪)状态的状态,不同之处是进入就绪状态的线程,会释放所持有的“同步锁”,而“挂起”状态不会,“挂起”状态相当于“暂停”,故容易导致“死锁”。

为什么那4个方法会被放弃?
我寻得一答案,阐述得很详细,我便不班门弄斧了,看这里 → 《《Java面向对象编程》导读-Thread类的被废弃的suspend()、resume()和stop()方法》(转发)。

我补充一张图:
在这里插入图片描述

1.3 落到实处

所谓“落到实处”,就是要想掌握线程生命周期,光如上文夸夸其谈当然还不够,我们要把线程五大状态和状态间转换对应到Thread源码中才行。

如下图:

在这里插入图片描述

我自己感觉有点乱,源码所示如此。

当然,这不是完整图,图中状态间转换仅做了部分举例。在此,我不作说明,相信用心看到这里的博友可以大致理解。当然,也不便做出说明,因为我目前对一些方法的了解停留在“会用”的程度(见下文),并未对相应源码进行解析。

补充一点:
大家对比这张“状态图”和上文线程生命周期图,大家会发现有点对不上。

其实,WAITING就是Runnable(就绪),在线程生命周期中,一般不说“就绪”,“就绪”是进程生命周期中的术语,上文这般使用是为了方便大家理解;而RUNNABLE就是Running

2、线程通信

启发博文:《线程间通信的几种实现方式》(转发)。

我暂未整理“线程通信”相关理论,故下文将以示例的形式进行阐述。

注:以下5个示例都成功实现线程通信,输出结果是:

唤醒t1
t1已唤醒

2.1 使用 volatile 关键字

示例:

private static volatile boolean isWait = true;

public static void main(String[] args) {
   
    Thread t1 = new Thread(() -> {
   
        while (true)
            if (!isWait) {
   
                System.out.println("t1已唤醒");
                break;
            }
    });
    Thread t2 = new Thread(() -> {
   
        System.out.println("唤醒t1");
        isWait = false;
    });
    t1.start();
    t2.start();
}

如果大家不了解volatile关键字,看这里

这里线程通信利用的是volatile关键字“保证可见性”的原理。

2.2 使用Object类的wait()notify()

示例:

Object lock = new Object();
Thread t1 = new Thread(() -> {
   
    synchronized (lock) {
   
        try {
   
            lock.wait();
        } catch (InterruptedException e) {
   
            e.printStackTrace();
        }
        System.out.println("t1已唤醒");
    }
});
Thread t2 = new Thread(() -> {
   
    synchronized (lock) {
   
        System.out.println("唤醒t1");
//      lock.notify();// 唤醒等待队列中的一个线程,不一定是 t1
        lock.notifyAll();
    }
});
t1.start();
t2.start();

大家还记得我在【1.1】中点出的这个问题吗?

为什么调用notify()/notifyAll(),线程由等待Blocked变为锁定Blocked

答案就在以上代码的执行过程中,我给大家捋一捋。

1、t1、t2都执行,t1在t2之前启动,先获得同步锁,t2阻塞。
2、t1调用wait()进入等待状态,释放同步锁,同步锁由t2获得,t2开始运行。
3、t2调用notify()唤醒t1,但此时同步锁仍由t2持有,t1继续等待。
4、t2运行完,释放同步锁,由t1获得,t1开始运行。

OK,就是第3点。

为什么一定要同步锁?
因为wait()notify()的底层逻辑要求必须是“先等待,再唤醒”,同步锁可以保证流程的正常执行。难道真的不能去掉同步锁?例如这样:

Object lock = new Object();
Thread t1 = new Thread(() -> {
   
    try {
   
        lock.wait();
    } catch (InterruptedException e) {
   
        e.printStackTrace();
    }
    System.out.println("t1已唤醒");
});
Thread t2 = new Thread(() -> {
   
    System.out.println("唤醒t1");
    lock.notifyAll();
});
t1.start();
t1.join();
t2.start();

很明显,不行。这样就出现了“死锁”。

t1 等待被唤醒,主线程等待 t1 运行完。

因此,必须使用同步锁,且必须是同一把锁(lock)、

2.3 使用JUC工具类 CountDownLatch

示例:

CountDownLatch latch = new CountDownLatch(1);// 这个 1 是同步状态,类似synchronized中的 count
Thread t1 = new Thread(() -> {
   
    try {
   
        latch.await();
    } catch (InterruptedException e) {
   
        e.printStackTrace();
    }
    System.out.println("t1已唤醒");
});
Thread t2 = new Thread(() -> {
   
    System.out.println("唤醒t1");
    latch.countDown();
});
t1.start();
t2.start();

可见,无需同步锁。为何?这就要涉及CountDownLatch类的源码了。当然,我们暂且不用深入了解,理解其底层逻辑即可。

看这里 → 《这一次,彻底搞懂Java中的synchronized关键字》(转发)。

大家找到【1.同步代码块】这一栏,底层逻辑相似。

2.4 使用 ReentrantLock 结合 Condition

示例:

Lock lock = new ReentrantLock();
Condition cond = lock.newCondition();

Thread t1 = new Thread(() -> {
   
    lock.lock();
    try {
   
        cond.await();
    } catch (InterruptedException e) {
   
        e.printStackTrace();
    }
    System.out.println("t1已唤醒");
    lock.unlock();
});
Thread t2 = new Thread(() -> {
   
    lock.lock();
    System.out.println("唤醒t1");
    cond.signal();
    lock.unlock();
});
t1.start();
t2.start();

这两条代码合起来相当于同步锁:

lock.lock();
...
lock.unlock();

2.5 使用 LockSupport

示例:

Thread t1 = new Thread(() -> {
   
    LockSupport.park();
    System.out.println("t1已唤醒");
});
Thread t2 = new Thread(() -> {
   
    System.out.println("唤醒t1");
    LockSupport.unpark(t1);
});
t1.start();
t2.start();

可见,LockSupport类不关注是否“在等待”。

最后

本文中的例子是为了方便大家理解和阐述知识点而简单举出的,旨在“阐明知识点”,简单为主,并不一定有实用性,仅是抛砖引玉。

如果大家想要快速地掌握这些知识点,我的建议是“自测中理解”。

本文完结。

目录
相关文章
|
8天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
10天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
10天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
11天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
33 3
|
11天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
92 2
|
19天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
46 6
|
2月前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
1月前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
2月前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
27天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####