文章目录
- 1、线程与进程、并行并发、同步异步概念
- 1.1、进程与线程
- 1.2、 并行与并发
- 1.3、同步和异步
- 2、线程的创建
- 2.1、通过继承Thread创建线程
- 2.2、使用Runnable配合Thread
- 2.3、使用FutureTask与Thread结合
- 2.4、使用线程池来创建线程
- 3、线程运行原理
- 3.1、虚拟机栈与栈帧
- 3.2、线程上下文切换(Thread Context Switch)
- 3.3、Thread的常见方法
- 3.4、sleep 与 yield
- 3.5、线程优先级
- 3.6、 join方法
- 3.7 interrupt 方法详解
- 3.8、终止模式之两阶段终止模式
- 3.9、sleep,yiled,wait,join 对比
- 3.10、过时方法
- 3.11、守护线程
- 4、线程状态
- 4.1、五种状态
- 4.2、六种状态
1、线程与进程、并行并发、同步异步概念
1.1、进程与线程
- 进程: 资源分配的最小单位(进程是线程的容器, 一个进程中包含多个线程, 真正执行任务的是线程)
- 线程: 资源调度的最小单位
进程
- 程序由指令和数据组成,但是这些 指令要运行,数据要读写,就必须将指令加载到cpu,数据加载至内存。在指令运行过程中还需要用到磁盘,网络等设备,进程就是用来加载指令,管理内存,管理IO的
- 当一个指令被运行,从磁盘加载这个程序的代码到内存,这时候就开启了一个进程
- 进程就可以视为程序的一个实例,大部分程序都可以运行多个实例进程(例如记事本,浏览器等),部分只可以运行一个实例进程(例如360安全卫士)
线程
- 一个进程之内可以分为多个线程
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
- Java 中,线程作为资源的最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器。
对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享; 进程间通信较为复杂
- 同一台计算机的进程通信称为 IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
1.2、 并行与并发
- 并发: 在单核CPU下, 一定是并发执行的, 也就是在同一个时间段内一起执行. 实际还是串行执行, CPU的时间片切换非常快, 给人一种同时运行的感觉
- 并行: 在多核CPU下, 能真正意义上实现并行执行, 在同一个时刻, 多个线程同时执行; 比如说2核cpu, 同时执行4个线程. 理论上同时可以有2个线程是并行执行的. 此时还是存在并发, 因为2个cpu也会同时切换不同的线程执行任务罢了
对比
- 并发(concurrent): 是同一时间应对(dealing with)多件事情的能力
- 并行(parallel): 是同一时间动手做(doing)多件事情的能力
1.3、同步和异步
- 如果需要等待结果返回才能继续运行的话就是同步
- 如果不需要等待就是异步
2、线程的创建
2.1、通过继承Thread创建线程
public class CreateThread {
public static void main(String[] args) {
Thread myThread = new MyThread();
// 启动线程
myThread.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("my thread running...");
}
}
- 优点:使用继承方式的好处是,在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法
- 缺点:Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码
2.2、使用Runnable配合Thread
public class Test {
public static void main(String[] args) {
//创建线程任务
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Runnable running");
}
};
//将Runnable对象传给Thread
Thread t = new Thread(r);
//启动线程
t.start();
}
}
- 通过实现Runnable接口,并且实现run()方法。在创建线程时作为参数传入该类的实例即可
使用lambda表达式简化操作
public class Test {
public static void main(String[] args) {
//创建线程任务
Runnable r = () -> {
//直接写方法体即可
System.out.println("Runnable running");
System.out.println("Hello Thread");
};
//将Runnable对象传给Thread
Thread t = new Thread(r);
//启动线程
t.start();
}
}
2.3、使用FutureTask与Thread结合
使用FutureTask可以用泛型指定线程的返回值类型(Runnable的run方法没有返回值)
public class Test3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//需要传入一个Callable对象
FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("线程执行!");
Thread.sleep(1000);
return 100;
}
});
Thread r1 = new Thread(task, "t2");
r1.start();
//获取线程中方法执行后的返回结果
System.out.println(task.get());
}
}
或
public class UseFutureTask {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(new MyCall());
Thread thread = new Thread(futureTask);
thread.start();
// 获得线程运行后的返回值
System.out.println(futureTask.get());
}
}
class MyCall implements Callable<String> {
@Override
public String call() throws Exception {
return "hello world";
}
}
2.4、使用线程池来创建线程
/**
* 创建线程的方式四:使用线程池
*
* 好处:
* 1.提高响应速度(减少了创建新线程的时间)
* 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
* 3.便于线程管理
* corePoolSize:核心池的大小
* maximumPoolSize:最大线程数
* keepAliveTime:线程没有任务时最多保持多长时间后会终止
*
*
* 面试题:创建多线程有几种方式?四种!
*/
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//3.关闭连接池
service.shutdown();
}
}
3、线程运行原理
3.1、虚拟机栈与栈帧
虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息,是属于线程的私有的。当Java中使用多线程时,每个线程都会维护它自己的栈帧!每个线程只能有一个活动栈帧(在栈顶),对应着当前正在执行的那个方法
3.2、线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程
- 线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
- 当Thread Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 线程的状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
3.3、Thread的常见方法
方法名 | 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() | 获取线程优先级 | ||
setPriority(int) | 修改线程优先级 | java中规定线程优先级1~10的整数,较大的优先级能提高该线程被CUP调度的几率 | |
getState() | 获取线程状态 | ||
isInterrupted() | 判断是否被打断 | 不会清除,打断标记(是否被线程打断过) | |
isAlive() | 线程是否存活(还没有运行完毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在sleep,wait,join会导致被打断的线程抛出InterruptedException,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记;park的线程被打断,也会设置打断标记 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除打断标记 |
currentThread() | static | 获取当前正在执行的线程 |
3.4、sleep 与 yield
sleep方法
- 调用 sleep() 会让当前线程从 Running(运行状态) 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出 InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
- 睡眠结束后的线程未必会立刻得到执行 (需要分配到cpu时间片)
- 建议用 TimeUnit 的 sleep() 代替 Thread 的 sleep()来获得更好的可读性
yield方法
- 调用 yield 会让当前线程从Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器(cup重新选择,可以是其他线程,也可能还是自己)
3.5、线程优先级
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它, 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
thread.setPriority(Thread.MAX_PRIORITY); //设置为优先级最高
3.6、 join方法
在主线程中调用t1.join,则主线程会等待t1线程执行完之后再继续执行
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
},"t1");
t1.start();
// t1.join();
// 这里如果不加t1.join(), 此时主线程不会等待t1线程给r赋值, 主线程直接就输出r=0结束了
// 如果加上t1.join(), 此时主线程会等待到t1线程执行完才会继续执行.(同步), 此时r=10;
log.debug("结果为:{}", r);
log.debug("结束");
}
3.7 interrupt 方法详解
- 打断标记(是否被其他线程打断过),默认为false
- 如果一个真正运行线程被打断,打断标记会被置为true
- 如果是打断因sleep wait join方法而被阻塞的线程,打断标记会被置为false,抛异常
运行时打断示例
public class Test2 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
System.out.println("被打断了, 退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
System.out.println("interrupt");
t1.interrupt();
System.out.println("打断标记为: "+t1.isInterrupted());
}
}
结果:
interrupt
打断标记为: true
被打断了, 退出循环
Process finished with exit code 0
- 打断正常运行的线程, 线程并不会暂停
- 被打断前Thread.currentThread().isInterrupted()的返回值为false,打断后返回值为true
- 通过Thread.currentThread().isInterrupted()的返回值来手动停止线程
阻塞时打断示例
public class Test3 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("sleep...");
try {
Thread.sleep(5000); // wait, join
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000);
System.out.println("iterrupt..");
t1.interrupt();
System.out.println(t1.isInterrupted()); // 如果是打断sleep,wait,join的线程, 即使打断了, 标记也为false
}
}
结果:
sleep...
iterrupt..
false
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.xc.day1.Test2.lambda$main$0(Test2.java:8)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
3.8、终止模式之两阶段终止模式
在一个线程T1中如何优雅终止线程T2?这里的优雅指的是给T2一个料理后事的机会。
public class Test4 {
public static void main(String[] args) throws InterruptedException {
Monitor monitor = new Monitor();
monitor.start();
Thread.sleep(3500);
monitor.stop();
}
}
class Monitor {
Thread monitor;
/**
* 启动监控器线程
*/
public void start() {
//设置线控器线程,用于监控线程状态
monitor = new Thread(() -> {
//开始不停的监控
while (true) {
Thread thread = Thread.currentThread();
//判断当前线程是否被打断了
if(thread.isInterrupted()) {
System.out.println("处理后续任务");
//终止线程执行
break;
}
System.out.println("监控器运行中...");
try {
//线程休眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//如果是在休眠的时候被打断,不会将打断标记设置为true,这时要重新设置打断标记
thread.interrupt();
}
}
});
monitor.start();
}
/**
* 用于停止监控器线程
*/
public void stop() {
//打断线程
monitor.interrupt();
}
}
结果:
监控器运行中...
监控器运行中...
监控器运行中...
监控器运行中...
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.xc.day1.Monitor.lambda$start$0(Test2.java:32)
at java.lang.Thread.run(Thread.java:748)
处理后续任务
Process finished with exit code 0
3.9、sleep,yiled,wait,join 对比
- sleep,join,yield,interrupted是Thread类中的方法
- wait/notify是object中的方法
- sleep 不释放锁、释放cpu
- join 释放锁、抢占cpu
- yiled 不释放锁、释放cpu
- wait 释放锁、释放cpu
3.10、过时方法
不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁。(原因:不释放锁)
方法名 | 功能说明 |
stop() | 停止线程运行 |
suspend() | 挂起(暂停)线程运行 |
resume() | 恢复线程运行 |
3.11、守护线程
- 当Java进程中有多个线程在执行时,只有当所有非守护线程都执行完毕后,Java进程才会结束
- 但当非守护线程全部执行完毕后,守护线程无论是否执行完毕,也会一同结束。
- 垃圾回收器线程就是一种守护线程
正常线程
public class Test5 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
}
},"t1");
t1.start();
System.out.println("main线程执行结束");
}
}
- main线程结束后,t1线程会一直处于运行中
- 只要有一个线程不停止,整个进程就不会终止
守护线程
t1.setDaemon(true);将t1设置为守护线程
public class Test5 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
}
},"t1");
t1.setDaemon(true);
t1.start();
System.out.println("main线程执行结束");
}
}
结果:t1线程为守护线程,其他线程执行完,则t1被强制停止
4、线程状态
4.1、五种状态
在操作系统的层面上
- 初始状态,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联
- 可运行状态,也称就绪状态,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行
- 运行状态,指线程获取了CPU时间片,正在运行
1)当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换 - 阻塞状态
1)如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
2)等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
3)与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片 - 终止状态,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
4.2、六种状态
从Thread.State 枚举,分为六种状态
新建状态、运行状态(就绪状态, 运行中状态)、阻塞状态、等待状态、定时等待状态、终止状态
- NEW (新建状态) 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE (运行状态) 当调用了 start() 方法之后,注意,Java API 层面的RUNNABLE 状态涵盖了操作系统层面的 【就绪状态】、【运行中状态】和【阻塞状态】(由于 BIO 导致的线程阻塞(线程等待io操作),在 Java 里无法区分,仍然认为 是可运行)
- BLOCKED (阻塞状态) , WAITING (等待状态) , TIMED_WAITING(定时等待状态) 都是 Java API 层面对【阻塞状态】的细分,如线程等待io操作或等待cup分配时间片就为BLOCKED,sleep就为TIMED_WAITING, join为WAITING状态。后面会在状态转换一节详述。
- TERMINATED (结束状态) 当线程代码运行结束
public class TestState {
public static void main(String[] args) throws IOException {
Thread t1 = new Thread("t1") { // new 状态
@Override
public void run() {
System.out.println("running...");
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
while(true) { // runnable 状态
}
}
};
t2.start();
Thread t3 = new Thread("t3") {
@Override
public void run() {
System.out.println("running...");
}
};
t3.start();
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (TestState.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 (TestState.class) { // blocked 状态
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 state "+t1.getState());
System.out.println("t2 state "+t2.getState());
System.out.println("t3 state "+t3.getState());
System.out.println("t4 state "+t4.getState());
System.out.println("t5 state "+t5.getState());
System.out.println("t6 state "+t6.getState());
}
}
结果:
running...
t1 state NEW
t2 state RUNNABLE
t3 state TERMINATED
t4 state TIMED_WAITING
t5 state WAITING
t6 state BLOCKED