目录
一、了解和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 并发与并行
-
并发( 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<Integer> 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> 查看某个进程(PID)的所有线程
- kill 杀死进程
- top 按大写 H 切换是否显示线程
- top -H -p <PID> 查看某个进程(PID)的所有线程
Java
- jps 命令查看所有 Java 进程
- jstack <PID> 查看某个 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 - main
19:39:14 [main] c.FileReader - read [1.mp4] start ...
19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms
19:39:18 [main] c.TestStart - do other things ...
19:41:30 [main] c.TestStart - do other things ...
19:41:30 [t1] c.TestStart - t1
19:41:30 [t1] c.FileReader - read [1.mp4] start ...
19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms
小结:
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
【sleep与yield方法】
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 阻塞状态
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
- 调用 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方法】
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();
}
}
输出结果:
【守护线程】
线程对象名.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:当线程代码运行结束
- 当调用 t.start() 方法时,由 NEW --> RUNNABLE
- t 线程用 synchronized(obj) 获取了对象锁后
-
调用 obj.wait() 方法时, t 线程 从 RUNNABLE -- > WAITING
-
调用 obj.notify() , obj.notifyAll() , t.interrupt() 时: 竞争锁成功, t 线程 从 WAITING -- > RUNNABLE; 竞争锁失败, t 线程 从 WAITING -- > BLOCKE
-
- 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING。注意是当前线程在t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
- 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE
- 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
- 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING。注意是当前线程在t 线程对象的监视器上等待
- 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE
- 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
- 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE
- t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
- 当前线程所有代码运行完毕,进入 TERMINATED