1、创建和运行线程
方法一、直接使用Thread
// 直接使用Thread(继承/匿名内部类)
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("hello1");
}
};
t1.start();
方法二、使用Runnable配合Thread
// 使用Runnable配合Thread(函数式接口)
Thread t2 = new Thread(() -> log.debug("hello2"), "t2");
t2.start();
原理
- 方法一是把线程和任务合并到了一起,方法二把线程任务分开了
- 用Runnable更容易与线程池等高级API融合
- 用Runnable让任务类脱离了Thread继承体系,更加灵活
方法三、FutureTask配合Thread
// FutrueTask配合Thread(实现Runnable)
FutureTask<Integer> task = new FutureTask<Integer>(() -> {
log.debug("hello3");
return 100;
});
Thread t3 = new Thread(task, "t3");
t3.start();
log.debug("结果是:{}", task.get());
2、观察多个线程同时运行
主要是理解
- 交替执行
- 谁先谁后,不用我们控制
3、查看进程线程的方法
windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
tasklist
查看进程taskkill
杀死进程
linux
ps -ef
查看所有进程ps fT -p <PID>
查看某个进程(PID)的所有线程状态kill
杀死进程top
按大写H切换是否显示线程top -H -p <PID>
查看某个进程(PID)的所有线程
Java
jps
命令查看所有java进程jstack <PID>
查看某个Java进程(PID)的所有线程状态jconsole
来查看某个Java中线程的运行情况(图形界面)
4、原理之线程运行
栈与栈帧
Java Virtual Machine Stacks (Java虚拟机栈)
我们都知道JVM中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
当Context Switch发生时,需要操作系统保存当前线程的状态,并恢复另一个线程的状态,Java职工对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条jvm指令的执行地址,是线程私有的
- 状态包括程序计数器,虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch频繁发生会影响性能
5、常见方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新的线程,在新的线程运行run方法中的代码 | start方法只是让线程进入就绪,里面的代码不一定立刻运行(CPU的时间片还没有分给他)。每个线程对象的start方法只能调用一次,如果调用多次会出现IllegaThreadStateException | |
run() | 新线程启动后会调用的方法 | 如果在构造Thread对象时传递了Runnable参数,则线程启动后会调用Runnable中的run方法,否则默认不执行任何操作。但可以创建Thread的子对象,来覆盖默认行为 | |
join() | 等待线程运行结束 | ||
join(long n) | 等待线程运行结束,最多等待n毫秒 | ||
getId() | 获取线程长整型id | id唯一 | |
getName() | 获取线程名称 | ||
setName() | 修改线程名称 | ||
getPriority() | 获取线程优先级 | ||
setPriority(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的使用 | 主要是为了测试和调试 |
6、start与run
- 直接调用run方法是在当前线程中执行run方法,没有启动新线程
- 使用start是启动新的线程,通过新的线程执行run中的代码
7、sleep与yield
sleep
- 调用sleep会让当前线程从RUNNING进入TIMED_WAITING状态(阻塞)
- 其他线程可以使用interrupt方法打断正在睡眠的线程,这是sleep方法会抛出InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议永TimeUnit的sleep代替Thread中的sleep来获得更好的可读性
yield
- 调用yield会让点前线程从RUNNING进入RUNNABLE就绪状态然后调度器执行其他线程
- 具体的实现依赖于操作系统的任务调度器
Thread t1 = new Thread(() -> {
int count = 0;
for (; ; )
log.debug("" + count++);
}, "t1");
Thread t2 = new Thread(() -> {
int count = 0;
for (; ; ) {
Thread.yield();
log.debug("" + count++);
}
}, "t2");
t1.start();
t2.start();
线程优先级
- 线程优先级会提示(hint)调度器有线调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没用
Thread t1 = new Thread(() -> {
int count = 0;
for (; ; )
log.debug("" + count++);
}, "t1");
Thread t2 = new Thread(() -> {
int count = 0;
for (; ; )
log.debug("" + count++);
}, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
8、join方法详解
为什么需要join
下面代码执行,打印r是什么
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("开始");
sleep(1);
log.debug("结束");
r = 10;
});
t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}
分析
- 因为主线程和线程1是并行的,t1线程需要一秒之后才能算出r=10
- 而主线程一开始就要打印r的结果,所以只能打印出
r=10
从调用的角度来讲,如果
- 需要等待结果返回,才能继续运行的就是同步
- 不需要等待结果返回,就能继续运行的就是异步
等待多个结果
问,下面代码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(() -> {
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
Thread t2 = new Thread(() -> {
try {
sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
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呢
最终结果还是一样的
有实效的join
等够时间
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
输出
00:42:42 [main] c.Sync - r1: 10 r2: 0 cost: 1018
没等够时间
static int r1 = 0;
static int r2 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
t1.join(1500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
输出
00:43:55 [main] c.Sync - r1: 0 r2: 0 cost: 1516
9、interrupt方法详解
打断sleep,wait,join的线程
这几个方法都会让线程进入阻塞状态
打断sleep的线程,会清空打断状态,以sleep为例
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
sleep(500);
t1.interrupt();
log.debug("打断状态:{}", t1.isInterrupted());
}
输出
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at thread.base.Test7.lambda$test1$0(Test7.java:18)
at java.lang.Thread.run(Thread.java:748)
00:48:01 [main] c.Sync - 打断状态:false
打断正常运行的线程
打断正常运行的线程,不会清空打断状态
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if (interrupted) {
log.debug("打断状态:{}", interrupted);
break;
}
}
});
t1.start();
sleep(500);
t1.interrupt();
}
输出
00:52:16 [Thread-0] c.Sync - 打断状态:true
终止模式之两阶段终止模式
Two Phase Termination
在一个线程T1中如何“优雅”的终止线程T2?这里的优雅指的是给T2一个料理后事的机会。
1.错误思路
- 使用线程对象的stop()方法停止线程
- stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死之后就再也没有机会释放锁,其它线程永远无法获得锁
- 使用System.exit(int)方法停止线程
- 目的仅是停止一个线程,但这种做法会让整个程序都停止
2.两阶段终止模式
2.1 利用isInterrupted
inturrept可以端端正在执行的线程,无论这个线程是在sleep,wait,还是正常运行
package termination;
import lombok.extern.slf4j.Slf4j;
/**
* @author: 言叶长琴
*/
@Slf4j(topic = "c.TPTInturrept")
public class TPTInturrept {
private Thread thread;
public void start() {
thread = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("将结果保存");
} catch (InterruptedException e) {
// e.printStackTrace();
current.interrupt();
}
}
}, "监控线程");
thread.start();
}
public void stop() {
thread.interrupt();
}
}
调用
TPTInturrept tptInturrept = new TPTInturrept();
tptInturrept.start();
Thread.sleep(3500);
log.debug("stop");
tptInturrept.stop();
输出
2022/03/01-01:26:53.291 [监控线程] c.TPTInturrept - 将结果保存
2022/03/01-01:26:54.309 [监控线程] c.TPTInturrept - 将结果保存
2022/03/01-01:26:55.329 [监控线程] c.TPTInturrept - 将结果保存
2022/03/01-01:26:55.789 [main] c.TPTTest - stop
2022/03/01-01:26:55.789 [监控线程] c.TPTInturrept - 料理后事
2.2利用停止标记
volatile关键字会在后面讲到
package termination;
import lombok.extern.slf4j.Slf4j;
/**
* @author: 言叶长琴
* 停止标记用 volatile是为了保证该变量在多个线程之间的可见性
* 我们的例子中,即主线程把它修改为true对t1线程可见
*/
@Slf4j(topic = "c.TPTVolatile")
public class TPTVolatile {
private Thread thread;
private volatile boolean stop = false;
public void start() {
thread = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (stop) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("将结果保存");
} catch (InterruptedException e) {
// e.printStackTrace();
}
}
}, "监控线程");
thread.start();
}
public void stop() {
stop = true;
thread.interrupt();
}
}
调用
TPTVolatile tptVolatile = new TPTVolatile();
tptVolatile.start();
Thread.sleep(3500);
log.debug("stop");
tptVolatile.stop();
输出
2022/03/01-01:33:27.293 [监控线程] c.TPTVolatile - 将结果保存
2022/03/01-01:33:28.297 [监控线程] c.TPTVolatile - 将结果保存
2022/03/01-01:33:29.307 [监控线程] c.TPTVolatile - 将结果保存
2022/03/01-01:33:29.798 [main] c.TPTTest - stop
2022/03/01-01:33:29.799 [监控线程] c.TPTVolatile - 料理后事
打断park线程
打断park线程,不会清空打断状态
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}
}, "t1");
t1.start();
sleep(1000);
t1.interrupt();
输出,注意第一次打断的时间
2022/03/01-01:40:54.285 [t1] c.Sync - park...
2022/03/01-01:40:55.295 [t1] c.Sync - unpark...
2022/03/01-01:40:55.296 [t1] c.Sync - 打断状态:true
2022/03/01-01:40:55.297 [t1] c.Sync - park...
2022/03/01-01:40:55.297 [t1] c.Sync - unpark...
2022/03/01-01:40:55.297 [t1] c.Sync - 打断状态:true
2022/03/01-01:40:55.297 [t1] c.Sync - park...
2022/03/01-01:40:55.297 [t1] c.Sync - unpark...
2022/03/01-01:40:55.297 [t1] c.Sync - 打断状态:true
2022/03/01-01:40:55.297 [t1] c.Sync - park...
2022/03/01-01:40:55.297 [t1] c.Sync - unpark...
2022/03/01-01:40:55.297 [t1] c.Sync - 打断状态:true
2022/03/01-01:40:55.297 [t1] c.Sync - park...
2022/03/01-01:40:55.297 [t1] c.Sync - unpark...
2022/03/01-01:40:55.298 [t1] c.Sync - 打断状态:true
如果要是打断标记恢复,则可以使用Thread.interrupted()
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
// log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
log.debug("打断状态:{}", Thread.interrupted());
}
}, "t1");
t1.start();
sleep(1000);
t1.interrupt();
输出
2022/03/01-01:44:24.388 [t1] c.Sync - park...
2022/03/01-01:44:25.403 [t1] c.Sync - unpark...
2022/03/01-01:44:25.403 [t1] c.Sync - 打断状态:true
2022/03/01-01:44:25.405 [t1] c.Sync - park...
10、不推荐的方法
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁
方法名 | static | 功能说明 |
---|---|---|
stop() | 停止线程运行 | |
suspend() | 挂起(暂时)线程运行 | |
resume() | 恢复线程运行 |
11、主线程和守护线程
默认情况下,Java进程需要等待所有守护线程运行结束,才会结束。有一种特殊的线程叫守护线程,只要其他非守护线程没有执行完,也会强制结束。
例:
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("运行结束...");
}, "t1");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1000);
log.debug("运行结束...");
输出
2022/03/01-01:52:57.671 [main] c.Sync - 开始运行...
2022/03/01-01:52:57.715 [t1] c.Sync - 开始运行...
2022/03/01-01:52:58.729 [main] c.Sync - 运行结束...
注意
- 垃圾回收线程就是一种守护线程
- Tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到shutdow命令后,不会等待它们处理完当前请求
12、五种状态
这是从操作系统层面来描述的
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统关联),可以由CPU调度执行
- 【运行状态】指获取了CPU时间片中的状态
- 当CPU时间片用完,会从【运行状态】转至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】
- 如果调用了阻塞API,如BIO读写文件,这时操作系统实际不会用到CPU,会导致线程上下文切换,进入【阻塞状态】
- 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说,只要它们一直唤不醒,调度器就会一直不考虑调度他们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态
13、六种状态
这是从Java API层面来描述的
根据Thread.State枚举,分为六种状态
NEW
线程刚被撞见,但是还没有调用start()方法RUNNABLE
当调用了start()方法之后,注意Java API层面的RUNNABLE
状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【阻塞状态】(由于BIO导致的线程阻塞,在Java里无法区分,仍然认为是可运行)BLOCKED
,WAITING
,TIMED_WAITING
都是Java API层面对【阻塞状态】的细分,后面会在状态转换一节详述TERMINATED
当线程代码运行结束
本章小结
本章的重点在于掌握
- 线程创建
- 线程重要 api,如 start,run,sleep,join,interrupt 等
- 线程状态
- 应用方面
- 异步调用:主线程执行期间,其它线程异步执行耗时操作
- 提高效率:并行计算,缩短运算时间
- 同步等待:join
- 统筹规划:合理使用线程,得到最优效果
- 原理方面
- 线程运行流程:栈、栈帧、上下文切换、程序计数器
- Thread 两种创建方式 的源码
- 模式方面
- 终止模式之两阶段终止