java线程 3.1 创建和运行线程 方法一,直接使用 Thread 1 2 3 4 5 6 7 8 9 Thread t1 = new Thread ("t1" ) { @Override public void run () { log.debug("hello" ); } }; t1.start();
方法二,使用 Runnable 配合 Thread 把【线程】和【任务】(要执行的代码)分开,Thread 代表线程,Runnable 可运行的任务(线程要执行的代码)Test2.java
1 2 3 4 5 6 7 8 9 10 Runnable task2 = new Runnable () { @Override public void run () { log.debug("hello" ); } };Thread t2 = new Thread (task2, "t2" ); t2.start();
小结 方法1 是把线程和任务合并在了一起, 方法2 是把线程和任务分开了,用 Runnable 更容易与线程池等高级 API 配合,用 Runnable 让任务类脱离了 Thread 继承体系,更灵活。 通过查看源码可以发现,方法二其实到底还是通过方法一执行的!
方法三,FutureTask 配合 Thread FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况 Test3.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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()); }
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过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; }
Future提供了三种功能:
判断任务是否完成;
能够中断任务;
能够获取任务执行结果。
FutureTask是Future和Runable的实现
3.2 线程运行原理 虚拟机栈与栈帧 拟机栈描述的是Java方法执行的内存模型: 每个方法被执行的时候都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,是属于线程的私有的。 当java中使用多线程时,每个线程都会维护它自己的栈帧!每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch) 因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)
垃圾回收
有更高优先级的线程需要运行
线程自己调用了 sleep
、yield
、wait
、join
、park
、synchronized
、lock
等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
3.3 Thread的常见方法
3.3.1 start 与 run 调用start 1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) { Thread thread = new Thread (){ @Override public void run () { log.debug("我是一个新建的线程正在运行中" ); FileReader.read(fileName); } }; thread.setName("新建线程" ); thread.start(); log.debug("主线程" ); }
输出:程序在 t1 线程运行, run()
方法里面内容的调用是异步的 Test4.java
1 2 3 4 11 :59:40.711 [main] DEBUG com.concurrent.test.Test4 - 主线程 11 :59:40.711 [新建线程] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中 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.Test4 - 我是一个新建的线程正在运行中 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 ms 12 :03:46.730 [main] DEBUG com.concurrent.test.Test4 - 主线程
小结 直接调用 run()
是在主线程中执行了 run()
,没有启动新的线程 使用 start()
是启动新的线程,通过新的线程间接执行 run()
方法 中的代码
3.3.2 sleep 与 yield sleep
调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出 InterruptedException
异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
建议用 TimeUnit 的 sleep()
代替 Thread 的 sleep()
来获得更好的可读性
yield
调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)
小结 yield使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;而sleep需要等过了休眠时间之后才有可能被分配cpu时间片
3.3.3 线程优先级 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
3.3.4 join 在主线程中调用t1.join,则主线程会等待t1线程执行完之后再继续执行 Test10.java
1 2 3 4 5 6 7 8 9 10 11 12 13 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 private static void test2 () 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); }
3.3.5 interrupt 方法详解 打断 sleep,wait,join 的线程 先了解一些interrupt()方法的相关知识:博客地址
sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态,以 sleep 为例Test7.java
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(); } }
3.3.6 sleep,yiled,wait,join 对比 关于join的原理和这几个方法的对比:看这里
补充:
sleep,join,yield,interrupted是Thread类中的方法
wait/notify是object中的方法
sleep 不释放锁、释放cpu join 释放锁、抢占cpu yiled 不释放锁、释放cpu wait 释放锁、释放cpu
3.4 守护线程 默认情况下,java进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完java进程也会停止。 普通线程t1可以调用t1.setDeamon(true);
方法变成守护线程
注意 垃圾回收器线程就是一种守护线程 Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
3.5 线程状态之五种状态 五种状态的划分主要是从操作系统的层面进行划分的
初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();
,还未与操作系统线程关联
可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
运行状态,指线程获取了CPU时间片,正在运行
当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
阻塞状态
如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
3.6 线程状态之六种状态 这是从 Java API 层面来描述的,我们主要研究的就是这种。状态转换详情图:地址 根据 Thread.State 枚举,分为六种状态 Test12.java
NEW 跟五种状态里的初始状态是一个意思
RUNNABLE 是当调用了 start()
方法之后的状态,注意,Java API 层面的 RUNNABLE
状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
BLOCKED
, WAITING
, TIMED_WAITING
都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述