一、线程与进程、并行并发、同步异步概念
1、进程与进程
进程: 资源分配的最小单位
- 进程是线程的容器, 一个进程中包含多个线程, 真正执行任务的是线程
线程: 资源调度的最小单位
进程程序
由指令
和数据
组成,但是这些 指令要运行,数据要读写,就必须将指令加载到cpu,数据加载至内存。在指令运行过程中还需要用到磁盘,网络等设备,进程就是用来加载指令,管理内存,管理IO的
- 当一个指令被运行,从磁盘加载这个程序的代码到内存,这时候就开启了一个进程
进程
就可以视为程序
的一个实例
,大部分程序都可以运行多个实例进程(例如记事本,浏览器等),部分只可以运行一个实例进程(例如360安全卫士)
- 一个进程之内可以分为
多个线程
。 一个线程
就是一个指令流
,将指令流中的一条条指令以一定的顺序交给 CPU 执行- Java 中,线程作为资源的最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器。
- 进程基本上
相互独立的
,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享; 进程间通信较为复杂 同一台计算机的进程通信称为 IPC(Inter-process communication)
不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更
轻量
,线程上下文切换成本一般上要比进程上下文切换低
并发: 在单核CPU下, 一定是并发执行
的, 也就是在同一个时间段内一起执行. 实际还是串行执行, CPU的时间片切换非常快, 给人一种同时运行的感觉。
并行: 在多核CPU下, 能真正意义上实现并行执行
, 在同一个时刻, 多个线程同时执行; 比如说2核cpu, 同时执行4个线程. 理论上同时可以有2个线程是并行执行的. 此时还是存在并发
, 因为2个cpu也会同时切换不同的线程执行任务罢了
微观串行, 宏观并行
- 在
单核 cpu
下,线程
实际还是串行执行
的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu 在线程间(时间片很短)的切换非常快
,给人的 感觉是同时运行的 。一般会将这种线程轮流使用 CPU
的做法称为并发(concurrent)
- 将
线程轮流使用cput
称为并发(concurrent)
多核 cpu
下,每个核(core) 都可以调度运行线程,这时候线程可以是并行
的,不同的线程同时使用不同的cpu在执行。
- 引用 Rob Pike 的一段描述:
并发(concurrent)
: 是同一时间应对(dealing with)多件事情的能力并行(parallel)
: 是同一时间动手做(doing)多件事情的能力
例子
- 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是
并发
- 家庭主妇雇了个保姆,她们一起这些事,这时
既有并发,也有并行
(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待) - 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是
并行
以调用方
的角度讲
- 如果
需要等待结果返回才能继续运行
的话就是同步
- 如果
不需要等待
就是异步
- 多线程可以让方法执行变为
异步
的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这5秒cpu什么都做不了,其它代码都得暂停
- 比如在项目中,视频文件需要转换格式等操作比较
费时
,这时开一个新线程处理视频转换
,避免阻塞主线程 - tomcat 的异步 servlet 也是类似的目的,
让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
- UI 程序中,开线程进行其他操作,避免阻塞 UI 线程
重点
)
1、创建一个线程(非主线程)
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类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码
public class Test2 {
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();
}
}
或者
public class CreateThread2 {
private static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("my runnable running...");
}
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
- 通过
实现Runnable接口
,并且实现run()方法
。在创建线程时作为参数传入该类的实例即可
- 当一个接口带有
@FunctionalInterface注解
时,是可以使用lambda来简化操作的 - 所以方法二中的代码可以被简化为
public class Test2 {
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();
}
}
原理之 Thread 与 Runnable 的关系
- 分析
Thread
的源码,理清它与 Runnable 的关系
小结
- 继承Thread方式: 是把线程和任务合并在了一起
- 实现Runnable方式: 是把线程和任务分开了
- 用 Runnable 更容易与线程池等高级 API 配合 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
使用FutureTask可以用泛型指定线程的返回值类型(Runnable的run方法没有返回值)
public class Test3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//需要传入一个Callable对象
FutureTask task = new FutureTask(new Callable() {
@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 futureTask = new FutureTask(new MyCall());
Thread thread = new Thread(futureTask);
thread.start();
// 获得线程运行后的返回值
System.out.println(futureTask.get());
}
}
class MyCall implements Callable {
@Override
public String call() throws Exception {
return "hello world";
}
}
4、使用线程池来创建线程
/**
* 创建线程的方式四:使用线程池
*
* 好处:
* 1.提高响应速度(减少了创建新线程的时间)
* 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
* 3.便于线程管理
* corePoolSize:核心池的大小
* maximumPoolSize:最大线程数
* keepAliveTime:线程没有任务时最多保持多长时间后会终止
*
*
* 面试题:创建多线程有几种方式?四种!
*/
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i {
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.guizy.ThreadPrintDemo.lambda$main$0(ThreadPrintDemo.java:14)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
打断正常运行的线程
- 打断正常运行的线程, 线程并不会暂停,只是调用方法
Thread.currentThread().isInterrupted();
的返回值为true,可以判断Thread.currentThread().isInterrupted();
的值来手动停止线程
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
3.6、 终止模式之两阶段终止模式
当我们在执行线程一时,想要终止线程二,这是就需要使用interrupt方法来优雅的停止线程二。
-
Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2线程一个处理其他事情的机会(如释放锁)。
-
如下所示:那么线程的
isInterrupted()
方法可以取得线程的打断标记
- 如果线程在睡眠
sleep
期间被打断,打断标记是不会变的,为false
,但是sleep
期间被打断会抛出异常,我们据此手动设置打断标记为true
; - 如果是在
程序正常运行期间被打断
的,那么打断标记就被自动设置为true
。处理好这两种情况那我们就可以放心地来料理后事啦!
- 如果线程在睡眠
下图①就是正常运行打断, ②是在睡眠中被打断
代码实现如下:
public class Test7 {
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() {
@Override
public void run() {
//开始不停的监控
while (true) {
//判断当前线程是否被打断了
if(Thread.currentThread().isInterrupted()) {
System.out.println("处理后续任务");
//终止线程执行
break;
}
System.out.println("监控器运行中...");
try {
//线程休眠
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//如果是在休眠的时候被打断,不会将打断标记设置为true,这时要重新设置打断标记
Thread.currentThread().interrupt();
}
}
}
};
monitor.start();
}
/**
* 用于停止监控器线程
*/
public void stop() {
//打断线程
monitor.interrupt();
}
}
3.7、sleep,yiled,wait,join 对比
补充:
- sleep,join,yield,interrupted是Thread类中的方法
- wait/notify是object中的方法
- sleep 不释放锁、释放cpu
- join 释放锁、抢占cpu
- yiled 不释放锁、释放cpu
- wait 释放锁、释放cpu
- 当
Java进程
中有多个线程
在执行时,只有当所有非守护线程都执行完毕后,Java进程才会结束。但当非守护线程全部执行完毕后,守护线程无论是否执行完毕,也会一同结束。
注意:
垃圾回收器线程
就是一种守护线程- Tomcat 中的
Acceptor 和 Poller 线程
都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等
- 在
操作系统
的层面上
初始状态
,仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();
,还未与操作系统线程关联可运行状态
,也称就绪状态
,指该线程已经被创建,与操作系统相关联,等待cpu给它分配时间片就可运行运行状态
,指线程获取了CPU时间片,正在运行- 当CPU时间片用完,线程会转换至【可运行状态】,等待 CPU再次分配时间片,会导致我们前面讲到的上下文切换
阻塞状态
- 如果调用了阻塞API,如BIO读写文件,那么线程实际上不会用到CPU,不会分配CPU时间片,会导致上下文切换,进入【阻塞状态】
- 等待BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU就一直不会分配时间片
终止状态
,表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
- 这是从 Java API 层面来描述的
- 根据
Thread.State 枚举,分为六种状态
新建状态
、运行状态
(就绪状态, 运行中状态)、阻塞状态
、等待状态
、定时等待状态
、终止状态
NEW (新建状态)
线程刚被创建,但是还没有调用 start() 方法RUNNABLE (运行状态)
当调用了start() 方法之后
,注意,Java API 层面的RUNNABLE 状态涵盖了操作系统层面的 【就绪状态】、【运行中状态】和【阻塞状态】
(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)BLOCKED (阻塞状态)
,WAITING (等待状态)
,TIMED_WAITING(定时等待状态)
都是 Java API 层面对【阻塞状态】的细分,如sleep就位TIMED_WAITING, join为WAITING状态。后面会在状态转换一节详述。TERMINATED (结束状态)
当线程代码运行结束
@Slf4j(topic = "c.TestState")
public class TestState {
public static void main(String[] args) throws IOException {
Thread t1 = new Thread("t1") { // new 状态
@Override
public void run() {
log.debug("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.debug("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();
}
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
}
}