进程 与 线程
进程
- 进程
资源分配
的最小单位,CPU从磁盘中读取一段程序到内存中,该执行程序的实例就叫做进程; - 一个程序如果被CPU多次读取到内存中,则变成多个独立的进程。
线程
- 线程是
程序执行
的最小单位,在一个进程中可以有多个不同的线程; - 采用多线程可以提高程序运行效率。
多线程优缺点
优点
- 提高系统的
吞吐量
,多线程编程可以使一个进程有多个并发(concurrent)
; - 提高
响应性
,服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间; 充分利用多核处理器资源
,通过多线程可以充分的利用CPU资源。
缺点
-
线程安全
问题,多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能产生数据一致性问题,如读取脏数据(过期的数据),如丢失数据更新。 -
线程活性(thread liveness)
问题,由程序自身的缺陷或者由资源稀缺性导致的线程一直处于非RUNNABLE状态
;
常见的线程活性问题:
死锁 (DeadLock)
:鹬蚌相争;
锁死 (Lockout)
:睡美人故事中王子挂啦;
活锁 (LiveLock)
:类似小猫一直咬自己的尾巴,但是咬不到;
饥饿 (Starvation)
:类似于健壮的雏鸟总是从母鸟嘴中抢食物。
-
上下文切换(Context Switch)
处理器从执行一个线程切换到执行另外一个线程; -
可靠性
可能会由一个线程导致JVM意外终止,其他线程也无法执行。
串行、并行的区别
CPU分时间片交替执行,宏观并行,微观串行,由OS负责调度。
如今的CPU已经发展到了多核CPU,真正存在并行。
CPU调度算法
多线程不一定提高效率,需要了解CPU调度算法:
调度算法 | 占用CPU方式 | 吞吐量 | 响应时间 | 开销 | 对进程的影响 | 饥饿问题 |
---|---|---|---|---|---|---|
FCFS | 非抢占 | 不强调 | 可能很慢,特别是当前进程的执行时间差别很大时 | 最小 | 对短进程不利;对I/O型的进程不利 | 无 |
Round Robin | 抢占(时间片用完时) | 若时间片小,吞吐量会很低 | 为短进程提供好的响应时间 | 最小 | 公平对待 | 无 |
SJF | 非抢占 | 高 | 为短进程提供好的响应时间 | 可能较大 | 对长进程不利 | 可能 |
SRTN | 抢占(到达时) | 高 | 为短进程提供好的响应时间 | 可能较大 | 对长进程不利 | 可能 |
HRRN | 非抢占 | 高 | 为短进程提供好的响应时间 | 可能较大 | 哼好的平衡 | 无 |
Feedback | 抢占(时间片用完时) | 不强调 | 不强调 | 可能较大 | 对I/O型的进程有利 | 可能 |
如果在生产环境中,开启很多线程,但是我们的服务器核数很低,我们这么多线程会在CPU上做上下文切换,反而会降低效率。
使用线程池来限制线程CPU数相同会比较好。
一、多线程基础
继承 Thread 类创建线程
@Slf4j
public class ThreadTest extends Thread {
@Override
public void run() {
log.info("我是子线程");
}
/*
* 创建对象进入初始状态,调用start()进入就绪状态。
* 直接调用run()方法,相当于在main中执行run。并不是新线程
*/
public static void main(String[] args) {
new ThreadTest().start();
}
}
使用匿名内部类形式创建线程
@Slf4j
public class ThreadTest {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
log.info("我是子线程");
}
}).start();
}
}
使用 Lambda 表达式创建
@Slf4j
public class ThreadTest {
public static void main(String[] args) {
new Thread(() -> log.info("我是子线程")).start();
}
}
实现 Runnable 接口创建线程
@Slf4j
public class ThreadTest implements Runnable {
@Override
public void run() {
log.info("我是子线程");
}
public static void main(String[] args) {
new Thread(new ThreadTest()).start();
}
}
使用 Callable 和 Future 创建线程
Callable 和 Future 线程可以获取到返回结果,底层基于LockSupport
。
Runnable 的缺点:
- run 没有返回值
- 不能抛异常
Callable 接口允许线程有返回值,也允许线程抛出异常
Future 接口用来接受返回值
@Slf4j
public class ThreadTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> integerFutureTask = new FutureTask<>(() -> {
log.info("返回1");
return 1;
});
new Thread(integerFutureTask).start();
// 通过api获取返回结果,主线程需要等待子线程返回结果
Integer result = integerFutureTask.get();
// main,1
log.info(result);
}
}
使用线程池创建
@Slf4j
public class ThreadTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> log.info("我是子线程1"));
// submit一个线程到线程池
executorService.submit(() -> {
log.info("返回1");
return 1;
});
}
}
Spring中的 @Async 创建
第一步:在入口类中开启异步注解
@SpringBootApplication
@EnableAsync
第二步:在当前方法上加上@Async
@Slf4j
@Component
public class ThreadTest {
@Async
public void asyncLog(){
try {
Thread.sleep(3000);
log.info("<2>");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
第三步:验证测试
@Slf4j
@RestController
public class Service {
@Autowired
private ThreadTest threadTest;
@RequestMapping("test")
public String Test(){
log.info("<1>");
threadTest.asyncLog();
log.info("<3>");
return "test";
}
}
访问localhost:8080/test
查看日志为:
Thread中的常用的方法
1.Thread.currentThread() 方法
可以获得当前线程。
Java中的任何一段代码都是运行在某个线程当中的,运行当前代码的线程就是当前线程。
2.setName()/getName
// 设置线程名称(提高程序的可读性)
thread.setName(线程名称);
// 返回线程名称
thread.getName()
3.isAlive()
// 判断当前线程是否处于活动状态
thread.isAlive();
4.sleep()
// 让当前线程休眠指定的毫秒数
Thread.sleep(millis);
5.getId()
Java中的线程都有一个唯一编号
6.yield()
放弃当前的CPU资源
// 可以让线程由运行转为就绪状态
Thread.yield();
7.setPriority()
设置线程的优先级
/**
* 设置线程的优先级(取值为1-10),如果超过范围会抛出异常 IllegalArugumentExption;
*
* 优先级越高的线程,获得cpu资源的概率越大。
* 优先级本质上只是给线程调度器一个提示信息,以便于线程调度器决定先调度哪些线程。不能保证优先级高的线程先运行。
* java优先级设置不当,可能导致某些线程永远无法得到运行,产生了线程饥饿。
* 线程的优先级并不是设置的越高越好,在开发时不必设置线程的优先级。
*/
thread.setPriority(num);
8.interrupt()
中断线程
Thread中的方法的interrupt()
方法只能中断阻塞中
的线程,而不能中断正在运行过程中的线程。
在运行中的线程使用
- 注意调用此方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程;
- 例如在线程1中调用线程b的
interrupt()
,在b线程中监听b线程的中断标志,来处理结束。
public static void main(String[] args) {
Thread thread1 = new Thread() {
@Override
public void run() {
for (int i = 1; i < 1000; i++) {
// 判断中断标志
if (this.isInterrupted()) {
//如果为true,结束线程
//break;
return;
}
log.info("子线程:{}", i);
}
}
};
// 开启子线程
thread1.start();
// 主线程
for (int i = 1; i < 100; i++) {
log.info("主线程:{}", i);
}
// 打印完main线程中100个后,中断子线程,仅仅是个标记,必须在线程中处理
thread1.interrupt();
}
9.setDaemon()
守护线程
- Java中的线程分为
用户线程
与守护线程
; 守护线程
是为其他线程提供服务的线程,如垃圾回收GC
就是一个典型的守护线程;守护线程
不能单独运行,当JVM
中没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM
会自动退出。
// 线程启动前
thread.setDaemon(true);
thread.start();
线程的状态
线程的状态:getState()
二、线程安全原理篇
当多个线程对同一个对象的实例变量,做写(修改)的操作时,可能会受到其他线程的干扰,发生线程安全的问题。
原子性 Atomic
不可分割
,访问(读/写)某个共享变量的时候,从其他线程来看,该操作要么已经执行完毕,要么尚未发生;- 其他线程看不到当前操作的中间结果;
- 访问同一组
共享变量
的原子操作是不能够交错的,如现实生活中从ATM取款。
Java中有两种方式实现原子性
锁
:锁具有排他性,可以保证共享变量某一时刻只能被一个线程访问;CAS指令
:直接在硬件层次上实现,看做是一个硬件锁。
可见性 Visbility
- 在多线程环境中,一个线程对某个
共享变量
更新之后,后续其他的线程可能无法立即读到这个更新的结果; - 如果一个线程对共享变量更新之后,后续访问该变量的其他线程可以读到更新的结果,称这个线程对共享变量的更新对其他线程可见;
- 否则称这个线程对共享变量的更新对其他线程不可见。
**注意:**多线程程序因为可见性问题可能会导致其他线程读取到旧数据(脏数据)。
有序性 Ordering
- 在什么情况下,一个处理器上运行的一个线程所执行的
内存访问操作
在另外一个处理器运行的其他线程来看是乱序
的(内存访问操作的顺序看起来发生了变化)。
重排序 与 happens-before
重排序
- 一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码执行的顺序可能不一致,这种现象成为重排序(不是必然出现);
- 重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能,但是可能对多线程程序的正确性产生影响,即可能导致线程安全问题。
指令重排序 —— 确实排序了【JVM】
指令重排序Instruction Reorder
是指,计算机在执行程序时,为了提高性能,编译器
和处理器
常常会对指令做重排,使得源码顺序
与程序顺序
不一致,或程序顺序
与执行顺序
不一致,虽然由此带来了乱序的问题,但是这点牺牲是值得的。
注意:javac
编译器一般不会执行指令重排序,而JIT
编译器可能执行指令重排序。
指令重排分为以下三种
编译器优化重排
:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;指令并行重排
:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序;内存系统重排
:由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。
存储子系统重排序 —— 没有真正的排序【CPU】
存储子系统
- 高速缓存(Cache):解决CPU中与内存处理速度不匹配;
- 写缓冲器(Store/Wirte buffer) :提高写高速缓存操作的效率。
读写内存操作
- 读内存、
Load
操作:从指定的RAM
地址中加载数据到寄存器
; - 写内存、
Store
操作:把数据存储到指定的地址表示的RAM
存储单元中。
内存重排序有以下四中可能:
Load-Load 重排序
:在一个处理器上先后执行两个读操作L1和L2,其他处理器对这两个内存操作的感知顺序可能是先L2;Store-Store 重排序
:一个处理器上先后执行两个写操作W1和W2,其他处理器对两个内存操作的感知顺序可能是先W2;Load-Store 重排序
:一个处理器上先执行读内存L1再执行写内存W1,其他内存感知顺序可能是W1在前;Store-Load 重排序
:一个处理器上先执行写操作W1再执行读内存L1,其他内存感知顺序可能是L1在前。
注意:
- 即使处理器严格按照程序顺序执行两个内存访问操作,在存储子系统的作用下,其他处理器对这两个操作的感知顺序与程序顺序不一致;
- 内存重排序与具体的处理器微架构有关,不同架构的处理器所允许的内存重排序不同;
- 内存重排序可能会导致线程的安全问题。
貌似串行语义
貌似串行语义指,重排序后给单线程
程序造成一种指令是按照源码顺序执行
的假象。
**注意:**为了保证内存访问的顺序性,可以使用volatile
关键字,synchronized
关键字实现有序性。
JVM与JMM
JVM运行时数据区
对于每一个线程来说,栈都是私有的,而堆是共有的。
- 在栈中的变量(局部变量、方法定义参数、异常处理器参数)不会在线程之间共享,也就不会有内存可见性的问题,也不受内存模型的影响;
- 在堆中的变量是共享的,本文称为共享变量。
所以,内存可见性是针对的共享变量
。
JMM模型
从图中可以看出:
- 所有的共享变量都存在主内存中;
- 每个线程都保存了一份该线程使用到的共享变量的副本;
- 如果线程A与线程B之间要通信的话,必须经历下面2个步骤
- 线程A将本地内存A中更新过的共享变量刷新到主内存中去;
- 线程B到主内存中去读取线程A之前已经更新过的共享变量。
线程A无法直接访问线程B的工作内存,线程间通信必须经过主内存。线程B并不是直接去主内存中读取共享变量的值,而是先在本地内存B中找到这个共享变量,发现这个共享变量已经被更新了,然后本地内存B去主内存中读取这个共享变量的新值,并拷贝到本地内存B中,最后线程B再读取本地内存B中的新值。
注意:
- 根据JMM的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读取;
- JMM通过控制主内存与每个线程的本地内存之间的交互,来提供内存可见性保证。
Java中的volatile
关键字可以保证多线程操作共享变量的可见性以及禁止指令重排序,synchronized
关键字不仅保证可见性,同时也保证了原子性(互斥性)。
在更底层,JMM通过内存屏障
来实现内存的可见性以及禁止重排序。为了程序员的方便理解,提出了happens-before
,它更加的简单易懂,从而避免了程序员为了理解内存可见性而去学习复杂的重排序规则以及这些规则的具体实现方法。
JMM 与 Java内存区域划分 的区别与联系
上面两小节分别提到了JMM和Java运行时内存区域的划分,这两者既有差别又有联系:
区别——两者是不同的概念层次
- JMM是
抽象
的,他是用来描述一组规则
,通过这个规则来控制各个变量的访问方式,围绕原子性、有序性、可见性等展开的; - Java运行时内存的划分是
具体
的,是JVM运行Java程序时,必要的内存划分。
联系——都存在私有数据区域和共享数据区域
- JMM中的主内存属于共享数据区域,他是包含了堆和方法区;
- JMM中的本地内存属于私有数据区域,包含了程序计数器、本地方法栈、虚拟机栈。
实际上,他们表达的是同一种含义,这里不做区分。
出现线程不安全的例子:
@Slf4j
public class ThreadTest implements Runnable {
private int count = 100;
@SneakyThrows
@Override
public void run() {
while (true) {
if (count > 0) {
log.info("当前计数:{}", count--);
Thread.sleep(100);
}
}
}
public static void main(String[] args) {
ThreadTest threadCount = new ThreadTest();
new Thread(threadCount).start();
new Thread(threadCount).start();
}
}
21:27:42.092 [Thread-2] INFO com.example.ThreadTest - 当前计数:99
21:27:42.092 [Thread-1] INFO com.example.ThreadTest - 当前计数:100
21:27:42.201 [Thread-1] INFO com.example.ThreadTest - 当前计数:98
21:27:42.201 [Thread-2] INFO com.example.ThreadTest - 当前计数:98
21:27:42.311 [Thread-2] INFO com.example.ThreadTest - 当前计数:97
21:27:42.311 [Thread-1] INFO com.example.ThreadTest - 当前计数:97
21:27:42.419 [Thread-1] INFO com.example.ThreadTest - 当前计数:96
21:27:42.419 [Thread-2] INFO com.example.ThreadTest - 当前计数:96
21:27:42.530 [Thread-2] INFO com.example.ThreadTest - 当前计数:95
21:27:42.530 [Thread-1] INFO com.example.ThreadTest - 当前计数:94
21:27:42.641 [Thread-2] INFO com.example.ThreadTest - 当前计数:92
21:27:42.641 [Thread-1] INFO com.example.ThreadTest - 当前计数:93
21:27:42.748 [Thread-1] INFO com.example.ThreadTest - 当前计数:91
... ...
如何解决线程安全的问题?
核心思想:上锁
在同一个JVM中,多个线程需要竞争锁的资源,最终只能够有一个线程能够获取到锁,多个线程同时抢一把锁,哪个线程能够获得到锁,谁就可以执行该代码,如果没有获取锁成功,中间需要经历锁的升级过程,一直没有获取到锁则会一直阻塞等待。
@Slf4j
public class ThreadTest implements Runnable {
private int count = 100;
@SneakyThrows
@Override
public void run() {
while (true) {
synchronized (this) {
if (count > 0) {
log.info("当前计数:{}", count--);
Thread.sleep(100);
}
}
}
}
public static void main(String[] args) {
ThreadTest threadCount = new ThreadTest();
new Thread(threadCount).start();
new Thread(threadCount).start();
}
}
三、线程同步
线程同步
多个线程并发访问共享数据产生线程安全问题,通过协调线程之间对共享数据的访问,使得线程之间按照一定的顺序
执行。
保证线程安全的机制:
锁
;volatile
关键字;final
关键字;static
关键字;- 以及相关的 API(如:
Objet.wait();
、Object.notify()
)。
锁
概念
将多个线程之间共享数据的并发
访问转为串行
访问,即一个共享数据一次只能被一个线程访问的这种思路来保证线程安全的。
一个线程只能在持有锁
的时候才能对共享数据进项访问,结束访问后必须释放锁
。
相关概念:
可重入性
(Reentrancy):一个线程持有一个锁的时候,能否再次申请该锁?排他性
:即一个锁只能被一个线程持有,这种锁被称为互斥锁;- 锁的
争用
与调度
:Java平台中内部锁属于非公平锁,显示Lock
既支持公平锁又支持非公平锁; - 锁的
粒度
:一个锁可以保护的共享数据的数量大小称为锁的粒度(粗/细);- 粗粒度,会导致线程在申请锁的时候会进行不必要的等待;
- 细粒度,会增加锁调度的开销;
- 临界区:指的是某一块代码区域,它同一时刻只能由一个线程执行
- 如果
synchronized
关键字在方法上,那临界区就是整个方法内部; - 如果
synchronized
关键字在代码块上,那临界区就指的是代码块内部的区域。
- 如果
分类
- 内部锁:内部所通过
synchronized
关键字实现; - 显示锁:通过
java.concurrent.locks.lock
接口实现类实现的。
作用
通过保障线程的原子性
、可见性
、有序性
,实现对共享数据的安全访问。
- 通过互斥保障
原子性
,一个锁只能被一个线程持有,这就保证临界区的代码一次只能被一个线程执行,使得操作不可分割; - 通过
写线程冲刷处理器的缓存
和读线程刷新处理器缓存
这两个动作实现可见性
(在java中,锁的获得隐含着刷新处理器缓存的动作,锁的释放隐含着冲刷处理器缓存的动作。保证写线程对数据的修改,第一时间推送到处理器的高速缓存中,保证读线程第一时间可见。); - 线程之间在临界区的操作,整体来看来像是完全按照源码顺序执行的,保证了
有序性
。
必要条件
- 线程间必须通过
同一把锁
访问共享数据时; - 这些线程即使仅仅是读
共享数据
,也需要使用锁。
内部锁 synchronized
Java中的每个对象都有一个与之关联的内部排他锁(也被称为监视器Monitor),保证原子性
、可见性
、有序性
。
synchronized的几种使用场景
synchronized
修饰 一个代码块
,被修饰的代码块称为同步语句块
,其作用的范围是大括号{}
括起来的代码,作用的对象是调用这个代码块的对象;synchronized
修饰 一个方法
,被修饰的方法称为同步方法
,其作用的范围是整个方法,作用的对象是调用这个方法的对象;synchronized
修饰 一个静态的方法
,其作用的范围是整个静态方法,作用的对象是这个类的所有对象
;synchronized
修饰 一个类
,其作用的范围是synchronized
后面括号括起来的部分,作用的对象是这个类的所有对象。
注意:
- 同步代码块比同步方法效率更高;
- 脏读是由于对共享数据的修改与读取不同步,需要对读取数据的代码块同步;
- 线程出现异常会自动释放锁。
死锁
定义
指多个线程之间因等待相互持有
的资源被释放,照成线程同时阻塞,被无限期地阻塞,程序不可能正常终止的情形。
死锁产生的四个必要条件
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用;
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放;
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有;
- 循环等待,即存在一个等待队列(如:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源),形成了一个等待环路。
注意:死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
下面用java代码来模拟一下死锁的产生:
@Slf4j
public class DeadLock {
static class SubThread extends Thread {
/**
* 共享资源
*/
private static final Object r1 = new Object();
private static final Object r2 = new Object();
@Override
public void run() {
if ("a".equals(Thread.currentThread().getName())) {
synchronized (r1) {
log.info("get R1.");
synchronized (r2) {
log.info("get R1 & R2.");
}
}
}
if ("b".equals(Thread.currentThread().getName())) {
synchronized (r2) {
log.info("get R2.");
synchronized (r1) {
log.info("get R1 & R2.");
}
}
}
}
}
public static void main(String[] args) {
SubThread t1 = new SubThread();
t1.setName("a");
t1.start();
SubThread t2 = new SubThread();
t2.setName("b");
t2.start();
}
}
Connected to the target VM, address: '127.0.0.1:4799', transport: 'socket'
22:35:03.051 [a] INFO com.example.DeadLock - get R1.
22:35:03.051 [b] INFO com.example.DeadLock - get R2.
解决死锁: 当需要获得多个锁时,所有线程获得锁的顺序保持一致。
volatile
在Java内存模型中介绍了JMM有一个主内存,每个线程有自己私有的工作内存,工作内存中保存了一些变量在主内存的拷贝。
内存可见性,指的是线程之间的可见性,当一个线程修改了共享变量时,另一个线程可以读取到这个修改后的值。
Java中的
volatile关键字
可以保证多线程操作共享变量的可见性以及禁止指令重排序,synchronized关键字
不仅保证可见性,同时也保证了原子性(互斥性)。
volatile作用
volatile
可以保证内存 可见性 & 禁止重排序;- 可以强制线程从公共内存中读取变量的值,而不是从工作内存中读取。
@Slf4j
public class ThreadTest {
static class PrintString {
private volatile boolean continuePrint = true;
public void printStringMethod() {
while (continuePrint) {
log.info("运行中...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setContinuePrint(boolean continuePrint) {
this.continuePrint = continuePrint;
}
}
public static void main(String[] args) {
PrintString printString = new PrintString();
// 打印字符串的方法
new Thread(() -> printString.printStringMethod()).start();
// main线程睡眠1000毫秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("主线程修改打印标志.");
printString.setContinuePrint(false);
// 在main修改玩打印标志后,子线程是否结束打印。
}
}
Connected to the target VM, address: '127.0.0.1:4874', transport: 'socket'
22:43:34.663 [Thread-0] INFO com.example.ThreadTest - 运行中...
22:43:35.173 [Thread-0] INFO com.example.ThreadTest - 运行中...
22:43:35.673 [Thread-0] INFO com.example.ThreadTest - 运行中...
22:43:35.673 [main] INFO com.example.ThreadTest - 主线程修改打印标志.
Disconnected from the target VM, address: '127.0.0.1:4874', transport: 'socket'
volatile 与 synchronized比较
volatile关键字
是线程同步的轻量级实现,所以volatile性能比synchronized更好,volatile只能修饰变量,而synchronized可以修饰方法,代码块;- 多线程访问
volatile
变量不会发生阻塞,而synchronized
可能会阻塞; volatile
能保证数据的可见性,不能保证原子
性,synchronized都可以保证,会数据同步;volatile
解决的是变量在多个线程之间的可见性;synchronized
解决多个线程之间访问公共资源的同步性。
volatile 非原子特性
volatile 关键字增加了实例变量在多个线程之间的可见性,但是不具备原子性。
@Slf4j
public class ThreadTest {
static class AddThread extends Thread {
public volatile static int count = 0;
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
count++;
}
log.info("count: {}", count);
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new AddThread().start();
}
}
}
Connected to the target VM, address: '127.0.0.1:5265', transport: 'socket'
23:17:25.428 [Thread-7] INFO com.example.ThreadTest - count: 7741
23:17:25.428 [Thread-9] INFO com.example.ThreadTest - count: 8918
23:17:25.428 [Thread-5] INFO com.example.ThreadTest - count: 6939
23:17:25.428 [Thread-3] INFO com.example.ThreadTest - count: 4000
23:17:25.428 [Thread-1] INFO com.example.ThreadTest - count: 2000
23:17:25.428 [Thread-8] INFO com.example.ThreadTest - count: 8854
23:17:25.428 [Thread-4] INFO com.example.ThreadTest - count: 5000
23:17:25.428 [Thread-2] INFO com.example.ThreadTest - count: 3000
23:17:25.428 [Thread-6] INFO com.example.ThreadTest - count: 6000
23:17:25.428 [Thread-0] INFO com.example.ThreadTest - count: 1153
Disconnected from the target VM, address: '127.0.0.1:5265', transport: 'socket'
发现有不是整千的,说明某个线程的for循环不是原子操作。
常用原子类进行自增自减操作
我们知道i++
不是原子操作,除了使用Synchornized
进行同步外,也可以使用Atomiclnteger/AtomicLong
原子类进行实现。
java.util.concurrent.atomic
的包里有AtomicBoolean
、AtomicInteger
、AtomicLong
、AtomicLongArray
、 AtomicReference
等原子类的类,主要用于在高并发环境下的高效程序处理,来帮助我们简化同步处理。
在Java语言中,++i
和i++
操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized
关键字。而AtomicInteger
则通过一种线程安全的加减操作接口。
CAS
概念
CAS
(Compare And Swap)比较并交换,其协议/算法是由硬件实现的。
在CAS中,有这样三个值:
- V:要更新的变量(var);
- E:预期值(expected);
- N:新值(new)
比较并交换的过程为:
- 判断
V
是否等于E
,如果等于,将V
的值设置为N
; - 如果不等,说明已经有其它线程更新了
V
,则当前线程放弃更新,什么都不做。
CAS可以将read-modify-write
这类的操作转换为原子操作。
i++
包括三个原子操作:
- 从主内存读取
i
变量的值 - 对
i
的值加1 - 再把加一之后的值保存到主内存
原理
在把数据更新到主内存时,再次读取主内存变量的值,如果现在变量的值与期望的值(操作起始时读取的值)一致就更新。
理想状态:
并发问题可能的状态:
CAS就是把数据更新到主内存的共享变量前,再次读取主内存共享变量的值,如果现在读取的共享变量的值与期望的值一样就更新:
使用CAS实现线程安全的计数器
/**
* 使用CAS实现一个线程安全的计数器
*/
@Slf4j
public class CasTest {
public static void main(String[] args) {
CASCounter counter = new CASCounter();
for (int i = 0; i < 10000; i++) {
new Thread(() -> log.info("累计值:{}", counter.incrementAndGet())).start();
}
}
}
@Slf4j
class CASCounter {
// 使用volatile修饰value的值,使线程可见
private volatile long value;
private boolean compareAndSwap(long expectedValue, long newValue) {
// 如果当前value的值与期望的expectedValue值一样,就把当时的value字段替换为newValue值
synchronized (this) {
if (value == expectedValue) {
value = newValue;
log.debug("替换");
return true;
}
log.debug("已更新");
return false;
}
}
// 定义自增的方法
public long incrementAndGet() {
long oldValue, newValue;
do {
oldValue = value;
newValue = oldValue + 1;
} while (!compareAndSwap(oldValue, newValue));
return newValue;
}
}
CAS 中的 ABA 问题
CAS
实现原子操作背后有一个假设:共享变量的当前值与当前线程提供的期望值相同,就认为这个变量没有被其他线程修改过。
实际上这个假设不一定总成立。
例如:有一个共享变量 count =0,A线程对count的值修改为10,B线程对count修改为20,C线程对count修改为10; 如果当前线程看到count变量的值为10,我们是否认为count变量的值没有被其他线程更新呢??这种结果是否能接受??
共享变量经历了 A -> B -> A 的更新
是否能够接受ABA的问题跟实现算法有关。如果想要规避ABA问题,可以为共享变量引入一个修订号(时间戳),每次修改共享变量时,相应的修订号就会增加1。
[A,0] -> [B,1] -> [A,2]
这也是AtomicStampedReference类就是基于这种思想产生的。
原子变量类
原子变量类基于CAS实现的,当对共享变量进行 read-modify-writer
更新操作时,通过原子变量类可以保障操作的原子性
与可见性
,对变量的read-modify-writer
更新操作是指当前操作不是一个简单的赋值,而是一个变量的新值依赖变量的旧值。
例如 i++
的操作就是 读 -> +1 -> 赋值
;
由于volatile
无法保证原子性,只能保证可见性,原子变量类内部就是借助一个volatile
变量,并且保障了该变量的 read-modify-writer
操作的原子性,有时把原子变量类看做增强的 volatile
变量。
原子变量类
分组 | 原子变量类 |
---|---|
基础数据类型 | AtomicInteger,AtomicLong,AtomicBoolean |
数组型 | AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray |
字段更新器 | AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater |
引用型 | AtomicReference,AtomicStampedReference,AtomicMarkableReference |
使用AtomicLong定义计数器
开发一个程序统计请求的总数,成功数,失败数。模拟多用户多线程访问。
/**
* 模拟服务器的请求总数,处理成功数,处理失败数。
*/
@Slf4j
public class AtomicTest {
public static void main(String[] args) {
// 通过线程模拟请求
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
// 每个线程都是一个请求
Indicator.getInstance().newRequestReceive();
if (new Random().nextInt() % 2 == 0) {
// 偶数模拟成功
Indicator.getInstance().requestSuccess();
} else {
Indicator.getInstance().requestFialure();
}
}).start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印结果
log.info("请求的总数:{}", Indicator.getInstance().getRequestCount());
log.info("请求成功数:{}", Indicator.getInstance().getSuccessCount());
log.info("请求失败数:{}", Indicator.getInstance().getFialureCount());
}
}
/**
* @author 结构化思维
* 使用原子变量类定义一个计数器
* 统计计数器,在整个程序中都能使用,并且所有地方都使用这一计数器,这个计数器可以设计为单例
*/
@Data
class Indicator {
// 构造方法私有化
private Indicator() {
}
// 定义一个私有的本类静态对象
private static final Indicator INSTANCE = new Indicator();
// 提供一个公共静态方法返回该类的唯一实例
public static Indicator getInstance() {
return INSTANCE;
}
/**
* 记录原子变量类保存请求总数,成功数,失败数。
*/
private final AtomicLong requestCount = new AtomicLong(0); //记录请求总数
private final AtomicLong successCount = new AtomicLong(0); //记录请求成功数
private final AtomicLong fialureCount = new AtomicLong(0); //记录请求失败数
/**
* 有新的请求的时候
*/
public void newRequestReceive() {
requestCount.incrementAndGet(); //总数增长
}
/**
* 处理成功的时候
*/
public void requestSuccess() {
successCount.incrementAndGet(); //成功数+1
}
/**
* 处理失败的时候
*/
public void requestFialure() {
fialureCount.incrementAndGet(); //失败数+1
}
}
23:55:11.323 [main] INFO com.example.AtomicTest - 请求的总数:10000
23:55:11.327 [main] INFO com.example.AtomicTest - 请求成功数:5033
23:55:11.327 [main] INFO com.example.AtomicTest - 请求失败数:4967
AtomicIntegerArray
原子更新数组
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicIntegerArray;
@Slf4j
public class AtomicTest {
public static void main(String[] args) {
// 1.创建一个指定长度的数组
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
log.info("{}", atomicIntegerArray);
// 2.返回指定位置的元素
log.info("返回指定下标的元素[{}]", atomicIntegerArray.get(2));
// 3.设置指定位置的元素、
atomicIntegerArray.set(0, 10);
// 4.在设置数组元素的新值时,同时返回数组元素。
log.info("返回1下标原来的元素,并设置一个元素[{}]", atomicIntegerArray.getAndSet(1, 11));
log.info("{}", atomicIntegerArray);
log.info("设置之后的元素[{}]", atomicIntegerArray.get(1));
// 5.修改数组元素把数组加上某个值
log.info("先加20,再返回[{}]", atomicIntegerArray.addAndGet(0, 20));
log.info("先返回再加[{}]", atomicIntegerArray.getAndAdd(1, 20));
log.info("{}", atomicIntegerArray);
// 6.CAS操作
log.info("如果0下标元素是30 [{}], 则赋值为222", atomicIntegerArray.compareAndSet(0, 30, 222));
log.info("{}", atomicIntegerArray);
// 7.自增、自减
log.info("先自增再使用:{}", atomicIntegerArray.incrementAndGet(3));
log.info("先使用再自增:{}", atomicIntegerArray.getAndIncrement(3));
log.info("{}", atomicIntegerArray.get(3));
log.info("自减:{}", atomicIntegerArray.getAndDecrement(3));
log.info("{}", atomicIntegerArray.get(3));
}
}
23:16:45.317 [main] INFO com.example.AtomicTest - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
23:16:45.320 [main] INFO com.example.AtomicTest - 返回指定下标的元素[0]
23:16:45.320 [main] INFO com.example.AtomicTest - 返回1下标原来的元素,并设置一个元素[0]
23:16:45.320 [main] INFO com.example.AtomicTest - [10, 11, 0, 0, 0, 0, 0, 0, 0, 0]
23:16:45.320 [main] INFO com.example.AtomicTest - 设置之后的元素[11]
23:16:45.320 [main] INFO com.example.AtomicTest - 先加20,再返回[30]
23:16:45.320 [main] INFO com.example.AtomicTest - 先返回再加[11]
23:16:45.320 [main] INFO com.example.AtomicTest - [30, 31, 0, 0, 0, 0, 0, 0, 0, 0]
23:16:45.320 [main] INFO com.example.AtomicTest - 如果0下标元素是30 [true], 则赋值为222
23:16:45.320 [main] INFO com.example.AtomicTest - [222, 31, 0, 0, 0, 0, 0, 0, 0, 0]
23:16:45.320 [main] INFO com.example.AtomicTest - 先自增再使用:1
23:16:45.320 [main] INFO com.example.AtomicTest - 先使用再自增:1
23:16:45.320 [main] INFO com.example.AtomicTest - 2
23:16:45.320 [main] INFO com.example.AtomicTest - 自减:2
23:16:45.320 [main] INFO com.example.AtomicTest - 1
多线程中使用原子数组
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicIntegerArray;
@Slf4j
public class AtomicThreadTest {
/**
* 定义原子数组
*/
static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
public static void main(String[] args) {
// 定义一个线程数组
Thread[] threads = new Thread[10];
// 给每个线程数组元素赋值
for (int i = 0; i < threads.length; i++) {
// 创建线程,修改原子数组
threads[i] = new Thread(() -> {
// 把原子数组的每个元素自增1000次
for (int index = 0; index < atomicIntegerArray.length(); index++) {
for (int j = 0; j < 1000; j++) {
atomicIntegerArray.getAndIncrement(index);
}
}
log.info("end.");
});
}
for (Thread thread : threads) {
try {
// 开启子线程
thread.start();
// 把所有子线程合并到当前主线程
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("{}", atomicIntegerArray);
}
}
AtomicIntegerFieldUpdater更新字段
AtomicIntegerUpdater可以对原子正整数字段进行更新,要求:
- 字段必须使用
volatile
修饰 ,使线程之间可见; - 只能是实例变量,不能是静态变量,也不能使用
final
修饰。
// 对 user 中的 age 字段修改
AtomicIntegerFieldUpdater<User> updater = AtmoicIntegerFieldUpdater.newUpdater(User.class, "age");
AtomicReference
可以原子读写一个对象。
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicReference;
/**
* 用原子对象操作字符串
*/
@Slf4j
public class AtomicReferenceTest {
static AtomicReference<String> atomicReference = new AtomicReference<>("abc");
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
if (atomicReference.compareAndSet("abc", "def")) {
log.info("把字符串abc更改为 def");
}
}).start();
new Thread(() -> {
if (atomicReference.compareAndSet("def", "abc")) {
log.info("把字符串还原为 abc");
}
}).start();
}
log.info("{}", atomicReference);
}
}
AtomicReference的ABA问题
-
使用AtomicStampedreference (带时间戳)
-
AtomicMarkableReference(带标志)
四、线程间的通信
等待 / 通知机制
定义
在多线程编程中,线程A执行条件暂时不满足,将线程A挂起,等待线程B更新条件后,线程A的条件已满足,再将A线程唤醒。
实现
Object.wait()
可以使当前执行代码的线程等待,暂停执行,直到接受到通知/被中断为止。
注意
wait()
方法只能在同步代码块中由锁对象调用;- 调用
wait()
方法后,当前线程会释放锁。
Object.notify()
可以唤醒线程,该方法也必须在同步代码块中由锁对象调用。没有使用锁对象调用wait()
/notify()
会抛出异常。
注意
如果有多个等待的线程,notify()方法只能唤醒其中的一个,并不会立即释放锁对象。一般将notify方法放在同步代码块的最后。
@Slf4j
public class ThreadNotifyTest {
public static void main(String[] args) throws InterruptedException {
// 定义一个字符串作为锁对象
String lock = "onovo";
Thread t1 = new Thread(() -> {
synchronized (lock) {
log.info("开始等待");
try {
// 线程等待
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("结束等待");
}
});
/**
* 定义线程2,用来唤醒线程1
*/
Thread t2 = new Thread(() -> {
// notify需要在同步代码块中由锁对象调用
synchronized (lock) {
log.info("开始唤醒");
lock.notify();
log.info("结束唤醒");
}
});
// 开启t1线程,main线程谁3秒,确保t1等待
t1.start();
Thread.sleep(3000);
t2.start();
}
}
01:01:35.068 [Thread-0] INFO com.example.ThreadNotifyTest - 开始等待
01:01:38.074 [Thread-1] INFO com.example.ThreadNotifyTest - 开始唤醒
01:01:38.075 [Thread-1] INFO com.example.ThreadNotifyTest - 结束唤醒
01:01:38.075 [Thread-0] INFO com.example.ThreadNotifyTest - 结束等待
Object.interrupt()
当线程处于wait()
等待状态时,调用线程对象的interrupt()
方法会中断线程等待状态,会产生InterruptedExceptiont
异常。
Object.notify() / Object.notifyAll()
notify()
一次只能唤醒一个线程,如果有多个等待的线程,只能随机唤醒其中的某一个;
notifyAll()
可以唤醒所有的线程。
Object.wait(long)
如果在参数指定的时间内没有被唤醒,超时后会自动唤醒。
Thread.join()
能让当前线程陷入等待
状态,等join
的这个线程执行完成后,再继续执行当前线程。
有时候,主线程创建并启动了子线程,如果子线程中需要进行大量的耗时运算,主线程往往将早于子线程结束之前结束。
如果主线程想等待子线程执行完毕后,获得子线程中的处理完的某个数据,就要用到join方法了。(插队)
示例代码:
@Slf4j
public class JoinTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
log.info("子线程日志01");
Thread.sleep(1000);
log.info("子线程日志02");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
thread.join();
log.info("主线程日志");
}
}
生产者消费者模式
生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。
生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒。
示意图:
解决思路
- 采用某种机制保护生产者和消费者之间的同步;
- 在生产者和消费者之间建立一个管道。
核心问题的
保证同一资源被多个线程并发访问时的完整性。常用的同步方法是采用信号或加锁机制,保证资源在任意时刻至多被一个线程访问。
方案一:同步机制
当生产者向缓冲区放入一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态;
当消费者从缓冲区取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。
@Slf4j
public class Storage {
// 仓库容量
private final int MAX_SIZE = 10;
// 仓库存储的载体
private List<Object> list = new LinkedList<>();
public void produce() {
synchronized (list) {
// 仓库满的情况
while (list.size() + 1 > MAX_SIZE) {
log.info("仓库已满");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产者生产
list.add(new Object());
log.info("生产一个产品,库存量:{}", list.size());
list.notifyAll();
}
}
public void consume() {
synchronized (list) {
// 仓库空了
while (list.size() == 0) {
log.info("仓库为空");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费者消费
list.remove(list.size() - 1);
log.info("消费一个产品,库存量:{}", list.size());
list.notifyAll();
}
}
}
@Slf4j
public class Producer implements Runnable {
private Storage storage;
public Producer() {
}
public Producer(Storage storage) {
this.storage = storage;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
storage.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
@Slf4j
public class Consumer implements Runnable {
private Storage storage;
public Consumer() {
}
public Consumer(Storage storage) {
this.storage = storage;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(3000);
storage.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
Storage storage = new Storage();
Thread p1 = new Thread(new Producer(storage));
Thread p2 = new Thread(new Producer(storage));
Thread p3 = new Thread(new Producer(storage));
Thread c1 = new Thread(new Consumer(storage));
Thread c2 = new Thread(new Consumer(storage));
Thread c3 = new Thread(new Consumer(storage));
p1.start();
p2.start();
p3.start();
c1.start();
c2.start();
c3.start();
}
}
01:31:11.452 [Thread-1] INFO com.example.Storage - 生产一个产品,库存量:1
01:31:11.468 [Thread-0] INFO com.example.Storage - 生产一个产品,库存量:2
01:31:11.468 [Thread-2] INFO com.example.Storage - 生产一个产品,库存量:3
01:31:12.480 [Thread-2] INFO com.example.Storage - 生产一个产品,库存量:4
01:31:12.480 [Thread-0] INFO com.example.Storage - 生产一个产品,库存量:5
01:31:12.480 [Thread-1] INFO com.example.Storage - 生产一个产品,库存量:6
01:31:13.449 [Thread-3] INFO com.example.Storage - 消费一个产品,库存量:5
01:31:13.449 [Thread-4] INFO com.example.Storage - 消费一个产品,库存量:4
01:31:13.449 [Thread-5] INFO com.example.Storage - 消费一个产品,库存量:3
01:31:13.481 [Thread-0] INFO com.example.Storage - 生产一个产品,库存量:4
01:31:13.481 [Thread-2] INFO com.example.Storage - 生产一个产品,库存量:5
01:31:13.481 [Thread-1] INFO com.example.Storage - 生产一个产品,库存量:6
01:31:14.492 [Thread-0] INFO com.example.Storage - 生产一个产品,库存量:7
01:31:14.492 [Thread-2] INFO com.example.Storage - 生产一个产品,库存量:8
01:31:14.492 [Thread-1] INFO com.example.Storage - 生产一个产品,库存量:9
01:31:15.503 [Thread-2] INFO com.example.Storage - 生产一个产品,库存量:10
01:31:15.503 [Thread-0] INFO com.example.Storage - 仓库已满
01:31:15.503 [Thread-1] INFO com.example.Storage - 仓库已满
01:31:16.449 [Thread-3] INFO com.example.Storage - 消费一个产品,库存量:9
01:31:16.449 [Thread-1] INFO com.example.Storage - 生产一个产品,库存量:10
01:31:16.449 [Thread-0] INFO com.example.Storage - 仓库已满
01:31:16.449 [Thread-4] INFO com.example.Storage - 消费一个产品,库存量:9
01:31:16.450 [Thread-5] INFO com.example.Storage - 消费一个产品,库存量:8
01:31:16.450 [Thread-0] INFO com.example.Storage - 生产一个产品,库存量:9
01:31:16.511 [Thread-2] INFO com.example.Storage - 生产一个产品,库存量:10
01:31:17.464 [Thread-1] INFO com.example.Storage - 仓库已满
... ...
一个生产者线程运行produce方法,睡眠1s;一个消费者运行一次consume方法,睡眠3s。此次实验过程中,有3个生产者和3个消费者,也就是我们说的多对多的情况。仓库的容量为10,可以看出消费的速度明显慢于生产的速度,符合设定。
注意:
notifyAll()方法可使所有正在等待队列中等待同一共享资源的全部
线程从等待状态退出,进入可运行状态。此时,优先级最高的哪个线程最先执行,但也有可能是随机执行的,这要取决于JVM虚拟机的实现。即最终也只有一个线程能被运行,上述线程优先级都相同,每次运行的线程都不确定是哪个,后来给线程设置优先级后也跟预期不一样,还是要看JVM的具体实现吧。
方案二:通过管道实现线程间的通信
在
java.io
中的PipeStream
管道流,用于在线程之间传送数据。
一个线程发送数据到输出管道,另一个线程从输入管道中读取数据。PipeInputStream
,PipeOutStream
,PipeReader
,PipedWriter
。`
@Slf4j
public class PipeStreamTest {
/**
* 定义方法向管道流中写入数据
*/
public static void writeData(PipedOutputStream out) throws IOException {
// 把0-100 之间的数写入管道中
for (int i = 0; i < 100; i++) {
String data = "-" + i;
// 把字节数组写入到输出管道流中
out.write(data.getBytes());
}
out.close();
}
/**
* 定义方法从管道中读取数据
*/
public static void readData(PipedInputStream input) throws IOException {
// 从管道中读取0-100
byte[] bytes = new byte[1024];
// 返回读到的字节数,如果没有读到任何数据返回-1
int len = input.read(bytes);
while (len != -1) {
// 把bytes数组中从0开始到len个字节转换为字符串打印出来
log.info(new String(bytes, 0, len));
// 继续从管道中读取数据
len = input.read(bytes);
}
input.close();
}
public static void main(String[] args) throws IOException {
// 定义管道字节流
PipedInputStream inputStream = new PipedInputStream();
PipedOutputStream outputStream = new PipedOutputStream();
inputStream.connect(outputStream);
// 创建两个线程向管道流中读写数据
new Thread(() -> {
try {
writeData(outputStream);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
readData(inputStream);
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
五、Callable 与 Future
使用Runnable
和Thread
创建的线程,run()
方法没有返回值;而使用Callable
接口与Future
接口则可以在线程执行任务结束后有一个返回值。
Callable 接口
Callable
与Runnable
类似,都是有一个抽象方法的函数式接口。不同的是,Callable
提供的方法是有返回值
的,而且支持泛型
。
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}
Callable
一般是配合ExecutorService
线程池工具来使用的。ExecutorService
可以使用submit()
方法来让一个Callable
接口执行,它会返回一个Future
,然后通过Future.get()
方法得到结果。
@Slf4j
public class ThreadTest {
public static void main(String args[]) throws Exception {
// 使用
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> future = executor.submit(() -> {
// 模拟计算需要一秒
Thread.sleep(1000);
return 1024 * 1024;
});
// 注意调用get()方法会阻塞当前线程,直到得到结果。
// 所以实际编码中建议使用可以设置超时时间的重载get()方法。
log.info("线程结果:{}", future.get());
}
}
输出结果:
15:43:17.352 [main] INFO com.example.ThreadTest - 线程结果:1048576
Future接口
Future
接口只有几个比较简单的方法:
boolean cancel(boolean paramBoolean);
boolean isCancelled();
boolean isDone();
V get();
V get(long paramLong, TimeUnit paramTimeUnit);
FutureTask类
FutureTask
是Future
接口的实现类,实现RunnableFuture
接口(RunnableFuture
同时继承了Runnable
接口和Future
接口)。Future
接口里面的cancel()
、get()
、isDone()
等方法自己实现都非常复杂
,所以 JDK 提供FutureTask
类来给我们使用。
@Slf4j
public class ThreadTest {
public static void main(String args[]) throws Exception {
// 使用
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> future = executor.submit(() -> {
// 模拟计算需要一秒
Thread.sleep(1000);
return 1024 * 1024;
});
// 注意调用get()方法会阻塞当前线程,直到得到结果。
// 所以实际编码中建议使用可以设置超时时间的重载get()方法。
log.info("线程结果:{}", future.get());
}
}
FutureTask的几个状态
/**
* state 可能的状态转变路径如下
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
state
表示任务的运行状态,初始状态为NEW。
运行状态只会在set
、setException
、cancel
方法中终止。
COMPLETING、INTERRUPTING是任务完成后的瞬时状态。
六、ThreadLocal
定义
ThreadLocal
主要作用是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的。
每个Thread
对象都有一个ThreadLocalMap
,每个ThreadLocalMap
可以存储多个ThreadLocal
使用
initialValue()
- initialValue()方法会返回当前线程对应的
初始值
,这是一个延迟加载的方法(只有在调用get()的时候,才会触发); - 当线程第一次使用get()方法访问变量时,将调用initialValue()方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue()方法;
- 若调用了remove()后,再调用get(),则会再次调用initialValue();
- 如果不重写initialValue()方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。
set()
// 把当前线程需要全局共享的value传入
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// map对象为空就创建,不为空就覆盖
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
get()
-
get()方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry()方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value;
-
这个map以及map中的key和value都是保存在线程中ThreadLocalMap的,而不是保存在ThreadLocal中;
-
getMap()方法:获取到当前线程内的ThreadLocalMap对象,每个线程内都有ThreadLocalMap对象,名为threadLocals,初始值为null
remove()
// 删除对应这个线程的值
public void remove() {
// 获取当前线程的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 移除这个ThreadLocal对应的值
m.remove(this);
}
使用场景
场景1
每个线程需要一个独享的对象(通常是工具类,典工具类型需要使用的类有SimpleDateFormat和Random)
使用ThreadLocal
(不仅线程安全,而且也没有synchronized
带来的性能问题,每个线程内有自己独享的SimpleDateFormat
对象)
@Slf4j
public class ThreadTest {
/**
* 为每个线程指定自己的SimpleDateFormat
*/
static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
/**
* 定义Runnable接口的实现类
*/
static class ParseDate implements Runnable {
private int i = 0;
public ParseDate(int i) {
this.i = i;
}
/**
* 把字符串转为日期
*/
@Override
public void run() {
//构建一个表示日期的字符串
String text = "2022年02月03日 20:10:" + (i % 60);
try {
// 先判断当前线程是否含有日期对象,如果没有就创建一个
if (threadLocal.get() == null) {
threadLocal.set(new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss"));
}
Date date = threadLocal.get().parse(text);
//
log.info("{}) 打印日期: {}", i, date);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// 创建100个线程
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(new ParseDate(i));
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
场景2
每个线程上下文(用户)信息,让不同方法直接使用,避免参数传递的麻烦。
七、显示锁Lock
锁的可重入性
当一个线程获得一个对象锁后,再次请求该对象锁时,是可以获得该对象的锁。
synchronized
不足之处
- 如果临界区是只读操作,其实可以多线程一起执行,但使用synchronized的话,同一时间只能有一个线程执行。
- synchronized无法知道线程有没有成功获取到锁
- 使用synchronized,如果临界区因为IO或者sleep方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待。
公平锁与非公平锁
这里的公平
,也就是先来先得(FIFO)。如果对一个锁来说,对锁先获取请求的线程一定会先被满足,后获取请求的线程后被满足,那这个锁就是公平的。反之,那就是不公平的。
注意:非公平锁能提升一定的效率;但是非公平锁可能会发生线程饥饿
情况,要根据实际的需求来选择非公平锁和公平锁。
在java.util.concurrent.locks
包下的ReentrantLock
可以根据构造方法的重载选择非公平锁或公平锁。
Lock中的方法
// 仅在调用时锁为空闲状态才获取该锁,可以响应中断
boolean tryLock();
// 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁
boolean tryLock(long time, TimeUnit unit);
// 如果当前线程未被中断,则获取锁,可以响应中断
lockInterruptibly();
// 返回绑定到此 Lock 实例的新 Condition 实例
Condition newCondition();
void lock();
获取锁,如果锁已被其他线程获取,则进行等待。
注意:使用Lock,须主动去释放锁,并且在发生异常时,不会自动释放锁。通常配合在try{}catch{}finally{}
块使用,在finally{}
块中释放锁,保证锁一定能被释放,防止死锁的发生。
void unlock();
释放锁,在finally中第一句执行。
boolean tryLock();
尝试获取锁,立即
返回是否获取到锁。
void lockInterruptibly();
获取锁,如果线程正在等待获取锁,则这个线程能够 中断响应
,即中断线程的等待状态。
例如,当两个线程同时通过lock.lockInterruptibly()
想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()
方法能够中断线程B的等待过程。
由于lockInterruptibly()
的声明中抛出了异常,所以lock.lockInterruptibly()
必须放在try块中或者在调用lockInterruptibly()
的方法外声明抛出InterruptedException
,但推荐使用后者,原因稍后阐述。
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
当一个线程获取了锁之后,是不会被interrupt()
方法中断的。因为**interrupt()
方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程**。
因此,当通过lockInterruptibly()
方法获取某个锁时,如果不能获取到,那么只有进行等待的情况下,才可以响应中断的。与synchronized
相比,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
newCondition();
关键字synchronized
与wait()
/notify
这两个方法一起使用可以实现等待通知模式
。
在Lock
显示锁中,newConditon()
方法返回Condition对象
,Condition
类也可以用await
/signal
实现等待通知模式
。
使用notify()
通知时,JVM会随机唤醒某个等待的线程;使用Condition
可以选择性的通知。
Condition:
await()
:会使当前线程等待,同时释放锁,当前其他线程调用signal()
时,线程会重新获得锁,并继续执行;signal()
/signalAll()
:用于唤醒等待的线程。
注意:在调用await
/signal
前,也需要线程持有相关的锁。
@Slf4j
public class ConditionTest {
// 定义锁
static Lock lock = new ReentrantLock();
// 获得Condition对象
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
// 定义线程子类
log.info("子线程启动....");
new Thread(() -> {
try {
lock.lock();
log.info("子线程获得锁...");
log.info("子线程即将等待....");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
log.info("子线程释放锁....");
}
}).start();
Thread.sleep(3000);
log.info("主线程睡了3s,现在唤醒子线程...");
// 注意:在调用方法之前需要持有锁
try {
lock.lock();
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
23:15:41.653 [main] INFO com.example.ConditionTest - 子线程启动....
23:15:41.703 [Thread-0] INFO com.example.ConditionTest - 子线程获得锁...
23:15:41.703 [Thread-0] INFO com.example.ConditionTest - 子线程即将等待....
23:15:44.715 [main] INFO com.example.ConditionTest - 主线程睡了3s,现在唤醒子线程...
23:15:44.715 [Thread-0] INFO com.example.ConditionTest - 子线程释放锁....
实例:两个线程交替打印
@Slf4j
public class ConditionTest {
static class MyService {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
// 打印标志
private boolean flag = true;
/**
* 打印方法1
*/
public void printOne() {
try {
lock.lock();
while (!flag) {
condition.await();
}
log.info("----------One-------");
flag = false;
// 通知领完的线程打印
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 打印方法2
*/
public void printTwo() {
try {
lock.lock();
while (flag) {
condition.await();
}
log.info("**********Two********");
flag = true;
// 通知领完的线程打印
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
MyService myService = new MyService();
//创建打印线程
new Thread(() -> {
for (int i = 0; i < 20; i++) {
myService.printOne();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 20; i++) {
myService.printTwo();
}
}).start();
}
}
23:23:13.184 [Thread-0] INFO com.example.ConditionTest - ----------One-------
23:23:13.186 [Thread-1] INFO com.example.ConditionTest - **********Two********
23:23:13.186 [Thread-0] INFO com.example.ConditionTest - ----------One-------
23:23:13.186 [Thread-1] INFO com.example.ConditionTest - **********Two********
23:23:13.186 [Thread-0] INFO com.example.ConditionTest - ----------One-------
23:23:13.186 [Thread-1] INFO com.example.ConditionTest - **********Two********
23:23:13.186 [Thread-0] INFO com.example.ConditionTest - ----------One-------
23:23:13.186 [Thread-1] INFO com.example.ConditionTest - **********Two********
……
ReetntranLock
可重入锁,唯一实现了Lock接口的类。
常用方法
// 返回当前线程调用 lock()的次数
int getHoldCount();
// 返回正等待获得锁的线程预估数
int getQueueLength();
// 返回与 condition 条件相关的线程的预估数
int getWaitQueueLength(Condition condition);
// 查看参数指定的线程是否在等待获得锁
boolean hasQueueThread(Thread thread);
// 查询是否还有线程在等待获得锁
boolean hasQueuedThreads();
// 查询是否有线程正在等待指定的Condition条件
boolean hasWaiters;
// 判断锁是否为公平锁
boolean isFair();
// 判断当前线程是否持有该锁
boolean isHeldByCurrentThread();
// 判断锁是否被线程持有
boolean isLocked();
synchronized 与 ReetntranLock
- 锁的实现
synchronized
是 JVM 实现的,而ReentrantLock
是 JDK 实现的。
- 性能
- 新版本 Java 对
synchronized
进行了很多优化,例如自旋锁等,synchronized
与ReentrantLock
大致相同。
- 等待可中断
- 当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock
可中断,而synchronized
不行。
- 公平锁
- 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized
中的锁是非公平的,ReentrantLock
默认情况下也是非公平的,但是也可以是公平的。
- 锁绑定多个条件
- 一个
ReentrantLock
可以同时绑定多个Condition
对象。
使用选择
除非需要使用ReentrantLock
的高级功能,否则优先使用synchronized
。这是因为
synchronized
是 JVM 实现的一种锁机制,JVM 原生地支持它,而ReentrantLock
不是所有的 JDK 版本都支持。并且使用
synchronized
不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
ReadWriteLock
ReadWriterLock接口中定义了 readLock()
返回读锁;writeLock()
方法返回写锁。该接口的实现类是ReentrantReadWriteLock.
读写锁和排它锁
synchronized
、ReentrantLock
都属于排它锁
,在同一时刻只允许一个线程进行访问。
读写锁
可以在同一时刻允许多个读线程访问,Java提供了ReentrantReadWriteLock
类作为读写锁的默认实现。
特点:
- 内部维护了两个锁:一个读锁,一个写锁;
- 读锁共享,写锁排它;
- 通过分离读锁和写锁,使得在
读多写少
的环境下,性能提高。
注意:即使用读写锁,在写线程访问时,所有的读线程和其它写线程均被阻塞。
ReentrantReadWriteLock
是JDK默认实现的
ReadWriteLock
接口的非抽象类;
它与ReentrantLock
的功能类似,同样是可重入的,支持非公平锁和公平锁;
不同的是,它还支持读写锁
。
注意: readLock()
与 writeLock()
返回的锁对象是同一个锁对象
的两个不同的角色。
@Slf4j
public class ReentrantReadWriteLockTest {
// 定义读写锁
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 获得读锁
Lock readlock = readWriteLock.readLock();
// 获得写锁
Lock writelock = readWriteLock.writeLock();
/**
* 读数据的方法
*/
void read(){
try {
// 申请读锁
readlock.lock();
} catch (Exception e) {
e.printStackTrace();
} finally {
readlock.unlock();
}
}
/**
* 写数据的方法
*/
void write(){
try {
// 申请写锁
writelock.lock();
} catch (Exception e) {
e.printStackTrace();
} finally {
writelock.unlock();
}
}
}
示例
读读共享
@Slf4j
public class ReentrantReadWriteLockTest {
static class Service{
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void read(){
try {
readWriteLock.readLock().lock();
log.info("获取到读锁,开始读取数据");
// 模拟读取数据用时3s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
public static void main(String[] args) {
Service service = new Service();
//创建五个线程调用read方法;
for (int i = 0; i < 5; i++) {
new Thread(() -> service.read()).start();
}
}
}
23:49:53.456 [Thread-3] INFO com.example.ReentrantReadWriteLockTest - 获取到读锁,开始读取数据
23:49:53.456 [Thread-4] INFO com.example.ReentrantReadWriteLockTest - 获取到读锁,开始读取数据
23:49:53.456 [Thread-2] INFO com.example.ReentrantReadWriteLockTest - 获取到读锁,开始读取数据
23:49:53.456 [Thread-1] INFO com.example.ReentrantReadWriteLockTest - 获取到读锁,开始读取数据
23:49:53.456 [Thread-0] INFO com.example.ReentrantReadWriteLockTest - 获取到读锁,开始读取数据
写写互斥
@Slf4j
public class ReentrantReadWriteLockTest {
static class Service{
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void write(){
try {
readWriteLock.writeLock().lock();
log.info("获取到写锁,开始写数据");
// 模拟写取数据用时3s;
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
}
public static void main(String[] args) {
Service service = new Service();
//创建五个线程调用write方法;
for (int i = 0; i < 5; i++) {
new Thread(() -> service.write()).start();
}
}
}
23:51:25.442 [Thread-0] INFO com.example.ReentrantReadWriteLockTest - 获取到写锁,开始写数据
23:51:28.449 [Thread-1] INFO com.example.ReentrantReadWriteLockTest - 获取到写锁,开始写数据
23:51:31.459 [Thread-2] INFO com.example.ReentrantReadWriteLockTest - 获取到写锁,开始写数据
23:51:34.467 [Thread-3] INFO com.example.ReentrantReadWriteLockTest - 获取到写锁,开始写数据
23:51:37.468 [Thread-4] INFO com.example.ReentrantReadWriteLockTest - 获取到写锁,开始写数据
读写互斥
@Slf4j
public class ReentrantReadWriteLockTest {
static class Service {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void write() {
try {
readWriteLock.writeLock().lock();
log.info("获取到写锁,开始写数据");
// 模拟写取数据用时3s;
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
public void read() {
try {
readWriteLock.readLock().lock();
log.info("获取到读锁,开始读取数据");
// 模拟读取数据用时3s;
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
public static void main(String[] args) {
Service service = new Service();
new Thread(() -> service.read()).start();
new Thread(() -> service.write()).start();
}
}
23:53:19.900 [Thread-0] INFO com.example.ReentrantReadWriteLockTest - 获取到读锁,开始读取数据
23:53:22.916 [Thread-1] INFO com.example.ReentrantReadWriteLockTest - 获取到写锁,开始写数据
AQS(AbstractQueuedSynchronizer)
AQS是一个实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,如下图所示。
AQS为一系列同步器依赖于一个单独的原子变量state
的同步器提供了一个非常有用的基础。子类们必须定义改变state
变量的protected()
方法,这些方法定义了state
是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制,子类也可以维护其他的state
变量,但是为了保证同步,必须原子地操作这些变量。
AQS中对state的操作是原子的,且不能被继承。所有的同步机制的实现均依赖于对改变量的原子操作。
为了实现不同的同步机制,我们需要创建一个非共有的(non-public internal)扩展了AQS类的内部辅助类来实现相应的同步逻辑。
AQS并不实现任何同步接口,它提供了一些可以被具体实现类直接调用的一些原子操作方法来重写相应的同步逻辑。同时提供了互斥模式(exclusive)和共享模式(shared)两种不同的同步逻辑。一般情况下,子类只需要根据需求实现其中一种模式,当然也有同时实现两种模式的同步类,如ReadWriteLock
。
state状态
AbstractQueuedSynchronizer维护了一个volatile int类型
的变量,用户表示当前同步状态。volatile
虽然不能保证操作的原子性,但是保证了当前变量state
的可见性。
state
的访问方式有三种原子操作:
- getState();
- setState();
- compareAndSetState(); // 实现依赖于Unsafe的compareAndSwapInt()方法
自定义资源共享方式
AQS定义两种资源共享方式:
- Exclusive:独占,只有一个线程能执行,如ReentrantLock;
- Share:共享,多个线程可同时执行,如Semaphore/CountDownLatch。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state
的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
// 该线程是否正在独占资源,只有用到condition才需要去实现它
isHeldExclusively();
// 独占方式。尝试获取资源,成功则返回true,失败则返回false
tryAcquire(int);
// 独占方式。尝试释放资源,成功则返回true,失败则返回false
tryRelease(int);
// 共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
tryAcquireShared(int);
// 共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false
tryReleaseShared(int);
八、线程的管理
线程组
定义
即一组相关的线程,还可以定义子线程组。
Thread类
有几个构造方法允许在创建线程时指定线程组,如果在创建线程时没有指定线程组,则该线程就属于父线层所在的线程组。
JVM在创建主线程时,会为它指定一个线程组,因此每个Java线程都有一个线程组与之关联。可以调用getThreadGroup()
方法返回线程组。
作用
- 线程组开始是处于安全的考虑设计来用区分不同的 Applet ,然而ThreadGroup 并未实现这一目标;
- 新开发的项目中已经不常用了。
开发方式
现在一般会将一组相关的线程存入一个数组或一个集合中,如果仅仅是用来区分线程时,可以使用线程名称来区分。
使用
创建线程组的两个构造
// 指定线程组的名称
ThreadGroup(String name)
// 指定父线程组and线程组的名称
ThreadGroup(ThreadGroup parent, Stirng name)
@Slf4j
public class ThreadGroupTest {
public static void main(String[] args) {
// main线程组
ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
log.info("{}", mainGroup);
// 定义线程组1
ThreadGroup group1 = new ThreadGroup("group1");
log.info("{}", group1);
// 定义线程组2,同时指定父组
ThreadGroup group2 = new ThreadGroup(group1, "group2");
log.info("{}", group2);
log.info("线程2的父线程为:{}", group2.getParent());
// 创建线程时指定线程组
Thread t1 = new Thread(group1, () -> log.info("当前线程的线程组为:{}", Thread.currentThread().getThreadGroup()));
t1.start();
}
}
捕获线程的执行异常
在线程的run()方法中,如果有受检异常必须进行捕获处理,如果想要获得
run()
方法中出现的运行时异常信息,可以通过回调接口UncaughtExceptionhandler
获得哪个线程出现了运行时异常。
Thread
中有关处理运行异常的方法有:
-getDefaultUncaughtException()
获得全局的默认
异常处理器
getUncaughtExceptionHandler()
获得当前线程的UncaughtExceptionHandler。
-setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
设置全局的UncaughtExceptionHandler。setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)
设置当前线程的UncaughtExceptionHandler。
当线程运行过程中出现异常,JVM会调用Thread类的dispatchUncaughtException(Throwable e)
方法, 该方法会调用getUncaughtExceptionHandler().uncaughtException(this, e)
; 如果想要获得线程中出现异常的信息,就需要设置线程的UncaughtExceptionHandler
。
@Slf4j
public class ThreadTest {
/**
* 在实际开发中,这种设计异常处理的方式还是比较常用的,尤其是异常执行的方法
* 如果线程产生了异常, JVM会调用dispatchUncaughtException()方法,在该方法中调用了getUncaughtExceptionHandler().uncaughtException(this, e);
* 如果当前线程设置了UncaughtExceptionHandler回调接口就直接调用它自己的uncaughtException方法,
* 如果没有设置则调用当前线程所在线程组UncaughtExceptionHandler回调接口的uncaughtException方法,
* 如果线程组也没有设置回调接口,则直接把异常的栈信息定向到System.err中
* @param args
*/
public static void main(String[] args) {
// 1) 设置线程全局的回调接口
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
//t参数接收发生异常的线程, e就是该线程中的异常
log.error("线程 [{}] 产生了 [{}] 异常!", t.getName(), e.getMessage());
});
new Thread(() -> {
log.info("开始运行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// 线程中的受检异常必须捕获处理
e.printStackTrace();
}
// 会产生算术异常
log.info("{}", 12 / 0);
}).start();
new Thread(() -> {
String txt = null;
// 会产生空指针异常
log.info("{}", txt.length());
}).start();
}
}
22:56:44.799 [Thread-0] INFO com.example.ThreadTest - 开始运行
22:56:44.799 [Thread-1] ERROR com.example.ThreadTest - 线程 [Thread-1] 产生了 [null] 异常!
22:56:46.815 [Thread-0] ERROR com.example.ThreadTest - 线程 [Thread-0] 产生了 [/ by zero] 异常!
注入Hook钩子线程
触发时机:当 JVM 退出的时候会执行Hook线程。
使用场景:清理临时文件。
通过Hook线程防止程序重复启动
@Slf4j
public class ThreadTest {
private static Path getLockFile() {
Path path = Paths.get("", "tmp.lock");
return path;
}
public static void main(String[] args) throws InterruptedException, IOException {
// 1) 注入Hook线程,在程序退出时删除.lock文件
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
File file = getLockFile().toFile();
log.info("程序结束 删除 [{}] 文件", file.getPath());
file.delete();
}));
// 2)程序运行时,检查lock文件是否存在,如果lock文件存在,则抛出异常
if (getLockFile().toFile().exists()) {
throw new RuntimeException("程序已启动");
} else {
// 文件不存在,说明程序是第一次启动,创建lock文件
File file = getLockFile().toFile();
file.createNewFile();
log.info("程序启动 创建 [{}] 文件", file.getPath());
}
// 模拟程序运行
for (int i = 0; i < 10; i++) {
log.info("程序正在运行.");
TimeUnit.SECONDS.sleep(1);
}
}
}
线程池
定义
线程池是一高效使用线程的容器,其内部可以预先创建一定数量的工作线程
;客户端代码直接将任务作为一个对象提交给线程池,线程池将这些任务缓存在工作队列
中;线程池中的工作线程不断地从队列中取出任务并执行。
线程开销主要包括:
- 创建/启动线程的开销;
- 线程销毁开销;
- 线程调度的开销;
- 线程数量受限CPU处理器数量。
优势:
- 控制并发的数量,并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃;
- 创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程;
- 可以对线程做统一管理。
ThreadPoolExecutor的构造方法
Java中的线程池ThreadPoolExecutor
是Executor
接口的实现类。
// 五个参数的构造函数
public ThreadPoolExecutor(int corePoolSize, // 该线程池中核心线程数最大值
int maximumPoolSize,// 该线程池中线程总数最大值
long keepAliveTime,// 非核心线程闲置超时时长
TimeUnit unit,// keepAliveTime的单位。
BlockingQueue<Runnable> workQueue)// 阻塞队列,维护着等待执行的Runnable任务对象。
// 六个参数的构造函数-1
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)// 创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线 程、线程的优先级等。如果不指定,会新建一个默认的线程工厂。
// 六个参数的构造函数-2
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)// 拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略
// 七个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
int corePoolSize
:该线程池中核心线程数最大值。 -
int maximumPoolSize
:该线程池中线程总数(含核心线程、非核心线程)最大值。 -
long keepAliveTime
:非核心线程闲置超时时长。 -
TimeUnit unit
:keepAliveTime 的单位。 -
BlockingQueue workQueue
:阻塞队列,维护着等待执行的Runnable任务对象。阻塞队列名称 类型 说明 LinkedBlockingQueue 链式阻塞队列 底层数据结构是链表,默认大小是 Integer.MAX_VALUE
,也可以指定大小。ArrayBlockingQueue 数组阻塞队列 底层数据结构是数组,需要指定队列的大小。 SynchronousQueue 同步队列 内部容量为0,每个put操作必须等待一个take操作,反之亦然。 DelayQueue 延迟队列 该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。 -
ThreadFactory threadFactory:线程工厂 ,用于批量创建线程,统一在创建线程时设置一些参数(如是否守护线程、线程的优先级等)。如果不指定,会新建一个默认的线程工厂。
-
RejectedExecutionHandler handler:拒绝处理策略,线程数量大于最大线程数就会采用拒绝处理策略
处理策略 说明 ThreadPoolExecutor.AbortPolicy 默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常 ThreadPoolExecutor.DiscardPolicy 丢弃新来的任务,但是不抛出异常 ThreadPoolExecutor.DiscardOldestPolicy 丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程) ThreadPoolExecutor.CallerRunsPolicy 由调用线程处理该任务
ThreadPoolExecutor的策略
线程池状态,用于管理整个线程池里各种任务、事务的调度线程(如:创建线程、销毁线程、任务队列管理、线程队列管理等等)。
线程池状态生命周期:
- 线程池创建后处于
RUNNING
状态。 - 调用shutdown()方法后处于
SHUTDOWN
状态,线程池不能接受新的任务,清除一些空闲worker,会等待阻塞队列的任务完成。 - 调用shutdownNow()方法后处于
STOP
状态,线程池不能接受新的任务,中断所有线程,阻塞队列中没有被执行的任务全部丢弃。此时,poolsize=0,阻塞队列的size也为0。 - 当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为
TIDYING(整洁)
状态。接着会执行 终止terminated()函数。
ThreadPoolExecutor 中有一个控制状态的属性叫
ctl
,它是一个AtomicInteger类型的变量。线程池状态就是通过AtomicInteger类型的成员变量ctl
来获取的。
获取的ctl
值传入runStateOf
方法,与~CAPACITY
位与运算(CAPACITY
是低29位全1的int变量)。
~CAPACITY
在这里相当于掩码,用来获取ctl的高3位,表示线程池状态;而另外的低29位用于表示工作线程数
- 线程池处在
TIDYING
状态时,执行完terminated()方法之后,就会由TIDYING
->TERMINATED
, 线程池被设置为TERMINATED
状态。
线程池主要的任务处理流程
处理流程:
- 线程总数量 < corePoolSize,无论线程是否空闲,都会新建一个核心线程执行任务。注意,这一步需要获得全局锁;
- 线程总数量 >= corePoolSize时,新来的线程任务会进入任务队列中等待,然后空闲的核心线程会依次去缓存队列中取任务来执行;
- 当缓存队列满了,说明这个时候任务已经多到爆棚,需要一些
临时工
来执行这些任务了。于是会创建非核心线程去执行这个任务。注意,这一步需要获得全局锁; - 缓存队列满了, 且总线程数达到了
maximumPoolSize
,则会采取拒绝策略进行处理。
ThreadPoolExecutor 线程复用
通过ThreadPoolExecutor在创建线程时,会将线程封装成工作线程worker
,并放入工作线程组
中,让线程执行完线程任务后不销毁,然后这个worker反复从阻塞队列中拿任务去执行。
自定义线程工厂
@Slf4j
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
// 创建线程池, 使用自定义线程工厂, 采用默认的拒绝策略是抛出异常
ExecutorService executorService = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadFactory() {
//自定义线程工厂
@Override
public Thread newThread(Runnable runnable) {
// 根据参数r接收的任务,创建一个线程
Thread thread = new Thread(runnable);
// 设置为守护线程, 当主线程运行结束,线程池中的线程会自动退出
thread.setDaemon(true);
log.info("创建线程: {}", thread.getName());
return thread;
}
});
// 提交5个任务, 当给当前线程池提交的任务超过5个时,线程池默认抛出异常
for (int i = 0; i < 5; i++) {
// 定义任务
executorService.execute(() -> {
int num = new Random().nextInt(10);
try {
TimeUnit.SECONDS.sleep(num);
log.info("睡眠: {}秒", num);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 主线程睡眠
Thread.sleep(10000);
// 主线程睡眠超时, 主线程结束, 线程池中的线程会自动退出
}
}
13:24:24.527 [main] INFO com.example.ThreadTest - 创建线程: Thread-0
13:24:24.531 [main] INFO com.example.ThreadTest - 创建线程: Thread-1
13:24:24.531 [main] INFO com.example.ThreadTest - 创建线程: Thread-2
13:24:24.531 [main] INFO com.example.ThreadTest - 创建线程: Thread-3
13:24:24.531 [main] INFO com.example.ThreadTest - 创建线程: Thread-4
13:24:26.538 [Thread-0] INFO com.example.ThreadTest - 睡眠: 2秒
13:24:27.537 [Thread-2] INFO com.example.ThreadTest - 睡眠: 3秒
13:24:31.539 [Thread-4] INFO com.example.ThreadTest - 睡眠: 7秒
13:24:32.539 [Thread-3] INFO com.example.ThreadTest - 睡眠: 8秒
13:24:33.535 [Thread-1] INFO com.example.ThreadTest - 睡眠: 9秒
监控方法
ThreadPoolExecutor
提供了一组方法用于监控线程池:
- int
getActiveCount()
获得线程池中当前活动线程的数量; - long
getCompletedTaskCount()
返回线程池完成任务的数量; - int
getCorePoolSize()
线程池中核心线程的数量; - int
getLargestPoolSize()
返回线程池曾经达到的线程的最大数; - int
getMaximumPoolSize()
返回线程池的最大容量; - int
getPoolSize()
当前线程池的大小; - BlockingQueue
getQueue()
返回阻塞队列; - long
getTaskCount()
返回线程池收到的任务总数。
扩展方法
通过继承ThreadPoolExecutor
,重写以下两个方法来实现一些增强功能。
protected void afterExecute(Runnable r, Throwable t);
: 在线程池执行任务前调用;protected void beforeExecute(Thread t, Runnable r);
:在任务结束后(任务异常退出)调用。
异常跟踪
使用ThreadPoolExecutor
进行submit()
提交任务时,有的任务抛出了异常,但是线程池并没有进行提示,即线程池把任务中的异常给catch掉了,可以把submit()
提交改为execute
执行,也可以对ThreadPoolExecutor
线程池进行扩展。对提交的任务进行包装。
@Slf4j
public class ThreadTest {
/**
* 定义类实现Runnable接口,用于计算两个数相除
*/
@Slf4j
static class DivideTask implements Runnable {
private int x;
private int y;
public DivideTask(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public void run() {
log.info("计算:{} / {} = {}", x, y, (x / y));
}
}
@Slf4j
static class TraceThreadPollExecutor extends ThreadPoolExecutor {
public TraceThreadPollExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
// 定义方法,对执行的任务进行包装,接收两个参数,第一个参数接收要执行的任务,第二个参数是一个Exception异常
private Runnable wrap(Runnable task, Exception exception) {
return new Runnable() {
@Override
public void run() {
try {
task.run();
} catch (Exception e) {
exception.printStackTrace();
throw e;
}
}
};
}
// 重写submit方法
@Override
public Future submit(Runnable task) {
return super.submit(wrap(task, new Exception("客户跟踪异常")));
}
@Override
public void execute(Runnable command) {
super.execute(wrap(command, new Exception("客户跟踪异常")));
}
}
public static void main(String[] args) {
// 使用自定义的线程池
ThreadPoolExecutor poolExecutor = new TraceThreadPollExecutor(0, Integer.MAX_VALUE, 0, TimeUnit.SECONDS, new SynchronousQueue<>());
// 向线程池中添加计算两个数相除的任务
for (int i = 0; i < 5; i++) {
poolExecutor.submit(new DivideTask(10, i));
}
}
}
常见线程池
submit() 和 execut的区别:
execut()
仅能添加一个Runable任务,submit()
不仅可以添加Runable任务还可以添加Callable任务;execut()
没有返回值,而submit()
在添加Callable任务时会有返回值;- 如果发生异常
execute()
会终止这个线程,而submit()
可以通过Future.get()
捕获抛出的异常; submit()
中抛出异常不管提交的是Runnable还是Callable类型的任务,如果不对返回值调用Future.get()方法,都会吃掉异常。
创建线程池静态方法
Executors
类中提供4个创建线程池的静态方法。
cachedThreadPool
一个任务创建一个线程。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
运行流程:
- 提交任务进线程池;
- 因为
corePoolSize
为0的关系,不创建核心线程,线程池最大为Integer.MAX_VALUE
; - 尝试将任务添加到
SynchronousQueue
队列; - 如果
SynchronousQueue
入列成功,等待被当前运行的线程空闲后拉取执行。如果当前没有空闲线程,那么就创建一个非核心线程,然后从SynchronousQueue
拉取任务并在当前线程执行; - 如果
SynchronousQueue
已有任务在等待,入列操作将会阻塞。
注意:
需要执行很多短时间
的任务时,CacheThreadPool的线程复用率
比较高, 会显著的提高性能
;
线程60s后会回收,意味着即使没有任务进来,CacheThreadPool并不会占用很多资源
。
示例:
@Slf4j
public class ThreadTest {
public static void main(String[] args) {
// 创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
// 向线程池中提交任务
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
log.info("任务执行开始.");
try {
// 模拟任务
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("任务执行结束.");
});
}
}
}
14:14:59.838 [pool-1-thread-3] INFO com.example.ThreadTest - 任务执行开始.
14:14:59.838 [pool-1-thread-10] INFO com.example.ThreadTest - 任务执行开始.
14:14:59.838 [pool-1-thread-2] INFO com.example.ThreadTest - 任务执行开始.
14:14:59.838 [pool-1-thread-9] INFO com.example.ThreadTest - 任务执行开始.
14:14:59.838 [pool-1-thread-8] INFO com.example.ThreadTest - 任务执行开始.
14:14:59.838 [pool-1-thread-7] INFO com.example.ThreadTest - 任务执行开始.
14:14:59.838 [pool-1-thread-4] INFO com.example.ThreadTest - 任务执行开始.
14:14:59.838 [pool-1-thread-5] INFO com.example.ThreadTest - 任务执行开始.
14:14:59.838 [pool-1-thread-6] INFO com.example.ThreadTest - 任务执行开始.
14:14:59.838 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:15:00.851 [pool-1-thread-2] INFO com.example.ThreadTest - 任务执行结束.
14:15:00.851 [pool-1-thread-5] INFO com.example.ThreadTest - 任务执行结束.
14:15:00.851 [pool-1-thread-3] INFO com.example.ThreadTest - 任务执行结束.
14:15:00.851 [pool-1-thread-9] INFO com.example.ThreadTest - 任务执行结束.
14:15:00.851 [pool-1-thread-7] INFO com.example.ThreadTest - 任务执行结束.
14:15:00.851 [pool-1-thread-6] INFO com.example.ThreadTest - 任务执行结束.
14:15:00.851 [pool-1-thread-10] INFO com.example.ThreadTest - 任务执行结束.
14:15:00.851 [pool-1-thread-8] INFO com.example.ThreadTest - 任务执行结束.
14:15:00.851 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
14:15:00.851 [pool-1-thread-4] INFO com.example.ThreadTest - 任务执行结束.
newFixedThreadPool
所有任务只能使用固定大小的线程
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
核心线程数量 == 总线程数量,都是传入的参数nThreads,所以只能创建核心线程,不能创建非核心线程。因为LinkedBlockingQueue的默认大小是Integer.MAX_VALUE,故如果核心线程空闲,则交给核心线程处理;如果核心线程不空闲,则入列等待,直到核心线程空闲。
FixedThreadPool与CachedThreadPool的区别:
- 因为 corePoolSize == maximumPoolSize ,所以FixedThreadPool只会创建核心线程; 而CachedThreadPool因为corePoolSize=0,所以只会创建非核心线程。
- 在 getTask() 方法,如果队列里没有任务可取,线程会一直阻塞在 LinkedBlockingQueue.take() ,线程不会被回收;CachedThreadPool会在60s后收回。
- 由于线程不会被回收,会一直卡在阻塞,所以没有任务的情况下, FixedThreadPool占用资源更多。
- 都几乎不会触发拒绝策略,但是原理不同。FixedThreadPool是因为阻塞队列可以很大(最大为Integer最大值),故几乎不会触发拒绝策略;CachedThreadPool是因为线程池很大(最大为Integer最大值),几乎不会导致线程数量大于最大线程数,故几乎不会触发拒绝策略。
示例:
@Slf4j
public class ThreadTest {
public static void main(String[] args) {
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 向线程池中提交任务
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
log.info("任务执行开始.");
try {
// 模拟任务
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("任务执行结束.");
});
}
}
}
结果:线程池中一直有5个线程,而且不会结束。
14:15:48.359 [pool-1-thread-4] INFO com.example.ThreadTest - 任务执行开始.
14:15:48.359 [pool-1-thread-2] INFO com.example.ThreadTest - 任务执行开始.
14:15:48.359 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:15:48.359 [pool-1-thread-3] INFO com.example.ThreadTest - 任务执行开始.
14:15:48.359 [pool-1-thread-5] INFO com.example.ThreadTest - 任务执行开始.
14:15:49.370 [pool-1-thread-2] INFO com.example.ThreadTest - 任务执行结束.
14:15:49.370 [pool-1-thread-3] INFO com.example.ThreadTest - 任务执行结束.
14:15:49.370 [pool-1-thread-2] INFO com.example.ThreadTest - 任务执行开始.
14:15:49.370 [pool-1-thread-5] INFO com.example.ThreadTest - 任务执行结束.
14:15:49.370 [pool-1-thread-3] INFO com.example.ThreadTest - 任务执行开始.
14:15:49.370 [pool-1-thread-4] INFO com.example.ThreadTest - 任务执行结束.
14:15:49.370 [pool-1-thread-5] INFO com.example.ThreadTest - 任务执行开始.
14:15:49.370 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
14:15:49.370 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:15:49.370 [pool-1-thread-4] INFO com.example.ThreadTest - 任务执行开始.
14:15:50.381 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
14:15:50.381 [pool-1-thread-3] INFO com.example.ThreadTest - 任务执行结束.
14:15:50.381 [pool-1-thread-4] INFO com.example.ThreadTest - 任务执行结束.
14:15:50.381 [pool-1-thread-2] INFO com.example.ThreadTest - 任务执行结束.
14:15:50.381 [pool-1-thread-5] INFO com.example.ThreadTest - 任务执行结束.
newSingleThreadExecutor
总的创建一个线程, 相当于大小为 1 的fixedThreadPool
。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
有且仅有一个核心线程( corePoolSize == maximumPoolSize=1),使用了LinkedBlockingQueue(容量很大),所以,不会创建非核心线程。所有任务按照先来先执行的顺序执行。如果这个唯一的线程不空闲,那么新来的任务会存储在任务队列里等待执行。
@Slf4j
public class ThreadTest {
public static void main(String[] args) {
// 创建线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
// 向线程池中提交任务
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
log.info("任务执行开始.");
try {
// 模拟任务
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("任务执行结束.");
});
}
}
}
14:25:17.127 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:25:18.135 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
14:25:18.135 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:25:19.148 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
14:25:19.148 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:25:20.149 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
14:25:20.149 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:25:21.154 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
14:25:21.154 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:25:22.167 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
14:25:22.167 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:25:23.172 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
14:25:23.172 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:25:24.173 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
14:25:24.173 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:25:25.186 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
14:25:25.186 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:25:26.196 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
14:25:26.196 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行开始.
14:25:27.210 [pool-1-thread-1] INFO com.example.ThreadTest - 任务执行结束.
newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
//ScheduledThreadPoolExecutor():
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
@Slf4j
public class ThreadTest {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
log.info("延迟一秒,每3s运行一次");
scheduledThreadPool.scheduleAtFixedRate(() -> log.info("触发"), 1, 3, TimeUnit.SECONDS);
}
}
14:34:37.463 [main] INFO com.example.ThreadTest - 延迟一秒,每3s运行一次
14:34:38.505 [pool-1-thread-1] INFO com.example.ThreadTest - 触发
14:34:41.508 [pool-1-thread-1] INFO com.example.ThreadTest - 触发
14:34:44.504 [pool-1-thread-2] INFO com.example.ThreadTest - 触发
14:34:47.515 [pool-1-thread-1] INFO com.example.ThreadTest - 触发
14:34:50.507 [pool-1-thread-3] INFO com.example.ThreadTest - 触发
14:34:53.516 [pool-1-thread-3] INFO com.example.ThreadTest - 触发
14:34:56.507 [pool-1-thread-3] INFO com.example.ThreadTest - 触发
注意:《阿里巴巴开发手册》不建议直接使用Executors类
中的线程池,而是通过ThreadPoolExecutor
的方式,能更明确线程池的运行规则,规避资源耗尽的风险。
ForkJoinPoll
分而治之
是一个有效的处理大数据的方法,MapReduce
就是采用这种分而治之的思路。
把一个大任务调用fork()
方法分解为若干小任务,把小任务的处理结果进行join()
合并为大任务的结果。
系统对ForkJoinPool
线程池进行了优化,提交的任务数量与线程的数量不一定是一对一关系。在多数情况下,一个物理线程实际上需要处理多个逻辑任务。
ForkJoinPool 线程池中最常用的方法:
ForkJoinTask submit(ForkJoinTask task)
向线程池提交一个ForkJoinTask
任务。
ForkJoinTask
任务支持fork()
分解与 join()
合并;它有两个重要的子类:RecursiveAction
任务没有返回值, RecursiveTask
任务可以带有返回值。
@Slf4j
public class ThreadTest {
/**
* 计算数列的和, 需要返回结果,可以定义任务继承 RecursiveTask
*/
private static class CountTask extends RecursiveTask<Long> {
// 定义数据规模的阈值,允许计算 10000个数内的和,超过该阈值的数列就需要分解
private static final int THRESHOLD = 10000;
// 定义每次把大任务分解为100个小任务
private static final int TASKNUM = 100;
// 计算数列的起始值
private long start;
// 计算数列的结束值
private long end;
public CountTask(long start, long end) {
this.start = start;
this.end = end;
}
/**
* 重写RecursiveTask类的compute()方法,计算数列的结果
* @return
*/
@Override
protected Long compute() {
// 保存计算的结果
long sum = 0;
// 判断任务是否需要继续分解,如果当前数列end与start范围的数超过阈值THRESHOLD,就需要继续分解
if (end - start < THRESHOLD) {
// 小于阈值可以直接计算
for (long i = start; i <= end; i++) {
sum += i;
}
} else {
// 数列范围超过阈值,需要继续分解
// 约定每次分解成100个小任务,计算每个任务的计算量
long step = (start + end) / TASKNUM;
// start = 0 , end = 200000, step = 2000, 如果计算[0,200000]范围内数列的和, 把该范围的数列分解为100个小任务,每个任务计算2000个数即可
// 注意,如果任务划分的层次很深,即THRESHOLD阈值太小,每个任务的计算量很小,层次划分就会很深,可能出现两种情况:一是系统内的线程数量会越积越多,导致性能下降严重; 二是分解次数过多,方法调用过多可能会导致栈溢出
// 创建一个存储任务的集合
List<CountTask> subTaskList = new ArrayList<>();
// 每个任务的起始位置
long pos = start;
for (int i = 0; i < TASKNUM; i++) {
// 每个任务的结束位置
long lastOne = pos + step;
// 调整最后一个任务的结束位置
if (lastOne > end) {
lastOne = end;
}
// 创建子任务
CountTask task = new CountTask(pos, lastOne);
// 把任务添加到集合中
subTaskList.add(task);
// 调用for()提交子任务
task.fork();
// 调整下个任务的起始位置
pos += step + 1;
}
// 等待所有的子任务结束后,合并计算结果
for (CountTask task : subTaskList) {
// join() 会一直等待子任务执行完毕返回执行结果
sum += task.join();
}
}
return sum;
}
}
public static void main(String[] args) {
// 创建ForkJoinPool线程池
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 创建一个大的任务
CountTask task = new CountTask(0L, 200000L);
// 把大任务提交给线程池
ForkJoinTask<Long> result = forkJoinPool.submit(task);
try {
// 调用任务的get()方法返回结果
log.info("结果:{}", result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
// 验证
long count = 0L;
for (long i = 0; i <= 200000; i++) {
count += i;
}
log.info("验证:{}", count);
}
}
15:28:59.648 [main] INFO com.example.ThreadTest - 结果:20000100000
15:28:59.653 [main] INFO com.example.ThreadTest - 验证:20000100000
九、保障线程安全的设计技术
Java运行时存储空间
Java运行时内存可以分为栈区
,堆区
与方法区(非堆空间)
。
栈空间(Stack Space)
为线程的执行准备一段固定大小的存储空间,每个线程都有独立的线程栈空间,创建线程时就为线程分配栈空间。在线程栈中每调用一个方法就给方法分配一个栈帧
,栈帧用于存储方法的局部变量,返回值等私有数据, 即局部变量存储在栈空间中,基本类型变量也是存储在栈空间中,引用类型变量值也是存储在栈空间中,引用的对象存储在堆中。由于线程栈是相互独立
的,一个线程不能访问另外一个线程的栈空间,因此线程对局部变量以及只能通过当前线程的局部变量才能访问的对象进行的操作具有固定的线程安全性。
堆空间(Heap Space)
用于存储对象,是在JVM启动时分配的一段可以动态扩容的内存空间。创建对象时,在堆空间中给对象分配存储空间,实例变量就是存储在堆空间中的,堆空间是多个线程之间可以共享
的空间,因此实例变量可以被多个线程共享。多个线程同时操作实例变量可能存在线程安全问题。
非堆空间(Non-Heap Space)
用于存储常量,类的元数据等,非堆空间也是在JVM启动时分配的一段可以动态扩容的存储空间。类的元数据包括静态变量
,类有哪些方法
及这些方法的元数据(方法名,参数,返回值等)
。非堆空间也是多个 线程可以共享
的,因此访问非堆空间中的静态变量
也可能存在线程安全问题。
总结
- 栈空间是线程私有的存储空间,局部变量存储在栈空间中,局部变量具有固定的线程安全性;
- 堆空间与非堆空间是线程可以共享的空间,实例变量与静态变量是线程可以共享的,可能存在线程安全问题。
无状态对象
状态变量:实例变量与静态变量。
无状态对象(Stateless Object):如果一个类的同一个实例被多个线程共享并不会使这些线程存储共享的状态。
有状态对象:如果一个类的实例被多个线程共享会使这些线程存在共享状态。
实际上无状态对象就是不包含任何实例变量也不包含任何静态变量的对象;使用无状态对象可以避免在多个线程之间共享数据,实现线程安全。
不可变对象
不可变对象是指一经创建它的状态就保持不变的对象,不可变对象具有固有的线程安全性。当不可变对象现实实体的状态发生变化时,系统会创建一个新的不可变对象,就如String字符串对象。
不可变对象需要满足以下条件:
- 类本身使用
final
修饰,防止通过创建子类来改变它的定义; - 所有的字段都是
final
修饰的,final
字段在创建对象时必须显示初始化,不能被修改; - 如果字段引用了其他状态可变的对象(集合,数组),则这些字段必须是
private
私有的。
使用场景:
- 被建模对象的状态变化不频繁;
- 同时对一组相关数据进行写操作,可以应用不可变对象,既可以保障原子性也可以避免锁的使用;
- 使用不可变对象作为安全可靠的Map键,HashMap键值对的存储位置与键的hashCode()有关,如果键的内部状态发生了变化会导致键的哈希码不同,可能会影响键值对的存储位置。如果HashMap的键是一个不可变对象,则hashCode()方法的返回值恒定,存储位置是固定的。
线程特有对象
我们可以选择不共享非线程安全的对象
,对于非线程安全的对象,每个线程都创建一个该对象的实例,各个线程线程访问各自创建的实例,一个线程不能访问另外一个线程创建的实例。这种各个线程创建各自的实例,一个实例只能被一个线程访问的对象就称为线程特有对象。线程特有对象既保障了对非线程安全对象的访问的线程安全,又避免了锁的开销。线程特有对象也具有固有的线程安全性。
ThreadLocal类相当于线程访问其特有对象的代理,即各个线程通过ThreadLocal对象可以创建并访问各自的线程特有对象,泛型T
指定了线程特有对象的类型。一个线程可以使用不同的ThreadLocal实例来创建并访问不同的线程特有对象。
ThreadLocal实例为每个访问它的线程都关联了一个该线程特有的对象,ThreadLocal实例都有当前线程与特有实例之间的一个关联。
装饰器模式
装饰器模式可以用来实现线程安全,基本思想是为非线程安全的对象创建一个相应的线程安全的外包装对象,客户端代码不直接访问非线程安全的对象而是访问它的外包装对象。外包装对象与非线程安全的对象具有相同的接口,即外包装对象的使用方式与非线程安全对象的使用方式相同,而外包装对象内部通常会借助锁,以线程安全的方式调用相应的非线程安全对象的方法。
在java.util.Collections
工具类中提供了一组synchronizedXXX(xxx)
可以把不是线程安全的xxx集合
转换为线程安全的集合
,它就是采用了这种装饰器模式。这个方法返回值就是指定集合的外包装对象.这类集合又称为同步集合
。
使用装饰器模式的一个好处就是实现关注点分离
,在这种设计中,实现同一组功能的对象的两个版本:非线程安全的对象与线程安全的对象。对于非线程安全的在设计时只关注要实现的功能,对于线程安全的版本只关注线程安全性。
十、锁的优化及注意事项
多核CPU时代,多线程能明显提高效率;但是锁的不当使用,会让效率下降。
减少锁持有时间
对于使用锁进行并发控制的应用程序来说,如果单个线程特有锁的时间过长,会导致锁的竞争更加激烈,会影响系统的性能。在程序中需要尽可能减少线程对锁的持有时间,如下面代码:
减少锁持有时间有助于降低锁冲突的可能性,提升系统的并发能力。
优化前:
public synchronized void syncMethod(){
// 不需要进行同步,耗时长
othercode1();
// 需要同步,耗时短
mutexMethod();
// 不需要进行同步,耗时长
othercode();
}
优化后:
public void syncMethod(){
// 不需要进行同步,耗时长
othercode1();
synchronized (this) {
// 需要同步,耗时短
mutexMethod();
}
// 不需要进行同步,耗时长
othercode();
}
减小锁的粒度
一个锁保护的共享数据的数量大小称为锁的粒度。
- 粒度粗:锁保护的共享数据的数量大;
- 粒度细:锁保护的共享数据的数量小。
锁的粒度过粗会导致线程在申请锁时需要进行不必要的等待。
减少锁粒度是一种削弱多线程锁竞争的一种手段,可以提高系统的并发性。
使用读写分离锁代替独占锁
- 使用
ReadWriteLock
读写分离锁可以提高系统性能,使用读写分离锁也是减小锁粒度的一种特殊情况; - 能分割数据结构实现减小锁的粒度,那么读写锁是对系统功能点的分割。
在多数情况下都允许多个线程同时读,在写的使用采用独占锁,在读多写少的情况下,使用读写锁可以大大提高系统的并发能力。
锁分离
将读写锁的思想进一步延伸就是锁分离。
- 读写锁是根据读写操作功能上的不同进行了锁分离。
- 根据应用程序功能的特点,也可以对独占锁进行分离。
如java.util.concurrent.LinkedBlockingQueue类
中take()
与put()
方法分别从队头取数据,把数据添加到队尾。 虽然这两个方法都是对队列进行修改操作,由于操作的主体是链表,take()
操作的是链表的头部,put()
操作的是链表的尾部,两者并不冲突。如果采用独占锁的话,这两个操作不能同时并发,在该类中就采用锁分离,take()
取数据时有取锁,put()
添加数据时有自己的添加锁,这样take()
与put()
相互独立实现了并发。
粗锁化
为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短。但是凡事都有一个度,如果对同一个锁不断的进行请求,同步和释放,也会消耗系统资源。
public void method1(){
synchronized( lock ){
同步代码块1
}
synchronized( lock ){
同步代码块2
}
}
JVM在遇到一连串不断对同一个锁进行请求和释放操作时,会把所有的锁整合成对锁的一次请求,从而减少对锁的请求次数,这个操作叫锁的粗化,如上一段代码会整合为:
public void method1(){
synchronized( lock ){
同步代码块1
同步代码块2
}
}
在开发过程中,也应该有意识的在合理的场合进行锁的粗化,尤其在循环体内请求锁时,如:
for(int i = 0 ; i< 100; i++){
synchronized(lock){}
}
这种情况下,意味着每次循环都需要申请锁和释放锁,所以一种更合理的做法就是在循环外请求一次锁,如:
synchronized( lock ){
for(int i = 0 ; i< 100; i++){}
}
JVM锁优化
偏向锁(乐观锁)
偏向锁是一种针对加锁操作的优化,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无须再做任何同步操作,这样可以节省有关锁申请的时间,提高了程序的性能。
偏向锁在没有锁竞争的场合可以有较好的优化效果,对于锁竞争比较激烈的场景,效果不佳,锁竞争激烈的情况下可能是每次都是不同的线程来请求锁,这时偏向模式失效。
量级锁(乐观锁)
如果偏向锁失败,JVM不会立即挂起线程,还会使用一种称为轻量级锁的优化手段。会将对象的头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁。如果线程获得轻量级锁成功,就进入临界区。如果获得轻量级锁失败,表示其他线程抢到了锁,那么当前线程的锁的请求就膨胀为重量级锁。当前线程就转到阻塞队列中变为阻塞状态。
一个对象刚开始实例化时,没有任何线程访问它,它是可偏向的,即它认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程。偏向第一个线程,这个线程在修改对象头成为偏向锁时使用CAS操作,将对象头中ThreadId改成自己的ID,之后再访问这个对象时,只需要对比ID即可。 一旦有第二个线程访问该对象,因为偏向锁不会主动释放,所以第二个线程可以查看对象的偏向状态,当第二个线程访问对象时,表示在这个对象上已经存在竞争了,检查原来持有对象锁的线程是否存活,如果挂了则将对象变为无锁状态,然后重新偏向新的线程;如果原来的线程依然存活,则马上执行原来线程的栈,检查该对象的使用情况,如果仍然需要偏向锁,则偏向锁升级为轻量级锁。
轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对同一个锁的操作会错开,,或者稍微等待一下(自旋)另外一个线程就会释放锁。当自旋超过一定次数,或者一个线程持有锁,一个线程在自旋,又来第三个线程访问时,轻量级锁会膨胀为重量级锁,重量级锁除了持有锁的线程外,其他的线程都阻塞。
自旋锁
锁膨胀后,JVM为了避免线程在真实的层面被挂起,,JVM还会做最后的努力,这就是自旋锁。 当前线程无法立即获得锁,但是在什么时候可以获得锁也不一定,也许在几个CPU周期后就可以得到锁,如果是这样的话,简单的将线程挂起可能是一种得不偿失的操作。 因此JVM会进行一次赌注:JVM期望在不久的将来可以得到锁。因为JVM会让当前的线程做几个空循环,在经过若干次循环后,如果可以得到锁就进入临界区,如果还不能得到锁则将线程真实的挂起。
锁消除
锁消除是一种更彻底的锁优化,JVM在JIT编译时,会通过扫描上下文,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。
多线程开发良好的实践
- 给线程起个有意义的名字,这样可以方便找 Bug。
- 缩小同步范围,从而减少锁争用。例如对于 synchronized,应该尽量使用同步块而不是同步方法。
- 多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善。
- 使用 BlockingQueue 实现生产者消费者问题。
- 多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
- 使用本地变量和不可变类来保证线程安全。
- 使用线程池而不是直接创建线程,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。readLocal实例都有当前线程与特有实例之间的一个关联。