概念
并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
-
线程与进程
进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多
个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创
建、运行到消亡的过程。
线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程
中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程Java线程
本章内容
创建和运行线程
- 查看线程
- 线程 API
- 线程状态
首先创建工具类:
就是 Thread.sleep()
public class Sleeper {
public static void sleep(int i) {
try {
TimeUnit.SECONDS.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void sleep(double i) {
try {
TimeUnit.MILLISECONDS.sleep((int) (i * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
读取文件(耗时的IO操作 效果等同于sleep)
@Slf4j(topic = "c.FileReader")
public class FileReader {
public static void read(String filename) {
int idx = filename.lastIndexOf(File.separator);
String shortName = filename.substring(idx + 1);
try (FileInputStream in = new FileInputStream(filename)) {
long start = System.currentTimeMillis();
log.debug("read [{}] start ...", shortName);
byte[] buf = new byte[1024];
int n = -1;
do {
n = in.read(buf);
} while (n != -1);
long end = System.currentTimeMillis();
log.debug("read [{}] end ... cost: {} ms", shortName, end - start);
} catch (IOException e) {
e.printStackTrace();
}
}
}
1、创建和运行线程
1.1 直接使用 Thread
1.2 使用 Runnable 配合 Thread
1.3 FutureTask 配合 Thread
1.4 线程池
2、观察多个线程同时运行
主要是理解
4、原理之线程运行
5、常见方法
6、start 与 run
直接调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();
//t1.start();
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 ...
程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的
调用 start
将上述代码的 t1.run() 改为 t1.start();
输出
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
程序在 t1 线程运行, FileReader.read() 方法调用是异步的
小结
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)(这时CPU也停下来了);
2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException;
3. 睡眠结束后的线程未必会立刻得到执行;
4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性;yield
yield是谦让的意思,会打断synchronize锁 让出CPU的执行权
1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
2. 具体的实现依赖于操作系统的任务调度器线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
8、join方法
public static void testJoin() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
R = 10;
});
t1.start(); //1、主线程和t1并行执行
//sleep(2); //2、如果主线程sleep时间大于 t1线程 下面那行打印的R 是 10
//sleep(0.2); //3、否则 下面那行打印的R 是 0
//t1.join(); //4、等t1线程结束后 主线程再往下执行
log.debug("结果为:{}", R);
log.debug("结束");
}
情况1:
因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 R=10 而主线程一开始就要打印 R 的结果,所以打印出 R=0
情况2:
主线程sleep时间大于 t1线程,在t1线程结束sleep后完成R=10 主线程才结束sleep,所有打印的R是10
情况3:
主线程sleep时间小于 t1线程,所以主线程打印R 在t1线程结束sleep后给 R赋值 之前,因此打印的R是0
情况4:
t1.join(),主线程执行到这里会停下来 等t1线程结束后 主线程再往下执行,因此打印的R是10
案例1:
以调用方角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
等待多个结果
问,下面代码 cost 大约多少秒?
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test2();
}
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
Thread t2 = new Thread(() -> {
sleep(2);
r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
分析如下
第一个 join:等待 t1 时, t2 并没有停止, 而在运行
第二个 join:1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s
如果颠倒两个 join 呢?
最终都是输出20:45:43.239 [main] c.TestJoin - r1: 10 r2: 20 cost: 2005
有时效的 join
等够时间
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
// 线程执行结束会导致 join 结束
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
输出20:48:01.320 [main] c.TestJoin - r1: 10 r2: 0 cost: 1010
没等够时间
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(2);
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
// 线程执行结束会导致 join 结束
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
输出20:52:15.623 [main] c.TestJoin - r1: 0 r2: 0 cost: 1502
9、interrupt方法
打断 sleep,wait,join 的线程
这几个方法都会让线程进入阻塞状态
打断 sleep线程
会清空打断状态,以 sleep 为例
public static void testInterrupt(){
Thread t1 = new Thread(()->{
log.debug(" sleep前-当前线程状态: {}", Thread.currentThread().getState());
sleep(1);
log.debug(" sleep后-当前线程状态: {}", Thread.currentThread().getState());
}, "t1");
t1.start();
sleep(0.5);
t1.interrupt();
log.debug(" 打断状态: {}", t1.isInterrupted());
}
输出
22:16:08.607 c.Sync [t1] - sleep前-当前线程状态: RUNNABLE
java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at java.base/java.lang.Thread.sleep(Thread.java:337)
at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446)
at com.xjt.javase.juc.utils.MySleeper.sleep(MySleeper.java:8)
at com.xjt.javase.juc.createThread.lambda$testInterrupt$1(createThread.java:107)
at java.base/java.lang.Thread.run(Thread.java:832)
22:16:09.119 c.Sync [main] - 打断状态: true
22:16:09.120 c.Sync [t1] - sleep后-当前线程状态: RUNNABLE
打断 park线程
打断 park 线程, 不会清空打断状态
private static void testInteruptPark() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
sleep(0.5);
t1.interrupt();
}
输出
22:20:59.146 c.Sync [t1] - park...
22:20:59.655 c.Sync [t1] - unpark...
22:20:59.655 c.Sync [t1] - 打断状态:true
10、不推荐的方法
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁
方法名 | static | 功能说明 |
---|---|---|
stop() | 停止线程运行(推荐用interrupt停止) | |
suspend() | 挂起(暂停)线程运行(推荐用wait 和 notify 暂停和唤醒线程) | |
resume() | 恢复(唤醒)线程运行 |
11、守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。
有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
上图中,主线程运行结束了,但是t1线程中Thread.currentThread().isInterrupted()
没有被打断(这个值是false),一直在while循环中没有结束,所以java程序不会停下来。
举例:
需求:刘关张桃园三结义:不求同年同月同日生但求同年同月同日死,刘备志在复兴汉室 积劳而死,
作为他的结拜兄弟 关张二人也要践行诺言 一起陪大哥上路(虽然他们阳寿还未尽)
private static void testDaemon() throws InterruptedException {
/*需求:刘关张桃园三结义:不求同年同月同日生但求同年同月同日死,刘备作为大哥 积劳而死,
作为他的结拜兄弟 关张二人也要践行诺言 一起陪大哥上路(虽然他们阳寿还未尽)*/
Thread liubei = new Thread(() -> {
for (int i=0;i<3;i++){
log.debug("刘备的寿命还有i="+i);
}
}, "刘备");
Thread guanyu = new Thread(() -> {
for (int i=0;i<80;i++){
log.debug("关羽的寿命还有i="+i);
}
}, "关羽");
Thread zhangfei = new Thread(() -> {
for (int i=0;i<60;i++){
log.debug("张飞的寿命还有i="+i);
}
}, "张飞");
guanyu.setDaemon(true);
zhangfei.setDaemon(true);
liubei.start();
guanyu.start();
zhangfei.start();
liubei.join(); //主线程要在这里等待liubei线程结束再运行
log.debug("主线程结束...");
}
输出
注意
- 垃圾回收器线程就是一种守护线程(主线程停止了垃圾回收线程也会被强制停止);
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等
12、线程状态
五种状态
从 操作系统 层面来讲有五种状态
- 【初始状态】仅是在语言层面创建了线程对象(new Thread),还未与操作系统线程关联;
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】
如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入
【阻塞状态】,等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
六种状态
这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态
- NEW
线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE
当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
- TERMINATED