总体路线
进程与线程 进程
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
线程
一个进程之内可以分为一到多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
二者对比
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
进程拥有共享的资源,如内存空间等,供其内部的线程共享
进程间通信较为复杂
同一台计算机的进程通信称为 IPC(Inter-process communication)
不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
并行与并发 并发 单核 cpu 下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是: 微观串行,宏观并行 ,一般会将这种 线程轮流使用 CPU 的做法称为并发, concurrent
CPU
时间片1
时间片2
时间片3
时间片4
core
线程1
线程2
线程3
线程4
并行 多核 cpu下,每个核(core) 都可以调度运行线程,这时候线程可以是并行的,不同的线程同时使用不同的cpu在执行。
CPU
时间片1
时间片2
时间片3
时间片4
core1
线程1
线程1
线程3
线程3
core2
线程2
线程4
线程2
线程4
引用 Rob Pike 的一段描述 :
并发(concurrent)是同一时间应对(dealing with)多件事情的能力
并行(parallel)是同一时间动手做(doing)多件事情的能力
应用 异步调用 以调用方 角度来讲,如果
需要等待结果返回,才能继续运行就是同步
不需要等待结果返回,就能继续运行就是异步
设计
多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…
1 2 3 4 5 6 7 8 @Slf4j(topic = "c.Sync") public class Sync { public static void main (String[] args) { FileReader.read(Constants.MP4_FULL_PATH); log.debug("do other things ..." ); } }
输出结果:
1 2 3 17 :23 :56.401 c.FileReader [main] - read [015 -hd1080.pro-german-korb-5643935. mp4] start ...17 :23 :56.503 c.FileReader [main] - read [015 -hd1080.pro-german-korb-5643935. mp4] end ... cost: 103 ms17 :23 :56.503 c.Sync [main] - do other things ...
1 2 3 4 5 6 7 8 @Slf4j(topic = "c.Async") public class Async { public static void main (String[] args) { new Thread (() -> FileReader.read(Constants.MP4_FULL_PATH)).start(); log.debug("do other things ..." ); } }
输出结果:
1 2 3 17 :25 :40.498 c.Async [main] - do other things ...17 :25 :40.498 c.FileReader [Thread-0 ] - read [015 -hd1080.pro-german-korb-5643935. mp4] start ...17 :25 :40.608 c.FileReader [Thread-0 ] - read [015 -hd1080.pro-german-korb-5643935. mp4] end ... cost: 111 ms
结论
比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
ui 程序中,开线程进行其他操作,避免阻塞 ui 线程
提高效率 充分利用多核 cpu 的优势,提高运行效率。想象下面的场景,执行 3 个计算,最后将计算结果汇总。
计算 1 花费 10 ms
计算 2 花费 11 ms
计算 3 花费 9 ms
汇总需要 1 ms
如果是串行执行,那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms
但如果是四核 cpu,各个核心分别使用线程 1 执行计算 1,线程 2 执行计算 2,线程 3 执行计算 3,那么 3 个线程是并行的,花费时间只取决于最长的那个线程运行的时间,即 11ms 最后加上汇总时间只会花费 12ms
注意
需要在多核 cpu 才能提高效率,单核仍然时是轮流执行
设计
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 @Fork(1) @BenchmarkMode(Mode.AverageTime) @Warmup(iterations=3) @Measurement(iterations=5) public class MyBenchmark { static int [] ARRAY = new int [1000_000_00 ]; static { Arrays.fill(ARRAY, 1 ); } @Benchmark public int c () throws Exception { int [] array = ARRAY; FutureTask<Integer> t1 = new FutureTask <>(()->{ int sum = 0 ; for (int i = 0 ; i < 250_000_00 ;i++) { sum += array[0 +i]; } return sum; }); FutureTask<Integer> t2 = new FutureTask <>(()->{ int sum = 0 ; for (int i = 0 ; i < 250_000_00 ;i++) { sum += array[250_000_00 +i]; } return sum; }); FutureTask<Integer> t3 = new FutureTask <>(()->{ int sum = 0 ; for (int i = 0 ; i < 250_000_00 ;i++) { sum += array[500_000_00 +i]; } return sum; }); FutureTask<Integer> t4 = new FutureTask <>(()->{ int sum = 0 ; for (int i = 0 ; i < 250_000_00 ;i++) { sum += array[750_000_00 +i]; } return sum; }); new Thread (t1).start(); new Thread (t2).start(); new Thread (t3).start(); new Thread (t4).start(); return t1.get() + t2.get() + t3.get()+ t4.get(); } @Benchmark public int d () throws Exception { int [] array = ARRAY; FutureTask<Integer> t1 = new FutureTask <>(()->{ int sum = 0 ; for (int i = 0 ; i < 1000_000_00 ;i++) { sum += array[0 +i]; } return sum; }); new Thread (t1).start(); return t1.get(); } }
在多核CPU上的输出结果:
1 2 3 Benchmark Mode Samples Score Score error Units c.i.MyBenchmark.c avgt 5 0.016 0.002 s/op c.i.MyBenchmark.d avgt 5 0.038 0.009 s/op
在单核CPU上的输出结果:
1 2 3 Benchmark Mode Samples Score Score error Units c.i.MyBenchmark.c avgt 5 0.041 0.002 s/op c.i.MyBenchmark.d avgt 5 0.038 0.009 s/op
结论
单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活
多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(参考后文的【阿姆达尔定律】)
也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
IO IO即Input/Output 输入输出,诺依曼结构将计算机分成5个基础部分:运算器、控制器、存储器、输入、输出 。
应用程序发起一次 IO 操作,包含两个阶段:
IO 调用:应用程序进程向操作系统内核发起调用。
IO 执行:操作系统内核完成 IO 操作。
IO 又分成 BIO(阻塞 IO)、NIO(非阻塞 IO)。
这两个概念是程序级别
的。主要描述的是程序请求操作系统 IO 操作后,如果 IO 资源没有准备好,那么程序该如何处理的问题: 前者等待;后者继续执行(并且使用线程一直轮询,直到有 IO 资源准备好了)
这两个概念是操作系统级别
的。主要描述的是操作系统在收到程序请求 IO 操作后,如果 IO 资源没有准备好,该如何响应程序的问题: 前者不响应,直到 IO 资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当 IO 资源准备好以后,再用事件机制返回给程序
Java 线程 创建和运行线程 直接使用 Thread 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 Thread t = new Thread (){ @Override public void run () { log.debug("running" ); } }; t.setName("t1" ); t.start(); ================================= Thread t1 = new Thread ("t1" ) { @Override public void run () { log.debug("hello" ); } }; t1.start(); ================================ Thread t2 = new Thread (()->{ log.debug("running" ); }, "t2" );t2.start();
使用 Runnable 配合 Thread 把【线程】和【任务】(要执行的代码)分开
Thread 代表线程
Runnable 可运行的任务(线程要执行的代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Runnable runnable = new Runnable () { public void run () { } }; Thread t1 = new Thread (runnable, "t1" );t1.start(); ================================ Runnable task2 = ()->log.debug("hello" );Thread t2 = new Thread (task2, "t2" );t2.start();
原理之 Thread 与 Runnable 的关系
Thread 是一个类,而 Runnable 是一个接口。
Thread 类实现了 Runnable 接口,Runnable 接口里只有一个抽象的 run() 方法。说明 Runnable 不具备多线程的特性。Runnable 依赖 Thread 类的 start 方法创建一个子线程,再在这个子线程里调用 run() 方法,才能让 Runnable 接口具备多线程的特性
小结
方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
用 Runnable 更容易与线程池等高级 API 配合
用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
FutureTask 配合 Thread FutureTask 能够接收 Callable 类型的参数 ,用来处理有返回结果的情况,Future 就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果 。
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 FutureTask<Integer> task3 = new FutureTask <>(() -> { log.debug("hello" ); return 100 ; }); new Thread (task3, "t3" ).start();Integer result = task3.get();log.debug("结果是:{}" , result); ===================================== public static void main (String[] args) throws ExecutionException, InterruptedException { FutureTask futureTask = new FutureTask <>(new Callable <Integer>() { @Override public Integer call () throws Exception { log.debug("多线程任务" ); Thread.sleep(100 ); return 100 ; } }); new Thread (futureTask,"我的名字" ).start(); log.debug("主线程" ); log.debug("{}" ,futureTask.get()); }
1 2 3 4 5 6 7 8 9 public interface Future <V> { boolean cancel (boolean mayInterruptIfRunning) ; boolean isCancelled () ; boolean isDone () ; V get () throws InterruptedException, ExecutionException; V get (long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
观察多个线程同时运行 主要是理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import lombok.extern.slf4j.Slf4j;@Slf4j(topic = "c.TestMultiThread") public class TestMultiThread { public static void main (String[] args) { new Thread (() -> { while (true ) { log.debug("running" ); } },"t1" ).start(); new Thread (() -> { while (true ) { log.debug("running" ); } },"t2" ).start(); } }
查看进程线程的方法 windows
任务管理器可以查看进程和线程数,也可以用来杀死进程
tasklist
查看进程
taskkill
杀死进程
linux
ps -fe
查看所有进程
ps -fT -p <PID>
查看某个进程(PID)的所有线程
kill
杀死进程
top
按大写 H 切换是否显示线程
top -H -p <PID>
查看某个进程(PID)的所有线程
Java
jps
命令查看所有 Java 进程
jstack <PID>
查看某个 Java 进程(PID)的所有线程状态
jconsole
来查看某个 Java 进程中线程的运行情况(图形界面)
jconsole 远程监控配置 需要以如下方式运行 java 类
1 2 3 java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -Dcom.sun.management.jmxremote.authenticate=是否认证 java类
修改 /etc/hosts 文件将 127.0.0.1 映射至主机名
如果要认证访问,还需要做如下步骤
复制 jmxremote.password 文件
修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
连接时填入 controlRole(用户名),R&D(密码)
原理之线程运行 栈与栈帧 Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch) 因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
线程的 cpu 时间片用完
垃圾回收
有更高优先级的线程需要运行
线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的。
状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
Context Switch 频繁发生会影响性能
常见方法
方法名
static
功能
注意
start()
启动一个新线程,在新的线程运行run方法中的代码
start方法只是让线程进入就绪,里面的代码不一定立刻运行(CUP的时间片还没有分给他)。每个线程对象的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()
判断是否被打断
不会清除"打断标记"
,如果该线程已经中断,则返回 true;否则返回 false。
isAlive()
线程是否存活 (还没有运行完 毕)
interrupt()
打断线程,中断一个线程(设置标志位)
如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记
;如果打断的正在运行的线程,则会设置打断标记
,线程并不会暂停;park 的线程被打断,也会设置打断标记
interrupted()
static
判断当前线程是否被打断
会清除打断标记
,如果当前线程 已经中断,则返回 true;否则返回 false。
currentThread()
static
获取当前正在执行的线程
sleep(long n)
static
让当前执行的线程休眠n毫秒, 休眠时让出 cpu 的时间片给其它 线程
yield()
static
提示线程调度器让出当前线程对 CPU的使用
主要是为了测试和调试
start 与 run 调用 start(能不能运行任务调度器说了算) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Test { public static void main (String[] args) { Thread thread = new Thread (){ @Override public void run () { log.debug("我是一个新建的线程正在运行中" ); FileReader.read(Constants.MP4_FULL_PATH); } }; thread.setName("新建线程" ); thread.start(); log.debug("主线程" ); } }
输出:程序在 t1 线程运行, run()
方法里面内容的调用是异步
的 Test.java
1 2 3 4 11 :59 :40.711 [main] DEBUG com.concurrent.test.Test - 主线程11 :59 :40.711 [新建线程] DEBUG com.concurrent.test.Test - 我是一个新建的线程正在运行中11 :59 :40.732 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] start ...11 :59 :40.735 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 3 ms
调用 run 将上面代码的thread.start();
改为 thread.run();
输出结果如下:程序仍在 main 线程运行, run()
方法里面内容的调用还是同步
的
1 2 3 4 12 :03 :46.711 [main] DEBUG com.concurrent.test.Test - 我是一个新建的线程正在运行中12 :03 :46.727 [main] DEBUG com.concurrent.test.FileReader - read [test] start ...12 :03 :46.729 [main] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 2 ms12 :03 :46.730 [main] DEBUG com.concurrent.test.Test - 主线程
小结
直接调用 run()
是在主线程中执行了 run()
,没有启动新的线程
使用 start()
是启动新的线程,通过新的线程间接执行 run()
方法 中的代码
当调用 start 方法后,线程状态会由“NEW
”变为“RUNABLE
”,此时再次调用 start 方法会报错 IllegalThreadStateException
(非法的状态异常)
sleep 与 yield sleep
调用 sleep 会让当前线程从 Running
进入 Timed Waiting
状态(阻塞 )
其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出 InterruptedException
异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
建议用 TimeUnit 的 sleep()
代替 Thread 的 sleep()
来获得更好的可读性
yield
调用 yield 会让当前线程从 Running
进入 Runnable 就绪状态
,然后调度执行其它线程
具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)
小结 yield 使 cpu 调用其它线程,但是 cpu 可能会再分配时间片给该线程;而 sleep 需要等过了休眠时间之后才有可能被分配 cpu 时间片
防止 CPU 占用 100% 在没有利用 CPU 来计算时,不要让 while(true) 空转浪费 CPU,这时可以使用 yield 或 sleep 来让出 CPU 的使用权给其他程序
1 2 3 4 5 6 7 while (true ){ try { Thread.sleep(50 ); } catch (InterruptedException e){ e.printStackTrace(); } }
可以用 wait 或 条件变量达到类似的效果
不同的是,后两种都需要加锁 ,并且需要相应的唤醒操作,一般适用于要进行同步 的场景
sleep 适用于无需锁同步 的场景
线程优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
join 等待线程运行结束 应用之同步 :在主线程中调用 t1.join,则主线程会等待 t1 线程执行完之后再继续执行 Test.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Test { private static void test1 () throws InterruptedException { log.debug("开始" ); Thread t1 = new Thread (() -> { log.debug("开始" ); sleep(1 ); log.debug("结束" ); r = 10 ; },"t1" ); t1.start(); t1.join(); log.debug("结果为:{}" , r); log.debug("结束" ); } }
等待多个线程结果 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 @Slf4j(topic = "c.TestJoin") public class TestJoin { static int r = 0 ; static int r1 = 0 ; static int r2 = 0 ; public static void main (String[] args) throws InterruptedException { test(); } private static void test () throws InterruptedException { Thread t1 = new Thread (() -> { sleep(1 ); r1 = 10 ; }); Thread t2 = new Thread (() -> { sleep(2 ); r2 = 20 ; }); t1.start(); t2.start(); long start = System.currentTimeMillis(); log.debug("join begin" ); t2.join(); log.debug("t2 join end" ); t1.join(); log.debug("t1 join end" ); long end = System.currentTimeMillis(); log.debug("r1: {} r2: {} cost: {}" , r1, r2, end - start); } }
输出结果:用时两秒 ,颠倒 join 加入时机,用时不变
1 2 3 4 21 :57 :38.013 c.TestJoin [main] - join begin21 :57 :40.035 c.TestJoin [main] - t2 join end21 :57 :40.035 c.TestJoin [main] - t1 join end21 :57 :40.035 c.TestJoin [main] - r1: 10 r2: 20 cost: 2023
有时效的join join(long n):等待线程运行结束,最多等待n毫秒
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 = "c.TestJoin") public class TestJoin { static int r = 0 ; static int r1 = 0 ; static int r2 = 0 ; public static void main (String[] args) throws InterruptedException { test2(); } public static void test3 () throws InterruptedException { Thread t1 = new Thread (() -> { sleep(2 ); r1 = 10 ; }); long start = System.currentTimeMillis(); t1.start(); log.debug("join begin" ); t1.join(3000 ); long end = System.currentTimeMillis(); log.debug("r1: {} r2: {} cost: {}" , r1, r2, end - start); } }
输出结果:t1.join(1500);
1 2 22 :05 :30.209 c.TestJoin [main] - join begin22 :05 :31.723 c.TestJoin [main] - r1: 0 r2: 0 cost: 1516
t1.join(3000):当线程运行结束时,join 设置的时效则失效,以实际线程运行时间为准
1 2 22 :09:12.349 c.TestJoin [main] - join begin22 :09:14.359 c.TestJoin [main] - r1: 10 r2: 0 cost: 2012
interrupt 方法详解 打断 sleep,wait,join 的线程 sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态,
打断 sleep 的线程,会清空打断标记 ,以 sleep 为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void main (String[] args) throws InterruptedException { Thread t1 = new Thread () { @Override public void run () { log.debug("线程任务执行" ); try { Thread.sleep(10000 ); } catch (InterruptedException e) { log.debug("被打断" ); } } }; t1.start(); Thread.sleep(500 ); log.debug("111是否被打断?{}" ,t1.isInterrupted()); t1.interrupt(); log.debug("222是否被打断?{}" ,t1.isInterrupted()); Thread.sleep(500 ); log.debug("222是否被打断?{}" ,t1.isInterrupted()); log.debug("主线程" ); }
输出结果:(我下面将中断和打断两个词混用)可以看到,打断 sleep 的线程, 会清空中断状态,刚被中断完之后 t1.isInterrupted()
的值为true
,后来变为false
,即中断状态会被清除。那么线程是否被中断过可以通过异常来判断。【同时要注意如果打断被join()
,wait()
blocked 的线程也是一样会被清除,被清除 (interrupt status will be cleared) 的意思即中断状态设置为 false
,被设置 (interrupt status will be set) 的意思就是中断状态设置为 true
】
1 2 3 4 5 6 17 :06 :11.890 [Thread-0 ] DEBUG com.concurrent.test.Test7 - 线程任务执行17 :06 :12.387 [main] DEBUG com.concurrent.test.Test7 - 111 是否被打断?false 17 :06 :12.390 [Thread-0 ] DEBUG com.concurrent.test.Test7 - 被打断17 :06 :12.390 [main] DEBUG com.concurrent.test.Test7 - 222 是否被打断?true 17 :06 :12.890 [main] DEBUG com.concurrent.test.Test7 - 222 是否被打断?false 17 :06 :12.890 [main] DEBUG com.concurrent.test.Test7 - 主线程
打断正常运行的线程 打断正常运行的线程, 线程并不会暂停,只是调用方法Thread.currentThread().isInterrupted();
的返回值为true,可以判断Thread.currentThread().isInterrupted();
的值来手动停止线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main (String[] args) throws InterruptedException { Thread t1 = new Thread (() -> { while (true ) { boolean interrupted = Thread.currentThread().isInterrupted(); if (interrupted) { log.debug("被打断了, 退出循环" ); break ; } } }, "t1" ); t1.start(); Thread.sleep(1000 ); log.debug("interrupt" ); t1.interrupt(); }
终止模式之两阶段终止模式 Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁)。
如下所示:那么线程的 isInterrupted()
方法可以取得线程的打断标记,如果线程在睡眠 sleep
期间被打断,打断标记是不会变的,为 false,但是 sleep
期间被打断会抛出异常 ,我们据此手动设置打断标记为 true
;如果是在程序正常运行期间被打断的,那么打断标记就被自动设置为true
。处理好这两种情况那我们就可以放心地来料理后事啦!
代码实现如下:
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 public class Test11 { public static void main (String[] args) throws InterruptedException { TwoParseTermination twoParseTermination = new TwoParseTermination (); twoParseTermination.start(); Thread.sleep(3000 ); twoParseTermination.stop(); } } @Slf4j class TwoParseTermination { Thread thread ; public void start () { thread = new Thread (()->{ while (true ){ if (Thread.currentThread().isInterrupted()){ log.debug("线程结束。。正在料理后事中" ); break ; } try { Thread.sleep(500 ); log.debug("正在执行监控的功能" ); } catch (InterruptedException e) { Thread.currentThread().interrupt(); e.printStackTrace(); } } }); thread.start(); } public void stop () { thread.interrupt(); } }
打断 park 线程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void main (String[] args) throws InterruptedException { test(); } private static void test () throws InterruptedException { Thread t1 = new Thread (() -> { log.debug("park..." ); LockSupport.park(); log.debug("unpark..." ); log.debug("打断状态:{}" , Thread.currentThread().isInterrupted()); }, "t1" ); t1.start(); }
输出结果:
1 2 11 :52 :02.063 c.Test14 [t1] - park...
加入:t1.interrupt() 方法后
1 2 3 11 :47 :41.455 c.Test14 [t1] - park...11 :47 :42.465 c.Test14 [t1] - unpark...11 :47 :42.465 c.Test14 [t1] - 打断状态:true
如果打断标记已经是 true,则 park 会失效,这时使用 Thread.interrupted() 方法会清除打断标记,park 方法又生效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private static void test4 () { Thread t1 = new Thread (() -> { for (int i = 0 ; i < 5 ; i++) { log.debug("park..." ); LockSupport.park(); log.debug("打断状态:{}" , Thread.interrupted()); } }); t1.start(); sleep(1 ); t1.interrupt(); }
输出结果
1 2 3 4 11 :54 :04.225 c.Test14 [Thread-0 ] - park...11 :54 :05.223 c.Test14 [Thread-0 ] - 打断状态:true 11 :54 :05.225 c.Test14 [Thread-0 ] - park...
不推荐的方法 还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁
sleep,yiled,wait,join 对比
sleep 不释放锁、释放 cpu
join 释放锁、抢占 cpu
yiled 不释放锁、释放 cpu
wait 释放锁、释放 cpu
补充:
sleep,join,yield,interrupted 是 Thread 类中的方法
wait/notify 是 objec t中的方法
守护线程 默认情况下,java 进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完 java 进程也会停止。普通线程 t1 可以调用t1.setDeamon(true);
方法变成守护线程
注意
垃圾回收器线程就是一种守护线程
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
线程状态之五种状态 五种状态的划分主要是从操作系统 的层面进行划分的
初始状态 :仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();
,还未与操作系统线程关联
可运行状态 :也称就绪状态 ,指该线程已经被创建,与操作系统相关联,等待 cpu 给它分配时间片就可运行
运行状态 :指线程获取了 CPU 时间片,正在运行
当 CPU 时间片用完,线程会转换至【可运行状态】,等待 CPU 再次分配时间片,会导致我们前面讲到的上下文切换
阻塞状态
如果调用了阻塞 API,如 BIO 读写文件,那么线程实际上不会用到 CPU,不会分配 CPU 时间片,会导致上下文切换,进入【阻塞状态】
等待 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU 就一直不会分配时间片
终止状态 :表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
线程状态之六种状态 这是从 Java API 层面来描述 的,我们主要研究的就是这种。状态转换详情图:地址 根据 Thread.State 枚举,分为六种状态
NEW 跟五种状态里的初始状态是一个意思
RUNNABLE 是当调用了 start()
方法之后的状态,注意,Java API 层面的 RUNNABLE
状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
BLOCKED
, WAITING
, TIMED_WAITING
都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 @Slf4j(topic = "c.TestState") public class TestState { public static void main (String[] args) throws IOException { Thread t1 = new Thread ("t1" ) { @Override public void run () { log.debug("running..." ); } }; Thread t2 = new Thread ("t2" ) { @Override public void run () { while (true ) { } } }; t2.start(); Thread t3 = new Thread ("t3" ) { @Override public void run () { log.debug("running..." ); } }; t3.start(); Thread t4 = new Thread ("t4" ) { @Override public void run () { synchronized (TestState.class) { try { Thread.sleep(1000000 ); } catch (InterruptedException e) { e.printStackTrace(); } } } }; t4.start(); Thread t5 = new Thread ("t5" ) { @Override public void run () { try { t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }; t5.start(); Thread t6 = new Thread ("t6" ) { @Override public void run () { synchronized (TestState.class) { try { Thread.sleep(1000000 ); } catch (InterruptedException e) { e.printStackTrace(); } } } }; t6.start(); try { Thread.sleep(500 ); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("t1 state {}" , t1.getState()); log.debug("t2 state {}" , t2.getState()); log.debug("t3 state {}" , t3.getState()); log.debug("t4 state {}" , t4.getState()); log.debug("t5 state {}" , t5.getState()); log.debug("t6 state {}" , t6.getState()); System.in.read(); } }
习题 阅读华罗庚《统筹方法》,给出烧水泡茶的多线程解决方案,提示
华罗庚《统筹方法》 统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复 杂的科研项目的组织与管理中,都可以应用。
怎样应用呢?主要是把工序安排好。
比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么办?
办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开 了,泡茶喝。
办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡茶喝。
办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。
哪一种办法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。
这是小事,但这是引子,可以引出生产管理等方面有用的方法来。
水壶不洗,不能烧开水,因而洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯,就不能泡茶,因而这些又是泡茶的前提。它们的相互关系,可以用下边的箭头图来表示:
从这个图上可以一眼看出,办法甲总共要16分钟(而办法乙、丙需要20分钟)。如果要缩短工时、提高工作效率,应当主要抓烧开水这个环节,而不是抓拿茶叶等环节。同时,洗茶壶茶杯、拿茶叶总共不过4分钟,大可利用“等水开”的时间来做。
是的,这好像是废话,卑之无甚高论。有如走路要用两条腿走,吃饭要一口一口吃,这些道理谁都懂得。但稍有变化,临事而迷的情况,常常是存在的。在近代工业的错综复杂的工艺过程中,往往就不是像泡茶喝这么简单了。任务多了,几百几千,甚至有好几万个任务。关系多了,错综复杂,千头万绪,往往出现“万事俱备,只欠东风”的情况。由于一两个零件没完成,耽误了一台复杂机器的出厂时间。或往往因为抓的不是关键,连夜三班,急急忙忙,完成这一环节之后,还得等待旁的环节才能装配。
洗茶壶,洗茶杯,拿茶叶,或先或后,关系不大,而且同是一个人的活儿,因而可以合并成为:
应用之统筹(烧水泡茶) 解法一:join 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 Thread t1 = new Thread (() -> { log.debug("洗水壶" ); sleep(1 ); log.debug("烧开水" ); sleep(15 ); }, "老王" ); Thread t2 = new Thread (() -> { log.debug("洗茶壶" ); sleep(1 ); log.debug("洗茶杯" ); sleep(2 ); log.debug("拿茶叶" ); sleep(1 ); try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } log.debug("泡茶" ); }, "小王" ); t1.start(); t2.start();
输出
1 2 3 4 5 6 19 :19 :37.547 [小王] c.TestMakeTea - 洗茶壶19 :19 :37.547 [老王] c.TestMakeTea - 洗水壶19 :19 :38.552 [小王] c.TestMakeTea - 洗茶杯19 :19 :38.552 [老王] c.TestMakeTea - 烧开水19 :19 :40.553 [小王] c.TestMakeTea - 拿茶叶19 :19 :53.553 [小王] c.TestMakeTea - 泡茶
解法1 的缺陷:
上面模拟的是小王等老王的水烧开了,小王泡茶,如果反过来要实现老王等小王的茶叶拿来了,老王泡茶呢?代码最好能适应两种情况
上面的两个线程其实是各执行各的,如果要模拟老王把水壶交给小王泡茶,或模拟小王把茶叶交给老王泡茶呢?
解法二:wait/notify 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 51 52 53 54 55 56 class S2 { static String kettle = "冷水" ; static String tea = null ; static final Object lock = new Object (); static boolean maked = false ; public static void makeTea () { new Thread (() -> { log.debug("洗水壶" ); sleep(1 ); log.debug("烧开水" ); sleep(5 ); synchronized (lock) { kettle = "开水" ; lock.notifyAll(); while (tea == null ) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (!maked) { log.debug("拿({})泡({})" , kettle, tea); maked = true ; } } }, "老王" ).start(); new Thread (() -> { log.debug("洗茶壶" ); sleep(1 ); log.debug("洗茶杯" ); sleep(2 ); log.debug("拿茶叶" ); sleep(1 ); synchronized (lock) { tea = "花茶" ; lock.notifyAll(); while (kettle.equals("冷水" )) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (!maked) { log.debug("拿({})泡({})" , kettle, tea); maked = true ; } } }, "小王" ).start(); } }
输出:
1 2 3 4 5 6 20 :04 :48.179 c.S2 [小王] - 洗茶壶20 :04 :48.179 c.S2 [老王] - 洗水壶20 :04 :49.185 c.S2 [老王] - 烧开水20 :04 :49.185 c.S2 [小王] - 洗茶杯20 :04 :51.185 c.S2 [小王] - 拿茶叶20 :04 :54.185 c.S2 [老王] - 拿(开水)泡(花茶)
解法2 解决了解法1 的问题,不过老王和小王需要相互等待,不如他们只负责各自的任务,泡茶交给第三人来做
解法三: 第三者协调 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 class S3 { static String kettle = "冷水" ; static String tea = null ; static final Object lock = new Object (); public static void makeTea () { new Thread (() -> { log.debug("洗水壶" ); sleep(1 ); log.debug("烧开水" ); sleep(5 ); synchronized (lock) { kettle = "开水" ; lock.notifyAll(); } }, "老王" ).start(); new Thread (() -> { log.debug("洗茶壶" ); sleep(1 ); log.debug("洗茶杯" ); sleep(2 ); log.debug("拿茶叶" ); sleep(1 ); synchronized (lock) { tea = "花茶" ; lock.notifyAll(); } }, "小王" ).start(); new Thread (() -> { synchronized (lock) { while (kettle.equals("冷水" ) || tea == null ) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("拿({})泡({})" , kettle, tea); } }, "王夫人" ).start(); } }
输出:
1 2 3 4 5 6 20 :13 :18.202 c.S3 [小王] - 洗茶壶20 :13 :18.202 c.S3 [老王] - 洗水壶20 :13 :19.206 c.S3 [小王] - 洗茶杯20 :13 :19.206 c.S3 [老王] - 烧开水20 :13 :21.206 c.S3 [小王] - 拿茶叶20 :13 :24.207 c.S3 [王夫人] - 拿(开水)泡(花茶)
本章小结 本章的重点在于掌握