单核 cpu 下,线程实际还是串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给
不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是 同时运行的 。总结为一句话就是: 微观串行,宏观并行 ,
一般会将这种线程轮流使用 CPU 的做法称为并发(concurrent
)
多核 cpu下,每个核(core) 都可以调度运行线程,这时候线程可以是并行的。
并发(concurrent)是同一时间应对(dealing with)多件事情的能力
并行(parallel)是同一时间动手做(doing)多件事情的能力
以调用方来看,如果:
FileReader.read("xxxx");
log.debug("do other things ...");
new Thread(() -> FileReader.read("xxxx")).start();
log.debug("do other things ...");
对于上述两段代码而言,可以明显感觉到,对于同步调用而言,某一个方法如果执行时间过长,特别地,IO 操作不占用 cpu,那么这个线程将会什么都
做不了傻傻的等着,使得后面的代码跟着都执行不了,但是如果使用异步调用,那么目标方法将会使用其他线程执行,其执行不再阻塞当前线程,使得当前线
程可以继续向下执行
某四核 CPU 场景下,需要执行下面三个计算,最后再将三个计算结果汇总
计算 1 花费 10 ms
计算 2 花费 11 ms
计算 3 花费 9 ms
汇总需要 1 ms
串行执行,简单明了,10 + 11 + 9 + 1 = 31ms
并行执行,核心1使用一个线程执行计算1,核心2使用一个线程执行计算2,核心3使用一个线程执行计算3,三者总运行时间只会取决于执行时间最长的计算
2,也就是11ms,最后核心4使用一个线程汇总三个结果,共用时11 + 1 = 12ms
单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没
法干活
多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
// 构造方法的参数是给线程指定名字,推荐
Thread t = new Thread("t") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t.start();
// 创建任务对象
Runnable task = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t = new Thread(task, "t");
t2.start();
// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task, "t").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task.get();
log.debug("结果是:{}", result);
创建 Thread 的时候,如果传入一个 Runnable 对象,那么 Thread 类会将当前的 Runnable 对象赋值给自己的一个名为 target 的成员变量中
当调用 Thread 类中的 start() 方法后,操作系统会启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到 cpu 时间片,就开始执行 run() 方法
这里方法 run() 称为线程体,它包含了要执行的这个线程的内容,run() 方法运行结束,此线程随即终止。
如果我们传入的不是 Runnable 对象,而是自己实现其 run() 方法,那么自然再线程得到 cpu 时间片后,就会走我们自己实现的 run() 方法
JVM 由栈,堆,方法区所组成,其中栈内存便是给线程使用的,每个线程启动后,JVM 都会为其分配一块栈内存
/**
* @author PengHuanZhi
* @date 2021年12月08日 18:41
*/
public class Frames {
public static void main(String[] args) throws InterruptedException {
method1();
}
private static void method1() {
int i = method2(1, 2);
System.out.println(i);
}
private static int method2(int a, int b) {
int c = a + b;
return c;
}
}
观察调试控制台
图中的 Frames 便是对应于咱们的 Java 虚拟机栈帧,当前代码执行到 Main 方法,还未进入 method1 方法,所以栈帧集合只有一个 main 方法,现在进入 method1,再次观察
同理,再次进入 method2
从 method2 方法中出来,再观察,可以发现,method2 被弹出去啦
线程上下文切换指的是因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
线程的 cpu 时间片用完
垃圾回收
有更高优先级的线程需要运行
线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当发生线程上下文切换时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(它的作用是记住下一条 JVM 指令的执行地址,是线程私有的
状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
频繁的线程上下文切换会影响性能
方法名 | 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() | 获取线程优先级 | ||
getPriority(int) | 修改线程优先级 | java中规定优先级是1~10的整数,比较大优先级能提高该线程被CPU调用的几率 | |
getState() | 获取线程状态 | Java 中线程状态是用 6 个 enum 表示,分别为: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED | |
isInterrupted() | 判断是否被打 断, | 不会清除 “打断标记” | |
isAlive() | 线程是否存活 (还没有运行完 毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断 的线程抛出 InterruptedException,并清除 打断标 记 ;如果打断的正在运行的线程,则会设置 打断标 记 ;park 的线程被打断,也会设置 打断标记 | |
interrupted() | static | 判断当前线程是 否被打断 | 会清除 打断标记 |
currentThread() | static | 获取当前正在执 行的线程 | |
sleep(long n) | static | 让当前执行的线 程休眠n毫秒, 休眠时让出 cpu 的时间片给其它 线程 | |
yield() | static | 提示线程调度器 让出当前线程对 CPU的使用 | 主要是为了测试和调试 |
对于这俩方法,上面在讲Runnable和Thread之间的关系有提到,这里不再赘述了
此方法可获取当前线程的状态,Java 中线程状态是用 6 个 enum 表示,分别为:
NEW:新创建了一个线程对象,但还没有调用start()方法。
RUNNABLE:JAVA 线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
BLOCKED:阻塞,表示线程阻塞于锁
WAITING:等待,进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
TIMED_WAITING:有限等待状态(有时限,对应sleep方法的睡眠时间)
TERMINATED:终止
简单看一下就可以了
Thread t = new Thread(() -> System.out.println("invoke..."), "t");
System.out.println(t.getState());
t.start();
System.out.println(t.getState());
同一个线程不可被多次调用,否则会抛出错误的线程状态异常 IllegalThreadStateException
调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(有限等待状态)
睡眠结束后的线程未必会立刻得到执行
建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
@SneakyThrows
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
//Do Nothing
}
}, "t");
System.out.println(t.getState());
t.start();
Thread.sleep(1000);
System.out.println(t.getState());
}
其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException 从而使得睡眠线程重新执行
Thread t = new Thread(() -> {
log.info("sleep...");
try {
Thread.sleep(5000); // wait, join
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t");
t1.start();
Thread.sleep(1000);
log.info("interrupt");
t1.interrupt();
log.info("打断标记:{}", t.isInterrupted());
Thread.sleep(1000);
log.info("打断标记:{}", t.isInterrupted());
对于 sleep、wait、join 的线程,将其打断后,线程会重新进入一个 WAITING 状态,打断状态在线程变为 WAITING 后会被清空
其他线程可以使用 interrupt 方法打断一个正常运行的线程,正常运行的线程还可以分为阻塞状态的线程和正在执行的线程
Thread t = new Thread(() -> {
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted) {
log.info("被打断了, 退出循环");
break;
}
}
}, "t");
t.start();
Thread.sleep(1000);
log.info("interrupt");
t.interrupt();
LockSupport 中的 park() 方法也可以使当前线程停下来
Thread t = new Thread(() -> {
log.info("park...");
LockSupport.park();
log.info("unpark...");
log.info("打断状态:{}", Thread.currentThread().isInterrupted());
log.info("unpark...");
}, "t");
t.start();
TimeUnit.SECONDS.sleep(1);
t.interrupt();
park 方法具有一个特点,那就是当前已经被 park 后的线程被打断后,再次执行 park 方法,当前线程便停不下来了
park 方法失效的原因便是它只对打断状态为 false 的线程生效,第一次被打断后,打断状态变为 true ,如果还想要 park 方法生效,可以这样做使用
interrupted 方法重置打断状态
Thread t = new Thread(() -> {
log.info("park...");
LockSupport.park();
log.info("unpark...");
log.info("打断状态:{}", Thread.currentThread().isInterrupted());
Thread.interrupted();
log.info("打断状态:{}", Thread.currentThread().isInterrupted());
LockSupport.park();
log.info("unpark...");
}, "t");
t.start();
TimeUnit.SECONDS.sleep(1);
t.interrupt();
本意是让出的意思
调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
具体的实现依赖于操作系统的任务调度器,如果当前系统资源很充裕,那么操作系统的任务调度器还是会将时间片交由当前线程执行
区别于 sleep,执行 yield 方法的线程,下一时间片仍然有机会执行,但是执行 sleep 方法的线程,只能等若干时间后才有机会执行,而且还不是立刻执行
Runnable task1 = () -> {
int count = 0;
for (; ; ) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (; ; ) {
System.out.println("---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.start();
t2.start();
给 t1 每次循环添加一个 yield 方法
Runnable task1 = () -> {
int count = 0;
for (; ; ) {
Thread.yield();
System.out.println("---->1 " + count++);
}
};
再次执行
Runnable task1 = () -> {
int count = 0;
for (; ; ) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (; ; ) {
System.out.println("---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
以上代码,笔者设备检测不出来太大差异,读者如果需要证明,可以尝试将上述代码运行在一个单核 CPU 的虚拟机上,最终结果应该是 t2 的执行次数要高于 t1
阻塞等待目标线程运行结束
private static int r = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
System.out.println("开始");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
System.out.println("结束");
});
t1.start();
System.out.println("结果为:" + r);
}
打印结果为
结果为:0
开始
结束
具体原因为
解决办法有两种
private static int r = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
System.out.println("开始");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
System.out.println("结束");
});
t1.start();
t1.join();
System.out.println("结果为:" + r);
}
打印结果为
开始
结束
结果为:10
这些方法已过时,容易破坏同步代码块,造成线程死锁
方法名 | 功能说明 |
---|---|
stop() | 停止线程运行 |
suspend() | 挂起(暂停)线程运行 |
resume() | 恢复线程运行 |
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。
Thread t = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
log.info("结束");
}, "t");
t.start();
Thread.sleep(1000);
log.info("结束");
有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束,只需要使用 setDaemon(true) 就可以将一个线程设置为守护线程。
Thread t = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
log.info("结束");
}, "t");
t.setDaemon(true);
t.start();
Thread.sleep(1000);
log.info("结束");
垃圾回收器线程就是一种守护线程
Tomcat 中的 Acceptor (接受请求)和 Poller (分发请求)线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求
从操作系统层面来看,线程有五种状态
从 JAVA API 层面描述,线程有六种状态,详情查阅 Thread.State
public enum State {
/**
* 尚未启动的线程的线程状态
*/
NEW,
/**
* 可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,
* 但它可能正在等待来自操作系统的其他资源,例如处理器。
*/
RUNNABLE,
/**
* 线程阻塞等待锁的线程状态。处于阻塞状态的线程正在等待解锁然后进入同步块方法
* 或调用 Object.wait 之后重新进入同步块方法。
*/
BLOCKED,
/**
* 等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:
* Object.wait
* Thread.join
* LockSupport.park
*
* 处于等待状态的线程正在等待另一个线程执行特定操作。
*
* 例如:
* 在一个对象上调用了 Object.wait() 的线程正在等待另一个线程调用
* Object.notify() 或 Object.notifyAll() 在那个对象上。
*
* 已调用 Thread.join() 的线程正在等待指定线程终止。
*/
WAITING,
/**
* 具有指定等待时间的等待线程的线程状态。由于以指定的等待时间调用以下方法之一,线程处于定时等待状态:
* Thread.sleep(long timeout)
* Object.wait(long timeout)
* Thread.join(long timeout)
* LockSupport.parkNanos(long timeout)
* LockSupport.parkUntil(long timeout)
*/
TIMED_WAITING,
/**
* 已终止线程的线程状态。线程已完成执行。
*/
TERMINATED;
}
NEW:线程刚被创建,但是还没有调用 start() 方法
RUNNABLE:当调用了线程的 start() 方法之后,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的 【就绪状态】、【运行状态】和【阻塞状
态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
BLOCKED WAITING TIMED_WAITING:都是 Java API 层面对【阻塞状态】的细分
TERMINATED:当线程代码运行结束
示例:
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.info("running...");
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
while (true) { // runnable
}
}
};
t2.start();
Thread t3 = new Thread("t3") {
@Override
public void run() {
log.info("running...");
}
};
t3.start();
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (Test.class) {
try {
Thread.sleep(1000000); // timed_waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join(); // waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (Test.class) { // t4已经拿到锁没有释放,所以这里会blocked
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("t1 state {}", t1.getState());
log.info("t2 state {}", t2.getState());
log.info("t3 state {}", t3.getState());
log.info("t4 state {}", t4.getState());
log.info("t5 state {}", t5.getState());
log.info("t6 state {}", t6.getState());
附:华罗庚《统筹方法》
统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复杂的科研项目的组织与管理中,都可以应用。
怎样应用呢?主要是把工序安排好。
比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么办?
哪一种办法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。
这是小事,但这是引子,可以引出生产管理等方面有用的方法来。
水壶不洗,不能烧开水,因而洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯,就不能泡茶,因而这些又是泡茶的前提。它们的相互关系,可以用下图来表示:
从这个图上可以一眼看出,办法甲总共要16分钟(而办法乙、丙需要20分钟)。如果要缩短工时、提高工作效率,应当主要抓烧开水这个环节,而不是抓拿茶叶等环节。同时,洗茶壶茶杯、拿茶叶总共不过4分钟,大可利用“等水开”的时间来做。
是的,这好像是废话,卑之无甚高论。有如走路要用两条腿走,吃饭要一口一口吃,这些道理谁都懂得。但稍有变化,临事而迷的情况,常常是存在的。在近代工业的错综复杂的工艺过程中,往往就不是像泡茶喝这么简单了。任务多了,几百几千,甚至有好几万个任务。关系多了,错综复杂,千头万绪,往往出现“万事俱 备,只欠东风”的情况。由于一两个零件没完成,耽误了一台复杂机器的出厂时间。或往往因为抓的不是关键,连夜三班,急急忙忙,完成这一环节之后,还得等待旁的环节才能装配。 洗茶壶,洗茶杯,拿茶叶,或先或后,关系不大,而且同是一个人的活儿,因而可以合并成为:
看来这是“小题大做”,但在工作环节太多的时候,这样做就非常必要了。 这里讲的主要是时间方面的事,但在具体生产实践中,还有其他方面的许多事。这种方法虽然不一定能直接 解决所有问题,但是,我们利用这种方法来考虑问题,也是不无裨益的。
其实文章已经分析的很清楚了,代码实现也极其简单
Thread t1 = new Thread(() -> {
log.info("洗水壶");
try {
TimeUnit.SECONDS.sleep(1);
log.info("烧开水");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "老王");
Thread t2 = new Thread(() -> {
log.info("洗茶壶");
try {
TimeUnit.SECONDS.sleep(1);
log.info("洗茶杯");
TimeUnit.SECONDS.sleep(2);
log.info("拿茶叶");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("泡茶");
}, "小王");
t1.start();
t2.start();