重点知识学习(8.4)--[线程池 , ThreadLocal]

Posted 小智RE0

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重点知识学习(8.4)--[线程池 , ThreadLocal]相关的知识,希望对你有一定的参考价值。

文章目录


1.线程池


说到线程的连接释放, 与数据库进行连接时,就是需要先创建连接,使用完后,进行销毁;这样频繁地创建连接以及销毁连接是比较耗费时间的; 后来学了数据库连接池, 比如说德鲁伊数据库连接池,进行相关配置后,指定数据库连接池的链接数量,最小连接数,最大连接数等参数; 数据库连接时从连接池取到即可,用完后不用销毁,放回连接池即可;

线程池也就是这样;在并发量过大的情况下,使用线程池无疑是一种最好的选择;

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率

可通过线程池来达到这样的效果。线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。

从 JDK5 开始,Java语言中内置支持线程池
提供了Executors 来创建不同类型的线程池。 实际开发不建议使用
Executors 中提供了以下常见的线程池创建方法:

  • newSingleThreadExecutor:一个单线程的线程池。如果因异常结束,会再创建一个新的,保证按照提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池。根据提交的任务逐个增加线程,直到最大值保持不变。如果因异常结束,会新创建一个线程补充。
  • newCachedThreadPool:创建一个可缓存的线程池。会根据任务自动新增或回
    收线程。
  • newScheduledThreadPool:支持定时以及周期性执行任务的需求。
  • newWorkStealingPool:JDK8 新增,根据所需的并行层次来动态创建和关闭线程,通过使用多个队列减少竞争,底层使用 ForkJoinPool 来实现。优势在于可以充分利用多 CPU,把一个任务拆分成多个“小任务”,放到多个处理器核心上并行执行;当多个“小任务”执行完成之后,再将这些执行结果合并起来即可。

java.uitl.concurrent.ThreadPoolExecutor 类是线程池的核心类

线程池构造函数中的7个参数

corePoolSize:表示创建的核心线程池数量, 其实在创建线程池后核心线程池数量默认为0, 发现要执行的任务后,才会去创建线程去执行,或者调用prestartAllCoreThreads()或者 prestartCoreThread()方法,进行预创建.

核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,在创建了线程池后,线程池中的线程数为 0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列当中;除非调用了prestartAllCoreThreads()或者 prestartCoreThread()方法,从这 2 个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize 个线程或者一个线程。

maximumPoolSize表示在线程池中最多能创建多少个线程

keepAliveTime非核心线程池中的线程,在没有执行任务时的存活时间.

参数设置线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于 corePoolSize 时,keepAliveTime 才会起作用,直到线程池中的线程数不大于 corePoolSize,即当线程池中的线程数大于 corePoolSize 时,如果一个线程空闲的时间达到 keepAliveTime,则会终止,
直到线程池中的线程数不超过 corePoolSize。

unit参数 keepAliveTime 的时间单位
在 TimeUnit 类中有这几种静态属性:

workQueue作为阻塞队列,用来存储等待执行的任务

threadFactory:线程工厂,用来创建线程;

handler:设置拒绝处理任务时的策略.


线程池的执行过程

创建 ThreadPoolExecutor 线程池后,当向线程池提交任务时,常用的是 execute 方法。
execute 方法的执行图:

  • 如果线程池中在corePoolSize中存活的核心线程数小于线程数时,线程池会创建一个核心线程去执行处理提交的任务。

  • 但是如果线程池核心线程数corePoolSize已满了,那么来了一个新提交的任务,就会被放进任务队列 workQueue中排队等待执行。

  • 当线程池里面存活的线程数已经等于 corePoolSize 了,且任务队列workQueue 也满,这时去判断线程数是否达到最大线程容纳数 maximumPoolSize,如果没到达,可以考虑创建一个非核心线程执行提交的任务。

  • 如果当前的线程总数达到了最大线程容纳数 maximumPoolSize,要是还来新的任务,就得采用拒绝策略处理。


线程的执行工作队列

  • ArrayBlockingQueue:数组实现的有界阻塞队列,按 FIFO的排序量。

  • LinkedBlockingQueue基于链表结构的阻塞队列,按 FIFO排序任务,
    它的容量可以进行设置,不设置的话,默认是无边界的阻塞队列
    最大长度为 Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQuene


execute 与 submit 的区别

其实区别就是submit执行时具有返回值

execute 适用于不需要返回值的场景,submit 方法适用于需要返回值的场景。

可调用get方法获取返回值结果


四个类型拒绝策略

参数RejectedExecutionHandler 用于指定线程池的拒绝策略。

  • AbortPolicy 策略:直接抛出异常,阻止系统正常工作。

  • DiscardOleddestPolicy 策略:丢弃最老的一个请求(即将被执行的任务),并尝试再次提交当前任务。

  • DiscardPolicy 策略:丢弃无法处理的任务,不予任何处理.

  • CallerRunsPolicy 策略:只要线程池还没有关闭,就会在调用者线程中运行当前的任务(如果任务被拒绝了,则由提交任务的线程(例如:main)直接执行此任务)。


关闭线程池方法 shutdownNow 和 shutdown

  • shutdownNow() 方法 :立即关闭线程池,正在执行的任务执行interrupt()方法,停止执行,还没开始执行的任务被全部取消,返回还没开始的任务列表。
  • shutdown( ) 方法 : 正常关闭线程池,注意它会等待已经提交或执行的任务执行完成; 且线程池不再接受新的任务

案例

/*
  执行的任务
 */
public class MyTask implements Runnable 

    private int taskNum;

    public MyTask(int num) 
        this.taskNum = num;
    

    @Override
    public void run() 
        try 
            Thread.currentThread().sleep(6000);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println(Thread.currentThread().getName()+":任务 "+taskNum+"执行完毕");
    

