Java 多线程
多线程介绍
什么是多线程?
多线程是指一个进程中包含的多个执行流(可执行的计算单元),即在一个进程中可以同时运行多个不同的线程,来执行不同的任务(注意,一个 CPU 同一时间只能执行一个线程)。
-
好处:
- 使用多线程的好处是可以
提高 CPU 的利用率
。在多线程程序中,当其中一个线程必须等待的时候,CPU 可以运行其它的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并行的线程来完成各自的任务。
- 使用多线程的好处是可以
-
坏处:
- 线程也是程序,所以线程需要占用内存。线程越多,占用内存也越多。
- 多线程需要协调和管理,所以需要 CPU 时间跟踪线程。
- 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
并发和并行
- 并发(concurrency):多个任务在同一个 CPU 核上,按细分的时间片轮流(交替)执行,由于每个时间片的时间非常短,看起来那些任务就像是同时执行。
- 并行(parallelism):单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。
现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。
进程 VS 线程
现代计算机的 CPU 有多个核心,有时甚至有多个处理器。为了利用所有计算能力,操作系统定义了一个底层结构,叫做线程,而一个进程(例如 Chrome 浏览器)能够生成多个线程,通过线程来执行系统指令。这样如果一个进程是要使用很多 CPU,那么计算负载就会由多个核心分担,最终使得绝大多数应用能更快地完成任务。
定义
- 进程是静态的概念:程序进入内存,并获得了系统所分配的对应资源。因此进程是系统资源分配的基本单元;线程是动态的概念:进程创建的同时也产生了一个主线程用来执行任务,因此线程是可执行的计算单元,是 CPU 调度的基本单位。
- 一个程序启动后至少有一个进程,一个进程至少有一个线程;线程不能够独立执行,必须依存在进程中。
- 进程与线程均能够完成多任务。比如一台电脑上能够同时运行多个 QQ(多进程);一个 QQ 中使用多个聊天窗口(多线程)。
- 从内核的角度看,进程的目的就是担当分配系统资源(CPU 时间、内存等)的基本单位;线程则是进程的一个执行流,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
拥有资源
- 每个进程都有自己独立的一块内存空间和一组系统资源。每一个进程的内部数据和状态都是完全独立的。
- 一个标准的线程则由线程 ID、当前指令指针和寄存器组合(即堆栈)组成。线程自己不拥有系统资源,只拥有少量在运行中必不可少的资源,但它可以通过共享进程的内存单元实现数据交换、实时通信和必要的同步操作。
资源消耗
总的来说,进程与线程最大的区别在于上下文切换过程中,线程不用切换虚拟内存,因为同一个进程内的线程都是共享虚拟内存空间的
,线程单单这一点不用切换,就比进程上下文切换的性能开销(空间和时间)减少了很多。据统计,一个进程的开销大约是一个线程开销的 30 倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
由于虚拟内存与物理内存的映射关系需要查询页表,页表的查询是很慢的过程,因此会把常用的地址映射关系缓存在 TLB 里的,这样便可以提高页表的查询速度,如果发生了进程切换,那 TLB 缓存的地址映射关系就会失效,缓存失效就意味着命中率降低,于是虚拟地址转为物理地址这一过程就会很慢。
优劣势
维度 | 多进程 | 多线程 | 优劣 |
---|---|---|---|
数据共享、同步 | 数据是分开的;共享复杂;同步简单 | 多线程共享进程数据:共享简单;同步复杂 | 各有优势 |
内存、CPU | 占用内存多,切换复杂,CPU 利用率低 | 占用内存少,切换简单,CPU 利用率高 | 线程占优 |
创建销毁、切换 | 创建销毁、切换复杂,速度慢 | 创建销毁、切换简单,速度快 | 线程占优 |
编程调试 | 编程简单,调试简单 | 编程复杂,调试复杂 | 进程占优 |
可靠性 | 进程间不会相互影响 | 一个线程挂掉将导致整个进程挂掉 | 进程占优 |
分布式 | 适应于多核、多机分布 ;如果一台机器不够,扩展到多台机器比较简单 | 适应于多核分布 | 进程占优 |
应用场景
-
需要频繁创建销毁的,优先用线程。
- 这种原则最常见的应用就是 web 服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的。
-
需要进行大量计算的,优先使用线程。
- 所谓大量计算,当然就是要耗费很多 CPU,切换频繁了,这种情况下线程是最合适的。
- 这种原则最常见的是图像处理、算法处理。
-
强相关的处理用线程,弱相关的处理用进程。
- 什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
- 一般的 server 需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
- 当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。
-
可能要扩展到多机分布的用进程,多核分布的用线程。
-
都满足需求的情况下,用你最熟悉、最拿手的方式。
-
至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度中的所谓“复杂、简单”应该怎么取舍,其实没有明确的选择方法。
有一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。
需要提醒的是:虽然有这么多的选择原则,但实际应用中基本上都是“进程+线程”的结合方式,千万不要真的陷入一种非此即彼的误区。
CPU 上下文切换
什么是上下文切换?
在多线程编程中,一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。
时间片是 CPU 分配给各个线程的时间,因为时间非常短,所以 CPU 不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。
概括来说就是:当前任务在执行完 CPU 时间片并切换到另一个任务之前,会先保存自己的状态,以便下次再切换回这个任务保证原来的状态不受影响,让任务看起来还是连续运行。任务从保存(旧任务状态)到加载(新任务状态)的过程就是一次上下文切换
。
频繁切换上下文的问题
上下文切换通常是计算密集型的,每次切换时,需要保存当前的状态起来,以便能够进行先前状态的恢复,而这个切换时非常损耗性能。
也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间。事实上,这可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
减少上下文切换的方式有哪些?
通常减少上下文切换的方式有:
- 无锁并发编程:可以参照 concurrentHashMap 锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
- CAS 算法:利用 Atomic 下的 CAS 算法来更新数据,即使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换。
- 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
单核 CPU 多线程执行有没有意义?
虽然单线程全部占有 CPU,但不代表全部利用。而多线程能更好的利用资源,前提是组织好程序(比如需要执行多个不同的任务),否则并发执行的效率不一定比串行执行高,因为多线程在执行的时候会有抢占 CPU 资源、上下文切换
的过程。
如果你的程序仅仅是做一种简单的计算,其间不涉及任何可能使线程挂起的操作,如 I/O 读写,等待某种事件等
,那么从表面上看,两个线程与单个线程相比,增加了切换的开销,应该比单线程慢才对。
但还得考虑操作系统的调度策略。通常,在支持线程的操作系统中,线程才是系统调度的单位,对同样一个进程来讲,多一个线程就可以多分到 CPU 时间,特别是从一个增加到两个的时候
。
举例来说,假如在你的程序启动前,系统中已经有 50 个线程在运行,那么当你的程序启动后,假如他只有一个线程,那么平均来讲,它将获得 1/51 的 CPU 时间,而如果他有两个线程,那么就会获得 2/52 的 CPU 时间(当然,这是一种非常理想的情况,它没有考虑系统中原有其他线程的繁忙或者空闲程度,也没有考虑线程切换)。
但是如果你的程序里面已经有 1000 个线程,那么你把它加到 1500,效果就不会有从 1 个线程加到 2 个线程来的明显。而且很可能造成系统的整体性能下降,因为线程之间的切换也需要时间。
设置多少个线程合适?(线程池设定多少核心线程?)
如何合理设置线程数,与线程占用 CPU 的时间强相关
。
简单思路:首先测算一个线程中,占用 CPU 的时间是多少,不占用 CPU 的时间是多少,算出两者的比值
,那么分子和分母的和即为合理的线程数,此时 CPU 利用率就能达到 100%。
示例:比如占用 CPU 时间和不占用 CPU 时间的比值是 1:1,则应设置 2 个线程;再比如占用 CPU 时间和不占用 CPU 时间的比值是 1:3,则应设置 4 个线程。
用户线程和内核线程
在操作系统中,用户级线程和内核级线程是什么?
在操作系统的设计中,为了防止用户操作敏感指令而对 OS 带来安全隐患,OS 被分成了用户空间(user space)和内核空间(kernel space)。
用户级线程
通过用户空间的库类实现的线程,就是用户级线程(user-level threads,ULT)。这种线程不依赖于操作系统核心,进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程
。
在图里,我们可以清楚的看到,线程表(Thread table,管理线程的数据结构)是处于进程内部的,完全处于用户空间层面,内核空间对此一无所知!当然,用户线程也可以没有线程表!
内核级线程
相应的,由 OS 内核空间直接掌控的线程,称为内核级线程(kernel-level threads,KLT)。其依赖于操作系统核心,由内核的内部需求进行创建和撤销
。
同样的,在图中,我们看到内核线程的线程表(Thread table)位于内核中,包括了线程控制块(TCB),一旦线程阻塞,内核会从当前或者其他进程(process)中重新选择一个线程保证程序的执行。
对于用户级线程来说,其线程的切换发生在用户空间,这样的线程切换至少比陷入内核要快一个数量级
。但是该种线程有个严重的缺点:如果一个线程开始运行,那么该进程中其他线程就不能运行,除非第一个线程自动放弃 CPU
。因为在一个单独的进程内部,没有时钟中断,所以不能用轮转调度(轮流)的方式调度线程。
这两种线程在多核 CPU 的计算机上是否都能并行?
-
同一进程中的用户级线程
,在不考虑调起多个内核级线程的基础上,是没有办法利用多核 CPU 的,其实质是并发而非并行
。 -
内核级线程是可以利用多核 CPU 的,即可以并行
,但该线程在内核中创建和撤销线程的开销比较大,需要考虑上下文切换的开销。
多线程实现
线程常用方法
方法名 | 说明 |
---|---|
void run() | 在线程开启后,此方法将被调用执行 |
void start() | 使此线程开始执行,JVM 会调用 run 方法 |
void setName(String name) | 将此线程的名称更改为等于参数 name |
String getName() | 返回此线程的名称 |
Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
多线程实现方式
方式一:继承 Thread 类
实现步骤:
- 自定义一个类,继承 Thread 类;
- 在自定义类中重写 run() 方法;
- 创建自定义类的对象;
- 启动线程,即执行 start() 方法。
解释:
-
为什么要重写 run() 方法?
- 因为 run() 是用来封装被线程执行的代码。
-
run() 方法和 start() 方法的区别?
- run():封装线程执行的代码,直接调用时相当于普通方法的调用。
- start():启动线程,即由 JVM 调用此线程的 run() 方法。
代码示例:
// 自定义的多线程类
class MyThread extends Thread {
@Override
public void run(){
for(int i=0; i<100; i++) {
System.out.println(i);
try{
Thread.sleep(1); // 单位是毫秒
}catch(InterruptedException e){
System.out.println(e);
}
}
}
}
// 测试类
public class Test{
public static void main(String[] args){
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
// 启动两个线程
t1.start();
t2.start();
}
}
方式二:实现 Runnable 接口
Thread 构造方法:
方法名 | 说明 |
---|---|
Thread(Runnable target) | 分配一个新的 Thread 对象 |
Thread(Runnable target, String name) | 分配一个新的 Thread 对象 |
实现步骤:
- 自定义一个类实现 Runnable 接口;
- 在自定义类中重写 run() 方法;
- 创建自定义类的对象;
- 创建 Thread 类的对象,把 MyRunnable 对象作为构造方法的参数;
- 启动线程,即调用 Thread 类对象的 start() 方法。
示例代码:
package com.demo;
public class Test{
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 自定义类作为Thread类的参数
Thread t1 = new Thread(myThread, "线程1");
Thread t2 = new Thread(myThread, "线程2");
t1.start();
t2.start();
}
}
// 自定义类实现 Runnable 接口
class MyThread implements Runnable {
@Override
public void run(){
for(int i=0; i<100; i++){
try {
Thread.sleep(1); // 单位:毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": "+i);
}
}
}
方式三:实现 Callable 接口
方法介绍:
方法名 | 说明 |
---|---|
V call() | 计算结果,如果无法计算结果,则抛出一个异常 |
FutureTask(Callable<V> callable) | 创建一个 FutureTask,一旦运行就执行给定的 Callable |
V get() | 如有必要,等待计算完成,然后获取其结果 |
实现步骤:
- 定义一个自定义类实现 Callable 接口;
- 在 MyCallable 类中重写 call() 方法;
- 创建自定义类的对象;
- 创建 Future 的实现类 FutureTask 对象,把自定义类对象作为构造方法的参数;
- 创建 Thread 类的对象,把 FutureTask 对象作为构造方法的参数;
- 启动线程;
- 再调用 get 方法,就可以获取线程结束之后的结果。
示例代码:
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("序号" + i);
}
// 返回值表示线程运行完毕之后的结果
return "答应";
}
}
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 线程开启之后需要执行里面的call方法
MyCallable mc = new MyCallable();
// 可以获取线程执行完毕之后的结果
FutureTask<String> ft = new FutureTask<>(mc);
// 创建线程对象
Thread t1 = new Thread(ft);
String s = ft.get();
// 开启线程
t1.start();
System.out.println(s);
}
}
三种实现方式的对比
继承 Thread 类
- 优势:编程比较简单,可以直接使用 Thread 类中的方法。
- 劣势:
- 可扩展性较差,不能再继承其他的类。
- 多个线程间无法共享线程类的实例变量(需要创建不同的 Thread 对象,自然不共享)。
实现 Runnable、Callable 接口
- 优势:扩展性强,实现该接口的同时还可以继承其他的类。
- 通过实现 Runnable 接口的线程类,是互相共享资源的(因为 Thread 对象才是真正的线程对象)。
- Callable 接口如同 Runnable 接口的升级版,其提供的 call() 方法作为线程的执行体,同时允许有返回值。
- 劣势:编程相对复杂,不能直接使用 Thread 类中的方法。
线程优先级
线程的两种调度方式:
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片。
- 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
Java 线程:
-
使用的是抢占式调度模型。
-
随机性:假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU 时间片(也就是使用权),才可以执行指令。所以说多线程程序的执行是有随机性的,因为谁抢到 CPU 的使用权是不一定的。
优先级相关方法:
方法名 | 说明 |
---|---|
final int getPriority() | 返回此线程的优先级 |
final void setPriority(int newPriority) | 更改此线程的优先级(线程默认优先级是 5;线程优先级的范围是 1-10) |
代码示例:
package com.demo;
public class Test{
public static void main(String[] args) {
MyThread myThread = new MyThread();
// 自定义类作为Thread类的参数
Thread t1 = new Thread(myThread, "线程1");
Thread t2 = new Thread(myThread, "线程2");
t1.setPriority(1);
System.out.println(t1.getPriority()); // 1
t2.setPriority(10);
System.out.println(t2.getPriority()); // 10
t1.start();
t2.start();
}
}
// 自定义类实现 Runnable 接口
class MyThread implements Runnable {
@Override
public void run(){
for(int i=0; i<5; i++){
try {
Thread.sleep(1); // 单位:毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": "+i);
}
}
}
守护线程
方法名 | 说明 |
---|---|
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,JVM 将退出 |
代码示例:
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName() + "---" + i);
}
}
}
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + "---" + i);
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
// 把第二个线程设置为守护线程
// 当普通线程执行完后,守护线程即使未执行完 也会跟着结束
t2.setDaemon(true);
t1.start();
t2.start();
}
}
如何停止一个正在运行的线程
- 可以在线程中使用 for 循环来判断线程是否终止状态,如果是,则后面的代码不再运行。
class MyThread extends Thread {
@Override
public void run() {
super.run();
for (int i=0;;i++) {
if (this.interrupted()) {
System.out.println("线程已终止,for循环不再执行");
break; // 也可使用 return
}
System.out.println("i="+(i+1));
}
}
}
public class Test {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
try {
Thread.sleep(2000);
thread.interrupt();
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
...
i=143977
i=143978
i=143979
线程已终止,for循环不再执行
- 使用 stop() 强制终止线程(该方法是不安全的,且是已被废弃的方法)。
class MyThread extends Thread {
int i = 0;
@Override
public void run() {
super.run();
while (true) {
try {
System.out.println("i="+i+1);
i++;
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.stop();
}
}
运行结果:
...
i=61
i=71
i=81
i=91
线程状态
介绍
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。而是在不同的时期有不同的状态。
那么 Java中 的线程存在哪几种状态呢?Java 中的线程状态被定义在了 java.lang.Thread.State 枚举类中,State 枚举类的源码如下:
public class Thread {
public enum State {
/* 新建 */
NEW ,
/* 可运行状态 */
RUNNABLE ,
/* 阻塞状态 */
BLOCKED ,
/* 无限等待状态 */
WAITING ,
/* 计时等待 */
TIMED_WAITING ,
/* 终止 */
TERMINATED;
}
// 获取当前线程的状态
public State getState() {
return jdk.internal.misc.VM.toThreadState(threadStatus);
}
}
通过源码我们可以看到 Java 中的线程存在 6 种状态,每种线程状态的含义如下:
线程状态 | 具体含义 |
---|---|
NEW | 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用 start 方法。MyThread t = new MyThread() 只有线程象,没有线程特征。 |
RUNNABLE | 当我们调用线程对象的 start 方法,那么此时线程对象进入了 RUNNABLE 状态。那么此时才是真正的在 JVM 进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与 CPU 的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。 |
BLOCKED | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态。 |
WAITING | 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用 Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为 wait() 而等待的线程正在等待另一个线程去调用 notify() 或 notifyAll();一个因为 join() 而等待的线程正在等待另一个线程结束。 |
TIMED_WAITING | 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long) |
TERMINATED | 一个完全运行完成的线程的状态,也称之为终止状态、结束状态。 |
各个状态的转换如下图所示:
线程阻塞的原因
什么是线程阻塞?
在某一时刻,某一个线程在运行一段代码时,另一个线程也需要运行,但是在运行过程中的该线程执行完成之前,另一个线程是无法获取到 CPU 执行权的(调用 sleep() 方法是进入到睡眠暂停状态,但是 CPU 执行权并没有交出去,而调用 wait() 方法则是将 CPU 执行权交给另一个线程),这个时候就会造成线程阻塞。
出现线程阻塞的原因:
-
睡眠状态
当一个线程执行代码的时候调用了 sleep() 方法后,线程处于睡眠状态,需要设置一个睡眠时间,此时有其他线程需要执行时就会造成线程阻塞,而且 sleep() 方法被调用之后,线程不会释放锁对象。也就是说,锁还在该线程手里,CPU 执行权也还在该线程手里,等睡眠时间一过,该线程就会进入就绪状态。 -
等待状态
当一个线程正在运行时,调用了 wait() 方法,此时该线程需要交出 CPU执行权,也就是将锁释放出去,交给另一个线程,该线程进入等待状态,但与睡眠状态不一样的是,进入等待状态的线程不需要设置睡眠时间,但是需要执行 notify() 方法或者 notifyAll() 方法来对其唤醒,自己是不会主动醒来的,等被唤醒之后,该线程也会进入就绪状态,但是进入该状态的该线程手里是没有执行权的,也就是没有锁,而睡眠状态的线程一旦苏醒,进入就绪状态时自己还拿着锁。 -
礼让状态
当一个线程正在运行时,调用了 yield() 方法之后,该线程会将执行权礼让给同等级的线程或者比它高一级的线程优先执行,此时该线程有可能只执行了一部分而此时把执行权礼让给了其他线程,这个时候也会进入阻塞状态,但是该线程会随时可能又被分配到执行权。 -
自闭状态
当一个线程正在运行时,调用了一个 join() 方法,此时该线程会进入阻塞状态,另一个线程会运行,直到运行结束后,原线程才会进入就绪状态。 -
suspend() 和 resume()
这两个方法是配套使用的,suspend() 是让线程进入阻塞状态,它的“解药”就是 resume(),没有 resume(),suspend() 自己是不会恢复的,由于这种比较容易出现死锁现象,所以 JDK 1.5 之后就已经被废除了,这两种方法就是相爱相杀的一对。
线程安全(线程同步)
定义:
-
线程同步:指进程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时则等待,直到消息到达时才被唤醒。
-
线程互斥:指对于共享的进程系统资源,每个线程访问时的排他性。线程互斥可以被看成是一种特殊的线程同步。
Java 中的线程安全就是线程同步的意思。
线程安全问题出现的条件:
- 多线程环境。
- 有共享数据。
- 有多条语句操作共享数据。
如何解决线程安全问题:把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。
同步控制:synchronized
synchronized 关键字用来修饰成员方法时,代表这个方法对于同一个对象来说,同一时间只允许一个线程执行,别的线程如果也调用这个实例的这个方法,就需要等待已经在执行这个方法的线程执行完毕,才能进入方法执行。
synchronized 还能 修饰代码块、静态方法。
同步的好处和弊端:
- 好处:解决了多线程的数据安全问题。
- 弊端:当线程很多时,由于每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
同步代码块
synchronized(任意对象) { // 相当于给代码加锁,任意对象就可以看成是一把锁
多条语句操作共享数据的代码
}
代码示例:
public class SellTicket implements Runnable {
private int tickets = 100;
private Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) { // 对可能有安全问题的代码加锁,多个线程必须使用同一把锁
// t1进来后,就会把这段代码给锁起来
if (tickets > 0) {
try {
Thread.sleep(100);
// t1休息100毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
// 窗口1正在出售第100张票
System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
tickets--; // tickets = 99;
}
}
// t1出来了,这段代码的锁就被释放了
}
}
}
public class SellTicketDemo {
public static void main(String[] args) {
SellTicket st = new SellTicket();
Thread t1 = new Thread(st, "窗口1");
Thread t2 = new Thread(st, "窗口2");
Thread t3 = new Thread(st, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
同步方法
同步方法,就是把 synchronized 关键字加到方法上:
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
- 同步方法的锁对象:this
同步静态方法,就是把 synchronized 关键字加到静态方法上:
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}
- 同步静态方法的锁对象:类名.class
代码示例:
public class MyRunnable implements Runnable {
private static int ticketCount = 100;
@Override
public void run() {
while(true){
if("窗口一".equals(Thread.currentThread().getName())){
//同步方法
boolean result = synchronizedMthod();
if(result){
break;
}
}
if("窗口二".equals(Thread.currentThread().getName())){
//同步代码块
synchronized (MyRunnable.class){
if(ticketCount == 0){
break;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
}
}
}
}
}
// 同步静态方法
private static synchronized boolean synchronizedMthod() {
if(ticketCount == 0){
return true;
}else{
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticketCount--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticketCount + "张票");
return false;
}
}
}
// 测试类
public class Demo {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.setName("窗口一");
t2.setName("窗口二");
t1.start();
t2.start();
}
}
Lock 锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁。于是,为了更清晰的表达如何加锁和释放锁,JDK5 以后提供了一个新的锁对象 Lock。
Lock 是接口,不能直接实例化,下面采用它的实现类 ReentrantLock 来实例化。
ReentrantLock 构造方法:
方法名 | 说明 |
---|---|
ReentrantLock() | 创建一个 ReentrantLock 的实例 |
加锁解锁方法:
方法名 | 说明 |
---|---|
void lock() | 获得锁 |
void unlock() | 释放锁 |
代码示例:
public class Ticket implements Runnable {
// 票的数量
private int ticket = 100;
private Object obj = new Object();
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// synchronized (obj){ // 多个线程必须使用同一把锁
try {
lock.lock();
if (ticket <= 0) {
//卖完了
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() + "在卖票,还剩下" + ticket + "张票");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
// }
}
}
}
// 测试类
public class Demo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket);
Thread t2 = new Thread(ticket);
Thread t3 = new Thread(ticket);
t1.setName("窗口一");
t2.setName("窗口二");
t3.setName("窗口三");
t1.start();
t2.start();
t3.start();
}
}
死锁
概述:线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
什么情况下会产生死锁:
- 资源有限
- 同步嵌套
示例代码:
public class Demo {
public static void main(String[] args) {
Object objA = new Object();
Object objB = new Object();
new Thread(()->{
while(true){
synchronized (objA){
// 线程一
synchronized (objB){
System.out.println("小康同学正在走路");
}
}
}
}).start();
new Thread(()->{
while(true){
synchronized (objB){
// 线程二
synchronized (objA){
System.out.println("小薇同学正在走路");
}
}
}
}).start();
}
}
乐观锁和悲观锁
定义:
-
乐观锁:一段执行逻辑加上乐观锁,不同线程同时执行时,线程可以同时进入执行阶段,在最后更新数据的时候要检查这些数据是否被其他线程修改了,没有修改则进行更新,否则放弃本次操作。
-
悲观锁:一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放。Java 中的 synchronized 和 Reentrantlock 等独占锁就是悲观锁思想的实现。
使用场景:
-
乐观锁适用于书写比较少的情况下,即冲突很少发生的时候,这样可以省去锁的开销,加大在整个系统的吞吐量。
-
如果是多写的情况下,使用乐观锁会产生冲突,导致上层应用会不断地进行重试,这样反倒降低了性能,因此在多写的情况下用悲观锁就比较合适。
生产者消费者模式
概述
生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。
所谓生产者消费者问题,实际上主要是包含了两类线程:
- 一类是生产者线程用于生产数据。
- 一类是消费者线程用于消费数据。
为了解耦生产者和消费者的关系,通常会采用共享的数据区域:
- 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为。
- 消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为。
Object 类的等待和唤醒方法
方法名 | 说明 |
---|---|
void wait() | 导致当前线程等待,直到另一个线程调用该对象的 notify() 方法或 notifyAll() 方法 |
void notify() | 唤醒正在等待对象监视器的单个线程 |
void notifyAll() | 唤醒正在等待对象监视器的所有线程 |
wait 方法注意:
- wait 方法必须在进入相应对象的 synchronized 块中才能调用。
- 执行 wait 方法之后,自动失去对象的 monitor,也就是说别的线程可以进入这个对象的 synchronized 代码块。
- 被唤醒的线程,就相当于执行过了 wait 方法,开始向下执行。
- 如果 wait 不是 synchronized 块中的最后一行,那么第一件事就是"排队"获取之前失去的 monitor 。
- synchronized 是非公平的,也就是说,不是谁先等待谁就能先获得。
notify/notifyAll 方法注意:
- notify/notifyAll 方法必须在进入相应对象的 synchronized 块中才能调用。
如果 notify/notifyAll 在 wait 之前,会怎么样?
- 如果执行 notify 的时候,线程还没有进入 wait 状态,那么 notify 是没有效果的。先 notify,后进入 wait,就是所谓的 lost notification 问题,可能造成线程无法进行。
- 如果让唤醒的线程 sleep 的比 worker 短(sleep 时间 +1 变 -1,或者干脆不 sleep),也就是先进行 notify,那么就可能会造成这个问题。
- 为什么说可能呢?因为 synchronized 还是阻碍了 notify 的执行,但是 notify 有机会在 wait 前执行了。
代码实现
案例需求:
-
桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量。
-
生产者类(Cooker):实现 Runnable 接口,重写 run() 方法,设置线程任务。
-
判断是否有包子,决定当前线程是否执行。
-
如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子。
-
生产包子之后,更新桌子上包子状态,唤醒消费者消费包子。
-
-
消费者类(Foodie):实现 Runnable 接口,重写run() 方法,设置线程任务。
-
判断是否有包子,决定当前线程是否执行。
-
如果没有包子,就进入等待状态,如果有包子,就消费包子。
-
消费包子后,更新桌子上包子状态,唤醒生产者生产包子。
-
-
测试类(Demo):里面有 main 方法,main 方法中的代码步骤如下:
-
创建生产者线程和消费者线程对象。
-
分别开启两个线程。
-
// 桌子类(锁对象)
public class Desk {
// 定义一个标记
// true 就表示桌子上有汉堡包的,此时允许吃货执行
// false 就表示桌子上没有汉堡包的,此时允许厨师执行
public static boolean flag = false;
// 汉堡包的总数量
public static int count = 10;
// 锁对象
public static final Object lock = new Object();
}
// 消费者
public class Foodie extends Thread {
@Override
public void run() {
// 1. 判断桌子上是否有汉堡包。
// 2. 如果没有就等待。
// 3. 如果有就开吃
// 4. 吃完之后. 桌子上的汉堡包就没有了
// 叫醒等待的生产者继续生产
// 汉堡包的总数量减一
//套路:
//1. while(true)死循环
//2. synchronized 锁,锁对象要唯一
//3. 判断,共享数据是否结束. 结束
//4. 判断,共享数据是否结束. 没有结束
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else{
if(Desk.flag){
//有
System.out.println("吃货在吃汉堡包");
Desk.flag = false;
Desk.lock.notifyAll();
Desk.count--;
}else{
//没有就等待
//使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
// 生产者
public class Cooker extends Thread {
// 生产者步骤:
// 1. 判断桌子上是否有汉堡包
// 如果有就等待. 如果没有才生产。
// 2. 把汉堡包放在桌子上。
// 3. 叫醒等待的消费者开吃。
@Override
public void run() {
while(true){
synchronized (Desk.lock){
if(Desk.count == 0){
break;
}else{
if(!Desk.flag){
//生产
System.out.println("厨师正在生产汉堡包");
Desk.flag = true;
Desk.lock.notifyAll();
}else{
try {
Desk.lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
// 测试类
public class Demo {
public static void main(String[] args) {
/*消费者步骤:
1. 判断桌子上是否有汉堡包。
2. 如果没有就等待。
3. 如果有就开吃
4. 吃完之后. 桌子上的汉堡包就没有了
叫醒等待的生产者继续生产
汉堡包的总数量减一*/
/*生产者步骤:
1. 判断桌子上是否有汉堡包
如果有就等待. 如果没有才生产。
2. 把汉堡包放在桌子上。
3. 叫醒等待的消费者开吃。*/
Foodie f = new Foodie();
Cooker c = new Cooker();
f.start();
c.start();
}
}
代码优化
- 将 Desk 类中的变量,采用面向对象的方式封装起来。
- 生产者和消费者类中构造方法接收 Desk 类对象,之后在 run 方法中进行使用。
- 创建生产者和消费者线程对象,构造方法中传入 Desk 类对象。
- 开启两个线程。
// 桌子类
public class Desk {
// 定义一个标记
// true 就表示桌子上有汉堡包的,此时允许吃货执行
// false 就表示桌子上没有汉堡包的,此时允许厨师执行
// public static boolean flag = false;
private boolean flag;
// 汉堡包的总数量
// public static int count = 10;
// 以后我们在使用这种必须有默认值的变量
// private int count = 10;
private int count;
// 锁对象
// public static final Object lock = new Object();
private final Object lock = new Object();
public Desk() {
this(false,10); // 在空参内部调用带参,对成员变量进行赋值,之后就可以直接使用成员变量了
}
public Desk(boolean flag, int count) {
this.flag = flag;
this.count = count;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public Object getLock() {
return lock;
}
@Override
public String toString() {
return "Desk{" +
"flag=" + flag +
", count=" + count +
", lock=" + lock +
'}';
}
}
// 生产者类
public class Cooker extends Thread {
private Desk desk;
public Cooker(Desk desk) {
this.desk = desk;
}
// 生产者步骤:
// 1. 判断桌子上是否有汉堡包
// 如果有就等待. 如果没有才生产。
// 2. 把汉堡包放在桌子上。
// 3. 叫醒等待的消费者开吃。
@Override
public void run() {
while(true){
synchronized (desk.getLock()){
if(desk.getCount() == 0){
break;
}else{
// System.out.println("验证一下是否执行了");
if(!desk.isFlag()){
// 生产
System.out.println("厨师正在生产汉堡包");
desk.setFlag(true);
desk.getLock().notifyAll();
}else{
try {
desk.getLock().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
// 消费者类
public class Foodie extends Thread {
private Desk desk;
public Foodie(Desk desk) {
this.desk = desk;
}
@Override
public void run() {
// 1. 判断桌子上是否有汉堡包。
// 2. 如果没有就等待。
// 3. 如果有就开吃
// 4. 吃完之后. 桌子上的汉堡包就没有了
// 叫醒等待的生产者继续生产
// 汉堡包的总数量减一
// 套路:
// 1. while(true)死循环
// 2. synchronized 锁,锁对象要唯一
// 3. 判断,共享数据是否结束. 结束
// 4. 判断,共享数据是否结束. 没有结束
while(true){
synchronized (desk.getLock()){
if(desk.getCount() == 0){
break;
}else{
// System.out.println("验证一下是否执行了");
if(desk.isFlag()){
// 有
System.out.println("吃货在吃汉堡包");
desk.setFlag(false);
desk.getLock().notifyAll();
desk.setCount(desk.getCount() - 1);
}else{
// 没有就等待
// 使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
try {
desk.getLock().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
public class Demo {
public static void main(String[] args) {
/*消费者步骤:
1. 判断桌子上是否有汉堡包。
2. 如果没有就等待。
3. 如果有就开吃
4. 吃完之后. 桌子上的汉堡包就没有了
叫醒等待的生产者继续生产
汉堡包的总数量减一*/
/*生产者步骤:
1. 判断桌子上是否有汉堡包
如果有就等待. 如果没有才生产。
2. 把汉堡包放在桌子上。
3. 叫醒等待的消费者开吃。*/
Desk desk = new Desk();
Foodie f = new Foodie(desk);
Cooker c = new Cooker(desk);
f.start();
c.start();
}
}
阻塞队列
阻塞队列的继承结构:
常见 BlockingQueue:
-
ArrayBlockingQueue:底层是数组,有界。
-
LinkedBlockingQueue:底层是链表,无界。但不是真正的无界,为 int 的最大值。
BlockingQueue 的核心方法:
-
put(anObject): 将参数放入队列,如果放不进去会阻塞。
-
take(): 取出第一个数据,取不到会阻塞。
代码示例:
public class Demo {
public static void main(String[] args) throws Exception {
// 创建阻塞队列的对象,容量为 1
ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
// 存储元素
arrayBlockingQueue.put("汉堡包");
// 取元素
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take()); // 取不到会阻塞
System.out.println("程序结束了");
}
}
阻塞队列实现等待唤醒机制:
-
生产者类(Cooker):实现 Runnable 接口,重写 run() 方法,设置线程任务。
-
构造方法中接收一个阻塞队列对象。
-
在run方法中循环向阻塞队列中添加包子。
-
打印添加结果。
-
-
消费者类(Foodie):实现 Runnable 接口,重写 run() 方法,设置线程任务。
-
构造方法中接收一个阻塞队列对象。
-
在run方法中循环获取阻塞队列中的包子。
-
打印获取结果。
-
-
测试类(Demo):里面有 main 方法,main 方法中的代码步骤如下:
-
创建阻塞队列对象。
-
创建生产者线程和消费者线程对象,构造方法中传入阻塞队列对象。
-
分别开启两个线程。
-
public class Cooker extends Thread {
private ArrayBlockingQueue<String> bd;
public Cooker(ArrayBlockingQueue<String> bd) {
this.bd = bd;
}
// 生产者步骤:
// 1,判断桌子上是否有汉堡包:如果有就等待,如果没有才生产
// 2,把汉堡包放在桌子上。
// 3,叫醒等待的消费者开吃。
@Override
public void run() {
while (true) {
try {
bd.put("汉堡包");
System.out.println("厨师放入一个汉堡包");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Foodie extends Thread {
private ArrayBlockingQueue<String> bd;
public Foodie(ArrayBlockingQueue<String> bd) {
this.bd = bd;
}
@Override
public void run() {
// 1. 判断桌子上是否有汉堡包。
// 2. 如果没有就等待。
// 3. 如果有就开吃,汉堡包的总数量减一
// 4. 吃完之后,桌子上的汉堡包就没有了
// 5. 叫醒等待的生产者继续生产
while (true) {
try {
String take = bd.take();
System.out.println("吃货将" + take + "拿出来吃了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo {
public static void main(String[] args) {
ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);
Foodie f = new Foodie(bd);
Cooker c = new Cooker(bd);
f.start();
c.start();
}
}
ThreadLocal:线程专属变量
ThreadLocal 原理:
// ThreadLocal一般都是 static 的
// 使用 ThreadLocal 后,这些数据就不会在应用程序之间传递,而是为每个线程保留自己的一份数据
private static final ThreadLocal<List<Phase>> PHASES = new ThreadLocal<>();
private static final ThreadLocal<Long> PHASE_START_TIME = new ThreadLocal<>();
ThreadLocalMap 里处理 hash 冲突的机制不是像 HashMap 一样使用 List,它采用的是另一种经典的处理方式,沿着冲突的索引向后查找空闲的位置。
volatile 关键字
volatile 关键字强制每次volatile 每次访问都要直达内存,不能使用缓存,主要针对指令重排有所影响。
- 保证可见性
- 保证有序性,即禁止指令重排
- 不保证原子性
指令重排
指令重排:程序真的是按照“顺序”执行的吗?并不是。
-
单个线程中的两条语句,未必是按顺序执行,这就是单线程的重排序,但必须保证最终一致性。
-
为什么会乱序?主要是为了提高效率(在等待费时的指令执行的时候,优先执行后面的指令)。
代码示例:
public class DataHolder {
int a, b, c, d, f, g;
// 有 volatile 修饰就会影响之前的指令重排
volatile long e;
public void operateData() {
// 按照这个顺序执行,g 的值是肯定小于等于 e 的。但是实际执行在执行的时候,可能会为了优化的目的重排
a += 1;
b += 1;
c += 1;
d += 1;
e += 1;
f += 1;
g += 1;
}
int counter;
public void check() {
// TODO 看似不可能的条件,实际可能被触发到
if (g > e) {
System.out.println("got it " + (counter++));
}
}
}
JMM
JMM(Java Memory Model)翻译为 Java 内存模型,我们可以简单的认为是一套 happens-before 标准,规定了内存同步和缓存失效等节点,限制了指令重排。
JMM 是 Java 的内涵之一。Java 字节码(Java Byte Code)使得 Java 在指令层面有了统一的标准。JMM 更让 Java 在执行优化层面也有了统一的标准。让各大厂商可以根据操作系统和硬件,在执行优化上放飞自我。
代码示例:
package com.geekbang.learnvolatile;
public class AccessMemoryVolatile {
public volatile long counterV = 0;
public long counter = 0;
public static void main(String[] args) {
int loopCount = Integer.MAX_VALUE / 30;
// 只是为了演示 volatile 每次访问都要直达内存,不能使用缓存,所以耗费的时间略多
AccessMemoryVolatile accessMemoryVolatile = new AccessMemoryVolatile();
Thread volatileAdder = new Thread(() -> {
long start = System.currentTimeMillis();
for (int i = 0; i < loopCount; i++) {
accessMemoryVolatile.counterV++;
}
System.out.println("volatile adder takes " + (System.currentTimeMillis() - start));
});
volatileAdder.start();
Thread justAdder = new Thread(() -> {
long start = System.currentTimeMillis();
for (int i = 0; i < loopCount; i++) {
accessMemoryVolatile.counter++;
}
System.out.println("simple adder takes " + (System.currentTimeMillis() - start));
});
justAdder.start();
}
}
线程池
基本原理
概述:
提到池,大家应该能想到的就是水池。水池就是一个容器,在该容器中存储了很多的水。那么什么是线程池呢?线程池也是可以看做成一个池子,在该池子中存储很多个线程。
线程池存在的意义:
-
系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互。当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理本身对系统资源的消耗,这样就有点"舍本逐末"了。
-
针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中成为空闲状态,等待下一次任务的执行。
线程池工作原理:
为了形象描述线程池执行,加深理解,打个比喻:
- 核心线程比作公司正式员工;
- 非核心线程比作外包员工;
- 阻塞队列比作需求池;
- 提交任务比作提需求。
- 当产品提个需求,正式员工(核心线程)先接需求(执行任务)。
- 如果正式员工都有需求在做,即核心线程数已满,产品就把需求先放需求池(阻塞队列)。
- 如果需求池(阻塞队列)也满了,但是这时候产品继续提需求,怎么办呢?那就请外包(非核心线程)来做。
- 如果所有员工(最大线程数也满了)都有需求在做了,那就执行饱和策略。
- 如果外包员工把需求做完了,他经过一段(keepAliveTime)空闲时间,就离开公司了。
线程池的饱和策略事件,主要有四种类型:
- AbortPolicy(抛出一个异常,默认的)
- DiscardPolicy(新提交的任务直接被抛弃)
- DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
- CallerRunsPolicy(交给线程池调用所在的线程进行处理,即将某些任务回退到调用者)
Executors 实现线程池
JDK 对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用 JDK 中自带的线程池。
我们可以使用 Executors 中所提供的静态方法来创建线程池。
static ExecutorService newCachedThreadPool()
:创建一个默认的线程池。static ExecutorService newFixedThreadPool(int nThreads)
:创建一个指定最多线程数量的线程池。
示例代码:默认线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) throws InterruptedException {
// 创建一个默认的线程池对象,池子中默认是空的,默认最多可以容纳int类型的最大值
ExecutorService executorService = Executors.newCachedThreadPool();
// Executors --- 可以帮助我们创建线程池对象
// ExecutorService --- 可以帮助我们控制线程池
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
//Thread.sleep(2000);
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
executorService.shutdown();
}
}
示例代码:创建指定上限的线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class Test {
public static void main(String[] args) {
// 参数不是初始值而是最大值
ExecutorService executorService = Executors.newFixedThreadPool(10);
ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
System.out.println(pool.getPoolSize()); // 0
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
executorService.submit(()->{
System.out.println(Thread.currentThread().getName() + "在执行了");
});
System.out.println(pool.getPoolSize()); // 2
// executorService.shutdown();
}
}
ThreadPoolExecutor 实现线程池
非默认的任务拒绝策略
RejectedExecutionHandler 是 JDK 提供的一个任务拒绝策略接口,它下面存在 4 个子类:
ThreadPoolExecutor.AbortPolicy
:丢弃任务并抛出 RejectedExecutionException 异常(是默认的策略)。ThreadPoolExecutor.DiscardPolicy
:丢弃任务,但是不抛出异常 这是不推荐的做法。ThreadPoolExecutor.DiscardOldestPolicy
:抛弃队列中等待最久的任务 然后把当前任务加入队列中。ThreadPoolExecutor.CallerRunsPolicy
:调用任务的 run() 方法绕过线程池直接执行。
注:明确线程池最多可执行的任务数 = 队列容量 + 最大线程数
ThreadPoolExecutor.AbortPolicy
public static void main(String[] args) {
/**
* 核心线程数量为1,最大线程池数量为3, 任务容器的容量为1 ,空闲线程的最大存在时间为20s
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 3, 20, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()) ;
// 提交5个任务,而该线程池最多可以处理4个任务,当我们使用AbortPolicy这个任务处理策略的时候,就会抛出异常
for(int x = 0; x < 5; x++) {
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
});
}
}
控制台输出结果:
pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务
pool-1-thread-3---->> 执行了任务
控制台报错,仅仅执行了 4 个任务,有 1 个任务被丢弃了。
### ThreadPoolExecutor.DiscardPolicy
~~~java
public static void main(String[] args) {
/**
* 核心线程数量为1,最大线程池数量为3,任务容器的容量为1,空闲线程的最大存在时间为 20s
*/
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 3, 20, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy());
// 提交5个任务,而该线程池最多可以处理4个任务,当我们使用DiscardPolicy这个任务处理策略的时候,控制台不会报错
for(int x = 0; x < 5; x++) {
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
});
}
}
控制台输出结果:
pool-1-thread-1---->> 执行了任务
pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务
控制台没有报错,仅仅执行了 4 个任务,有 1 个任务被丢弃了。
ThreadPoolExecutor.DiscardOldestPolicy
public static void main(String[] args) {
/**
* 核心线程数量为1,最大线程池数量为3,任务容器的容量为1,空闲线程的最大存在时间为20s
*/
ThreadPoolExecutor threadPoolExecutor;
threadPoolExecutor = new ThreadPoolExecutor(1, 3, 20, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardOldestPolicy());
// 提交5个任务
for(int x = 0; x < 5; x++) {
// 定义一个变量,来指定当前执行的任务,这个变量需要被final修饰
final int y = x ;
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务" + y);
});
}
}
控制台输出结果:
pool-1-thread-2---->> 执行了任务2
pool-1-thread-1---->> 执行了任务0
pool-1-thread-3---->> 执行了任务3
pool-1-thread-1---->> 执行了任务4
由于任务 1 在线程池中等待时间最长,因此任务 1 被丢弃。
ThreadPoolExecutor.CallerRunsPolicy
public static void main(String[] args) {
/**
* 核心线程数量为1,最大线程池数量为3,任务容器的容量为1,空闲线程的最大存在时间为20s
*/
ThreadPoolExecutor threadPoolExecutor;
threadPoolExecutor = new ThreadPoolExecutor(1, 3, 20, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
// 提交5个任务
for(int x = 0; x < 5; x++) {
threadPoolExecutor.submit(() -> {
System.out.println(Thread.currentThread().getName() + "---->> 执行了任务");
});
}
}
控制台输出结果:
pool-1-thread-1---->> 执行了任务
pool-1-thread-3---->> 执行了任务
pool-1-thread-2---->> 执行了任务
pool-1-thread-1---->> 执行了任务
main---->> 执行了任务
通过控制台的输出,我们可以看到次策略没有通过线程池中的线程执行任务,而是直接调用任务的 run() 方法绕过线程池直接执行。
原子性
所谓的原子性,是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。
案例
问题
问题现象:当 A 线程修改了共享数据时,B 线程没有及时获取到最新的值,如果还在使用原先的值,就会出现问题。原因如下:
- 堆内存是唯一的,而每一个线程都有自己的线程栈。
- 每一个线程在使用堆里面变量的时候,都会先拷贝一份到变量的副本中。
- 在线程中,每一次使用是从变量的副本中获取的。
Volatile 关键字:强制线程每次在使用的时候,都看一下共享区域最新的值。
public class Test {
public static void main(String[] args) {
MyAtomThread atom = new MyAtomThread();
for (int i = 0; i < 100; i++) {
new Thread(atom).start();
}
}
}
class MyAtomThread implements Runnable {
private volatile int count = 0; // 送冰淇淋的数量
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 1. 从共享数据中读取数据到本线程栈中
// 2. 修改本线程栈中变量副本的值
// 3. 把本线程栈中变量副本的值赋给共享数据
count++;
System.out.println("已经送了" + count + "个冰淇淋");
}
}
}
执行结果:
……
已经送了5个冰淇淋
已经送了6个冰淇淋
已经送了9个冰淇淋
已经送了8个冰淇淋
已经送了7个冰淇淋
已经送了11个冰淇淋
已经送了13个冰淇淋
已经送了12个冰淇淋
……
问题:volatile 关键字不能保证原子性。因为 count++ 不是一个原子性操作, 他在执行的过程中,有可能被其他线程打断。
优化
我们可以给 count++ 操作添加锁,那么 count++ 操作就是临界区中的代码,临界区中的代码一次只能被一个线程去执行,所以 count++ 就变成了原子操作。
public class Test {
public static void main(String[] args) {
MyAtomThread atom = new MyAtomThread();
for (int i = 0; i < 100; i++) {
new Thread(atom).start();
}
}
}
class MyAtomThread implements Runnable {
private volatile int count = 0; // 送冰淇淋的数量
private Object lock = new Object();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 1. 从共享数据中读取数据到本线程栈中
// 2. 修改本线程栈中变量副本的值
// 3. 会把本线程栈中变量副本的值赋值给共享数据
synchronized(lock){
count++;
System.out.println(Thread.currentThread().getName()+"已经送了" + count + "个冰淇淋");
}
}
}
}
执行结果:
Thread-0已经送了1个冰淇淋
Thread-3已经送了2个冰淇淋
Thread-4已经送了3个冰淇淋
Thread-4已经送了4个冰淇淋
Thread-4已经送了5个冰淇淋
Thread-4已经送了6个冰淇淋
Thread-4已经送了7个冰淇淋
……
atomic 实现原子性
概述:JAVA 从 JDK1.5 开始提供了 java.util.concurrent.atomic 包(简称 Atomic 包)。这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。
因为变量的类型有很多种,所以在 Atomic 包里一共提供了 13 个类,属于 4 种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。
下面只讲解使用原子的方式更新基本类型,使用原子的方式更新基本类型 Atomic 包提供了以下 3 个类:
- AtomicBoolean:原子更新布尔类型
- AtomicInteger:原子更新整型
- AtomicLong:原子更新长整型
以上 3 个类提供的方法几乎一模一样,所以本节仅以 AtomicInteger 为例进行讲解,AtomicInteger 的常用方法如下:
- public AtomicInteger():初始化一个默认值为 0 的原子型 Integer。
- public AtomicInteger(int initialValue):初始化一个指定值的原子型 Integer。
- int get():获取值。
- int getAndIncrement():以原子方式将当前值加 1。注意,这里返回的是自增前的值。
- int incrementAndGet():以原子方式将当前值加 1。注意,这里返回的是自增后的值。
- int addAndGet(int data):以原子方式将输入的数值与实例中的值(AtomicInteger 里的 value)相加,并返回结果。
- int getAndSet(int value):以原子方式设置为 newValue 的值,并返回旧值。
代码示例:
import java.util.concurrent.atomic.AtomicInteger;
class MyAtomIntergerDemo {
// public AtomicInteger():初始化一个默认值为0的原子型Integer
// public AtomicInteger(int initialValue):初始化一个指定值的原子型Integer
// int get(): 获取当前值
// int getAndIncrement(): 以原子方式将当前值加1。注意,这里返回的是自增前的值
// int incrementAndGet(): 以原子方式将当前值加1。注意,这里返回的是自增后的值
// int addAndGet(int data): 以原子方式将参数与对象中的值相加,并返回结果
// int getAndSet(int value): 以原子方式设置为value的值,并返回旧值
public static void main(String[] args) {
AtomicInteger ac1 = new AtomicInteger(10);
System.out.println(ac1.get()); // 10
AtomicInteger ac2 = new AtomicInteger(10);
int andIncrement = ac2.getAndIncrement();
System.out.println(andIncrement); // 10
System.out.println(ac2.get()); // 11
AtomicInteger ac3 = new AtomicInteger(10);
int i1 = ac3.incrementAndGet();
System.out.println(i1); // 11
System.out.println(ac3.get()); // 11
AtomicInteger ac4 = new AtomicInteger(10);
int i2 = ac4.addAndGet(20);
System.out.println(i2); // 30
System.out.println(ac4.get()); // 30
AtomicInteger ac5 = new AtomicInteger(100);
int andSet = ac5.getAndSet(20);
System.out.println(andSet); // 100
System.out.println(ac5.get()); // 20
}
}
并发工具类
ConcurrentHashMap
ConcurrentHashMap 出现的原因 :
- 在集合类中 HashMap 是比较常用的集合对象,但是 HashMap 是线程不安全的(多线程环境下可能会存在问题)。
- 为了保证数据的安全性我们可以使用 Hashtable,但是 Hashtable 的效率低下。
基于以上两个原因我们可以使用 JDK1.5 以后所提供的 ConcurrentHashMap。
体系结构 :
- HashMap 是线程不安全的,多线程环境下会有数据安全问题。
- Hashtable 是线程安全的,但是会将整张表锁起来,效率低下。
- ConcurrentHashMap 也是线程安全的,效率较高(在 JDK7 和 JDK8 中,底层原理不一样)。
代码示例:
import java.util.concurrent.ConcurrentHashMap;
public class Test {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, String> hm = new ConcurrentHashMap<>(100);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 25; i++) {
hm.put(i + "", i + "");
}
});
Thread t2 = new Thread(() -> {
for (int i = 25; i < 51; i++) {
hm.put(i + "", i + "");
}
});
t1.start();
t2.start();
System.out.println("----------------------------");
// 休眠,把数据全部添加完毕
Thread.sleep(1000);
for (int i = 0; i < 51; i++) {
System.out.println(hm.get(i + "")); // 0 1 2 3 .... 50
}
}
}
CountDownLatch
使用场景:让某一条线程等待其他线程执行完毕之后再执行。
方法 | 解释 |
---|---|
public CountDownLatch(int count) | 参数传递线程数,表示等待线程数量 |
public void await() | 让线程等待 |
public void countDown() | 当前线程执行完毕 |
代码示例:
import java.util.concurrent.CountDownLatch;
// 主线程
public class MyCountDownLatchDemo {
public static void main(String[] args) {
// 1. 创建CountDownLatch的对象,需要传递给4个线程
// 在底层就定义了一个计数器,此时计数器的值就是3
CountDownLatch countDownLatch = new CountDownLatch(3);
// 2. 创建四个线程对象并开启他们
MotherThread motherThread = new MotherThread(countDownLatch);
motherThread.start();
ChileThread1 t1 = new ChileThread1(countDownLatch);
t1.setName("小明");
ChileThread2 t2 = new ChileThread2(countDownLatch);
t2.setName("小红");
ChileThread3 t3 = new ChileThread3(countDownLatch);
t3.setName("小刚");
t1.start();
t2.start();
t3.start();
}
}
// 子线程:孩子吃饺子
class ChileThread1 extends Thread {
private CountDownLatch countDownLatch;
public ChileThread1(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// 1. 吃饺子
for (int i = 1; i <= 10; i++) {
System.out.println(getName() + "在吃第" + i + "个饺子");
}
// 2. 吃完说一声
// 每一次 countDown 方法的时候,就让计数器 -1
countDownLatch.countDown();
}
}
// 子线程:孩子吃饺子
class ChileThread2 extends Thread {
private CountDownLatch countDownLatch;
public ChileThread2(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// 1. 吃饺子
for (int i = 1; i <= 15; i++) {
System.out.println(getName() + "在吃第" + i + "个饺子");
}
// 2. 吃完说一声
// 每一次 countDown 方法的时候,就让计数器 -1
countDownLatch.countDown();
}
}
// 子线程:孩子吃饺子
class ChileThread3 extends Thread {
private CountDownLatch countDownLatch;
public ChileThread3(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// 1. 吃饺子
for (int i = 1; i <= 20; i++) {
System.out.println(getName() + "在吃第" + i + "个饺子");
}
// 2. 吃完说一声
// 每一次 countDown 方法的时候,就让计数器 -1
countDownLatch.countDown();
}
}
// 子线程:妈妈收拾碗筷
class MotherThread extends Thread {
private CountDownLatch countDownLatch;
public MotherThread(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
// 1. 等待
try {
// 当计数器变成0的时候,会自动唤醒这里等待的线程。
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 2. 收拾碗筷
System.out.println("妈妈在收拾碗筷");
}
}
总结 :
- CountDownLatch(int count):参数表示等待线程的数量,并定义了一个计数器。
- await():让线程等待,当计数器为 0 时,会唤醒等待的线程。
- countDown(): 线程执行完毕时调用,会将计数器 -1。
Semaphore
使用场景:可以控制访问特定资源的线程数量。
实现步骤 :
- 需要有人管理这个通道。
- 当有车进来了,发通行许可证。
- 当车出去了,收回通行许可证。
- 如果通行许可证发完了,那么其他车辆只能等着。
import java.util.concurrent.Semaphore;
public class Test {
public static void main(String[] args) {
MyRunnable mr = new MyRunnable();
for (int i = 0; i < 100; i++) {
new Thread(mr).start();
}
}
}
class MyRunnable implements Runnable {
// 1.获得管理员对象,
private Semaphore semaphore = new Semaphore(2);
@Override
public void run() {
// 2.获得通行证
try {
semaphore.acquire();
// 3.开始行驶
System.out.println("获得了通行证开始行驶");
Thread.sleep(2000);
System.out.println("归还通行证");
// 4.归还通行证
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}