目录
九、多线程
1.概述
线程
多线程并行和并发的区别
Java程序运行原理
多线程为什么快
如何设置线程数比较合理
CPU密集型程序
I/O密集型程序
注意
线程的五种状态
新建状态(new)
就绪(runnable)
运行状态(running)
阻塞状态(blocked):
终止状态(terminated)
线程方法
2.多线程实现方式
多线程程序实现的方式1 继承Thread
多线程程序实现的方式2 实现Runnable接口
多线程的实现方式3,实现Callable接口
三种方式的区别
多线程的实现方式4,线程池
3.线程安全问题
4.线程同步
5.死锁
6.线程池
概述
使用线程池的好处
线程池体系结构
线程池参数理解
线程池工作原理
基本使用
其它的内置功能线程池
总结
线程池提交任务,任务代码抛出异常会怎么样
线程池两种提交任务方式的区别
7.ThreadLocal
概述
ThreadLocal与Synchronized的区别
ThreadLocal的简单示例
ThreadLocal常见使用场景
如何正确的使用ThreadLocal
内存泄漏原因
ThreadLocal哈希冲突解决方案:线性探测
如果想共享线程的 ThreadLocal 数据怎么办
8.CountDownLatch
概念
两种用法
9.CyclicBarrier
10.Semaphore
11.@Async-方法默认异步执行
九、多线程
1.概述
线程
线程是程序执行的一条路径,一个进程中可以包含多条线程
多线程并发执行可以提高程序的效率,可以同时完成多项工作
多线程并行和并发的区别
并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行,需要多核CPU的支持
并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行
比如跟两个人聊天,左手操作一个电脑跟甲聊,同时右手操作另一台电脑跟乙聊天,这就叫并行
如果先给甲发个消息,然后再给乙发个消息,然后再跟甲聊,再跟乙聊,这就叫并发
Java程序运行原理
Java命令会启动JVM(java虚拟机),即启动了一个进程,该进程会自动启动一个主线程,然后主线程去调用某个类的 main 方法
JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的,垃圾回收线程会和主线程并发执行
多线程为什么快
充分利用CPU和I/O
CPU密集型程序,I/O在短时间内完成,CPU有相对大量运算要处理
I/O密集型程序,CPU运算短时间完成,I/O相对占用更长时间
如何设置线程数比较合理
CPU密集型程序
单核CPU:由于每个线程都需要长时间占用CPU才能完成任务,所以单核CPU多线程对速度提升不明显
多核CPU:通常保持线程数=核心数+1,最大化利用CPU核心数,只要有需要就不让任何核心处于长期空闲,多开一条是为了保持异常情况下也能有任务补充进CPU
I/O密集型程序
单核CPU
由于每个线程在执行过程中CPU都存在较长空闲时间所以单核CPU多线程也能提升速度,显然,同样的任务,CPU空闲的越久,应开的线程就越多
假设CPU运算时长为X,I/O是CPU运算时间的两倍即2X,那么整个程序执行就是3X,那么为了3X时长内CPU尽可能的在运算而不是空闲,显然应开三条线程
多核CPU
和单核一样的逻辑,每多一个核心,多开单核情况的的一倍
通常保持线程数=核心数×(1/CPU利用率)=核心数×(1+IO时长/CPU运算时长)
这样的理论只是估算,实际执行速度受多方综合影响,实际上线程开多少条还是需要在估算值的基础上进行一定量的测试
注意
多线程会带来并发问题,如无必要,尽量不要使用多线程,尤其是存在共享数据时
线程的五种状态
新建状态(new)
使用 new 创建一个线程,仅仅只是在堆中分配了内存空间
新建状态下,线程还没有调用 start()方法启动,只是存在一个线程对象而已
Thread t = new Thread();//这就是t线程的新建状态
就绪(runnable)
新建状态调用 start() 方法,线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源
注意:线程对象只能调用一次 start() 方法,否则报错:illegaThreadStateExecptiong
运行状态(running)
线程获得CPU资源正在执行任务(run()方法)
阻塞状态(blocked):
正在运行的线程因为某种原因放弃 CPU,暂时停止运行,就会进入阻塞状态
此时 JVM 不会给线程分配 CPU,直到线程重新进入就绪状态,才有机会转到运行状态
注意:阻塞状态只能先进入就绪状态,不能直接进入运行状态
阻塞状态分为多种情况,举例:
同步锁:当线程 A 处于可运行状态中,试图获取同步锁时,发现锁已经被 B 线程获取,此时 JVM 把当前 A 线程放入锁池中,A线程进入阻塞状态
IO 请求:当线程处于运行状态时,发出了 IO 请求,此时线程进入阻塞状态
计时等待timed waiting:用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态
等待waiting:调用wait()方法(JVM 把该线程放入等待池中)(调用notify()方法回到就绪状态)
被另一个线程所阻塞:调用suspend()方法(调用resume()方法恢复),调用join()方法
终止状态(terminated)
通常称为死亡状态,表示线程终止
正常终止,执行完 run() 方法,正常结束
强制终止,如调用 stop() 方法或 destory() 方法
异常终止,执行过程中发生异常
线程方法
Thread.currentThread() 获取当前线程对象
getName() 获取线程名
setName(String name) 设置线程名
sleep(long millis) 线程休眠:让执行的线程暂停一段时间,进入计时等待状态
调用此方法后,当前线程放弃 CPU 资源,在指定的时间内,sleep 所在的线程不会获得可运行的机会,此状态下的线程不会释放同步锁
wait() 线程等待:一旦一个线程执行到wait(),就释放当前的锁
注意:此方法必须在同步代码块或同步方法中
sleep() 和 wait() 的区别:sleep指定时间内当前线程放弃 CPU 资源,线程不会释放同步锁,wait 会放弃 CPU 资源,同时也会放弃 同步锁
notify()/notifyAll() 唤醒:唤醒wait的一个或所有的线程
注意:此方法需和wait()成对使用,必须在同步代码块或同步方法中
join() 联合线程:表示这个线程等待另一个线程完成后(死亡)才执行,join 方法被调用之后,线程对象处于阻塞状态,写在哪个线程中,哪个线程阻塞(调用join()的线程运行)
这种也称为联合线程,就是说把当前线程和当前线程所在的线程联合成一个线程
yield() 礼让线程:表示当前线程对象提示调度器自己愿意让出 CPU 资源
调用该方法后,线程对象进入就绪状态,所以完全有可能:某个线程调用了 yield() 方法,但是线程调度器又把它调度出来重新执行
sleep() 和 yield() 方法的区别:
都能使当前处于运行状态的线程放弃 CPU资源,把运行的机会给其他线程
sleep 方法会给其他线程运行的机会,但是不考虑其他线程优先级的问题;yield 方法会优先给更高优先级的线程运行机会
调用 sleep 方法后,线程进入计时等待状态,调用 yield 方法后,线程进入就绪状态
setDaemon(Boolean boolean) 守护线程:设置一个线程为守护线程,该线程不会单独执行,当其他非守护线程都执行结束后,自动退出,当其他线程没有退出时,守护线程正常执行
volatile标志位 用来中断线程
interrupt中断机制 用来中断线程
2.多线程实现方式
多线程程序实现的方式1 继承Thread
定义线程类继承Thread,重写run方法
创建对象,start()开启新线程, 内部会自动执行run方法
每一个thread只能被start()一次
public static void main(String[] args) {
MyThread mt = new MyThread(); //4.创建自定义类的对象
mt.start(); //5.开启线程
//匿名内部类的方式
new Thread() { //1.new 类(){}继承这个类
public void run() { //2.重写run方法
for(int i = 0; i < 3000; i++) {//3.将要执行的代码,写在run方法中
System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
}
}
}.start();
}
class MyThread extends Thread { //1.定义类继承Thread
public void run() { //2.重写run方法
for (int i = 0; i < 3000; i++) { //3.将要执行的代码,写在run方法中
System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
}
}
}
多线程程序实现的方式2 实现Runnable接口
定义线程类实现Runnable接口,重写run方法;创建自定义的Runnable的子类对象,创建Thread对象, 传入Runnable对象;
调用start()开启新线程, 内部会自动调用Runnable的run()方法
原理:实际上是Thread的run()方法中调用了Runable的run()方法
public static void main(String[] args) {
MyRunnable mr = new MyRunnable(); //4.创建自定义类对象
//Runnable target = new MyRunnable();
Thread t = new Thread(mr); //5.将其当作参数传递给Thread的构造函数
t.start(); //6.开启线程
//匿名内部类的方式
new Thread(new Runnable(){ //1.new 接口(){}实现这个接口
public void run() { //2.重写run方法
for(int i = 0; i < 3000; i++) {//3.将要执行的代码,写在run方法中
System.out.println("bb");
}
}
}).start();
}
class MyRunnable implements Runnable { //1.自定义类实现Runnable接口
@Override
public void run() { //2.重写run方法
for (int i = 0; i < 3000; i++) { //3.将要执行的代码,写在run方法中
System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaaaaa");
}
}
}
多线程的实现方式3,实现Callable接口
定义线程类实现Callable接口,重写call(),call()可以有返回值
创建Callable接口的实现类的实例,使用FutureTask类包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
使用FutureTask对象作为Thread类的构造函数的target参数创建并启动线程
调用FutureTask对象的get()来获取子线程执行结束的返回值
这种方式实际上几乎不用,因为代码复杂,多线程程序一般也不需要获取返回值
public static void main(String[] args) {
Callable<Object> oneCallable = new Tickets<Object>();
FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable);
Thread t = new Thread(oneTask);
System.out.println(Thread.currentThread().getName());
t.start();
}
class Tickets<Object> implements Callable<Object>{
//重写call方法
@Override
public Object call() throws Exception {
System.out.println(Thread.currentThread().getName()+"-->我是通过实现Callable接口通过FutureTask包装器来实现的线程");
return null;
}
}
三种方式的区别
继承Thread
优势:简单,可以直接使用Thread类中的方法
缺点:线程类无法继承别的类,java的单继承机制
实现Runnable接口
优势:线程类可以有父类,避免了单继承的限制;可以被多个线程共享,适用于多个线程干一件的情况;线程池只能提交Runnable和Callable对象
缺点:代码相对复杂
实现Callable接口
优势:可以有返回值,可以抛出异常
缺点:代码复杂
多线程的实现方式4,线程池
3.线程安全问题
多线程并发操作同一数据时,有可能出现线程安全问题,即共享数据不可控,无法准确知道共享数据在某个时刻的值有没有发生变化
解决:控制线程同步,把用到共享数据的代码设置为同步的
public static void main(String[] args) {
new Thread() {
public void run() {
try {
while (ticktNum > 0) {
Thread.sleep(10);
System.out.println(getName() + "...这是第" + ticktNum + "号");
ticktNum--;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
new Thread() {
public void run() {
try {
while (ticktNum > 0) {
Thread.sleep(10);
System.out.println(getName() + "...这是第" + ticktNum + "号");
ticktNum--;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
//输出结果,卖出了同号票,这显然不对
Thread-1...这是第10号
Thread-0...这是第10号
Thread-1...这是第8号
Thread-0...这是第7号
Thread-0...这是第6号
Thread-1...这是第5号
Thread-0...这是第4号
Thread-1...这是第3号
Thread-0...这是第2号
Thread-1...这是第2号
Thread-0...这是第0号
4.线程同步
synchronized关键字
使用synchronized关键字加上一个任意的锁对象来定义一段代码,这就叫同步代码块
多个同步代码块如果使用相同的锁对象,那么他们就是同步的
使用synchronized关键字修饰一个方法,该方法中所有的代码都是同步的
非静态同步函数的锁是this,这个this指的是当前对象;静态的同步函数的锁是当前类的字节码对象
public static void main(String[] args) {
Object obj = new Object();
new Thread() {
public void run() {
try {
while (true) {
synchronized (obj) {
if (ticktNum <= 0) {
break;
}
Thread.sleep(1000);
System.out.println(getName() + "...这是第" + ticktNum + "号");
ticktNum--;
}
Thread.sleep(1000); //为了让另一个线程可以执行
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
new Thread() {
public void run() {
try {
while (true) {
synchronized (obj) {
if (ticktNum <= 0) {
break;
}
Thread.sleep(1000);
System.out.println(getName() + "...这是第" + ticktNum + "号");
ticktNum--;
}
Thread.sleep(1000); //为了让另一个线程可以执行
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
//输出结果
Thread-0...这是第10号
Thread-0...这是第9号
Thread-0...这是第8号
Thread-0...这是第7号
Thread-0...这是第6号
Thread-0...这是第5号
Thread-0...这是第4号
Thread-0...这是第3号
Thread-0...这是第2号
Thread-0...这是第1号
5.死锁
多线程同步的时候如果同步代码嵌套,使用相同锁,就有可能出现死锁
都在等待下个资源,但是下个资源已经被别的线程锁住了
private static String s1 = "1";
private static String s2 = "2";
public static void main(String[] args) {
new Thread() {
public void run() {
while(true) {
synchronized(s1) {
System.out.println(getName() + "...拿到" + s1 + "等待" + s2);
synchronized(s2) {
System.out.println(getName() + "...拿到" + s2 + "开吃");
}
}
}
}
}.start();
new Thread() {
public void run() {
while(true) {
synchronized(s2) {
System.out.println(getName() + "...拿到" + s2 + "等待" + s1);
synchronized(s1) {
System.out.println(getName() + "...拿到" + s1 + "开吃");
}
}
}
}
}.start();
}
6.线程池
概述
线程池里的每一个线程执行完任务后,并不会销毁,而是再次回到线程池中成为空闲状态,等待下一个对象来使用
JDK5以后不再需要手动编写线程池,提供了内置线程池
使用线程池的好处
降低资源消耗:创建和销毁线程需要消耗资源,线程池通过重复利用线程来减少这一过程的资源消耗
提高性能:当需要多线程执行任务时,无需等待线程创建即可执行,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池
方便管理线程:通过监控,调度,更合理的使用线程,利用系统资源
线程池体系结构
Executor:线程池顶级接口,JDK5增加,定义了一个执行无返回值任务的方法:void execute(Runnable command);
ExecutorService:线程池次级接口,对Executor做了一些扩展,增加了一些功能,这些功能就是线程池提供的服务,比如
关闭线程池:void shutdown();
执行有返回值的任务: Future submit(Runnable task, T result);
ScheduledExecutorService:线程池接口,extends ExecutorService,对ExecutorService做了一些扩展,增加了一些定时任务相关的功能
AbstractExecutorService:implements ExecutorService,抽象类,运用模板方法设计模式实现了一部分方法
Executors:线程池工具类,定义了一些快速实现线程池的方法
ThreadPoolExecutor:普通线程池类,包含最基本的一些线程池操作相关的方法实现
ScheduledThreadPoolExecutor:定时任务线程池类,用于实现定时任务相关的功能
ForkJoinPool:新型线程池类,Java7中新增的线程池类,基于工作窃取理论(WorkStealing)实现,运用于大任务拆分为小任务,任务很多的场景
线程池参数理解
corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收
maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞
keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收
unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)
workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中
基于阻塞队列实现的,即采用生产者消费者模式,需要实现 BlockingQueue 接口,Java 已经为我们提供了 7 种阻塞队列的实现
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)
LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,在未指明容量时,容量默认为 Integer.MAX_VALUE
PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现 Comparable 接口也可以提供 Comparator 来对队列中的元素进行比较。跟时间没有任何关系,仅仅是按照优先级取任务
DelayQueue:类似于PriorityBlockingQueue,是二叉堆实现的无界优先级阻塞队列。要求元素都实现 Delayed 接口,通过执行时延从队列中提取任务,时间没到任务取不出来
SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用 take() 方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用 put() 方法的时候也会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回
LinkedBlockingDeque: 使用双向队列实现的有界双端阻塞队列。双端意味着可以像普通队列一样 FIFO(先进先出),也可以像栈一样 FILO(先进后出)
LinkedTransferQueue: 它是ConcurrentLinkedQueue、LinkedBlockingQueue 和 SynchronousQueue 的结合体,但是把它用在 ThreadPoolExecutor 中,和 LinkedBlockingQueue 行为一致,但是是无界的阻塞队列
有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义
threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法,不指定时会使用默认线程工厂DefaultThreadFactory
handler(可选):拒绝策略。提交的任务数大于(workQueue.size() + maximumPoolSize ),触发拒绝策略
任务先申请核心线程,超过数量以后,放入阻塞队列,队列满了再申请额外线程,总线程数超过最大线程数,触发拒绝策略
需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor) 方法
Executors 框架已经为我们实现了 4 种拒绝策略
AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常
CallerRunsPolicy:由调用线程处理该任务
DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式
DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务
线程池工作原理
基本使用
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE,
TimeUnit.SECONDS,
sPoolWorkQueue,
sThreadFactory);
// 向线程池提交任务
threadPool.submit(new Runnable() {
@Override
public void run() {
... // 线程执行的任务
}
});
// 向线程池提交任务并执行
threadPool.execute(new Runnable() {
@Override
public void run() {
... // 线程执行的任务
}
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
其它的内置功能线程池
FixedThreadPool:定长线程池
特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
应用场景:控制线程最大并发数
使用:
// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务并执行
fixedThreadPool.execute(task);
ScheduledThreadPool:定时线程池
特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。
应用场景:执行定时或周期性的任务
使用:
// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务并执行
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
CachedThreadPool:可缓存线程池
特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
应用场景:执行大量、耗时少的任务
使用:
// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务并执行
cachedThreadPool.execute(task);
SingleThreadExecutor:单线程化线程池
特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等
使用:
// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务并执行
singleThreadExecutor.execute(task);
总结
Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
其实 Executors 的 4 个功能线程有如下弊端:
FixedThreadPool 和 SingleThreadExecutor:主要问题是堆积的请求处理队列均采用 LinkedBlockingQueue,可能会耗费非常大的内存,甚至 OOM-Out of Memory
CachedThreadPool 和 ScheduledThreadPool:主要问题是线程数最大数是 Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至 OOM
线程池提交任务,任务代码抛出异常会怎么样
当前线程会被回收,线程池会创建新的线程代替这个被回收的线程
线程池两种提交任务方式的区别
submit();execute()
提交任务的类型:既能提交Runnable类型任务也能提交Callable类型任务;只能提交Runnable类型的任务
异常的处理方式:会吃掉异常,可通过Future的get方法将任务执行时的异常重新抛出;会直接抛出任务执行时的异常
是否有返回值:有返回值,需要返回值的时候可以使用submit,通过Future.get()获取返回值;没有返回值
7.ThreadLocal
概述
ThreadLocal是与线程绑定的一个变量,可以看作线程的局部变量,用来实现多线程的数据隔离
ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量,对其他线程而言是隔离的,不存在多线程间共享的问题
一个线程可以拥有多个ThreadLocal
在JDK8+中每个Thread内部都维护了一个ThreadLocalMap的数据结构,内部是由内部类Entry组成的table数组,Entry的key就是ThreadLocal对象,而value则存放了对应的变量副本
因此获取ThreadLocal保存的副本信息,分为两步
第一步,根据线程获取 ThreadLocalMap
第二步,根据自身从 ThreadLocalMap 中获取值,所以它的 this 就是 Map 的 Key
ThreadLocal与Synchronized的区别
Synchronized用于线程间的数据共享;ThreadLocal则用于线程间的数据隔离
Synchronized是利用锁的机制,使代码在某一时该只能被一个线程访问,实现数据共享;ThreadLocal为每一个线程都提供了变量副本 ,使得每个线程在某一时间访问到的并不是同一个对象,实现数据隔离
ThreadLocal的简单示例
public class ThreadLocaDemo {
private static ThreadLocal<String> localVar = new ThreadLocal<String>();
static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " :" + localVar.get());
//清除本地内存中的本地变量
localVar.remove();
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
public void run() {
ThreadLocaDemo.localVar.set("local_A");
print("A");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
},"A").start();
Thread.sleep(1000);
new Thread(new Runnable() {
public void run() {
ThreadLocaDemo.localVar.set("local_B");
print("B");
System.out.println("after remove : " + localVar.get());
}
},"B").start();
}
}
// 结果
A :local_A
after remove : null
B :local_B
after remove : null
ThreadLocal常见使用场景
每个线程需要有自己单独的实例
实例需要在多个方法中共享,但不希望被多线程共享
具体的应用
ThreadLocal来存储Session
SimpleDateFormat线程安全问题
ThreadLocalRandom
项目中的应用:多数据源,多线程操作,将初始数据源放到ThreadLocal中,确保方法获取的初始数据源不受操作影响
如何正确的使用ThreadLocal
将ThreadLocal变量定义成private static,ThreadLocal就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值
static:ThreadLocal 能实现线程的数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap,所以ThreadLocal可以只实例化一次,只分配一块存储空间就可以了,没有必要作为成员变量多次被初始化
每次使用完ThreadLocal,都调用它的remove()方法,清除数据,否则会导致内存泄漏
内存泄漏原因
内存泄露的根本原因在于 ThreadLocalMap 的生命周期与当前线程 CurrentThread 的生命周期相同,且 ThreadLocal 使用完没有进行手动删除,导致只要CurrentThread仍在运行,那么虽然我们已经无法使用ThreadLocal的Entry,但Entry仍然存在于内存中,这与Entry的key和ThreadLocalMap的强弱引用无关,即无论强弱引用,都可能导致内存泄漏
直接原因:使用不规范,ThreadLocalRef 用完后 Entry 没有手动删除,ThreadLocalRef 用完后 CurrentThread 依然在运行就会导致内存泄漏
ThreadLocal哈希冲突解决方案:线性探测
和 HashMap 不同,ThreadLocalMap 结构中没有 next 引用,也就是说 ThreadLocalMap 中解决哈希冲突的方式并非链表的方式,而是采用线性探测的方式,当发生哈希冲突时就将步长加1或减1,寻找下一个相邻的位置
根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置 i;
如果当前位置是 null,就初始化一个 Entry 对象放在位置 i 上;
如果位置 i 已经有 Entry 对象了,如果这个 Entry 对象的 key 与即将设置的 key 相同,那么重新设置 Entry 的 value;
如果位置 i 的 Entry 对象和 即将设置的 key 不同,那么寻找下一个空位置
如果想共享线程的 ThreadLocal 数据怎么办
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值
private void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("主线程的ThreadLocal的值");
Thread t = new Thread() {
@Override
public void run() {
super.run();
Log.i( "我是子线程,我要获取其他线程的ThreadLocal的值 ==> " + threadLocal.get());
}
};
t.start();
}
8.CountDownLatch
概念
基于AQS共享模式的同步计数器,主要作用是使一个或一组线程在其他线程执行完毕之前,一直处于等待状态,直到其他线程执行完成后再继续执行
等待的线程通过latch.await()实现等待,计数器初始值为需要等待执行完毕的线程数,每个线程执行完就调用latch.countDown()让计数器减一
当计数器为零,在CountDownLatch上等待的线程即可继续执行
CountDownLatch不可重复使用,因为计数值只有初始化时才可以赋值
两种用法
1.一个线程等待多个线程执行完,比如主线程上有一个数据集合,需要多个子线程进行不同的操作,最后主线程返回数据
2.多个线程等待一个线程执行完,比如多个子线程都在等待一个数据,接收到数据后分别开始执行
//X代表待执行数量
final CountDownLatch latch = new CountDownLatch(X);
executor.execute(() -> {
...
// 计数器减一
latch.countDown();
}
executor.execute(() -> {
...
latch.countDown();
}
...
// 等待,直到计数器清零
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return ApiResult.ok();
9.CyclicBarrier
循环栅栏,通过 CyclicBarrier 可以实现一组线程之间的相互等待,当所有线程都到达屏障点之后再执行后续的操作
通过 await() 方法可以实现等待,当最后一个线程执行完,会使得所有在相应 CyclicBarrier 实例上的等待的线程被唤醒,而最后一个线程自身不会被暂停
CyclicBarrier 没有像 CountDownLatch 和 ReentrantLock 使用 AQS 的 state 变量,它是直接借助 ReentrantLock 加上 Condition 等待唤醒的功能进而实现
传入的值会赋值给 CyclicBarrier 内部维护的变量 count,同时也会赋值给 parties 变量(这是可以复用的关键)
线程调用 await() 表示线程已经到达栅栏,每次调用 await() 时,会将 count 减一,操作 count 值是直接使用 ReentrantLock 来保证线程安全性的,如果 count 不为 0,则添加到 condition 队列中,如果 count 等于 0,则把节点从 condition 队列中移除并添加到 AQS 队列中进行全部唤醒,并且将 parties 的值重新赋值给 count 从而实现复用
10.Semaphore
Semaphore 信号量,主要用于控制并发访问共享资源的线程数量
底层基于 AQS 共享模式,并依赖 AQS 的变量 state 作为许可证 permit,通过控制许可证的数量,来保证线程之间的配合
线程使用 acquire() 获取访问许可,只有拿到 “许可证” 后才能继续运行,当 Semaphore 的 permit 不为 0 的时候,对请求资源的线程放行,同时 permit 的值减1,当 permit 的值为 0 时,那么请求资源的线程会被阻塞直到其他线程释放访问许可,当线程对共享资源操作完成后,使用 release() 归还访问许可
不同于 CyclicBarrier 和 ReentrantLock,Semaphore 不会使用到 AQS 的 Condition 条件队列,都是在 CLH 同步队列中操作,只是当前线程会被 park。另外 Semaphore 是不可重入的
Semaphore 的公平和非公平两种模式
Semaphore 通过自定义两种不同的同步器(FairSync 和 NonfairSync)提供了公平和非公平两种工作模式,两种模式下分别提供了限时/不限时、响应中断/不响应中断的获取资源的方法(限时获取总是及时响应中断的),而所有的释放资源的 release() 操作是统一的。
公平模式:遵循 FIFO,调用 acquire() 方法获取许可证的顺序时,先判断同步队列中是不是存在其他的等待线程,如果存在就将请求线程封装成 Node 结点加入同步队列,从而保证每个线程获取同步状态都是按照先到先得的顺序执行的,否则对 state 值进行减操作并返回剩下的信号量
非公平模式:是抢占式的,通过竞争的方式获取,不管同步队列中是否存在等待线程,有可能一个新的获取线程恰好在一个许可证释放时得到了这个许可证,而前面还有等待的线程
11.@Async-方法默认异步执行
概述
spring提供的用于实现默认异步执行的注解
使用
配置类
配置TaskExecutor任务执行器,使用ThreadPoolTaskExecutor获取一个TaskExecutor
配置任务执行器AsyncConfigurer
注解
@EnableAsync 开启多线程执行,应用在springboot启动类上
@Async 声明异步方法,应用在异步方法上
@Scope 控制实例作用域,将方法所在类标记为多例
注意
1.@Async注解的方法会默认启用一个额外的线程执行方法,因为多线程是防注入的,所以不可以直接使用@Autowire获取的对象,需要通过参数传入
2.不可以new任务类对象来调用方法,这样会导致一直都是一个线程在执行任务,就是这个创建对象的线程
1.启动类上开启多线程接口调用
@SpringBootApplication
@EnableAsync
public class SampleController {
public static void main(String[] args) throws Exception {
SpringApplication.run(SampleController.class, args);
}
}
2.添加配置类,配置任务线程池,可以不配置,@Async会使用spring内置的默认线程池
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class AsyncTaskConfig implements AsyncConfigurer {
/**
* 核心线程数
*/
// @Value("${threadPool.corePoolSize}")
private int corePoolSize = 10;
/**
* 最大线程数
*/
private int maxPoolSize = 50;
/**
* 线程池缓冲队列容量
*/
private int queueCapacity = 10;
/**
* 空闲线程销毁前等待时长
*/
private int awaitTerminationSeconds = 10;
/**
* 线程名前缀
*/
private String threadNamePrefix = "Sample-Async-";
/**
* ThreadPoolTaskExcutor运行原理
* 当线程池的线程数小于corePoolSize,则新建线程入池处理请求
* 当线程池的线程数等于corePoolSize,则将任务放入Queue中,线程池中的空闲线程会从Queue中获取任务并处理
* 当Queue中的任务数超过queueCapacity,则新建线程入池处理请求,但如果线程池线程数达到maxPoolSize,将会通过RejectedExecutionHandler做拒绝处理
* 当线程池的线程数大于corePoolSize时,空闲线程会等待keepAliveTime长时间,如果无请求可处理就自行销毁
*/
@Override
@Bean
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
threadPool.setCorePoolSize(corePoolSize);
threadPool.setMaxPoolSize(maxPoolSize);
threadPool.setQueueCapacity(queueCapacity);
threadPool.setAwaitTerminationSeconds(awaitTerminationSeconds);
threadPool.setThreadNamePrefix(threadNamePrefix);
//关机时,是否等待任务执行完
threadPool.setWaitForTasksToCompleteOnShutdown(true);
//设置拒绝策略
//CALLER_RUNS:由调用者所在的线程执行该任务
threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//初始化线程
threadPool.initialize();
return threadPool;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}
3.标记所在类为多例,标记方法为多线程调用
import org.springframework.context.annotation.Scope;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
@Scope("prototype")
public class AsyncTaskTestService {
@Async
public void asyncTaskTest() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "aaaaaaaaaaaa");
}
}
}
测试
@Autowired
private AsyncTaskTestService asyncTaskTestService;
@PostMapping("/asyncTest")
@ApiOperation(value = "多线程测试")
public void asyncTest() {
for (int i = 0; i < 5; i++) {
//每调用一次,就会使用一个额外的线程执行任务
asyncTaskTestService.asyncTaskTest();
}
}