Java并发3

Java基础 并发编程

ThreadLocal

ThreadLocal作用

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?

JDK 中自带的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。

如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。他们可以使用 get()set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。

ThreadLocal原理

Thread类源代码入手。

public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法。

使用ThreadLocal类的set()方法时,value实际上存放在当前线程的ThreadLocalMap中。ThreadLocal可以理解为只是ThreadLocalMap的封装,传递了变量值。ThrealLocal类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

比如我们在同一个线程中声明了两个 ThreadLocal 对象的话, Thread内部都是使用仅有的那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。

ThreadLocal 数据结构如下图所示:

ThreadLocalMapThreadLocal的静态内部类。

ThreadLocal内存泄露

ThreadLocalMap中使用的key为ThreadLocal的弱引用,value为强引用。所以如果ThreadLocal没有被外部强引用的情况下,在GC时,key会被清理,而value不会清理。进而在ThreadLocalMap中产生了key为null的键值对,如果不做任何措施,value永远不会被GC。这样就会发生内存泄露。

ThreadLocalMap实现中考虑到了这个情况,在调用get(), set(), remove()方法的时候会清理掉key为null的键值对。

线程池

线程池是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程不会立即被销毁,而是等待下一个任务。

使用线程池的好处

如何创建线程池

方式一:通过ThreadPoolExecutor构造函数来创建。(推荐)

方式二:通过Executor框架的工具类Executors来创建。

我们可以创建多种类型的ThreadPoolExecutor

为何不推荐使用Executors创建?

Executors返回线程池对象的弊端主要是OOM

线程池常见参数

ThreadPoolExecutor 3 个最重要的参数:

ThreadPoolExecutor其他常见参数 :

线程池的饱和策略

线程池的阻塞队列

不同的线程池会选用不同的阻塞队列,我们可以结合内置线程池来分析。

线程池处理任务流程

线程池合适大小

如果线程池太小,同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的,CPU 根本没有得到充分利用。

如果设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

有一个简单并且适用面比较广的公式: