1.1 项目准备

1.1.1 何谓JUC

java.util.concurrent

1.1.2 Maven工程

pom.xml:

  1. <properties>
  2. <maven.compiler.source>1.8</maven.compiler.source>
  3. <maven.compiler.target>1.8</maven.compiler.target>
  4. <junit.version>4.13.1</junit.version>
  5. <lombok.version>1.18.12</lombok.version>
  6. <slf.version>1.7.32</slf.version>
  7. <logback.version>1.2.3</logback.version>
  8. <jmh.version>1.28</jmh.version>
  9. <jol.version>0.10</jol.version>
  10. <jedis.version>3.3.0</jedis.version>
  11. </properties>
  12. <dependencies>
  13. <dependency>
  14. <groupId>org.projectlombok</groupId>
  15. <artifactId>lombok</artifactId>
  16. <version>${lombok.version}</version>
  17. </dependency>
  18. <dependency>
  19. <groupId>junit</groupId>
  20. <artifactId>junit</artifactId>
  21. <version>${junit.version}</version>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.slf4j</groupId>
  25. <artifactId>slf4j-api</artifactId>
  26. <version>${slf.version}</version>
  27. </dependency>
  28. <dependency>
  29. <groupId>ch.qos.logback</groupId>
  30. <artifactId>logback-classic</artifactId>
  31. <version>${logback.version}</version>
  32. </dependency>
  33. <dependency>
  34. <groupId>ch.qos.logback</groupId>
  35. <artifactId>logback-core</artifactId>
  36. <version>${logback.version}</version>
  37. </dependency>
  38. <dependency>
  39. <groupId>org.openjdk.jmh</groupId>
  40. <artifactId>jmh-core</artifactId>
  41. <version>${jmh.version}</version>
  42. </dependency>
  43. <dependency>
  44. <groupId>org.openjdk.jol</groupId>
  45. <artifactId>jol-core</artifactId>
  46. <version>${jol.version}</version>
  47. </dependency>
  48. <dependency>
  49. <groupId>redis.clients</groupId>
  50. <artifactId>jedis</artifactId>
  51. <version>${jedis.version}</version>
  52. </dependency>
  53. </dependencies>

1.1.3 日志配置

logback.xml

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration scan="true" scanPeriod="60 seconds" debug="false">
  3. <property name="PROJECT" value="juc" />
  4. <property name="LOG_DIR" value="logs" />
  5. <property name="FILE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{30} - %msg%n" />
  6. <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %boldRed(%-5level) %yellow([%thread]) %cyan(%-40.40logger{39}) - %msg%n"/>
  7. <contextName>${PROJECT}</contextName>
  8. <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  9. <encoder>
  10. <pattern>${CONSOLE_LOG_PATTERN}</pattern>
  11. </encoder>
  12. </appender>
  13. <appender name="FILE" class="ch.qos.logback.core.FileAppender">
  14. <file>${LOG_DIR}/${PROJECT}.log</file>
  15. <append>true</append>
  16. <encoder>
  17. <pattern>${FILE_LOG_PATTERN}</pattern>
  18. </encoder>
  19. </appender>
  20. <appender name="RollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
  21. <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
  22. <fileNamePattern>${LOG_DIR}/${PROJECT}-%d{yyyy-MM-dd}.log</fileNamePattern>
  23. <maxHistory>30</maxHistory>
  24. </rollingPolicy>
  25. <encoder>
  26. <pattern>${FILE_LOG_PATTERN}</pattern>
  27. </encoder>
  28. </appender>
  29. <root level="DEBUG">
  30. <appender-ref ref="STDOUT" />
  31. <appender-ref ref="FILE" />
  32. <appender-ref ref="RollingFile" />
  33. </root>
  34. </configuration>

1.2 相关概念

1.2.1 线程与进程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如IntelliJ IDEA),也有的程序只能启动一个实例进程(例如QQ音乐)。

线程

  • 一个进程之内可以分到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
  • Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在Windows中进程是不活动的,只是作为线程的容器。

区别

  • 线程是程序执行的最小单位,进程是OS分配资源的最小单位。
  • 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线。
  • 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集和堆等)及一些进程级的资源,一个进程内的线程在其他进程不可见。
  • 调度和切换:线程上下文切换比进程上下文切换要快得多。

进程间的通信方式

(1)管道:分为无名管道和有名管道,都是半双工的通信方式

无名管道:数据只能单向流动,而且只能在具有亲缘关系的进程间使用。

