并发--基本概念
一、基本概念
1、进程与线程
- 进程: 资源分配的最小单位。进程是线程的容器, 一个进程中包含多个线程, 真正执行任务的是线程
- 线程: 资源调度的最小单位
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
线程
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行 。
- Java 中,线程作为小调度单位,进程作为资源分配的小单位。 在 windows 中进程是不活动的,只是作 为线程的容器
二者对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享;
- 进程间通信较为复杂
-- 同一台计算机的进程通信称为 IPC(Inter-process communication)
-- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP - 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
2、并发与并行
并发 (concurrent)
- 微观串行, 宏观并行
- 在单核 cpu下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu 在线程间(时间片很短)的切换非常快,给人的 感觉是同时运行的 。一般会将这种线程轮流使用 CPU的做法称为并发(concurrent)
- 将线程轮流使用cpu称为并发(concurrent)

并行
-
多核 cpu
下,每个核(core) 都可以调度运行线程,这时候线程可以是并行
的,不同的线程同时使用不同的cpu在执行。
1583408812725
二者对比
- 并发(concurrent): 是同一时间应对(dealing with)多件事情的能力
- 并行(parallel): 是同一时间动手做(doing)多件事情的能力例子
3、同步和异步
以调用方的角度讲
- 如果需要等待结果返回才能继续运行的话就是同步
- 如果不需要等待就是异步
1 设计
多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这5秒cpu什么都做不了,其它代码都得暂停。
2 结论
- 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
- tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
- UI 程序中,开线程进行其他操作,避免阻塞 UI 线程
二、Java线程
1、线程的创建
方法1、继承于Thread类,重写run()方法。
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...");
}
}
缺点:Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与线程代码耦合,当多个线程执行一样的任务时需要多份任务代码。
下面是使用匿名内部类的方式创建:
public static void main(String[] args) {
// 匿名内部类方式创建 Thread
Thread t = new Thread("t1") {
@Override
public void run() {
log.debug("running");
}
};
t.start();
log.debug("running");
}
方法2、使用Runnable配合Thread(推荐)
实现Runnable接口,并且实现run()方法。在创建线程时作为参数传入该类的实例即可。
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) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
使用lambda表达式简化
当一个接口带有@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();
}
}
通过查看源码可以发现,方法2其实还是通过使用 Thread 类中的 run 方法执行的,Thread类中的run方法是先判断target(Runnable实现类)不为空时就调用target中的run方法。
方法3、通过Callable和FutureTask创建线程
- 实际上FutureTask继承了Runnable,可以作为线程Thread对象的构造参数
- Callable接口里也只有一个call方法,可以用lamada表达式简化
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());
}
}
方法4、使用线程池
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i<10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
class Number2Thread implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 1;i<=10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
sum+=i;
}
return sum;
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);//创建一个可重用固定线程数为10的线程池
//查看该对象是哪个类造的
System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor
//设置线程池的属性
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合使用于Runnable
Future future = service.submit(new Number2Thread());//适合使用于Callable
try {
System.out.println(future.get());//输出返回值
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//3.关闭连接池
service.shutdown();
}
}
runnable 和 callable 有什么区别?
相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
主要区别:
- Runnable 接口 run 方法无返回值;
- Callable 接口 call 方法有返回值,支持泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
- 注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
2、线程的运行原理
栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 内存由堆、栈、方法区所组成,其中栈内存是给谁用的呢?
- 其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换(Thread Context Switch
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
3、常用方法
(1)start() vs run()
- 被创建的Thread对象直接调用重写的run方法时, run方法是在主线程中被执行的,这个线程对象会处一直处在新建状态。
- 使用 start 方式,CPU 会为创建的线程分配时间片,线程进入运行状态,然后线程调用 run 方法执行逻辑。
(2)sleep()与yield()
sleep (使线程阻塞)
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞),可通过state()方法查看;
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 。如:
//休眠一秒 TimeUnit.SECONDS.sleep(1); //休眠一分钟 TimeUnit.MINUTES.sleep(1);
在while(true)的情况下可以使用sleep来避免cpu空转(持续100%),sleep可以使当前线程状态变成time_waiting状态。(无需加锁就能实现)
yield (让出当前线程)
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态(仍然有可能被执行),然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
线程优先级
-
线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
-
如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
-
设置方法:
thread1.setPriority(Thread.MAX_PRIORITY); //设置为优先级最高
(3)join()方法
用于等待某个线程结束。哪个线程内调用join()方法,就等待哪个线程结束,然后再去执行其他线程。
如在主线程中调用ti.join(),则是主线程等待t1线程结束
Thread thread = new Thread();
//等待thread线程执行结束
thread.join();
//最多等待1000ms,如果1000ms内线程执行完毕,则会直接执行下面的语句,不会等够1000ms
thread.join(1000);
(4)interrupt()方法
用于打断阻塞(sleep wait join…)的线程。 处于阻塞状态的线程,CPU不会给其分配时间片。
每一个线程都有一个boolean类型的标志,此标志是当前的请求是否请求中断,默认为false。当一个线程A调用了线程B的interrupt方法时,那么线程B的是否请求的中断标志变为true。而线程B可以调用方法检测到此标志的变化。
- 打断的是阻塞方法时(sleep、wait、join):如果线程B调用了阻塞方法,如果是否请求中断标志变为了true,那么它会抛出InterruptedException异常。抛出异常的同时它会将线程B的是否请求中断标志重置为false;
- 打断的是非阻塞方法时:只是会将打断标记为true,并不会对程序有其他影响,程序不会停止,会继续执行。如果要让线程在被打断后停下来,需要使用打断标记来判断。
while(true) {
if(Thread.currentThread().isInterrupted()) {
break;
}
}
- 可以通过线程B的isInterrupted方法进行检测是否请求中断标志,另外还有一个静态的方法interrupted方法也可以检测标志,但是静态方法它检测完以后会自动的将是否请求中断标志位置为false。
//用于查看打断标记,返回值被boolean类型
t1.isInterrupted();
//静态方法检测中断标志,并检测完后重置false
Thread.interrupted();
interrupt方法的应用——两阶段终止模式
当我们在执行线程一时,想要终止线程二,这是就需要使用interrupt方法来优雅的停止线程二。

