程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

2023-06(4)

Java多线程篇--threadlocal和线程池

发布于2021-06-14 11:19     阅读(846)     评论(0)     点赞(13)     收藏(2)


在之前的文章中,已经发布了常见的面试题,这里我花了点时间整理了一下对应的解答,由于个人能力有限,不一定完全到位,如果有解答的不合理的地方还请指点,在此谢过。

本文主要描述的是java多线程中提供的一些常用框架threadlocal线程池。线程池是面试的一大热点问题,非常有必要掌握线程池的原理和相关源码;Threadlocal在某些特定的场合下作用非常大,掌握其核心内容和使用方法也非常有必要。如果对java多线程感兴趣的同学可以看下公众号里多线程系列的文章,也许会对你有些帮助。

ThreadLocal有了解么?ThreadlocalMap的key 和value是什么?怎么保证内存不会泄露?

Threadlocal是线程本地变量类,该类的作用是可以在一个线程的保存变量,该变量只有该线程可以访问,这样可以在一定程度上避免使用锁。另一个作用是避免同一个线程上下文传递。Threadlocal的值是通过Thread类中的ThreadLocalMap进行存储。ThreadLocalMap的每个entry对象是一个弱引用对象,key是threadLocal对象,value是要存入的值。我们看下ThreadLocalMap的定义:

  1. static class ThreadLocalMap {  
  2.   //静态内部类,每个entry对象key都是一个弱引用对象  
  3.   static class Entry extends WeakReference<ThreadLocal<?>> {  
  4.             Object value;  
  5.             //key 是ThreadLocal 对象,value是具体的值  
  6.             Entry(ThreadLocal<?> k, Object v) {  
  7.                 super(k);  
  8.                 value = v;  
  9.             }  
  10.         }  
  11.   private static final int INITIAL_CAPACITY = 16;//初始值 16  
  12.   private Entry[] table;//entry对象  
  13.   private int size = 0;//entry的大小  
  14.   private int threshold;//扩容的阈值  
  15. }  

需要了解的是,ThreadLocalMap在解决hash冲突的时候,使用的是线性探测的方式来解决的。

  1. private void set(ThreadLocal<?> key, Object value) {  
  2. Entry[] tab = table;  
  3.     int len = tab.length;  
  4.     int i = key.threadLocalHashCode & (len-1);  
  5.     for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {//一直寻找下一个空位  
  6.        //...         
  7.     }  
  8. }  

Threadlocal为什么会发生内存泄漏?在这里,先提一下java的四种引用及其回收机制:强、软、弱、虚。强引用是不会被垃圾回收器回收的,软引用是没有内存的时候会被回收,弱引用和虚引用在发生垃圾回收的时候被回收。所以如果我们不希望软、弱被回收的话,我们只要使用一个强引用指向软、弱引用。

下图是threadlocal的在堆的存储图,如果threadlocal对象有强引用的话,那么key不会被回收,当失去强引用连接时,entry中的key就会被清除,但是value是一个强引用,所以value不会被清除,这样就有可能导致内存泄漏了

如何避免内存泄漏,实际上是在我们每次使用完threadlocal的时候,调用remove方法进行删除就行。现在看下remove方法的实现:

  1. private void remove(ThreadLocal<?> key) {  
  2. Entry[] tab = table;  
  3.     int len = tab.length;  
  4.     int i = key.threadLocalHashCode & (len-1);  
  5.     for (Entry e = tab[i];  
  6. e != null;  
  7.         e = tab[i = nextIndex(i, len)]) {  
  8.         if (e.get() == key) {  
  9.    e.clear();//找到清除  
  10.             expungeStaleEntry(i);//同时清除过期的key,key为null的值  
  11.             return;  
  12.         }  
  13.     }  
  14. }  

 

Java的线程池是怎么实现的?其原理是什么?线程池怎么设计核心线程数,拒绝策略怎么选择?怎么优雅关闭一个线程池?

在java语言中,Executors提供了四种线程池的创建方式,newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor。除了周期任务的线程池会在某些定时任务上使用,其他的线程池一般是不建议在实际中使用。而实际上,我们更应该关注线程池的底层创建ThreadPoolExecutor。我们看下构造函数:

  1. public ThreadPoolExecutor(int corePoolSize,//核心线程数  
  2.              int maximumPoolSize,//最大线程数  
  3.              long keepAliveTime,//最大线程活跃时间  
  4.              TimeUnit unit,//最大线程的时间单位  
  5.              BlockingQueue<Runnable> workQueue,//工作队列  
  6.              ThreadFactory threadFactory,//线程创建工作  
  7.            RejectedExecutionHandler handler)//拒绝策略

这几个参数中,核心的参数要记住:核心线程数,最大线程数,阻塞队列,拒绝策略

我们看下线程池的实现原理:

这是线程池的经典原理图:

  1.  当提交任务到线程池执行的时候,会先到核心线程开始执行,如果刚开始没有线程,会创建核心线程
  1. 如果核心线程满了会到阻塞队列进行排队,当核心线程完成当前任务时,会尝试从阻塞队列中获取待执行的任务
  2. 当阻塞队列满了之后,会扩充线程数到最大线程数,执行当前任务
  3. 当最大线程数满了之后,会进入拒绝策略,jdk提供了几种常用的拒绝策略,常用的是拒绝,拒绝最老的,以提交当前任务的线程执行该任务。

