一、线程与进程、并行并发、同步异步概念
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());
}
}
