Java - JUC详解
目录
一、了解和JUC相关的概念
二、Java线程
三、线程共享模型
一、了解和JUC相关的概念
1.1 什么是JUC?
JUC是java.util.concurrent包的简称,在Java5.0添加,目的就是为了更好的支持高并发任务。让开发者进行多线程编程时减少竞争条件和死锁的问题!
1.2 什么是进程?
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还要用到磁盘、网络等设备。进程就是用来加载指令,管理内存管理IO的。
- 当一个进程被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为一个实例。大部分程序可以同时运行多个实例(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360安全卫士)。
1.3 什么是线程?
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
- Java中,线程作为最小的调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器。
进程与线程的对比:
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂 。 同一台计算机的进程通信称为 IPC ( Inter-process communication )。不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP。
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
1.4 并发与并行
单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片( windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是: 微观串行,宏观并行 , 一般会将这种线程轮流使用 CPU 的做法称为并发( concurrent)。 多核 cpu 下,每个 核( core ) 都可以调度运行线程,这时候线程可以是并行(Parallel)的。简单来说:
- 并发( concurrent )是同一时间段分别应对( dealing with )多件事情的能力
- 并行(parallel )是同一时间段同时动手做( doing )多件事情的能力
1.5 同步和异步
从方法调用的角度来讲,如果:
- 需要等待结果返回,才能继续运行的就是同步
- 不需要等待结果返回,就能继续运行的就是异步
注意:同步在多线程中还有另外一层意思,是让多个线程步调一致
二、Java线程
2.1 创建线程的三种方法
方法一:直接使用Thread
// 创建线程对象Thread t = new Thread() { public void run() { // 要执行的任务 }};// 启动线程t.start();
方法二:使用Runnable配合Thread
Runnable runnable = new Runnable() { public void run(){ // 要执行的任务 }};// 创建线程对象Thread t = new Thread( runnable );// 启动线程t.start();
Java 8以后可以使用lambda精简代码:
// 创建任务对象Runnable task2 = () -> log.debug("hello");// 参数1 是任务对象; 参数2 是线程名字,推荐Thread t2 = new Thread(task2, "t2");t2.start();
- 方法一 是把线程和任务合并在了一起,方法二 是把线程和任务分开了
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
方法三:FutureTask 配合 Thread
// 创建任务对象FutureTask task3 = new FutureTask(() -> { log.debug("hello"); return 100;});// 参数1 是任务对象; 参数2 是线程名字,推荐new Thread(task3, "t3").start();// 主线程阻塞,同步等待 task 执行完毕的结果Integer result = task3.get();log.debug("结果是:{}", result);
2.2 查看进程线程的方法
windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
linux
- ps -fe 查看所有进程
- ps -fT -p 查看某个进程(PID)的所有线程
- kill 杀死进程
- top 按大写 H 切换是否显示线程
- top -H -p 查看某个进程(PID)的所有线程
Java
- jps 命令查看所有 Java 进程
- jstack 查看某个 Java 进程(PID)的所有线程状态
- jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
2.3 线程有关的常见方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
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 | |
interrupted() | 判断是否被打断 | 不会清除 打断标记 | |
isAlive() | 线程是否存活(还没有运行完毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在 sleep , wait , join 会导致被打断 的线程抛出 InterruptedException ,并清除 打断标 记 ;如果打断的正在运行的线程,则会设置 打断标 记 ; park 的线程被打断,也会设置 打断标记 | |
interrupted() | static | 判断当前线程是 否被打断 | 会清除 打断标记 |
currentThread() | static | 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线 程休眠 n 毫秒, 休眠时让出 cpu 的时间片给其它 线程 | |
yield() | static | 提示线程调度器 让出当前线程对 CPU 的使用 | 主要是为了测试和调试 |
【start与run方法】
我们通过代码示例可以看出start和run的区别:使用 t1.run() 时:
public static void main(String[] args) { Thread t1 = new Thread("t1") { @Override public void run() { log.debug(Thread.currentThread().getName()); FileReader.read(Constants.MP4_FULL_PATH); } }; t1.run(); log.debug("do other things ...");}
输出:
19:39:14 [main] c.TestStart - main19:39:14 [main] c.FileReader - read [1.mp4] start ...19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms19:39:18 [main] c.TestStart - do other things ...
不调用start,程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的,如果将上述代码的 t1.run() 改为 t1.start(),则输出为:
19:41:30 [main] c.TestStart - do other things ...19:41:30 [t1] c.TestStart - t119:41:30 [t1] c.FileReader - read [1.mp4] start ...19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms
程序在 t1 线程运行, FileReader.read() 方法调用是异步的。
小结:
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
【sleep与yield方法】
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 阻塞状态
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态 ,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器(可能出现没 “让” 出去地的现象)
【jion方法】
- 等待线程运行结束
static int r = 0;public static void main(String[] args) throws InterruptedException { test1();}private static void test1() throws InterruptedException { log.debug("开始"); Thread t1 = new Thread(() -> { log.debug("开始"); Thread.sleep(1000); log.debug("结束"); r = 10; }); t1.start(); t1.jion();// 在start后调用join log.debug("结果为:{}", r); log.debug("结束");}
不加 t1.join 输出结果r为 0 ,加了 t1.join() 后输出结果r为 10;原因是加了 t1.join() ,主线程运行到此行后会等待 t1 运行结束后再继续向下运行,即让 main 线程同步等待 t1 线程。
【interrupt方法】
1、打断 sleep ,wait,join 的线程(会让线程进入阻塞状态的线程)时:会清空打断状态,即打断标志置为false;同时,在catch中抛出异常 以 sleep 为例
private static void test1() throws InterruptedException { Thread t1 = new Thread(()->{ log.debug("sleep..."); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace; } }, "t1"); t1.start(); Thread.sleep(1000); log.debug("interrupt"); t1.interrupt(); log.debug(" 打断标记: {}", t1.isInterrupted());}
输出:
2、打断正常运行的线程时:不会清空打断状态,即打断标志置为true;同时,interrupt打断正常运行的线程时,不会让线程停下来,线程会继续执行。若想让线程停下来,需要根据对打断标志为 true 的判断从而手动让线程停下来。
private static void test2() throws InterruptedException { Thread t1 = new Thread(()->{ while(true) { Thread current = Thread.currentThread(); if(current.isInterrupted()) { log.debug("被打断了,退出循环"); break; } } }, "t1"); t1.start(); Thread.sleep(1000); t1.interrupt();}
输出:
下面来一道关于interrupt的常见面试题:在一个线程T1中如何优雅地终止线程T2?这里的优雅是指给T2一个料理后事的机会。
1. 如果使用线程的 stop() 方法停止线程:stop() 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当他被杀死后就再也没有机会释放锁,其他线程将永远无法获取该锁。(容易破坏代码块,造成死锁的方法还有suspend():挂起/暂停线程运行、resume():恢复线程运行,这些方法已经过时)
2. 若使用 System.exit(int) 方法停止线程:该方法是让整个进程都停止,而我们只想要一个线程通知,这种做法明显不划算。
此时,使用 interrupt() 方法的两阶段终止模式为最优解:
代码实现如下:
class TwoPhaseTermination{ private Thread monitor; // 启动监控线程 public void start() { monitor = new Thread(() -> { while(true) { Thread current = Thread.currentThread(); if(current.isInterrupted()) { log.debug("料理后事"); break; } try { Thread.sleep(1000);// 情况1:中断发生在线程睡眠时,会在catch中抛出异常,中断标志为false log.debug("执行监控记录");// 情况2:中断发生在正常运行线程时,中断标志置为true } catch (InterruptedException e) { e.printStackTrace(); // 重新设置打断标志 current.interrupt(); } } }); monitor.start();// 启动线程 } // 停止监控线程 public void stop() { monitor.interrupt(); }}
输出结果:
【守护线程】
默认情况下, Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
线程对象名.setDaemon(true);// 设置守护线程
守护线程示例:
- 垃圾回收器线程就是一种和守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 tomcat 接收到 shutdown 命令后,不会等待他们处理完当前请求
2.4 线程的五种状态
从 操作系统 层面来描述:
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调 度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态 。
- 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
2.5 线程的六种状态
从 Java API 层面来描述:
- NEW: 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE:当调用了 start() 方法之后。注意, Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED(无锁) , WAITING(join) , TIMED_WAITING(sleep): 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
- TERMINATED:当线程代码运行结束
注:下面列举顺序与上图箭头序号一致: (下面列举的这几种情况可以在看完第三部分:线程共享模型之管程中的内容后就容易理解了) 情况 1: NEW --> RUNNABLE
- 当调用 t.start() 方法时,由 NEW --> RUNNABLE
情况 2: RUNNABLE <--> WAITING
- t 线程用 synchronized(obj) 获取了对象锁后
- 调用 obj.wait() 方法时, t 线程 从 RUNNABLE -- > WAITING
- 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时: 竞争锁成功, t 线程 从 WAITING -- > RUNNABLE; 竞争锁失败, t 线程 从 WAITING -- > BLOCKE
情况 3: RUNNABLE <--> WAITING
- 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING。注意是当前线程在t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
情况 4: RUNNABLE < -- > WAITING
- 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE
情况 5: RUNNABLE < -- > TIMED_WAITING
- t 线程用 synchronized(obj) 获取了对象锁后
- 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
- t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时:竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE;竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
情况 6: RUNNABLE < -- > TIMED_WAITING
- 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING。注意是当前线程在t 线程对象的监视器上等待
- 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE
情况 7: RUNNABLE < -- > TIMED_WAITING
- 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
情况 8: RUNNABLE < -- > TIMED_WAITING
- 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE
情况 9: RUNNABLE < -- > BLOCKED
- t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
情况 10 RUNNABLE <--> TERMINATED
- 当前线程所有代码运行完毕,进入 TERMINATED
三、线程共享模型
线程共享模型详解