ThreadLocal

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

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

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

再举个简单的例子:两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。

数据结构

其实不是ThreadLocal有这个数据结构,是Thread持有的,有一个ThreadLocalMap的数组,专门放键值对,K为ThreadLocal的类对象,V为ThreadLocal泛型的数据。

也就是说,在一个线程中,如果有多个ThreadLocal,查找Map键为这个ThreadLocal变量,就可以很轻松拿到存的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void set(T value) {
//获取当前请求的线程
Thread t = Thread.currentThread();
//取出 Thread 类内部的 threadLocals 变量(哈希表结构)
ThreadLocalMap map = getMap(t);
if (map != null)
// 将需要存储的值放入到这个哈希表中
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

内存泄露问题

这个Map里面,Key是弱引用的,也就是说每次gc都会回收key,而value是强引用的。这个时候就会出现,key被gc回收了为null,value还有的情况。这个时候就会产生内存泄露。

解决方法:释放的时候手动remove。

线程池的四个种类

  1. newCachedThreadPool创建一个可缓存的线程池,默认阻塞队列是SynchronousQueue
  2. newFixedThreadPool创建一个定长的线程池,默认阻塞队列是LinkedBlockingQueue
  3. newSingleThreadExecutor创建一个单例线程,默认也是LinkedBlockingQueue
  4. newScheduled创建一个可以设置定时任务的线程

线程池的核心参数

除了上面四种封装好的,还可以自己创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
int maximumPoolSize,//线程池的最大线程数
long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

七个核心参数:

  1. 核心线程数量
  2. 最大线程数量
  3. 过期时间:如果线程池的线程数量大于核心线程数量,如果没有新的任务提交,那么已经到期的线程不会立刻销毁,而是等一段时间销毁
  4. 过期时间单位:可以是秒,毫秒
  5. 阻塞队列:刚刚提的那些,后面还会说
  6. 饱和缩略
  7. 线程工厂类:一般都是默认的,可以定制线程对象的创建,例如设置线程名字、是否是守护线程等

饱和策略

当阻塞队列满了,而且最大线程数量也满了,就会触发饱和策略

  1. 抛出异常,不让加了
  2. 线程不走线程池,提交线程的那个线程自己来运行
  3. 不报错,直接丢弃
  4. 丢弃队列最前面那个,然后加进队列

如何确定线程数量

  • io密集型:2n+1
  • cpu密集型:n+1
    这里的n都是当前机器的虚拟内核数量,io密集型主要都是io时间多,对于cpu负载并不大