1.1 项目准备 1.1.1 何谓JUC java.util.concurrent
1.1.2 Maven工程 pom.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <?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" > <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" > <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 创建线程 (1)方式一:继承Thread
,重写run()
方法。
优点:在run()
方法内获取当前线程直接使用this
即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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(); } }
(2)方式二:实现Runnable
,实现run()
方法。
优点:任务与逻辑分离,多个线程可以执行相同的任务。
1 2 3 4 5 6 7 8 9 10 11 12 13 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(); } }
(3)方式三:实现Callable
接口,实现call
方法,通过FutureTask
创建线程。
优点:任务可以携带返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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)的所有线程
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、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
表示,分别为: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
方法打断正在睡眠的线程。
睡眠结束后的线程未必会立刻得到执行。
建议使用TimeUnit
的sleep
代替Thread
的sleep
来获得更好的可读性。
yield
方法
调用yield
会让当前线程从RUNNING
进入RUNNABLE
就绪状态,然后调度执行其他线程。
具体的实现依赖于操作系统的任务调度器。
1.3.7 线程优先级
线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。
如果CPU比较忙,那么优先级高的线程会获得更多的时间片,但是CPU闲时,优先级几乎没有作用。
1.3.8 join 在下面的程序中,想让update
输出i修改后的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @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" ); } }
结果如下:
1 2 3 4 5 2021 -04 -06 16 :16 :52.692 [main] DEBUG Join - main thread start2021 -04 -06 16 :16 :52.726 [t1] DEBUG Join - child thread start2021 -04 -06 16 :16 :52.726 [main] DEBUG Join - i => [3 ]2021 -04 -06 16 :16 :52.727 [main] DEBUG Join - main thread end2021 -04 -06 16 :16 :53.734 [t1] DEBUG Join - child thread end
在第22行后添加t1.join();
方法后的输出结果:
1 2 3 4 5 2021 -04 -06 16 :14 :53.431 [main] DEBUG Join - main thread start2021 -04 -06 16 :14 :53.461 [t1] DEBUG Join - child thread start2021 -04 -06 16 :14 :54.474 [t1] DEBUG Join - child thread end2021 -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
。
证明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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()) { log.debug("{}" , thread.isInterrupted()); } log.debug("{}" , thread.isInterrupted()); }, "t" ); t.start(); TimeUnit.MILLISECONDS.sleep(1 ); t.interrupt(); } }
注释(2)结果:
1 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)结果:
1 2 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 两阶段终止模式 1 2 3 4 5 6 7 8 9 graph TD w("while(true)") --> a a("有没有被打断") -- 是 --> b(料理后事) b --> c((结束循环)) a -- 否 --> d(睡眠2S) d -- 无异常 --> e(执行监控记录) d -- 有异常 --> i(设置打断标记) i --> w e --> w
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @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(); } }
运行结果:
1 2 3 4 5 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 interrupted2021 -04 -24 14 :26 :25.823 [Thread-0 ] DEBUG TwoStageTermination - 料理后事
LockSupport
的park()
可用于暂停当前线程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @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(); } }
运行结果:
1 2 3 4 5 6 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进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
守护线程示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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()); }
运行结果:
1 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
枚举,分为六种状态:
1 2 3 4 5 6 NEW 新建状态 RUNNABLE 运行状态(就绪状态、运行中状态) BLOCKED 阻塞状态 WAITING 等待状态 TIMED_WAITING 计时等待状态 TERMINATED 终止状态
NEW线程刚被创建,但是还没有调用start()
方法
RUNNABLE当调用了start()
方法之后,注意,Java API层面的RUNNABLE状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行)。
BLOCKED、WAITING、TIMED_WAITING都是Java API层面对【阻塞状态】的细分。‘
TERMINATED当线程代码运行结束。
图示:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @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()); log.debug("t2 => [{}]" , t2.getState()); log.debug("t3 => [{}]" , t3.getState()); log.debug("t4 => [{}]" , t4.getState()); log.debug("t5 => [{}]" , t5.getState()); log.debug("t6 => [{}]" , t6.getState()); } }
1.3.14 习题 应用之统筹(烧水泡茶)
1 2 3 4 5 6 7 8 9 10 统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复杂的科研项目的组织与管理中,都可以应用。 怎样应用呢?主要是把工序安排好。 比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么办? - 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开了,泡茶喝。 - 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡茶喝。 - 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。 哪一种方法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。 ——华罗庚《统筹方法》
1 2 3 4 5 graph LR a(洗水壶) --> b(烧开水) c(洗茶壶,洗茶杯,拿茶叶) b --> d(泡茶) c --> d(泡茶)
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Slf4j(topic = "BoilWaterToMakeTea") public class PlanDemo { public static void main (String[] args) { Thread t1 = new Thread(() -> { log.debug("洗水壶" ); sleep(60 ); log.debug("烧开水" ); sleep(60 * 5 ); }, "t1" ); Thread t2 = new Thread(() -> { log.debug("洗茶壶" ); sleep(60 ); log.debug("洗茶杯" ); sleep(60 * 2 ); log.debug("拿茶叶" ); sleep(60 ); try { t1.join(); } 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(); } } }
运行结果:
1 2 3 4 5 6 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