对线面试官
目录
- 前言
- 注解
- 泛型
- NIO
- 组成
- io复用
- select
- epoll
- 零拷贝
- 反射
- 动态代理
- JDK
- CGLIB
- 场景
- 多线程
- 场景
- 线程安全
- 死锁
- CAS
- ABA问题
- 解决
- 其他
- synchronized
- 原理
- Mark Word
- monitor
- 优化
- 1.6-
- 1.6+
- 偏向锁
- 轻量级锁
- 重量级锁
- 小结
- AQS&ReentrantLock
- 公平锁
- 非公平锁
- AQS
- ReentrantLock
- 线程池
- ThreadLocal
- Java中四种引用
- 强引用
- 软引用
- 弱引用
- 虚引用
- Q:为什么不将ThreadLocalMap的key设为强引用?
- Q:ThreadLocal内存泄露?
前言
本文内容提炼自微信公众号:对线面试官
注解
代码中的特殊标记
可在编译、类加载或运行时被读取,并执行相应操作
例如:
Spring的@Controller、@Param、@Select
Lombok的@Slf4j、@Data
Java的@Override、@Deprecated、@FunctionalInterface
Java原生注解大多用于标记和检查
Java原生还有元注解,用于修饰注解,例如@Retention和@Target
@Retention用于设置注解的生命周期(SOURCE、CLASS、RUNTIME)
@Target用于设置注解的修饰对象(TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE、ANNOTATION_TYPE、PACKAGE、TYPE_PARAMETER、TYPE_USE)
.java -> .class过程有个注解抽象语法树
若想在编译期处理注解,需要将注解的@Retention设为SOURCE和CLASS,并且需继承AbstractProcessor类并重写process方法(lombok的AnnotationProcessor便是如此)
上述两级别加载到jvm中后,注解将被抹除
自定义注解@Retetion一般设为RUNTIME级别,配合反射机制获取到运行时信息,并处理
泛型
在创建对象或调用方法时才明确下具体的类型
好处:
无需强制转换,代码更加简洁
程序更加健壮,编译器无警告,运行期不会抛ClassCast Exception异常
应用场景:集合
写基础组件时,可利用泛型保证组件的通用性
NIO
始于JDK1.4
出现是为了提高速度
non-blocking io / new io
传统io是以字节为单位处理,nio是以块为单位处理
传统io只能是阻塞的,而nio可做到非阻塞
组成
buffer(缓冲区):存数据
channel(管道):运数据
selector(选择器):检查channel状态
Unix下IO模型有:
阻塞io、非阻塞io、io复用、信号驱动和异步io
io复用
以Linux系统为例,其对文件的操作是通过文件描述符(fd)
io复用模型:通过一个进程监听多个fd,一旦某个fd就绪,就去通知程序可以往下处理
linux下io复用模型用的函数有select/poll和epoll
select
最大连接数为1024或2048
传入fd_set参数,只能是1024或2048(具体看操作系统位数)
fd_set底层为bitmap,0表示数据未到缓冲区,1表示数据已到达
select会不断遍历fd_set,判断标志位变更情况,若有变化通知程序往下处理
epoll
linux2.6内核正式提出,完善了select一些缺点
它定义了epoll_event结构体,不存在最大连接数的限制
epoll不再遍历所有fd,它专门为就绪的fd维护了一片空间,每次从中获取使用
零拷贝
以读请求为例,会调用read相关的系统函数,用户态->内核态
cpu让dma去磁盘把数据拷贝到内核空间
等内核缓冲区有数据后,cpu会把内核缓冲区数据拷贝到用户缓冲区
最终用户程序可以获取到数据
零拷贝将黄字过程省略,相当于磁盘数据直接拷贝到用户程序中,由此提高了效率和性能
常见零拷贝技术:
mmap(内核缓冲区与用户缓冲区共享)、sendfile(系统底层函数支持)
反射
通过反射可以在运行时获取类的信息,使代码更具通用性和灵活性
运行时指jvm加载.class运行的时期
Q:既然泛型会擦除,那为什么反射能获取到泛型的信息呢?
A:泛型虽然会擦除,但擦除是有范围的,定义在类上的泛型信息是不会被擦除的,Java编译器仍在class文件以Signature属性的方式保留了泛型信息
动态代理
代理模式的一种
代理模式有静态代理和动态代理
静态代理需要自己写代理类,实现对应的接口,比较麻烦
在Java中,动态代理有两种实现方式:JDK和CGLIB
JDK
JDK动态代理使用了反射机制
JDK动态代理会帮我们实现接口的方法,通过invokeHandler对需要增强的方法进行增强
CGLIB
CGLIB代理使用了ASM框架,通过修改其字节码生成子类
场景
Mybatis不用写实现类,只写接口就能执行SQL
Spring AOP
多线程
进程是资源分配的基本单位
线程是资源调度的基本单位
场景
Tomcat会从线程连接池取线程去处理请求
数据库连接池Druid、C3P0、DBCP
比如现在要跑一个定时任务,该任务的链路执行时间和过程都非常长,那么就用一个线程池处理该定时任务,提高了系统的吞吐量
线程安全
多个线程去执行某类,这个类始终能表现出正常的行为,那么这个类是线程安全的
解决线程安全问题的思路:
1.保证原子性,考虑atomic包
2.保证操作可见性,考虑volatile关键字
3.涉及对线程的控制,考虑CountDownLatch、Semaphore等并发工具类
4.涉及集合,考虑java.util.concurrent包下的集合类
5.若synchronized无法满足需求,考虑lock包下的类
确实存在线程安全问题 -> 上述方法选取
无脑使用synchronized会影响程序性能
死锁
原因:当前线程拥有其他线程需要的资源,当前线程等待其他线程拥有的资源,大家都不释放自己拥有的资源
死锁避免方案:
1.固定加锁的顺序,例如使用hash值的大小来确定加锁的先后
2.减小锁的粒度
3.使用可释放的定时锁
CAS
compare and swap,比较并交换,原子性操作
CAS有三个操作数:当前值A、内存值V、要修改的新值B
若A与V相等,将V改成B;若A与V不等,重试或放弃
synchronized每次只会让一个线程去操作共享变量,而CAS相当于没有加锁,多个线程可以直接操作共享资源,在实际去修改的时候才去判断能否修改成功
ABA问题
假设线程A读到当前值是10,可能线程B把值修改为100,然后线程C又把值修改为10,等到线程A拿到执行权时,因为当前值和内存值是一致的,线程A认为值自始至终没有改变,符合修改条件
解决
Java提供AtomicStampedReference,对比内存值和版本是否一致
其他
阿里巴巴开发手册推荐使用LongAdder对象,它减少了乐观锁的重试次数,比AtomicLong性能更好
AtomicLong做累加时是多个线程操作同一个目标资源,在高并发下,只有一个线程会执行成功,其他线程都会失败并不断自旋(重试),自旋将成为瓶颈
而LongAdder将目标资源分散到数组Cell中,每个线程对自己的Cell变量的value进行原子操作,大大降低了失败次数
synchronized
它是一个Java的关键字,能够将代码块或方法锁起来
互斥锁,一次只能允许一个线程进入锁住的代码块
若其修饰实例方法,对应的锁是对象实例
若其修饰静态方法,对应的锁是当前类的Class实例
若其修饰代码块,对应的锁是传入synchronized的对象实例
原理
通过反编译可知
当其修饰方法时,编译器将生成ACC_SYNCHRONIZED关键字作为标识
当其修饰代码块时,其底层是monitorenter和monitorexit指令
Mark Word
在内存中,对象一般由三部分组成:对象头、对象实际数据、对齐填充,其中对象头中有个Mark Word,它会记录对象锁相关的信息
monitor
每个对象都会有一个与之对应的monitor对象
monitor对象中存储:
1.当前持有锁的线程
2.等待锁的线程队列
优化
synchronized在jdk1.6之后做了很多优化
1.6-
synchronized在jdk1.6之前是重量级锁
线程进入对象a的同步代码块/方法时,对象a的monitor对象会把此线程的id进行存储,并将monitor对象地址设置在a的mark word中,同时其他阻塞的线程会存储到monitor的等待队列中
1.6前,加锁是依赖操作系统的mutex相关指令,会有用户态和核心态的切换,性能消耗十分明显
1.6+
1.6后,引入在jvm层面实现加锁的偏向锁和轻量级锁,不再依赖操作系统,避免了状态切换的开销
Mark Word中的四种锁状态:
1.无锁
2.偏向锁
3.轻量级锁
4.重量级锁
引入偏向锁和轻量级锁是为了在不同的使用场景使用不同的锁,进而提高效率
偏向锁
jvm认为只有单个线程会来执行同步代码(无竞争环境)
直接在Mark Word记录线程id
当有线程来时,对比线程id是否相等,相等则代表当前线程可获取到锁,执行同步代码
若不相等,则用CAS来尝试修改当前线程的id,如果CAS修改成功,代表当前线程可获取到锁,执行同步代码
若CAS失败,说明处于竞争环境,撤销偏向锁,升级为轻量级锁
轻量级锁
在轻量级锁的状态下,当前线程会在栈帧下创建Lock Record,Lock Record会把Mark Word的信息拷贝进去,且有个Owner指针指向加锁的对象
线程执行到同步代码时,则用CAS试图将Mark Word指向到线程栈帧到Lock Record
如果成功则获取到轻量级锁,如果失败则自旋重试,经过一定次数后,升级为重量级锁
重量级锁
参考1.6-
小结
锁只有升级,没有降级
只有一个线程进入临界区 — 偏向锁
多个线程交替进入临界区 — 轻量级锁
多个线程同时进入临界区 — 重量级锁
AQS&ReentrantLock
公平锁
在竞争环境下,先到临界区的线程一定比后到的线程更快地获取到锁
非公平锁
在竞争环境下,先到临界区的线程未必比后到的线程更快地获取到锁
Q:如何实现上述锁?
A:公平锁可以把竞争的线程放在一个先进先出的队列上,只要持有锁的线程执行完了,唤醒队列的下一个线程去获取锁;非公平锁则让到达线程先尝试能否获取到锁,获取不到则将其放在队尾
公平和非公平的核心:线程执行同步代码块时,是否会去尝试获取锁
synchronized是非公平锁,因为无论它处于哪种锁,线程都会先尝试获取:
1.偏向锁,CAS尝试获取
2.轻量级锁,CAS尝试获取
3.重量级锁,进入monitor对象的队前会先尝试获取
AQS
Abstract Queued Synchronizer
实现自定义锁的一个框架
底层维护了一个先进先出的队列以及state状态变量
队列中有一个个Node结点,结点标识线程、状态值、模式(独占or共享)、前驱后继结点
AQS定义了模版,具体实现由各个子类完成
ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore都是基于AQS实现的
AQS支持两种模式:独占(锁只会被一个线程独占)和共享(多个线程可同时共享锁)
ReentrantLock
加解锁过程
线程池
ThreadPoolExecutor重要参数:
corePoolSize(核心线程数)
maximumPoolSize(最大线程数)
keepAliveTime(线程空余时间)
workQueue(阻塞队列)
handler(任务拒绝策略)
1.若当前线程数小于corePoolSize,创建线程执行任务
2.若1不满足,若workQueue未满,将任务放入其中
3.若2不满足,若当前线程数小于maximumPoolSize,创建线程执行任务
4.若3不满足,执行任务拒绝策略
一般我们将corePoolSize和maximumPoolSize设为相同数量
若当前线程数大于核心线程数,线程达到keepAliveTime,就会对线程进行回收
Q:如果自定义线程数?
首先考量业务,是cpu密集型还是io密集型
根据经验,cpu密集型可以先给到N+1,io密集型可以给到2N试试,具体给多少还要结合压测结果
ThreadLocal
通过它可以拿到线程的变量,让每个线程都可以通过set/get操作局部变量
各线程间的局部变量不会冲突,实现了线程间的数据隔离
ThreadLocal本身并不存储值,它只是帮助线程从ThreadLocalMap中获取相应的value
一个ThreadLocal对应一个私有变量
ThreadLocal对象会被对应的栈引用关联,ThreadLocalMap的key也指向着ThreadLocal
Java中四种引用
强引用
最常见,只要把一个对象与一个引用变量关联上,这个引用变量就是一个强引用
只要对象没有被解除关联(引用设为null),在gc时就不会被回收
软引用
需要继承SoftReference
若内存充足,只有软引用指向的对象不会被回收,内存不足时才会被回收
弱引用
需要继承WeakReference
只要发生gc,弱引用指向的对象就会被回收
ThreadLocalMap的key是弱引用,指向ThreadLocal
虚引用
需要继承PhantomReference
主要用于跟踪对象垃圾回收的状态,当回收时通过引用队列做些通知类的工作
Q:为什么不将ThreadLocalMap的key设为强引用?
因为外界是通过ThreadRef->ThreadLocal来访问ThreadLocalMap的
若ThreadRef->null,此时外界无法访问ThreadLocalMap
那key的强引用也毫无意义
Q:ThreadLocal内存泄露?
内存泄露是用户申请内存后,使用完毕后未进行释放
导致用户无法再使用该块内存空间,系统又无法回收的情况
当ThreadLocal被回收后,ThreadLocalMap Entry的key没有了指向,但Entry仍然有ThreadRef->Thread->ThreadLocalMap->Entry value->Object这条引用链存在,导致了内存泄漏
长期性内存泄漏需要满足:ThreadLocal被回收&&线程被复用(线程池)&&线程复用后不再调用ThreadLocal的set/get/remove方法(调用时它会检查key的情况,会把指向null的清掉)
对线面试官
目录
- 前言
- 注解
- 泛型
- NIO
- 组成
- io复用
- select
- epoll
- 零拷贝
- 反射
- 动态代理
- JDK
- CGLIB
- 场景
- 多线程
- 场景
- 线程安全
- 死锁
- CAS
- ABA问题
- 解决
- 其他
- synchronized
- 原理
- Mark Word
- monitor
- 优化
- 1.6-
- 1.6+
- 偏向锁
- 轻量级锁
- 重量级锁
- 小结
- AQS&ReentrantLock
- 公平锁
- 非公平锁
- AQS
- ReentrantLock
- 线程池
- ThreadLocal
- Java中四种引用
- 强引用
- 软引用
- 弱引用
- 虚引用
- Q:为什么不将ThreadLocalMap的key设为强引用?
- Q:ThreadLocal内存泄露?
前言
本文内容提炼自微信公众号:对线面试官
注解
代码中的特殊标记
可在编译、类加载或运行时被读取,并执行相应操作
例如:
Spring的@Controller、@Param、@Select
Lombok的@Slf4j、@Data
Java的@Override、@Deprecated、@FunctionalInterface
Java原生注解大多用于标记和检查
Java原生还有元注解,用于修饰注解,例如@Retention和@Target
@Retention用于设置注解的生命周期(SOURCE、CLASS、RUNTIME)
@Target用于设置注解的修饰对象(TYPE、FIELD、METHOD、PARAMETER、CONSTRUCTOR、LOCAL_VARIABLE、ANNOTATION_TYPE、PACKAGE、TYPE_PARAMETER、TYPE_USE)
.java -> .class过程有个注解抽象语法树
若想在编译期处理注解,需要将注解的@Retention设为SOURCE和CLASS,并且需继承AbstractProcessor类并重写process方法(lombok的AnnotationProcessor便是如此)
上述两级别加载到jvm中后,注解将被抹除
自定义注解@Retetion一般设为RUNTIME级别,配合反射机制获取到运行时信息,并处理
泛型
在创建对象或调用方法时才明确下具体的类型
好处:
无需强制转换,代码更加简洁
程序更加健壮,编译器无警告,运行期不会抛ClassCast Exception异常
应用场景:集合
写基础组件时,可利用泛型保证组件的通用性
NIO
始于JDK1.4
出现是为了提高速度
non-blocking io / new io
传统io是以字节为单位处理,nio是以块为单位处理
传统io只能是阻塞的,而nio可做到非阻塞
组成
buffer(缓冲区):存数据
channel(管道):运数据
selector(选择器):检查channel状态
Unix下IO模型有:
阻塞io、非阻塞io、io复用、信号驱动和异步io
io复用
以Linux系统为例,其对文件的操作是通过文件描述符(fd)
io复用模型:通过一个进程监听多个fd,一旦某个fd就绪,就去通知程序可以往下处理
linux下io复用模型用的函数有select/poll和epoll
select
最大连接数为1024或2048
传入fd_set参数,只能是1024或2048(具体看操作系统位数)
fd_set底层为bitmap,0表示数据未到缓冲区,1表示数据已到达
select会不断遍历fd_set,判断标志位变更情况,若有变化通知程序往下处理
epoll
linux2.6内核正式提出,完善了select一些缺点
它定义了epoll_event结构体,不存在最大连接数的限制
epoll不再遍历所有fd,它专门为就绪的fd维护了一片空间,每次从中获取使用
零拷贝
以读请求为例,会调用read相关的系统函数,用户态->内核态
cpu让dma去磁盘把数据拷贝到内核空间
等内核缓冲区有数据后,cpu会把内核缓冲区数据拷贝到用户缓冲区
最终用户程序可以获取到数据
零拷贝将黄字过程省略,相当于磁盘数据直接拷贝到用户程序中,由此提高了效率和性能
常见零拷贝技术:
mmap(内核缓冲区与用户缓冲区共享)、sendfile(系统底层函数支持)
反射
通过反射可以在运行时获取类的信息,使代码更具通用性和灵活性
运行时指jvm加载.class运行的时期
Q:既然泛型会擦除,那为什么反射能获取到泛型的信息呢?
A:泛型虽然会擦除,但擦除是有范围的,定义在类上的泛型信息是不会被擦除的,Java编译器仍在class文件以Signature属性的方式保留了泛型信息
动态代理
代理模式的一种
代理模式有静态代理和动态代理
静态代理需要自己写代理类,实现对应的接口,比较麻烦
在Java中,动态代理有两种实现方式:JDK和CGLIB
JDK
JDK动态代理使用了反射机制
JDK动态代理会帮我们实现接口的方法,通过invokeHandler对需要增强的方法进行增强
CGLIB
CGLIB代理使用了ASM框架,通过修改其字节码生成子类
场景
Mybatis不用写实现类,只写接口就能执行SQL
Spring AOP
多线程
进程是资源分配的基本单位
线程是资源调度的基本单位
场景
Tomcat会从线程连接池取线程去处理请求
数据库连接池Druid、C3P0、DBCP
比如现在要跑一个定时任务,该任务的链路执行时间和过程都非常长,那么就用一个线程池处理该定时任务,提高了系统的吞吐量
线程安全
多个线程去执行某类,这个类始终能表现出正常的行为,那么这个类是线程安全的
解决线程安全问题的思路:
1.保证原子性,考虑atomic包
2.保证操作可见性,考虑volatile关键字
3.涉及对线程的控制,考虑CountDownLatch、Semaphore等并发工具类
4.涉及集合,考虑java.util.concurrent包下的集合类
5.若synchronized无法满足需求,考虑lock包下的类
确实存在线程安全问题 -> 上述方法选取
无脑使用synchronized会影响程序性能
死锁
原因:当前线程拥有其他线程需要的资源,当前线程等待其他线程拥有的资源,大家都不释放自己拥有的资源
死锁避免方案:
1.固定加锁的顺序,例如使用hash值的大小来确定加锁的先后
2.减小锁的粒度
3.使用可释放的定时锁
CAS
compare and swap,比较并交换,原子性操作
CAS有三个操作数:当前值A、内存值V、要修改的新值B
若A与V相等,将V改成B;若A与V不等,重试或放弃
synchronized每次只会让一个线程去操作共享变量,而CAS相当于没有加锁,多个线程可以直接操作共享资源,在实际去修改的时候才去判断能否修改成功
ABA问题
假设线程A读到当前值是10,可能线程B把值修改为100,然后线程C又把值修改为10,等到线程A拿到执行权时,因为当前值和内存值是一致的,线程A认为值自始至终没有改变,符合修改条件
解决
Java提供AtomicStampedReference,对比内存值和版本是否一致
其他
阿里巴巴开发手册推荐使用LongAdder对象,它减少了乐观锁的重试次数,比AtomicLong性能更好
AtomicLong做累加时是多个线程操作同一个目标资源,在高并发下,只有一个线程会执行成功,其他线程都会失败并不断自旋(重试),自旋将成为瓶颈
而LongAdder将目标资源分散到数组Cell中,每个线程对自己的Cell变量的value进行原子操作,大大降低了失败次数
synchronized
它是一个Java的关键字,能够将代码块或方法锁起来
互斥锁,一次只能允许一个线程进入锁住的代码块
若其修饰实例方法,对应的锁是对象实例
若其修饰静态方法,对应的锁是当前类的Class实例
若其修饰代码块,对应的锁是传入synchronized的对象实例
原理
通过反编译可知
当其修饰方法时,编译器将生成ACC_SYNCHRONIZED关键字作为标识
当其修饰代码块时,其底层是monitorenter和monitorexit指令
Mark Word
在内存中,对象一般由三部分组成:对象头、对象实际数据、对齐填充,其中对象头中有个Mark Word,它会记录对象锁相关的信息
monitor
每个对象都会有一个与之对应的monitor对象
monitor对象中存储:
1.当前持有锁的线程
2.等待锁的线程队列
优化
synchronized在jdk1.6之后做了很多优化
1.6-
synchronized在jdk1.6之前是重量级锁
线程进入对象a的同步代码块/方法时,对象a的monitor对象会把此线程的id进行存储,并将monitor对象地址设置在a的mark word中,同时其他阻塞的线程会存储到monitor的等待队列中
1.6前,加锁是依赖操作系统的mutex相关指令,会有用户态和核心态的切换,性能消耗十分明显
1.6+
1.6后,引入在jvm层面实现加锁的偏向锁和轻量级锁,不再依赖操作系统,避免了状态切换的开销
Mark Word中的四种锁状态:
1.无锁
2.偏向锁
3.轻量级锁
4.重量级锁
引入偏向锁和轻量级锁是为了在不同的使用场景使用不同的锁,进而提高效率
偏向锁
jvm认为只有单个线程会来执行同步代码(无竞争环境)
直接在Mark Word记录线程id
当有线程来时,对比线程id是否相等,相等则代表当前线程可获取到锁,执行同步代码
若不相等,则用CAS来尝试修改当前线程的id,如果CAS修改成功,代表当前线程可获取到锁,执行同步代码
若CAS失败,说明处于竞争环境,撤销偏向锁,升级为轻量级锁
轻量级锁
在轻量级锁的状态下,当前线程会在栈帧下创建Lock Record,Lock Record会把Mark Word的信息拷贝进去,且有个Owner指针指向加锁的对象
线程执行到同步代码时,则用CAS试图将Mark Word指向到线程栈帧到Lock Record
如果成功则获取到轻量级锁,如果失败则自旋重试,经过一定次数后,升级为重量级锁
重量级锁
参考1.6-
小结
锁只有升级,没有降级
只有一个线程进入临界区 — 偏向锁
多个线程交替进入临界区 — 轻量级锁
多个线程同时进入临界区 — 重量级锁
AQS&ReentrantLock
公平锁
在竞争环境下,先到临界区的线程一定比后到的线程更快地获取到锁
非公平锁
在竞争环境下,先到临界区的线程未必比后到的线程更快地获取到锁
Q:如何实现上述锁?
A:公平锁可以把竞争的线程放在一个先进先出的队列上,只要持有锁的线程执行完了,唤醒队列的下一个线程去获取锁;非公平锁则让到达线程先尝试能否获取到锁,获取不到则将其放在队尾
公平和非公平的核心:线程执行同步代码块时,是否会去尝试获取锁
synchronized是非公平锁,因为无论它处于哪种锁,线程都会先尝试获取:
1.偏向锁,CAS尝试获取
2.轻量级锁,CAS尝试获取
3.重量级锁,进入monitor对象的队前会先尝试获取
AQS
Abstract Queued Synchronizer
实现自定义锁的一个框架
底层维护了一个先进先出的队列以及state状态变量
队列中有一个个Node结点,结点标识线程、状态值、模式(独占or共享)、前驱后继结点
AQS定义了模版,具体实现由各个子类完成
ReentrantLock、ReentrantReadWriteLock、CountDownLatch、Semaphore都是基于AQS实现的
AQS支持两种模式:独占(锁只会被一个线程独占)和共享(多个线程可同时共享锁)
ReentrantLock
加解锁过程
线程池
ThreadPoolExecutor重要参数:
corePoolSize(核心线程数)
maximumPoolSize(最大线程数)
keepAliveTime(线程空余时间)
workQueue(阻塞队列)
handler(任务拒绝策略)
1.若当前线程数小于corePoolSize,创建线程执行任务
2.若1不满足,若workQueue未满,将任务放入其中
3.若2不满足,若当前线程数小于maximumPoolSize,创建线程执行任务
4.若3不满足,执行任务拒绝策略
一般我们将corePoolSize和maximumPoolSize设为相同数量
若当前线程数大于核心线程数,线程达到keepAliveTime,就会对线程进行回收
Q:如果自定义线程数?
首先考量业务,是cpu密集型还是io密集型
根据经验,cpu密集型可以先给到N+1,io密集型可以给到2N试试,具体给多少还要结合压测结果
ThreadLocal
通过它可以拿到线程的变量,让每个线程都可以通过set/get操作局部变量
各线程间的局部变量不会冲突,实现了线程间的数据隔离
ThreadLocal本身并不存储值,它只是帮助线程从ThreadLocalMap中获取相应的value
一个ThreadLocal对应一个私有变量
ThreadLocal对象会被对应的栈引用关联,ThreadLocalMap的key也指向着ThreadLocal
Java中四种引用
强引用
最常见,只要把一个对象与一个引用变量关联上,这个引用变量就是一个强引用
只要对象没有被解除关联(引用设为null),在gc时就不会被回收
软引用
需要继承SoftReference
若内存充足,只有软引用指向的对象不会被回收,内存不足时才会被回收
弱引用
需要继承WeakReference
只要发生gc,弱引用指向的对象就会被回收
ThreadLocalMap的key是弱引用,指向ThreadLocal
虚引用
需要继承PhantomReference
主要用于跟踪对象垃圾回收的状态,当回收时通过引用队列做些通知类的工作
Q:为什么不将ThreadLocalMap的key设为强引用?
因为外界是通过ThreadRef->ThreadLocal来访问ThreadLocalMap的
若ThreadRef->null,此时外界无法访问ThreadLocalMap
那key的强引用也毫无意义
Q:ThreadLocal内存泄露?
内存泄露是用户申请内存后,使用完毕后未进行释放
导致用户无法再使用该块内存空间,系统又无法回收的情况
当ThreadLocal被回收后,ThreadLocalMap Entry的key没有了指向,但Entry仍然有ThreadRef->Thread->ThreadLocalMap->Entry value->Object这条引用链存在,导致了内存泄漏
长期性内存泄漏需要满足:ThreadLocal被回收&&线程被复用(线程池)&&线程复用后不再调用ThreadLocal的set/get/remove方法(调用时它会检查key的情况,会把指向null的清掉)
发布评论