核心的执行代码如下:

  1. public void execute(Runnable command) {  
  2. if (command == null)//判空  
  3. throw new NullPointerException();  
  4.     int c = ctl.get();//获取当前线程数量  
  5.     if (workerCountOf(c) < corePoolSize) {//如果数量小于核心线程数  
  6.         if (addWorker(command, true))//创建线程并执行任务,有可能添加失败  
  7.             return;  
  8.         c = ctl.get();//更新当前线程数  
  9.     }  
  10.     if (isRunning(c) && workQueue.offer(command)) {//超过核心数且正在运行,添加到阻塞队列中  
  11.         int recheck = ctl.get();//再次获取线程数  
  12.         if (! isRunning(recheck) && remove(command))//如果线程已经关闭并且从阻塞队列中移除  
  13.             reject(command);//执行拒绝策略  
  14.         else if (workerCountOf(recheck) == 0)//如果当前线程数为0  
  15.             addWorker(nullfalse);//创建线程  
  16.     }  
  17.     else if (!addWorker(command, false))//如果创建最大线程数并执行失败  
  18.         reject(command);//执行拒绝策略  
  19. }  

 

线程池设计核心线程数的原则上主要分io密集型和cpu密集型。如果是io密集型,就说明cpu需要等待,那么多提供一点线程数量去执行其他任务一定是能增加效率的。如果是cpu密集型,那就意味着cpu不可能闲置,如果线程的数量过高,反而因为线程上下文来回切换导致效率低下,这个时候线程数就不宜设置过高。了解上面的原则之后,实际中,我们也不会说具体到多少做个试验,一般就是参考其他作者的试验结果,如下其他书籍试验的结果(以供参考,原文来自:https://www.cnblogs.com/651434092qq/p/14240406.html ):

Java并发实战的书中给出的计算公式:

核心线程数 = CPU 核心数 * (1 + IO 耗时/ CPU 耗时)

(io耗时和cpu耗时使用工具统计)

Java虚拟机并发编程书中提供的计算公司:

核心线程数 = CPU 核心数 / (1 - 阻塞系数)

(其中计算密集型阻塞系数为 0,IO 密集型阻塞系数接近 1,一般认为在 0.8 ~ 0.9 之间)

 

拒绝策略的选择也很关键,默认提供了以下几种策略:

CallerRunsPolicy - 使用调用线程直接运行任务。一般不允许失败的情况下使用但是,如果并发量很大的话,我们就有可能导致执行线程阻塞,一般不建议

AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略

DiscardPolicy - 直接丢弃

DiscardOldestPolicy -  丢弃阻塞队列中最老的一个任务,并将新任务加入

 

一般建议使用抛异常,并且自己将异常进行处理,如果不允许失败的话,那就重新执行或者保存起来下次执行,如果有必要,建议自行实现拒绝策略,在实战中,我们就出现过因为拒绝策略导致整个环境阻塞严重的问题。

 

线程池的优雅关闭也是一个很重要的内容,尤其是当我们的系统不能中断服务的时候,这个时候就要确保执行的线程已经完成,新的任务不在提交到该线程池中。我们先看下线程池的几个状态:

  1. private static final int RUNNING    = -1 << COUNT_BITS;//运行状态,接收任务的添加并且处理队列中的任务  
  2. private static final int SHUTDOWN   =  0 << COUNT_BITS;//关闭状态,不接受新的任务,但是队列中的任务会处理  
  3. private static final int STOP       =  1 << COUNT_BITS;//停止状态,不接受新任务,不处理队列中的任务,并且中断当前运行的任务  
  4. private static final int TIDYING    =  2 << COUNT_BITS;//清理状态,所有的任务都不在执行,运行的线程已经为0,执行关闭钩子的方法  
  5. private static final int TERMINATED =  3 << COUNT_BITS;//结束状态,关闭钩子的方法已经执行完,线程池真正关闭完了  

 

线程池提供的几个关闭方法:

Shutdown方法: 将线程池的状态从running->shutdown

shutdownNow:将线程池的状态从(running/shutdown->stop)

awaitTermination:等待线程池的状态变成terminated状态

 

线程池的优雅关闭代码如下:

  1. public void close{
  2. threadPool.shutdown(); // 关闭
  3. try {
  4. if (!threadPool.awaitTermination(30, TimeUnit.SECONDS)) {// 等待30s
  5. threadPool.shutdownNow();// 调用 shutdownNow 取消正在执行的任务
  6. if (!threadPool.awaitTermination(30, TimeUnit.SECONDS))// 再次等待30s
  7. log.error("线程池关闭异常,有任务在一直执行")
  8. }
  9. catch (InterruptedException ex) {
  10. threadPool.shutdownNow();// 捕获异常,调用shutdownNow强行关闭
  11. }
  12. }

 

在关闭中,我们要设置一个关闭等待时间,如果时间内没有关闭,我们一般会认为线程这个时候是阻塞住了,那么就强行关闭。

线程池是本文的核心内容,要了解线程的关键参数,原理,有必要的话,看下底层的实现代码。线程池参数的设计和关闭是实战过程中需要关注的;Threadlocal在某些特定的场景下使用,比如分库分表,这些线程本地变量能够带来很大的方便。Threadlocal在使用上不难,要关注一下内存泄漏的问题,要知道,在java语言中,内存泄漏并不常见。

 

本文的内容就这么多,如果你觉得对你的学习和面试有些帮助,帮忙点个赞或者转发一下哈,谢谢。

 

 

想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈

 

                                   

原文链接:https://blog.csdn.net/rqc112233/article/details/117729828



所属网站分类: 技术文章 > 博客

作者:怎么没有鱼儿上钩呢

链接:http://www.javaheidong.com/blog/article/222771/103e099cba9ac9cadd58/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

13 0
收藏该文
已收藏

评论内容:(最多支持255个字符)