有名管道:允许无亲缘关系进程间的通信。(亲缘关系一般指父子进程)

(2)信号量

信号量是一个计数器,可以用来控制多个线程对共享资源的访问。信号量常作为一种锁机制,用于实现进程间的互斥与同步(或者同一个进程间的不同进程间的同步),而不是用于存储进程间通信数据。

特点:

  • 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
  • 信号量基于操作系统的PV操作,程序对信号量的操作都是原子操作。
  • 每次对信号量的PV操作不仅限于对信号量值加1或减1,而且可以加减任意正整数。
  • 支持信号量组。

(3)消息队列

消息队列是消息的链表,存放在内核中。一个消息队列由一个队列标识符(队列ID)来标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

特点:

  • 消息队列是面向记录的,其中的消息具有特定的格式和特定的优先级。
  • 消息队列独立于发送和接收进程。进程终止时,消息队列及其内容并不会被删除。
  • 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

(4)共享内存

共享内存,指两个或多个进程共享一个给定的存储区。

特点:

  • 共享内存是最快的一种IPC,因为进程是直接对内存进行存取。
  • 因为多个进程可以同时操作,所以需要进行同步。
  • 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

(5)信号

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

(6)套接字

套接字也是一种进程间通信机制,可用于不同主机间的进程通信。

1.2.2 并发与并行

并发

单核CPU下,线程实际还是串行执行的。操作系统的任务调度器将CPU的时间片(Windows下时间最小约15ms)分给不同的线程使用,只是由于CPU在线程间(时间片很短)的切换非常快,人类感觉是同时运行的。微观串行,宏观并行。

一般会将这种线程轮流使用CPU的做法叫做并发,concurrent。

并行

多核CPU下,每个核都可以调度运行线程,这时候线程可以是并行的,parallel。

区别

并发:多线程操作一个资源(不一定同时),CPU一核交替运行多条线程

并行:多个线程同时执行(同时),CPU多核同时执行多条线程

BFBX.jpg

TIP:查看CPU核数:Runtime.getRuntime().availableProcessors()

1.2.3 应用

异步调用

从方法调用的角度来讲,如果:

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

注意:同步在多线程中还有一层意思,是让多个线程步调一致。

设计

多线程可以让方法 执行变为异步的。

  • 比如在项目中,视频文件需要转换格式等操作比较耗时,这时开一个线程处理视频转换,避免阻塞主线程。
  • tomcat的异步servlety也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞tomcat的工作线程。
  • UI程序中,开线程进行其他操作,避免阻塞UI线程。

1.3 Java线程

1.3.1 创建线程

(1)方式一:继承Thread,重写run()方法。

优点:在run()方法内获取当前线程直接使用this即可。

  1. public class ThreadTest {
  2. public static class MyThread extends Thread {
  3. @Override
  4. public void run() {
  5. System.out.println("I am a child thread.");
  6. }
  7. }
  8. public static void main(String[] args) {
  9. MyThread thread = new MyThread();
  10. thread.start();
  11. }
  12. }

(2)方式二:实现Runnable,实现run()方法。

优点:任务与逻辑分离,多个线程可以执行相同的任务。

  1. public class RunnableTaskTest implements Runnable{
  2. @Override
  3. public void run() {
  4. System.out.println("I am a child thread.");
  5. }
  6. public static void main(String[] args) {
  7. RunnableTaskTest task = new RunnableTaskTest();
  8. new Thread(task).start();
  9. new Thread(task).start();
  10. }
  11. }

(3)方式三:实现Callable接口,实现call方法,通过FutureTask创建线程。

