1.1 项目准备

1.1.1 何谓JUC

java.util.concurrent

1.1.2 Maven工程

pom.xml:

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <lombok.version>1.18.12</lombok.version>
    <logback.version>1.1.2</logback.version>
    <jmh.version>1.28</jmh.version>
    <junit.version>4.13.1</junit.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>${logback.version}</version>
    </dependency>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>${jmh.version}</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit.version}</version>
    </dependency>
</dependencies>

1.1.3 日志配置

logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">

    <!-- 定义日志文件的存储地址 -->
    <property name="LOG_HOME" value="log/" />

    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!-- 格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,%msg:日志消息,%n是换行符 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 文件日志,按照每天生成日志文件 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件输出的文件名 -->
            <FileNamePattern>${LOG_HOME}/juc.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!-- 日志文件保留天数 -->
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!-- 格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!-- 日志文件最大的大小 -->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <!-- 日志输出级别 -->
    <root level="DEBUG">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="FILE"/>
    </root>

</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多核同时执行多条线程

TIP

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

1.2.3 应用

异步调用

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

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

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

设计

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

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

1.3 Java线程

1.3.1 创建线程

Thread

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

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

public class ThreadTest {

    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("I am a child thread.");
        }
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

Runnable

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

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

public class RunnableTaskTest implements Runnable{

    @Override
    public void run() {
        System.out.println("I am a child thread.");
    }

    public static void main(String[] args) {
        RunnableTaskTest task = new RunnableTaskTest();
        new Thread(task).start();
        new Thread(task).start();
    }
}

FutureTask

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

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

public class CallerTaskTest {

    public static class CallerTask implements Callable<String> {
        @Override
        public String  call() throws Exception {
            return "KHighness";
        }
    }

    public static void main(String[] args) throws InterruptedException{
        FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
        new Thread(futureTask).start();
        try {
            String result = futureTask.get();
            System.out.println(result);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

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)的所有线程

Java

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

jconsole远程监控

需要如下方式运行类:

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、sychronized、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表示,分别为:NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED
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修改后的结果:

@Slf4j(topic = "Join")
public class JoinDemo {

    static int i = 3;

    public static void main(String[] args) throws InterruptedException {
        update();
    }

    public static void update() throws InterruptedException {
        log.debug("main thread start");
        Thread t1 = new Thread(() -> {
            log.debug("child thread start");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("child thread end");
            i *= i;
        }, "t1");
        t1.start();
        log.debug("i => [{}]", i);
        log.debug("main thread end");
    }
}

结果如下:

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

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

2021-04-06 16:14:53.431 [main] DEBUG Join - main thread start
2021-04-06 16:14:53.461 [t1] DEBUG Join - child thread start
2021-04-06 16:14:54.474 [t1] DEBUG Join - child thread end
2021-04-06 16:14:54.474 [main] DEBUG Join - i => [9]
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

证明

@Slf4j(topic = "InterruptAPI")
public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            Thread thread = Thread.currentThread();
            while (!thread.isInterrupted()) {  // (1) isInterrupted
//            while (!Thread.interrupted()) {      // (2) interrupted(static)
                log.debug("{}", thread.isInterrupted());
            }
            log.debug("{}", thread.isInterrupted());
        }, "t");
        t.start();
        TimeUnit.MILLISECONDS.sleep(1);
        t.interrupt();
    }
}

注释(2)结果:

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

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

注释(1)结果:

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

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

1.3.10 两阶段终止模式

图示

graph TD
w("while(true)") --> a
a("有没有被打断") -- 是 --> b(料理后事)
b --> c((结束循环))
a -- 否 --> d(睡眠2S)
d -- 无异常 --> e(执行监控记录)
d -- 有异常 --> i(设置打断标记)
i --> w
e --> w

代码

@Slf4j(topic = "TwoStageTermination")
class TwoStageTermination {
     Thread monitor;

    // 启动监控线程
    public void start() {
        monitor = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                // 正在运行的线程被打断,直接设置打断标记
                if (current.isInterrupted()) {
                    log.debug("料理后事");
                    break;
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                    log.info("执行监控...");
                } catch (InterruptedException e) {
                    log.debug(e.getMessage());
                    // 正在运行的线程被打断,会被清除打断标记,需要重新设置设置打断标记
                    current.interrupt();
                }
            }
        });
        monitor.start();
    }

    // 停止监控线程
    public void stop() {
        monitor.interrupt();
    }
}