测试执行

public class Test 
    public static void main(String[] args) 
        //创建线程池, 核心线程池容量为 3;最大线程数为 8;
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                3, 8, 200,
                TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<>(2),
                Executors.defaultThreadFactory(),
                //采用丢弃最老请求策略;
                new ThreadPoolExecutor.DiscardOldestPolicy());
        
        for (int i = 1; i <= 10; i++) 
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);//添加任务到线程池
        
        executor.shutdown();
    

执行结果之1

pool-1-thread-1:任务 1执行完毕
pool-1-thread-2:任务 2执行完毕
pool-1-thread-3:任务 3执行完毕
pool-1-thread-4:任务 6执行完毕
pool-1-thread-5:任务 7执行完毕
pool-1-thread-6:任务 8执行完毕
pool-1-thread-7:任务 9执行完毕
pool-1-thread-8:任务 10执行完毕
pool-1-thread-2:任务 5执行完毕
pool-1-thread-1:任务 4执行完毕


2.ThreadLocal(线程变量)


关于线程封闭

对象封闭在一个线程里,即使这个对象不是线程安全的,也不会出现并发安全问题。

例如 栈封闭:就是用栈(stack)来保证线程安全

public void testThread() 
StringBuilder s = new StringBuilder();
s.append("Hello");

StringBuilder 是线程不安全的,但是它只是个局部变量,局部变量存储在虚拟机栈,虚拟机栈是线程隔离的,所以不会有线程安全问题

  • 线程封闭的指导思想是封闭,而不是共享。
  • ThreadLocal 是用来解决变量共享的并发安全问题

初探执行原理

ThreadLocal 为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
达到线程隔离的效果;互相执行各自的操作,互不影响;

其中维护了一个ThreadLocalMap集合;
ThreadLocalMap作为ThreadLocal的内部类

其中使用了set() 方法,get() 方法其实就是调用了ThreadLocalMap中的方法;

比如说这个set方法;

  • 调用set方法时, 先获取正在执行的线程对象, 为当前线程创建ThreadLocalMap对象

  • ThreadLocalMap的键就是ThreadLocal对象 , 值就是输入的值;

若当前线程中,没有Map;就去创建map,注意这时的key其实还是当前的ThreadLocal对象

然后看看get方法;
先根据自己线程去找ThreadLocal对象维护的的ThreadLocalMap集合


ThreadLocal带来的内存泄漏问题

“存储渗漏”。严格来说,只有对象不会再被程序用到了,但是 GC 又不能回收他们的情况,才叫内存泄漏。
尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现 OutofMemory 异常,导致程序崩溃。
注意,这里的存储空间并不是指物理内存,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。

TreadLocalMap 中 使用了 ThreadLocal 的弱引用作为 key,如果一个 ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被 GC 回收,
这样就会导致ThreadLocalMap 中 key 为 null, 而 value 还存在着强引用,当 thead 线程退出以后,value 的强引用链条才会断掉

注意:要是线程还不结束;那么这些key 为 null 的 Entry 的强引用 value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
那么就会产生内存泄漏


现在分析一下TreadLocalMap 中 的Key使用强引用 / 弱引用的情况;

  • 当 ThreadLocalMap 的 key 为 强 引 用的ThreadLocal 时 , 由于ThreadLocalMap 还 持 有 ThreadLocal 的 强 引 用 , 若不使用 手 动 删 除 ,ThreadLocal 不会被回收,还是会导致 Entry 内存泄漏。

  • 当 ThreadLocalMap 的 key 为 弱 引 用 的ThreadLocal 时, 由 于ThreadLocalMap 持 有 ThreadLocal 的 弱 引 用 , 即 使 没 有 手 动 删 除 ,ThreadLocal 也会被回收。当 key 为 null 后,在下一次 ThreadLocalMap 调用set(),get(),remove()方法的时候就会清除 value 值。


建议 每次使用完 ThreadLocal 都调用它的 remove()方法清除数据 ,防止出现内存泄漏



案例

public class ThreadLocalDemo 

    //创建一个ThreadLocal对象,用来为每个线程会复制保存一份变量,实现线程封闭
    private static ThreadLocal<Integer> localNum = new ThreadLocal<Integer>() 
        //初始化 ThreadLocal中的默认值
        @Override
        protected Integer initialValue() 
            return 0;
        
    ;


    public static void main(String[] args) 
        new Thread() 
            @Override
            public void run() 
                //设置副本的数值;
                localNum.set(1);
                try 
                    Thread.sleep(2000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                localNum.set(localNum.get() + 10);
                System.out.println(Thread.currentThread().getName() + ":" + localNum.get());//11
                //当线程中不再使用线程变量时,将变量值清除
                localNum.remove();
            
        .start();

        new Thread() 
            @Override
            public void run() 
                //设置副本的数值;
                localNum.set(3);
                try 
                    Thread.sleep(2000);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                
                localNum.set(localNum.get() + 20);
                System.out.println(Thread.currentThread().getName() + ":" + localNum.get());//23
                //当线程中不再使用线程变量时,将变量值清除
                localNum.remove();
            
        .start();

        //主线程的操作数,用的是默认的0;
        System.out.println(Thread.currentThread().getName() + ":" + localNum.get());//0
    


以上是关于重点知识学习(8.4)--[线程池 , ThreadLocal]的主要内容,如果未能解决你的问题,请参考以下文章

Java线程池参数

线程池

线程池

线程池实现。

Java 自定义线程池

Java多线程系列--“JUC线程池”02之 线程池原理