这一块知识,那真是有的啃了。
直接先看速成基础,再直接吃掉高频考点。
每个小知识点,直接看短视频,浅浅了解,在写下来就是自己的资料。
一个进程有多个线程,多个线程共享进程的堆和方法区,每个线程独有PC、VM Stack、NM Stack
程序计数器主要有俩个作用:
所以程序计数器私有主要是为了线程切换后能正确恢复到原来·的执行位置。
所以虚拟机栈与本地方法栈私有是为了保证线程中的局部变量不被其他线程访问到。
一个线程可以调用多个方法,而一个方法又可以被多个线程调用
CPU密集型线程主要进行计算和逻辑处理,需要占用大量CPU资源;IO密集型线程主要进行大量输入输出如读写文件、网络通信等,需等待IO设备相应,而不用一直占用CPU。
因此对于CPU密集型任务,那么开多线程需要频繁线程切换影响效率;对于IO密集型任务,开多线程会提高效率,当然也不能超过系统上限。
start()
。start()
等待运行的状态。正常横向过程:线程创建之后处于NEW状态,调用start()方法开始运行,这时处于READY状态,当此线程获得了CPU时间片后就处于RUNNING状态,执行完run()方法后就终止。
进入等待状态:线程执行wait()方法后,线程进入WAITING状态,依靠其他线程通知返回运行状态
进入超时等待状态:在等待基础上增加超市限制,sleep()、wait()执行后进入,超时时间结束后线程将返回RUNNING状态。
进入阻塞状态:进入synchronized方法或调用wait(),但锁被其他线程占有,线程就进入阻塞状态
上下文:线程在执行过程中的运行条件和状态信息。(比如程序计数器、栈信息等)
上下文切换:(保存 -> 加载) 当发生线程切换时,需要保存当前线程的上下文,留待下次线程占用CPU时恢复现场,并加载下一个将要占用CPU的线程的上下文。
上下文切换条件:
线程死锁:两个及以上线程在执行过程中,因争夺资源而造成互相等待的现象,无外力作用下这些线程将一直相互等待无法继续运行。
线程死锁四个条件:
如何避免线程死锁? -- 至少破坏死锁发生的一个条件
共同点:都是让线程阻塞等待
wait()是让获得对象锁的线程实现等待,自动释放当前线程占有的对象锁。每个Object类的对象都有对象锁,既然要释放当前线程占有的对象锁并让其进入WAITING状态,自然要操作对应的对象(Object)而不是当前的线程(Thread)
sleep()方法是让当前线程暂停执行,不涉及对象类,也不需要获得对象锁。
基本知识:start()方法会在新的线程中执行run() 方法对应的内容,run()方法只在当前线程中执行
NEW一个线程,线程进入了新建状态。调用start()方法,会启动该线程进入就绪状态,当分配到时间片后就可以执行。start()执行线程相应的准备工作,然后自动执行run()对应的内容,这是多线程工作。
但直接执行run()会把run()方法当成一个main线程下的普通方法执行,并不会在新建的线程中执行
并发带来的数据不一致问题:在并发环境下,多个线程会对同一个资源进行争抢,就会导致数据不一致的问题。
为了解决此问题,就引入锁机制。通过一种抽象的锁来对资源进行锁定。
对于线程私有的程序计数器、虚拟机栈、本地方法栈不存在数据竞争,数据能够保证正确性唯一性,是线程安全的。
但对于堆、方法区是线程共享的,就会存在线程安全问题,因此引入锁机制。
锁三大类型:
锁大致可以分为互斥锁、共享锁、读写锁(在读读下是共享锁,读写、写写下是互斥锁)
Java内存模型规定
在不同硬件厂商和不同操作系统下,内存的访问有一定的差异,会造成同一套代码运行在不同系统上会出现各种问题。所以JMM屏蔽掉各种硬件和操作系统的内存访问差异,以实现Java程序在各种平台下都能达到一致的并发效果。
指令重排序
指令重排序三种情况:
禁止重排序方式:
内存屏障:一种CPU指令。用来禁止处理器指令发生重排序,从而保障指令执行的有序性;在处理器写入、读取值之前将主内存的值写入高速缓存,清空无效队列,从而保障变量的可见性。
四大类内存屏障:
volatile关键字主要有两个作用:
1、原子性
一个或多个操作,要么全部执行且执行过程中不会被任何因素打断,要么就都不执行。
经典案例:银行转账问题,涉及A账户减少,B账户增加,要么同时执行,要么都不执行,整个转账过程算做一个原子操作。
实现原子性方式:
synchronized、Lock以及各种原子类。
synchronized和Lock可以保证任意时刻只有一个线程访问该代码块,因此可保证原子性;各种原子类是利用CAS操作来保证原子操作。
2、可见性
当一个线程对主内存中的共享变量进行了修改,其他线程可立即看到修改后的最新值。
实现可见性方式:
借助synchronized、volatile以及各种Lock。
volatile修饰的变量,当一个线程改变了该变量的值,其他线程是立即可见的。普通变量则需要重新读取才能获得最新值。
3、有序性
Java内存模型中,允许编译器和处理器对指令进行重排序。重排序不会影响单线程程序的执行,却会影响多线程并发执行的正确性。
实现有序性方式:
synchronized、Lock、volatile关键字。
synchronized和Lock保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。
read后必须load;store后必须write
Java内存结构和Java虚拟机运行时区域有关,定义了JVM运行时如何分区存储程序数据。
Java内存模型与Java并发编程相关,抽象了线程和主内存的关系。目的是简化多线程编程增强程序可移植性。
意义:前一个操作的结果对后一个操作是可见的,无论俩个操作是否在同一个线程。
定义:
常见规则:
start()
方法 happens-before 于此线程的每一个动作。如果两个操作不满足上述任意一个 happens-before 规则,那么这两个操作就没有顺序的保障,JVM 可以对这两个操作进行重排序。
synchronized理解为加锁,而不是锁。这样更好理解线程同步。
synchronized就是同步的意思,是Java中一个关键字。主要用于解决多线程之间访问资源的同步性,保证被他修饰的方法或代码块在任意时刻只有一个线程执行。
Synchronized通过使用内置锁、与对象关联的锁、可重入性以及内存屏障等机制来实现线程的同步和锁的管理,以保证对共享资源的访问具有互斥性和可见性。
synchronize会根据锁竞争情况,从偏向锁-->轻量级锁-->重量级锁升级
普通方法 :锁对象是this,所谓的方法锁(本质上属于对象锁)
也就是多个线程访问方法say()会有锁的限制
public synchronized void say(){ //对方法say()加锁
System.out.println("Hello,everyone...");
}
同步代码块(方法中):锁对象是synchronized(obj)的对象,所谓的对象锁
public void say(boolean isYou){ //对对象obj加锁
synchronized (obj){
System.out.println("Hello");
}
}
同步静态方法:锁对象是当前类的Class对象,即(XXX.class),所谓的类锁
public static synchronized void work(){ //对类work加锁
System.out.println("Work hard...");
}
乐观锁和悲观锁是两种思想,用于解决并发场景下的数据竞争问题。
悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)
悲观锁:悲观锁在操作数据时比较悲观,认为别人会同时修改数据。因此操作数据时直接把数据锁住,直到操作完成后才会释放锁;上锁期间其他人不能修改数据。
Java 中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
public void performSynchronisedTask() {
synchronized (this) {
// 需要同步的操作
}
}
private Lock lock = new ReentrantLock();
lock.lock();
try {
// 需要同步的操作
} finally {
lock.unlock();
}
### 对象结构
对象结构:对象头 + 实例数据 + 对齐填充字节
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了。
版本号机制
读取version -> 操作 -> 验证当前version ->新旧同则更新,不同则重试
一般是在数据表中加上一个数据版本号 version
字段,表示数据被修改的次数。当数据被修改时,version
值会加一。
当线程 A 要更新数据值时,在读取数据的同时也会读取 version
值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 version
值相等时才更新,否则重试更新操作,直到更新成功
Compare And Swap本质:用一个预期值与要更新的变量值比较,两值相等才会更新。
CAS是一个原子操作,底层依赖于一条CPU的原子指令。
(原子操作:最小不可拆分操作,操作一旦开始,就不能被打断,直到操作完成)
CAS涉及到三个操作数:
当且仅当V的值等于E时,CAS通过原子方式用新值N来更新变量值V。如果不等,说明已经有其他线程更新了变量值V,则当前线程放弃更新。
ABA问题
如果一个变量V初次读取值为A,在准备赋值时检查到仍然是A,在这期间不能保证他的值是否被修改过最后又修改成了A。
解决ABA方式:在变量前追加版本号或时间戳。AtomicStampedReference类的compareAndSet()方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志。全部相等则以原子方式将该变量和标志的值设置为给定的更新值。
循环时间开销大
CAS使用自旋操作来重试,若长时间不成功会给CPU带来很大的执行开销。
解决循环时间开销大:若JVM能支持处理器提供的pause指令则效率会有一定的提升。
pause作用:
只能保证一个共享变量的原子操作
CAS只对单个共享变量有效,当操作涉及跨多个共享变量时CAS无效。
解决方式:引入AtomicReference类保证引用对象之间的原子性
使用锁或利用AtomicReference类把多个共享变量合并成一个共享变量来操作。
池化技术:通过复用对象、连接等资源,减少创建对象/连接,降低垃圾回收(GC)的开销,适当使用池化相关技术能够显著提高系统效率,优化性能。
线程池管理线程好处:
当一个并发任务提交给线程池,线程池分配线程去执行任务:(三阶段)
1、先判断线程池中核心线程池所有线程是否都在执行任务。没有则新创建一个线程执行刚提交的任务,否则,核心线程池中所有线程都在执行任务,进行下一步判断;
2、判断当前阻塞队列是否已满,未满则将提交的任务添加到阻塞队列中,否则进行下一步判断;
3、判断线程池中所有线程是否都在执行任务,没有则创建一个新的线程执行任务,否则交给饱和策略处理。
源码流程:
创建线程池主要是ThreadPoolExecutor类完成,ThreadPoolExecutor的构造方法为:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:表示核心线程池的大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到corePoolSize,则会创建新的线程来执行所提交的任务,即使当前核心线程池有空闲的线程。如果当前核心线程池的线程个数已经达到了corePoolSize,则不再重新创建线程。如果调用了
prestartCoreThread()
或者prestartAllCoreThreads()
,线程池创建的时候所有的核心线程都会被创建并且启动。- workQueue:阻塞队列。用于保存任务的阻塞队列,可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。
- maximumPoolSize:表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过maximumPoolSize的话,就会创建新的线程来执行任务。
- keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。
- unit:时间单位。为keepAliveTime指定时间单位。
- threadFactory:创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。
- handler:饱和策略。当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。
ThreadPollExecutors三个重要的参数为corePoolSize、workQueue、maximumSize,这三个参数基本决定了线程池对于任务的处理策略。
ThredPoolExecutors提供了Set方法动态修改线程池参数
没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 ResizableCapacityLinkedBlockIngQueue
的队列(主要就是把LinkedBlockingQueue
的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。
不同的线程池会选用不同的阻塞队列,主要有无界队列、同步队列、延迟阻塞队列
ThreadPoolExecutor主要有以下四种方法处理线程饱和:
任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量(N+1)。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程(默认2N,N为CPU 核心数)。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()
方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。
使用jdk自带的Executors创建线程池,存在资源耗尽的风险。
FixedThreadPool
和 SingleThreadExecutor
:使用的是无界的LinkedBlockingQueue
,任务队列最大长度为 Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。CachedThreadPool
:使用的是同步队列 SynchronousQueue
, 允许创建的线程数量为 Integer.MAX_VALUE
,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。ScheduledThreadPool
和 SingleThreadScheduledExecutor
: 使用的无界的延迟阻塞队列DelayedWorkQueue
,任务队列最大长度为 Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。主要有俩种:
一、ThreadPoolExecutor构造函数:最原始的创建线程池的⽅式,它包含了 7 个参数可供设置
二、Execute框架的Executors工具类
俩个方法:shutdown和shutdownNow
共同点:遍历线程池中所有线程,依次中断线程
不同点:
绝对的好文章
如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。
Java线程池实现原理及其在美团业务中的实践 - 美团技术团队
AQS 就是一个抽象类,java.util.concurrent.locks
包下,主要用来构建锁和同步器
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {}
定义了一套多线程访问共享资源的同步器框架
AQS维护一个volatile int state变量 + CLH虚拟双向队列
state变量:
state
可以通过 protected
类型的getState()
、setState()
和compareAndSetState()
进行操作。并且,这几个方法都是 final
修饰的,在子类中无法被重写。CLH虚拟双向队列:
举例:
- 以
ReentrantLock
为例,state
初始值为 0,表示未锁定状态。A 线程lock()
时,会调用tryAcquire()
独占该锁并将state+1
。此后,其他线程再tryAcquire()
时就会失败,直到 A 线程unlock()
直到state=
0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state
会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。- 再以
CountDownLatch
以例,任务分为 N 个子线程去执行,state
也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后countDown()
一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即state=0
),会unpark()
主调用线程,然后主调用线程就会从await()
函数返回,继续后续动作。
排他锁:ReentrantLock重入锁
共享锁:CountDownLatch、Semaphore
也就是资源共享方式:独占(Exclusive)、共享(Share)
AQS 是多线程同步器,它是 J.U.C 包中多个组件的底层实现,如 Lock、 CountDownLatch、Semaphore 等都用到了 AQS. 从本质上来说,AQS 提供了两种锁机制,分别是排它锁和共享锁。 排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该 共享资源,也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的 ReentrantLock 重入锁实现就是用到了 AQS 中的排它锁功能。 共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如 CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能。
设计AQS整个体系需要解决的三个核心的问题:①互斥变量的设计以及多线程同时更新互斥变量时的安全性②未竞争到锁资源的线程的等待以及竞争到锁资源的线程释放锁之后的唤醒③锁竞争的公平性和非公平性。
AQS采用了一个int类型的互斥变量state用来记录锁竞争的一个状态,0表示当前没有任何线程竞争锁资源,而大于等于1表示已经有线程正在持有锁资源。一个线程来获取锁资源的时候,首先判断state是否等于0,如果是(无锁状态),则把这个state更新成1,表示占用到锁。此时如果多个线程进行同样的操作,会造成线程安全问题。AQS采用了CAS机制来保证互斥变量state的原子性。未获取到锁资源的线程通过Unsafe类中的park方法对线程进行阻塞,把阻塞的线程按照先进先出的原则加入到一个双向链表的结构中,当获得锁资源的线程释放锁之后,会从双向链表的头部去唤醒下一个等待的线程再去竞争锁;另外关于公平性和非公平性问题,AQS的处理方式是,在竞争锁资源的时候,公平锁需要判断双向链表中是否有阻塞的线程,如果有,则需要去排队等待;而非公平锁的处理方式是,不管双向链表中是否存在等待锁的线程,都会直接尝试更改互斥变量state去竞争锁。
Synchronized、ReentrantLock均一次只允许一个线程访问某个资源。使用比较单一。主要讨论的就是这三个同步工具类/共享锁:Semaphore、CountDownLatch、CyclicBarrier。
作用:控制同时访问特定资源的线程数量。
//初始共享资源的线程数量为5
final Semaphore semaphore = new Semaphore(5);
//获得1个许可
semaphore.acquire();
//释放1个许可
semaphore.release();
假设有N个线程来获取Semaphore中的共享资源,上面代码中只有5个线程能获取到共享资源,其他线程都会阻塞,只有获取到共享资源的线程才能继续执行。等到有线程释放了共享资源,其他阻塞的线程才能获取到。
当初始的线程资源个数为1时,Semaphore退化为排他锁。
使用场景:
Semaphore
通常用于那些资源有明确访问数量限制的场景比如限流(仅限于单机模式,实际项目中推荐使用 Redis +Lua 来做限流)。
Semaphore有俩种模式:
Semaphore是共享锁的一种实现。默认构造AQS的state值为permits,可将permits值理解为许可证数量,只有获得许可证的线程才能执行。
调用semaphore.acquire(),线程尝试获取许可证:
调用semaphore.release(),线程尝试释放许可证:
面试:
Semaphore
是共享锁的一种实现。它默认构造 AQS 的 state
为 permits
。当执行任务的线程数量超出 permits
,那么多余的线程将会被放入等待队列 Park
,并自旋判断 state
是否大于 0。只有当 state
大于 0 的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行 release()
方法,release()
方法使得 state 的变量加 1,那么自旋的线程便会判断成功。 如此,每次只有最多不超过 permits
数量的线程能自旋成功,便限制了执行任务线程的数量。
实现一个或多个线程等待其他线程完成某个操作后再继续执行。
CountDownLatch
允许 count
个线程阻塞在一个地方,直至所有线程的任务都执行完毕。
CountDownLatch
是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch
使用完毕后,它不能再次被使用。
共享锁的一种实现。默认构造AQS的state值为count。
当线程使用countDown()方法时,本质使用了tryReleaseShared()方法以CAS的操作来减少state,直至state为0.
当调用await()方法时,如果state不为0,说明任务还没有执行完毕,await()会一直阻塞,即await()方法后的语句都不会被执行。
直到count个线程调用了countDown()是state值减为0,或者await()线程被中断,该线程才会从阻塞中被唤醒,await()之后的语句得到执行。
CountDownLatch
的计数器初始化为 n (new CountDownLatch(n)
),每当一个任务线程执行完毕,就将计数器减 1 (countdownlatch.countDown()
),当计数器的值变为 0 时,在 CountDownLatch 上 await()
的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。CountDownLatch
对象,将其计数器初始化为 1 (new CountDownLatch(1)
),多个线程在开始执行任务前首先 coundownlatch.await()
,当主线程调用 countDown()
时,计数器变为 0,多个线程同时被唤醒。当计数器的值还未到达时,线程都会阻塞等待,直到达到设定的数值,所有的线程都会被释放
让一组线程都到达屏障(同步点)之后,屏障才会打开,所有被拦截的线程才会工作。
基于ReentrantLock和Condition实现
CyclicBarrier
内部通过一个 count
变量作为计数器,count
的初始值为 parties
属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减 1。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。
1、触发条件不同
CyclicBarrier在等待的线程数量达到指定值时,会触发一个屏障操作,所有的线程都会被释放
CountDownLatch通过计数器来触发等待操作,计数器的初始值为等待的线程数量,每当一个线程完成任务后,计数器减1,直到计数器为0时,所有等到的线程将被释放
2、重用性不同
CyclicBarrier可以被重用。可以通过reset()方法重置CyclicBarrier的状态
CountDownLatch不能被重用。一旦计数器减为0就不能再使用
3、线程协作方式不同
CyclicBarrier适合在一组线程相互等待达到共同的状态然后同时开始或继续执行后续操作,并且可以额外设置一个Runnable参数,当一组线程达到屏障点后可以优先触发。
CountDownLatch适用于一个或多个线程等待其他线程执行完某个操作后再继续执行。
ThreadLocal:是一种隔离机制方法
Thread:是类名
ThreadLocalMap:是一种数据结构,定制化HashMap
ThreadLocal是一种基于共享变量副本的隔离机制,保证多线程环境下对共享变量修改的安全性。
在多线程访问共享变量场景中,一般解决办法是对共享变量加锁,从而保证在同一时刻只有一个线程能对共享变量进行更新。弊端:加锁会造成性能下降。
ThreadLocal使用了空间换时间的思想:也就是在每个线程内都有一个容器来存储共享变量的副本,每个线程只对自己的共享变量副本做更新操作。
优点:1、解决了线程安全问题;2、避免了多线程竞争加锁的开销
共享变量的副本存储在Thread类里面的成员变量ThreadLocalMap(可看做定制的HashMap)
Thread类源码
public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}
ThreadLocal数据结构:
1、线程上下文传递:在跨线程调用的场景中,可使用ThreadLocal在不修改方法签名的情况下传递线程上下文信息。比如框架和中间件的请求头中的用户信息、请求ID等存储在ThreadLocal中,在后续的请求链路中,都可以方便的访问这些信息。
2、数据库连接管理:在使用数据库连接池的情况下,可以将数据库的连接信息存储在ThreadLocal中,每个线程可以独立管理自己的数据库连接,避免了线程之间的竞争和冲突。比如MyBatis中的sqlSession对象使用了ThreadLocal存储当前现成的数据库会话信息。
3、事务管理:在需要手动管理事务的场景中,可使用ThreadLocal存储事务的上下文,每个线程独立的控制自己的事务,保证事务的隔离性
内存泄漏:对象或变量占用的内存不会再被使用也不能被回收
强引用:一个对象具有强引用,不会被垃圾回收器回收。当内存不足,JVM抛出OOM也不会回收强引用对象。显式的将引用赋值为null,JVM合适时间可以回收。
弱引用:JVM垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
static class Entry extends WeakReference{}
GC垃圾回收机制-JVM如何找到需要回收的对象:
1、引用计数法:每个对象有一个引用计数属性,新增引用+1,释放引用-1,计数为0可以回收
2、可达性分析法:从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的,JVM则判断是可回收对象。
引用计数法存在两个对象互相引用永远无法回收的问题。
ThreadLocal内存泄漏原因:
ThreadLocalMap使用ThreadLocal的弱引用作为Entry数组的Key,如果ThreadLocal不存在外部强引用时,Key就会被GC回收,这样就会导致ThreadLocalMap的Key为null,而value还存在强引用。只有thread线程退出后,value的强引用链条才会断掉。如果当前线程一直不结束,Key为null的Entry的value就会一直存在一条强引用连无法回收,造成内存泄漏。
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
如何避免内存泄漏?
1、每次使用完ThreadLocal,调用remove方法清除数据
2、ThreadLocal变量设置为static final,共用一个ThreadLocal,保证强引用
3、ThreadLocal内部优化:在我们调用set方法时,进行全量清理,清理出Key为null的值,扩容也会继续检查。
### Future类作用?
作用:获取异步任务执行后的结果。
大白话:有一个任务不需要立刻获得结果,因此采用异步方式让子线程去执行该任务,继续做主要的任务,之后直接通过future获取最后执行的结果。
Future接口包含五个方法:
// V 代表了Future执行的任务返回值的类型
public interface Future {
// 取消任务执行
// 成功取消返回 true,否则返回 false
boolean cancel(boolean mayInterruptIfRunning);
// 判断任务是否被取消
boolean isCancelled();
// 判断任务是否已经执行完成
boolean isDone();
// 获取任务执行结果
V get() throws InterruptedException, ExecutionException;
// 指定时间内没有返回计算结果就抛出 TimeOutException 异常
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutExceptio
}
FutureTask实现Future接口,封装了Callable、Runnable,具有任务取消、查看、任务是否执行完成以及获取任务执行结果的方法。
FutureTask
实现类有两个构造函数,可传入 Callable 或者 Runnable
对象。实际上,传入 Runnable
对象也会在方法内部转换为Callable 对象。
封装Callable,管理着任务执行的情况,存储了 Callable 的 call
方法的任务执行结果。
Future局限性如:不支持异步任务的编排组合、获取结果的get()为阻塞调用
CompletableFuture实现俩接口:Future、CompletionStage
public class CompletableFuture implements Future, CompletionStage {}
面试满分回答: 定义 + 五种异步回调方式
CompletableFuture是JDK1.8里面引入的一个基于事件驱动的异步回调类,CompletableFuture弥补了原本Future的不足,使得程序可以在非阻塞的状态下完成异步的回调机制。
简单来说,就是当使用异步线程去执行一个任务时,希望在任务结束以后触发一个后续的动作。
举个简单的例子,比如在一个批量支付的业务逻辑里面,涉及到查询订单、支付、发送邮件通知这三个逻辑。
使用Future的话,这三个逻辑是按照顺序同步去实现的,也就是先查询到订单以后,再针对这个订单发起支付,支付成功以后再发送邮件通知。
这种设计方式导致这个方法的执行性能比较慢。
可以直接使用CompletableFuture,(如图),也就是说把查询订单的逻辑放在一个异步线程池里面去处理。然后基于CompletableFuture的事件回调机制的特性,可以配置查询订单结束后自动触发支付,支付结束后自动触发邮件通知。
CompletableFuture提供了5种不同的方式,把多个异步任务组成一个具有先后关系的处理链,然后基于事件驱动任务链的执行。
原子类:具有原子操作特征的类。
作用:和锁类似,是为了保证并发情况下的线程安全。
原子操作:不可拆分的操作。一旦执行不可中断,要么全部执行要么不执行。
根据操作的类型,JUC包中共有4种类型原子类:
1、基本类型
使用原子的方式更新基本类型
AtomicInteger
:整型原子类AtomicLong
:长整型原子类AtomicBoolean
:布尔型原子类2、数组类型
使用原子的方式更新数组里的某个元素
AtomicIntegerArray
:整型数组原子类AtomicLongArray
:长整型数组原子类AtomicReferenceArray
:引用类型数组原子类3、引用类型
AtomicReference
:引用类型原子类AtomicMarkableReference
:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来AtomicStampedReference
:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。4、对象的属性修改类型
AtomicIntegerFieldUpdater
:原子更新整型字段的更新器AtomicLongFieldUpdater
:原子更新长整型字段的更新器AtomicReferenceFieldUpdater
:原子更新引用类型里的字段粒度更细
原子变量可以把竞争范围缩小到变量级别,通常情况下锁的粒度大于原子变量的粒度
效率更高
除了在高并发之外,使用原子类的效率往往比使用同步互斥锁的效率更高,因为原子类底层利用了CAS,不会阻塞线程。