结果:

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

park

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

@Slf4j(topic = "Park")
public class ParkDemo {

    public static void main(String[] args) throws InterruptedException {
        park();
    }

    public static void park() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            log.debug("park...");
            log.debug("isInterrupted => [{}]", Thread.currentThread().isInterrupted());
            LockSupport.park();
            log.debug("unpark...");
            log.debug("isInterrupted => [{}]", Thread.currentThread().isInterrupted());
            LockSupport.park();
            Thread.interrupted();
            log.debug("isInterrupted => [{}]", Thread.currentThread().isInterrupted());
            log.debug("unpark...");
        }, "t1");
        t1.start();

        TimeUnit.SECONDS.sleep(1);
        t1.interrupt();
    }
}

结果:

2021-04-06 20:23:08.234 [t1] DEBUG Park - park...
2021-04-06 20:23:08.237 [t1] DEBUG Park - isInterrupted => [false]
2021-04-06 20:23:09.234 [t1] DEBUG Park - unpark...
2021-04-06 20:23:09.234 [t1] DEBUG Park - isInterrupted => [true]
2021-04-06 20:23:09.234 [t1] DEBUG Park - isInterrupted => [false]
2021-04-06 20:23:09.234 [t1] DEBUG Park - unpark...

打断线程的错误思路

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

以下方法不推荐使用:

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

1.3.11 主线程与守护线程

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

守护线程示例

@Test
public void test1() throws InterruptedException{
    Thread t1 = new Thread(() -> {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                break;
            }
        }
        log.debug("{} end", Thread.currentThread().getName());
    }, "t1");
    t1.start();
    TimeUnit.SECONDS.sleep(1);
    log.debug("{} end", Thread.currentThread().getName());
}

结果:

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

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

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

1.3.12 五种状态

图示

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

1.3.13 六种状态

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

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

图示

示例

@Slf4j(topic = "State")
public class StateDemo {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> log.debug("{} running...", Thread.currentThread().getName()), "t1");
        Thread t2 = new Thread(() -> { while (true) {} }, "t2");
        Thread t3 = new Thread(() -> log.debug("{} running...", Thread.currentThread().getName()), "t3");
        Thread t4 = new Thread(() -> {
            synchronized (StateDemo.class) {
                try {
                    TimeUnit.SECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t4");
        Thread t5 = new Thread(() -> {
            try {
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t5");
        Thread t6 = new Thread(() -> {
            synchronized (StateDemo.class) {
                try {
                    TimeUnit.SECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t4");
        t2.start();
        t3.start();
        t4.start();
        t5.start();
        t6.start();
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("t1 => [{}]", t1.getState());   // TERMINATED
        log.debug("t2 => [{}]", t2.getState());   // RUNNABLE
        log.debug("t3 => [{}]", t3.getState());   // TERMINATED
        log.debug("t4 => [{}]", t4.getState());   // TIMED_WAITING
        log.debug("t5 => [{}]", t5.getState());   // WAITING
        log.debug("t6 => [{}]", t6.getState());   // BLOCKED
    }
}

1.3.14 习题

应用之统筹(烧水泡茶)

统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复杂的科研项目的组织与管理中,都可以应用。
怎样应用呢?主要是把工序安排好。
比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么办?

- 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开了,泡茶喝。
- 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡茶喝。
- 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。

哪一种方法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。
——华罗庚《统筹方法》
graph LR
a(洗水壶) --> b(烧开水)
c(洗茶壶,洗茶杯,拿茶叶)
b --> d(泡茶)
c --> d(泡茶)

代码:

@Slf4j(topic = "BoilWaterToMakeTea")
public class PlanDemo {

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("洗水壶");
            sleep(60);      // 洗水壶 1分钟
            log.debug("烧开水");
            sleep(60 * 5);  // 烧开水 5分钟
        }, "t1");
        Thread t2 = new Thread(() -> {
            log.debug("洗茶壶");
            sleep(60);      // 洗水壶 1分钟
            log.debug("洗茶杯");
            sleep(60 * 2);  // 洗茶杯 2分钟
            log.debug("拿茶叶");
            sleep(60 );     // 烧开水 1分钟
            try {
                t1.join();               // 等待t1烧水
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("泡茶");
        }, "t2");
        t1.start();
        t2.start();
    }

    public static void sleep(int nanoSeconds) {
        try {
            TimeUnit.NANOSECONDS.sleep(nanoSeconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果:

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

小结

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