线程(一)
First And MOST Important
1. 线程的生命周期
生命周期2. 线程池的原理,为什么要创建线程池?
目的:
创建线程需要分配本地方法栈、虚拟机栈、程序计数器等内存空间;
销毁线程需要回收所分配的资源;
创建线程池可以减少两部分的消耗。
优点:
周期任务,定时执行等与时间相关的功能;
复用线程、控制最大并发数目;
隔离线程环境。
3. 什么是线程安全,如何实现线程安全
关于线程安全,可以说一千个人有一千个哈姆雷特。
针对类来说,当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
针对数据来说,数据应该是正确的。
针对逻辑来说,多线程下的行为是人所预支,会按照开发者的预期进行执行。
实现线程安全的方法
-
锁同步
- 锁能使其保护的代码以串行的形式来访问,当给一个复合操作加锁后,能使其成为原子操作。一种错误的思想是只要对写数据的方法加锁,其实这是错的,对数据进行操作的所有方法都需加锁,不管是读还是写
- 加锁时需要考虑性能问题,不能总是一味地给整个方法加锁synchronized就了事了,应该将方法中不影响共享状态且执行时间比较长的代码分离出去
- 加锁的含义不仅仅局限于互斥,还包括可见性。为了确保所有线程都能看见最新值,读操作和写操作必须使用同样的锁对象
-
不共享状态
- 无状态对象: 无状态对象一定是线程安全的
- 线程关闭: 仅在单线程环境下使用
-
不可变对象
- 可以使用final修饰的对象保证线程安全,由于final修饰的引用型变量(除String外)不可变是指引用不可变,但其指向的对象是可变的,所以此类必须安全发布,也即不能对外提供可以修改final对象的接口
4. JDK创建线程池有哪几个核心参数? 如何合理配置线程池的大小?
1.核心和最大的线程容量
TPE会根据corePoolSize和maximumPoolSize自动调整线程池的大小。
当一个任务提交到线程池里:
如果当前运行的线程池大小小于corePoolSize,则不管是否有空闲线程,都会创建一个新的线程来运行任务。
如果当前运行的线程池容量大于corePoolSize,小于maximumPoolSize,只有在队列满的时候才会创建新的线程。
设置core=maximum即创建一个固定大小的线程池。设置maximum为Integer.MAX_VALUE即创建一个可无限创建线程边界的线程池。
一般情况下,在构造TPE时就会设置好了这两个参数,但是也可以通过set方法动态设置。
2.线程存活时间
如果线程池的大小超过了corePoolSize,如果超过这个大小的线程在指定时间内是空闲的,则会被终止掉。
默认只终止超过corePoolSize的线程,但是可以通过设置allowCoreThreadTimeOut来关闭超时的core线程。
注:慎重的设置这个参数,设置不当会失去创建线程池所带来的性能提升。
3.线程等待队列
任意BlockingQueue都可用于传输和保存提交的任务。使用这个队列与线程池大小进行交互:
如果当前运行的线程小于corePoolSize,则Executor会选择创建一个新线程而不是入队。
如果当前运行的线程大于或等于corePoolSize,则Executor会选择将任务入队而不是创建一个新线程。
如果无法将任务入队,则创建新的线程---除非线程池大小已经达到了maximumPoolSize,这种情况下任务会被拒绝。
使用队列有三种通用策略:
无界队列。使用无界队列(如无预定义容量的LinkedBlockingQueue),当所有corePoolSize线程都工作时,新任务都入队等待。这样,创建的线程永远不会超过corePoolSize(也就是说设置maximumPoolSize的值不会起任何作用)。当每一个任务都完全独立于其他任务时,即任务都不影响其他任务的执行时,适合选择无界队列;比如在Web页服务器中。无界队列可用于平滑处理突然激增的请求,当请求以超过队列所能处理的平均值连续到达时,无界队列能增加自己的容量。
有界队列。当使用有限的maximumPoolSizes时,有界队列(如ArrayBlockingQueue)有助于防止资源耗尽,但是可能比较难调整和控制。需要权衡设置有界队列和maximumPoolSizes的大小:使用大容量的队列和小的线程池可降低CPU的使用率、操作系统资源和上下文切换开销,但是可能导致吞吐量降低。如果任务频繁阻塞(比如阻塞在I/O操作上),则系统可能会为超过你许可的更多线程安排时间。使用小容量的队列一般需要较大容量的线程池,会使CPU利用率较高,但是可能遇到不能接受的调度开销,也会使吞吐量降低。
直接提交。工作队列默认选择SynchronousQueue,它将任务直接提交给线程而不hold它们。如果不存在可立即运行任务的线程,则尝试入队将会失败,因此会创建一个新的线程。这个策略可以避免在处理可能具有内部依赖的请求时出现锁。直接提交通常要求无限大的maximumPoolSizes以避免拒绝新提交的任务。当任务以超过所能处理的平均数连续到达时,此策略允许无界线程持续增加。
注:合理使用三种队列,注意其中的参数设置,防止踩坑。
4.拒绝任务的策略
当Executor已经关闭/有界的最大线程值和队列容量都已经饱和时,提交的新任务将被拒绝。
在以上两种情况下, execute方法都将调用RejectedExecutionHandler的RejectedExecutionHandler.rejectedExecution方法。
这个类预定义了四种处理策略:
默认使用ThreadPoolExecutor.AbortPolicy,提交任务被拒绝时将抛出运行时异常:RejectedExecutionException。
使用ThreadPoolExecutor.CallerRunsPolicy,线程池调用提交该任务的线程去执行。这个策略提供了简单的反馈控制机制,能够减缓新任务的提交速度。
使用ThreadPoolExecutor.DiscardPolicy,不能执行的任务将被丢弃掉。
使用ThreadPoolExecutor.DiscardOldestPolicy,如果执行程序尚未关闭,则位于队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。
我们也可以定义和使用其他形式的RejectedExecutionHandler类,但是当策略仅用于特定容量或排队策略时要非常非常小心。
5. ThreadLocal什么时候会OOM?ThreadLocal为什么会内存泄漏?
结构图ThreadLocal实现原理:每个Thread维护一个ThreadLocalMap,这个map的key是ThreadLocal实例本身,value 是需要存储的对象,也就是说ThreadLocal本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。
ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
注意:关系比较乱,最好看看源码自己画下图,加深理解
内存泄露的点:ThreadLocalMap的生命周期与Thread相同,线程未结束时,线程里会维护着ThreadLocalMap,这个map对应的key--threadLocal尽管回收掉了,但是这个map却还是存在的,所以到value的引用还在,不能回收导致泄露内存。
private static final Boolean FIX_OOM = false;
...
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executorService.execute(() -> {
threadLocal.set(new ThreadLocalOutOfMemory().getData());
Thread t = Thread.currentThread();
log.info(t.getName());
if (FIX_OOM) {
// getMap(Thread.currentThread()).remove(this) 从Map里移除当前threadLocal对应的对象
threadLocal.remove();
}
});
try {
Thread.sleep(500L);
} catch (InterruptedException ignore) {
Thread.currentThread().interrupt();
}
}
如何解决呢?上述代码里也写了,调用threadLocal的remove方法即可。
6. ThreadLocal的使用场景
ThreadLocal提供了线程本地的实例。它与普通变量的区别在于每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
ThreadLocal 适用于,具体如web应用里的session等:
- 每个线程需要有自己单独的实例
- 实例需要在多个方法中共享,但不希望被多个线程所共享[变量在线程间隔离而在方法或类间共享]
注意:ThreadLocal不是用来解决对象共享访问问题的!!!!
7. 内存可见性、原子性、synchronized、synchronized锁粒度、volatile
Java内存模型参考文章---JVM(一)
- Java所有变量都存储在主内存中,也就是堆区
- 每个线程都有自己独立的工作内存,里面保存当前线程的使用到的变量副本,线程对共享变量的操作必须在自己的工作内存中进行,修改后通过主内存进行传递。
可见性:一个线程修改共享变量,其他线程能够及时看到
原子性:操作不可再分
如x++操作就不是原子性操作,x++分为3个操作:
读取变量count的当前值 --> count和1相加 --> 将增加后的值赋给count。
volatile、synchronized两者的区别联系
volatile | synchronized |
---|---|
只能变量级别 | 可以在变量、方法、类级别 |
变量需要从主存中读取 | 锁定当前变量,具有排他性,只有当前线程可以访问该变量 |
保证变量可见性、不保证原子性 | 保证变量可见性和原子性 |
volatile变量不会被编译器优化,禁止指令重排序 | 变量可以被编译器优化 |
synchronized锁粒度:
修饰方法 | 修饰的方法是普通的成员方法 | 对象锁 |
---|---|---|
修饰的方法是静态方法 | 类锁 | |
修饰一个代码块 | 锁this | 对象锁 |
锁一个类的class对象 | 类锁 | |
锁普通对象,对象是静态对象 | 类锁 | |
锁普通对象,对象为非静态对象 | 对象锁 |