1.进程与线程
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。进程就可以视为程序的一个实例。
线程
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
- Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
区别
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂
- 同一台计算机的进程通信称为 IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
2.并行与并发
单核CPU下线程实际是串行执行的。操作系统中的组件任务调度器,将CPU的时间片分给不同的程序使用,速度极快。
多核CPU下,每个核都可以独立的运行线程,此时线程是可以并行的。
- 串行: 在时间上不可能发生重叠,前一个任务未完成,下一个任务只能等待
- 并行: 同一时间,多个任务互不干扰的同时执行
- 并发: 允许两个任务彼此干扰,同一时间Ian,之一个任务运行,交替执行
应用: 多线程可以让方法执行变为异步,比如说读取磁盘文件时,假设读取操作花费了2秒钟,如果没有线程调度机制,这两秒钟CPU将停顿,等待文件读取完毕
结论:
-
单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用cpu ,不至于一个线程总占用 cpu,别的线程调度
-
多核 cpu 可以并行跑多个线程,但是不一定能提高程序的运行效率
-
IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
3.Java线程
3.1.创建线程的几种方式:
- 直接继承 Thread
- 实现Runnable接口
- FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
// 创建任务对象
FutureTask<Integer> task3 = new FutureTask<>(() -> {
doSomeThing();
return 100;
})
3.2常见方法
Static方法:
sleep(long n):
让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程
yield() :
让线程调度器让出当前线程对CPU的使用权,进入就绪状态,会进行争抢CPU时间片
interrupted() :
判断当前线程是否被打断 会清除 打断标记
currentThread() :
获取当前正在执行的线程
非Static方法:
start() 启动一个新线程,让线程进入就绪状态,多次调用会抛出IllegalThreadStateException
join() 等待线程运行结束
join(long n) 等待线程运行结束,最多等待n毫秒
getName() 获取线程名
getId() 获取线程ID
setName() 修改线程名称
setPriority(int n) 设置线程优先级
getPriority() 设置线程优先级
isInterrupted() 判断是否被打断, 不会清除 打断标记
interrupt() 打断线程(设置标记),如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记 ;如果打断的正在运行的线程,就设置打断标记
isAlive() 判断线程是否存活(运行完)
getState() 获取线程状态Java 中线程状态
运行状态:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
sleep和yield的区别:
sleep:
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield:
5. 调用 yield 会让当前线程从 Running 进入Runnable 就绪状态
6. 具体的实现依赖于操作系统的任务调度器
join方法
执行后线程立即进入阻塞状态,例如:在线程B中调用了线程A的join方法,线程B就会进入阻塞状态,知道A线程执行完毕或中断(底层原理就是wait)
线程优先级:
线程优先级会提醒任务调度器优先调用该线程,在CPU忙时,优先级高的线程会获得更多的时间片,但CPU闲时几乎没有作用
3.3.Interrupt优雅的终止线程
特别注意:打断sleep,wait,join等阻塞的线程会清空打断标记(打断状态置为false),并抛出InterruptException
打断正常运行的线程不会抛出异常,不影响查线程正常运行,仅仅将打断标记置为true
利用Interrupt退出线程:
Thread t2 = new Thread(()->{
while(true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if(interrupted) {
log.debug(" 打断状态: {}", interrupted);
break;
}
}
}, "t2");
t2.start();
Thread.sleep(5000);
t2.interrupt();
//运行结果
//23:53:15 Thread [t2] - 打断状态: true
两阶段终止模式(取代Stop等过时方法)
在一个线程t1中优雅的终止线程t2,给t2一个终止前处理的时间(释放锁,处理资源等)
终止线程的错误方法:
- stop方法会杀死线程,如果这时线程锁住共享的资源,那么线程死亡后也无法释放锁,导致其他线程永远无法释放锁
- System.exit() 方法会停止整个程序
运行流程分析
两阶段终止模式实现:
public class Test2 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
TimeUnit.SECONDS.sleep(3);
tpt.stop();
}
}
class TwoPhaseTermination {
private Thread monitor;
// 启动监控线程
public void start() {
monitor = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
System.out.println("处理线程结束前置操作...");
break;
}
try {
TimeUnit.SECONDS.sleep(1); // 情况1被打断 打断标记FALSE
System.out.println("执行监控记录...");// 情况2被打断 打断标记TRUE
} catch (InterruptedException e) {
e.printStackTrace();
// 重新设置打断标记
current.interrupt();
}
}
});
monitor.start();
}
// 停止监控线程
public void stop() {
monitor.interrupt();
}
}
3.4.几个过时的方法
方法名 | 方法作用 |
---|---|
stop() | 停止线程运行 |
suspend() | 挂起(暂停)线程运行 |
resume() | 恢复线程运行 |
不推荐使用这些方法,容易破坏同步代码块造成线程死锁,可使用两阶段终止模式替代
3.5.守护线程
定义: 默认情况下,Java进程需要等待所有线程都运行结束才会结束。守护线程就是一张特殊的线程,只要其它非守护线程都运行结束了(即使守护线程代码未执行完),也会强制结束
// 设置线程未守护线程
setDaemon(true)
应用:
- 垃圾回收器线程
- Tomcat 中的 Acceptor 和
Poller 线程都是守护线程(用于接收请求和分发请求),所以当 Tomcat 接收到shutdown命令后,不会等待它们处理完当前请求
3.6.线程的五种状态
这是从操作系统层面来描述的
初始状态: 仅创建了线程对象,还未与操作系统线程相关联
可运行(就绪)状态: 线程已经被创建(Start),可以由CPU进行调度
运行状态: 获取了CPU时间片,运行中的状态。当CPU时间片用完,会从【运行状态】转换为【就绪状态】,导致CPU线程上下文切换
阻塞状态: 调用了阻塞API,或锁
结束状态: 线程已经执行完毕,生命周期结束,不会再转换为其他状态
3.7.线程的六种状态
JAVA层面
NEW :线程刚被创建,但是还没有调用 start() 方法
RUNNABLE :当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的
【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为
是可运行)
BLOCKED , WAITING , TIMED_WAITING :都是 Java API 层面对【阻塞状态】的细分,对应(获取不到锁阻塞 join, wait无时间阻塞 sleep有时间阻塞)
TERMINATED :线程代码运行结束