核心原理
- ThreadLocal 处理的是线程的专属对象,各个线程的对象都是独立的。
- ThreadLocal 用来辅助平衡效率与资源分配。
- ThreadLocal 不是同步机制,也不解决共享对象的多线程竞态条件问题。
基本设计
首先看一个熟悉的场景:排队买票。
从理论上看,针对“排队买票”这个场景,我们可以有以下几种方案设计:
第一种. 固定设置一个售票窗口,每新来一个购票者就排在队伍的最后面。如图:
第二种. 动态设置售票窗口数量,每新来一个购票者就新开一个窗口来进行接待。如图:
第三种. 固定设置 N 个售票窗口,每新来一个购票者就选择一个售票窗口。如图:
比较一下上面的三种方案,很明显现实社会中使用的是第三种,那么各自有什么优势与缺点呢?
首先,整个卖票过程中,售票员是稀缺资源,售票窗口也是要消耗资源的。而且卖票的瓶颈也在售票窗口,因为售票员与购票者的交流需要花费很多时间,购票者多是在等待而已。
第一种:
优势是资源消耗低,只需要一个售票窗口就可以了,另一个好处就是不用处理不同售票窗口卖出了同一张票的问题(即竞态条件处理),
因为任一时刻只会有一个售票窗口,一次只会卖一张票,后台数据中心按顺序出票就行了。
但是劣势更明显,就是排队的效率低,如果购票者人数比较多,那么购票者的队伍会非常长。
第二种:
优势是排队效率高(根本就不用排队)。
劣势也很明显,不可能提供无限的售票窗口来满足大量的购票需求,另外,新建售票窗口也是需要时间的。
另一个问题就是,如果不同售票窗口都想卖出同一张票,这时后台数据中心就需要进行协调处理了(即竞态条件处理)。
第三种:
这种方式与上面两种并无绝对优势,只是一种平衡而已。
相比第一种方式,购票者需要排的队短很多;相比第二种方式,在售票窗口的资源投入上会少很多。
本文不讨论竞态条件的问题,因为为了防止卖出同一张票,你也可以让所有的售票窗口自己协商(比如任一时刻只能有一个售票窗口卖票,等等)。
那么,这个场景怎样对应到具体的程序中呢?
后台数据中心 —— 数据库
售票窗口 —— 数据库连接
购票者 —— 单个消息(也就是具体的单个任务,此处我称之为“消息”)
购票队列 —— 消息队列
其中:
一组 售票窗口 + 购票队列 + 购票者
属于同一个线程的内部变量。
注意,本文的核心是关注点是售票窗口资源消耗。这个时候就是 ThreadLocal 出场的时候,看一个演示程序:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
| public class ThreadLocalTest { public static void main(String[] args) { new Thread() { @Override public void run() { TicketWindow ticketWindow = TicketWindow.prepare(); IntStream.range(0, 5).forEach(i -> new Buyer("buyer_" + i).enqueue()); ticketWindow.open(); } }.start();
new Thread() { @Override public void run() { TicketWindow ticketWindow = TicketWindow.prepare(); IntStream.range(5, 10).forEach(i -> new Buyer("buyer_" + i).enqueue()); ticketWindow.open(); } }.start(); } }
class TicketWindow {
private static ThreadLocal<TicketWindow> sTicketWindow = new ThreadLocal<TicketWindow>() { public TicketWindow initialValue() { return new TicketWindow(Thread.currentThread().getName()); } };
public static TicketWindow prepare() { return sTicketWindow.get(); }
private String name; private BuyerQueue buyerQueue;
public TicketWindow(String name) { this.name = name; this.buyerQueue = new BuyerQueue(); }
public String getName() { return name; }
public void enqueue(Buyer buyer) { buyerQueue.enqueue(buyer); System.out.println("enqueue:" + buyer.getName()); }
public void open() { while (true) { Buyer buyer = buyerQueue.poll(); if (buyer != null) { System.out.println(getName() + " turn:" + buyer.getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } buyer.buy(); System.out.println(getName() + " sold:" + buyer.getName()); } } } }
class BuyerQueue { private Queue<Buyer> queue = new LinkedBlockingDeque<>();
public void enqueue(Buyer buyer) { queue.add(buyer); }
public Buyer poll() { return queue.poll(); }
}
class Buyer {
private String name;
public Buyer(String name) { this.name = name; }
public String getName() { return name; }
public void enqueue() { TicketWindow ticketWindow = TicketWindow.prepare(); ticketWindow.enqueue(this); }
public void buy() { } }
|
其中,TicketWindow 在每个 Thread 只会有一个,每个 Thread 的 TicketWindow 都不同。
在使用的过程中,既不需要显示的给线程设置 TicketWindow,其他线程也访问不了当前线程的 TicketWindow,也起到了排除干扰的效果。
当然,不用 ThreadLocal,直接用 Map 也可以,ThreadLocal 也是用 Map 实现的
一个实际的应用
SimpleDateFormat 是线程不安全的,如果每次调用都 new 一个新的 SimpleDateFormat 对象,实在浪费。所以这个时候就可以用 ThreadLocal
来让每个线程都持有一个 SimpleDateFormat 的实例,避免创建无用的对象。
ThreadLocal 与 clone
到这里,似乎不需要 ThreadLocal,直接用 clone() 也可以达到相同的效果。
这当然没错,不过使用 clone() 的话,就需要处理更多的细节,而且与 ThreadLocal#initialValue() 的作用是一致的。
存在问题
通常来讲,ThreadLocal 不会造成什么大问题,因为在 Thread 执行完毕的时候,所有的资源都会被自动释放掉。
唯一的问题就是——线程池。
如果我们在线程池中使用了 ThreadLocal ,由于我们通常无法直接控制线程池线程的创建与释放,也就意味着除非手动清除掉 ThreadLocal 中的资源,
否则,ThreadLocal 持有的资源可能永远无法释放,内存泄漏的隐患由此而生。
结束语
ThreadLocal 把对象级别的变量共享变为线程级别的变量共享,从而辅助平衡资源与效率,本身并不是一种同步机制,因为每个线程持有的都是不同的对象。
至于对象所要处理的状态问题,交给别的角色去处理了。所以,本质上是将整个同步的时机向后延迟了(或者说,转移了同步责任)。