代码:
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,但抛出InterruptedException异常后又重置为false。所以此时需要再次设置为true
Thread.currentThread().interrupt();//非阻塞方法设置打断标记为true
}
}
}
};
monitor.start();
}
/**
* 用于停止监控器线程
*/
public void stop() {
//打断线程
monitor.interrupt();
}
}
(5)不推荐使用的打断方法
- stop方法 停止线程运行(可能造成共享资源无法被释放,其他线程无法使用这些共享资源)
- suspend(暂停线程)/resume(恢复线程)方法
(6)守护线程
当JAVA进程中有多个线程在执行时,只有当所有非守护线程都执行完毕后,JAVA进程才会结束。但当非守护线程全部执行完毕后,守护线程无论是否执行完毕,也会一同结束。
//将线程设置为守护线程, 默认为false
monitor.setDaemon(true);
守护线程的应用
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求
4、线程的状态
(1)五种状态
从 操作系统 层面来描述:

- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联(例如线程调用了start方法)
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
-当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换 - 【阻塞状态】
-如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】
-等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
-与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们 - 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
(2)六种状态
从 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 当线程代码运行结束
演示线程6中状态:
@Slf4j(topic = "heyuanjun.test")
public class Thread6StatusCode {
public static void main(String[] args) {
//NEW
Thread t1 = new Thread(() -> {
log.info("----t1-----");
}, "t1");
//RUNNABLE
Thread t2 = new Thread(() -> {
while (true) {
// log.info("----t2-----");
}
}, "t2");
t2.start();
//TERMINATED
Thread t3 = new Thread(() -> {
log.info("----t3-----");
}, "t3");
t3.start();
//time_waiting
Thread t4 = new Thread(() -> {
synchronized (Thread6StatusCode.class) {
try {
Thread.sleep(10 * 1000);
log.info("----t4-----");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t4.start();
//waiting
Thread t5 = new Thread(() -> {
try {
t2.join();
log.info("----t5-----");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t5.start();
//blocked
Thread t6 = new Thread(() -> {
synchronized (Thread6StatusCode.class) {
log.info("----t6-----");
}
});
t6.start();
try {
Thread.sleep(2 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("t1 线程状态:{}", t1.getState());
log.info("t2 线程状态:{}", t2.getState());
log.info("t3 线程状态:{}", t3.getState());
log.info("t4 线程状态:{}", t4.getState());
log.info("t5 线程状态:{}", t5.getState());
log.info("t6 线程状态:{}", t6.getState());
log.info("============结束==============");
}
}
执行结果:
13:39:38.408 [t3] INFO heyuanjun.test - ----t3-----
13:39:40.406 [main] INFO heyuanjun.test - t1 线程状态:NEW
13:39:40.408 [main] INFO heyuanjun.test - t2 线程状态:RUNNABLE
13:39:40.408 [main] INFO heyuanjun.test - t3 线程状态:TERMINATED
13:39:40.408 [main] INFO heyuanjun.test - t4 线程状态:TIMED_WAITING
13:39:40.408 [main] INFO heyuanjun.test - t5 线程状态:WAITING
13:39:40.408 [main] INFO heyuanjun.test - t6 线程状态:BLOCKED
13:39:40.408 [main] INFO heyuanjun.test - ============结束==============
13:39:48.405 [t4] INFO heyuanjun.test - ----t4-----
13:39:48.405 [t6] INFO heyuanjun.test - ----t6-----