优点:任务可以携带返回值。

  1. public class CallerTaskTest {
  2. public static class CallerTask implements Callable<String> {
  3. @Override
  4. public String call() throws Exception {
  5. return "KHighness";
  6. }
  7. }
  8. public static void main(String[] args) throws InterruptedException{
  9. FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
  10. new Thread(futureTask).start();
  11. try {
  12. String result = futureTask.get();
  13. System.out.println(result);
  14. } catch (ExecutionException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }

1.3.2 查看线程

Windows系统:

  • taskmgr:打开任务管理器
  • tasklist | findstr <pid>/<pname>:根据进程id或者名称查找进程
  • taskkill /pid <pid> :根据进程id终止进程
  • taskkill /im <pname>:格局进程名称终止进程
  • /t:终止指定的进程和由它启用的子进程
  • /f:指定强制终止进程

Linux系统:

  • ps -ef:查看所有进程
  • ps -fT -p <pid>:查看某个进程(ID)的所有线程
  • kill:终止进程
  • top -H -p <pid>:查看某个进程(ID)的所有线程

jvm:

  • jps:查看所有java进程
  • jstack <pid>:查看某个Java进程的所有线程状态

jconsole远程监控

需要如下方式运行类:

  1. java -Djava.rmi.server.hostname=<ip> -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=<port> -Dcom.sun.management.jmxremote.ssl=<true/false> -Dcom.sun.management.jmxremote.authenticate=<true/false> <class>

如果认证访问则需要:

  • 复制jmxremote.password文件
  • 修改jmxremote.password和jmxremote.access文件的权限为600
  • 连接时候填入c

1.3.3 运行原理

栈与栈帧

Java Virtual Machine Stacks(Java虚拟机栈)

  • 每个栈由多个栈帧组成,对应着每次方法调用时所占的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

线程上下文切换

因为以下原因导致CPU不再执行当前的线程,转而执行另一个线程的代码:

  • 线程的CPU时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了sleep、yield、wait、join、park、synchronzied、lock等方法

当Thread Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条JVM指令的执行地址,是线程私有的。

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Thread Context Switch频繁发生会影响性能

1.3.4 常见方法

方法名 功能 说明
start() 启动一个新线程,在新的线程运行run方法中的代码 start方法只是让线程进入就绪,里面代码不一定理科运行(CPU的时间片还没分给它)。每个线程对象的start方法只能调用一次,如果调用了多次会出现
IllegalThreadStateException
run() 新线程启动后会调用的方法 如果在构造
Thread
对象时传递了
Runnable
参数,则线程启动后会调用
Runnable
中的run方法,否则默认不执行任何操作。但可以创建
Thread
的子类对象,来覆盖默认行为。
join() 等待线程运行结束
join(long n) 等待线程运行结束,最多等待n毫秒
getId() 获取线程长整型的id id唯一
getName() 获取线程名
setName(String) 修改线程名
getPriority() 获取线程优先级
setPriority(int) 修改线程优先级 Java中规定线程优先级是1~10的整数,较大的优先级能提高该线程被CPU调度的几率
getState() 获取线程状态 Java中线程状态是用6个
enum
表示,分别为:
NEW

RUNNABLE

BLOCKED

WAITING

TIMED_WAITING

TERMINATED
isInterrupted() 判断是否给打断 不会清除打断标记。线程的中断标记不受此方法的影响。
isAlive() 线程是否存活
interrupt() 打断线程 如果被打断线程正在sleep、wait、join会导致被打断的线程抛出
InterruptedException
,并清除打断标记;如果打断的是正在运行的线程,则会设置打断标记;park的线程被打断,也会设置打断标记。
interrupted()
static
判断当前线程是否被打断 清除线程的中断标记。如果这个方法连续调用两次,那么第二次调用将返回false。
currentThread()
static
获取当前线程
sleep()
static
让当前执行的线程休眠n毫秒,休眠时间让出CPU的时间片给其他线程
yield()
static
提示线程调度器让出当前线程对CPU的使用 主要是为了测试和调试

1.3.5 start和run

  • start:让线程处于就绪状态,并没有运行,一旦得到CPU时间片,就开始运行run()方法。
  • run:如果该线程使用独立的Runnable运行对象构造的,则调用该Runnable对象的run()方法,否则,该方法不执行任何操作返回。
  • 总结:调用start方法可启动线程,而run方法只是Thread类中的一个普通调用,还是在主线程里执行。

1.3.6 sleep与yield

sleep方法

  • 调用sleep会让当前线程从RUNNING进入到TIMED_WAITING状态
  • 其他线程可以使用interrupt方法打断正在睡眠的线程。
  • 睡眠结束后的线程未必会立刻得到执行。
  • 建议使用TimeUnitsleep代替Threadsleep来获得更好的可读性。

yield方法

  • 调用yield会让当前线程从RUNNING进入RUNNABLE就绪状态,然后调度执行其他线程。
  • 具体的实现依赖于操作系统的任务调度器。

1.3.7 线程优先级

  • 线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。
  • 如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但是CPU闲时,优先级几乎没有作用。

1.3.8 join

在下面的程序中,想让update输出i修改后的结果:

  1. @Slf4j(topic = "Join")
  2. public class JoinDemo {
  3. static int i = 3;
  4. public static void main(String[] args) throws InterruptedException {
  5. update();
  6. }
  7. public static void update() throws InterruptedException {
  8. log.debug("main thread start");
  9. Thread t1 = new Thread(() -> {
  10. log.debug("child thread start");
  11. try {
  12. TimeUnit.SECONDS.sleep(1);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. log.debug("child thread end");
  17. i *= i;
  18. }, "t1");
  19. t1.start();
  20. log.debug("i => [{}]", i);
  21. log.debug("main thread end");
  22. }
  23. }

结果如下:

  1. 2021-04-06 16:16:52.692 [main] DEBUG Join - main thread start
  2. 2021-04-06 16:16:52.726 [t1] DEBUG Join - child thread start
  3. 2021-04-06 16:16:52.726 [main] DEBUG Join - i => [3]
  4. 2021-04-06 16:16:52.727 [main] DEBUG Join - main thread end
  5. 2021-04-06 16:16:53.734 [t1] DEBUG Join - child thread end

在第22行后添加t1.join();方法后的输出结果:

  1. 2021-04-06 16:14:53.431 [main] DEBUG Join - main thread start
  2. 2021-04-06 16:14:53.461 [t1] DEBUG Join - child thread start
  3. 2021-04-06 16:14:54.474 [t1] DEBUG Join - child thread end
  4. 2021-04-06 16:14:54.474 [main] DEBUG Join - i => [9]
  5. 2021-04-06 16:14:54.475 [main] DEBUG Join - main thread end

1.3.9 interrupt

相关API:

  • interrupt():声明此线程中断,但是线程并不会立即中断
  • isInterrupted:判断此线程是否已中断,判断完后不修改县城管的中断状态
  • interrupted():判断此线程已中断,判断完后清除线程的中断状态

理解如下:

  • interrupt():皇上(线程)每晚挑选一个妃子侍寝,到了时间,太监会告诉皇上,时间到了(声明线程中断),皇上知道了,停不停还是皇上说了算。
  • isInterrupted():如果(isInterrupted = true)则可以控制皇上(线程)停止,皇上停止后,线程还是中断状态,即interruptes = true
  • interrupted():如果(isInterrupted = true)则可以控制皇上(线程)停止,皇上停止后,线程会清除中断状态,即interruptes = false

证明:

  1. @Slf4j(topic = "InterruptAPI")
  2. public class InterruptDemo {
  3. public static void main(String[] args) throws InterruptedException {
  4. Thread t = new Thread(() -> {
  5. Thread thread = Thread.currentThread();
  6. while (!thread.isInterrupted()) { // (1) isInterrupted
  7. // while (!Thread.interrupted()) { // (2) interrupted(static)
  8. log.debug("{}", thread.isInterrupted());
  9. }
  10. log.debug("{}", thread.isInterrupted());
  11. }, "t");
  12. t.start();
  13. TimeUnit.MILLISECONDS.sleep(1);
  14. t.interrupt();
  15. }
  16. }

注释(2)结果:

  1. 2021-04-24 14:04:26.851 [t] DEBUG InterruptAPI - false
  2. 2021-04-24 14:04:26.853 [t] DEBUG InterruptAPI - true

证明isInterrupted()不会修改中断状态。

注释(1)结果:

  1. 2021-04-24 14:04:54.046 [t] DEBUG InterruptAPI - false
  2. 2021-04-24 14:04:54.049 [t] DEBUG InterruptAPI - false

证明interrupted()会重置中断状态。

1.3.10 两阶段终止模式

1. 线程基础 - 图2 代码:

  1. @Slf4j(topic = "TwoStageTermination")
  2. class TwoStageTermination {
  3. Thread monitor;
  4. // 启动监控线程
  5. public void start() {
  6. monitor = new Thread(() -> {
  7. while (true) {
  8. Thread current = Thread.currentThread();
  9. // 正在运行的线程被打断,直接设置打断标记
  10. if (current.isInterrupted()) {
  11. log.debug("料理后事");
  12. break;
  13. }
  14. try {
  15. TimeUnit.SECONDS.sleep(1);
  16. log.info("执行监控...");
  17. } catch (InterruptedException e) {
  18. log.debug(e.getMessage());
  19. // 正在运行的线程被打断,会被清除打断标记,需要重新设置设置打断标记
  20. current.interrupt();
  21. }
  22. }
  23. });
  24. monitor.start();
  25. }
  26. // 停止监控线程
  27. public void stop() {
  28. monitor.interrupt();
  29. }
  30. }

运行结果:

  1. 2021-04-24 14:26:23.321 [Thread-0] INFO TwoStageTermination - 执行监控...
  2. 2021-04-24 14:26:24.329 [Thread-0] INFO TwoStageTermination - 执行监控...
  3. 2021-04-24 14:26:25.336 [Thread-0] INFO TwoStageTermination - 执行监控...
  4. 2021-04-24 14:26:25.823 [Thread-0] DEBUG TwoStageTermination - sleep interrupted
  5. 2021-04-24 14:26:25.823 [Thread-0] DEBUG TwoStageTermination - 料理后事

LockSupportpark()可用于暂停当前线程

  1. @Slf4j(topic = "Park")
  2. public class ParkDemo {
  3. public static void main(String[] args) throws InterruptedException {
  4. park();
  5. }
  6. public static void park() throws InterruptedException {
  7. Thread t1 = new Thread(() -> {
  8. log.debug("park...");
  9. log.debug("isInterrupted => [{}]", Thread.currentThread().isInterrupted());
  10. LockSupport.park();
  11. log.debug("unpark...");
  12. log.debug("isInterrupted => [{}]", Thread.currentThread().isInterrupted());
  13. LockSupport.park();
  14. Thread.interrupted();
  15. log.debug("isInterrupted => [{}]", Thread.currentThread().isInterrupted());
  16. log.debug("unpark...");
  17. }, "t1");
  18. t1.start();
  19. TimeUnit.SECONDS.sleep(1);
  20. t1.interrupt();
  21. }
  22. }

运行结果:

  1. 2021-04-06 20:23:08.234 [t1] DEBUG Park - park...
  2. 2021-04-06 20:23:08.237 [t1] DEBUG Park - isInterrupted => [false]
  3. 2021-04-06 20:23:09.234 [t1] DEBUG Park - unpark...
  4. 2021-04-06 20:23:09.234 [t1] DEBUG Park - isInterrupted => [true]
  5. 2021-04-06 20:23:09.234 [t1] DEBUG Park - isInterrupted => [false]
  6. 2021-04-06 20:23:09.234 [t1] DEBUG Park - unpark...

打断线程的错误思路

  • 使用线程对象的stop()方法停止线程:stop()方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法释放锁。
  • 使用System.exit(int)方法终止线程:目的仅是停止一个线程,但这种会让整个程序都停止。

以下方法不推荐使用:

方法名 功能
stop() 停止线程运行
suspend() 挂起线程运行
resume() 恢复线程运行

1.3.11 主线程与守护线程

默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

守护线程被用于完成支持性工作,但在JVM退出时守护线程中的finally块不一定执行,因为JVM中没有非守护线程需要立即退出,所有守护线程都将立即终止,不能靠在守护线程使用finally确保关闭资源。

守护线程示例:

  1. @Test
  2. public void test1() throws InterruptedException{
  3. Thread t1 = new Thread(() -> {
  4. while (true) {
  5. if (Thread.currentThread().isInterrupted()) {
  6. break;
  7. }
  8. }
  9. log.debug("{} end", Thread.currentThread().getName());
  10. }, "t1");
  11. t1.start();
  12. TimeUnit.SECONDS.sleep(1);
  13. log.debug("{} end", Thread.currentThread().getName());
  14. }

运行结果:

  1. 2021-04-07 10:37:04.886 [main] DEBUG Daemon - main end

虽然main线程终止,但是t1线程并未终止。

但是t1.setDaemon(true);之后,main线程终止,t1线程立马终止。

1.3.12 五种状态

image-20210407110528028.png

  • 【初始状态】:仅是在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】:该线程已经被创建,与操作系统关联,处于就绪状态,可以由CPU调度执行
  • 【运行状态】:获取了CPU时间片运行中的状态
    • 当CPU时间片用完,会从运行状态转换成可运行状态,会导致线程的上下文切换
  • 【阻塞状态】:
    • 如果调用了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致上下文切换,进入阻塞状态
    • 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
  • 【终止状态】:表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态。

1.3.13 六种状态

从Java API层面描述,根据Thread.State枚举,分为六种状态:

  1. NEW 新建状态
  2. RUNNABLE 运行状态(就绪状态、运行中状态)
  3. BLOCKED 阻塞状态
  4. WAITING 等待状态
  5. TIMED_WAITING 计时等待状态
  6. TERMINATED 终止状态
  • NEW线程刚被创建,但是还没有调用start()方法
  • RUNNABLE当调用了start()方法之后,注意,Java API层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行)。
  • BLOCKED、WAITING、TIMED_WAITING都是Java API层面对【阻塞状态】的细分。‘
  • TERMINATED当线程代码运行结束。

图示:

Thread.svg

示例:

  1. @Slf4j(topic = "State")
  2. public class StateDemo {
  3. public static void main(String[] args) {
  4. Thread t1 = new Thread(() -> log.debug("{} running...", Thread.currentThread().getName()), "t1");
  5. Thread t2 = new Thread(() -> { while (true) {} }, "t2");
  6. Thread t3 = new Thread(() -> log.debug("{} running...", Thread.currentThread().getName()), "t3");
  7. Thread t4 = new Thread(() -> {
  8. synchronized (StateDemo.class) {
  9. try {
  10. TimeUnit.SECONDS.sleep(100);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }, "t4");
  16. Thread t5 = new Thread(() -> {
  17. try {
  18. t2.join();
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. }, "t5");
  23. Thread t6 = new Thread(() -> {
  24. synchronized (StateDemo.class) {
  25. try {
  26. TimeUnit.SECONDS.sleep(100);
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. }, "t4");
  32. t2.start();
  33. t3.start();
  34. t4.start();
  35. t5.start();
  36. t6.start();
  37. try {
  38. TimeUnit.MILLISECONDS.sleep(500);
  39. } catch (InterruptedException e) {
  40. e.printStackTrace();
  41. }
  42. log.debug("t1 => [{}]", t1.getState()); // TERMINATED
  43. log.debug("t2 => [{}]", t2.getState()); // RUNNABLE
  44. log.debug("t3 => [{}]", t3.getState()); // TERMINATED
  45. log.debug("t4 => [{}]", t4.getState()); // TIMED_WAITING
  46. log.debug("t5 => [{}]", t5.getState()); // WAITING
  47. log.debug("t6 => [{}]", t6.getState()); // BLOCKED
  48. }
  49. }

1.3.14 习题

应用之统筹(烧水泡茶)

  1. 统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复杂的科研项目的组织与管理中,都可以应用。
  2. 怎样应用呢?主要是把工序安排好。
  3. 比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么办?
  4. - 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开了,泡茶喝。
  5. - 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡茶喝。
  6. - 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。
  7. 哪一种方法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。
  8. ——华罗庚《统筹方法》

代码:

  1. @Slf4j(topic = "BoilWaterToMakeTea")
  2. public class PlanDemo {
  3. public static void main(String[] args) {
  4. Thread t1 = new Thread(() -> {
  5. log.debug("洗水壶");
  6. sleep(60); // 洗水壶 1分钟
  7. log.debug("烧开水");
  8. sleep(60 * 5); // 烧开水 5分钟
  9. }, "t1");
  10. Thread t2 = new Thread(() -> {
  11. log.debug("洗茶壶");
  12. sleep(60); // 洗水壶 1分钟
  13. log.debug("洗茶杯");
  14. sleep(60 * 2); // 洗茶杯 2分钟
  15. log.debug("拿茶叶");
  16. sleep(60 ); // 烧开水 1分钟
  17. try {
  18. t1.join(); // 等待t1烧水
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. log.debug("泡茶");
  23. }, "t2");
  24. t1.start();
  25. t2.start();
  26. }
  27. public static void sleep(int nanoSeconds) {
  28. try {
  29. TimeUnit.NANOSECONDS.sleep(nanoSeconds);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. }
  34. }

运行结果:

  1. 2021-04-07 14:12:12.989 [t1] DEBUG top.parak.demo.BoilWaterToMakeTea - 洗水壶
  2. 2021-04-07 14:12:12.989 [t2] DEBUG top.parak.demo.BoilWaterToMakeTea - 洗茶壶
  3. 2021-04-07 14:12:12.994 [t1] DEBUG top.parak.demo.BoilWaterToMakeTea - 烧开水
  4. 2021-04-07 14:12:12.994 [t2] DEBUG top.parak.demo.BoilWaterToMakeTea - 洗茶杯
  5. 2021-04-07 14:12:12.996 [t2] DEBUG top.parak.demo.BoilWaterToMakeTea - 拿茶叶
  6. 2021-04-07 14:12:12.998 [t2] DEBUG top.parak.demo.BoilWaterToMakeTea - 泡茶

:::info 小结

  • sleep不释放锁,释放CPU
  • join释放锁,抢占CPU
  • yield不释放锁,释放CPU
  • wait释放锁,释放CPU :::