并行设计模式--Thread Specific Storge模式
多线程的不安全在于共享了变量实例,因此Thread Specific Storge
模式的思路是把变量与单一线程绑定,那么就不存在共享,自然就避免了加锁消耗以及其他高并发所需要的策略。Thread Specific Storge
一般有两种策略:1. ThreadLocal策略,也就是与当前线程实例绑定。 2. 借用模式对象池策略,由对象池进行管理,控制对象只能同一时间被一个单线程使用。
ThreadLocal设计与应用
ThreadLocal策略
ThreadLocal策略比较简单,其原理是在Thread
类中私有化一个属性变量java.lang.ThreadLocal.ThreadLocalMap
,该Map存储着与当前线程绑定的相关变量。一个ThreadLocal
的基本使用如下:
1 | private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>(); |
上述代码在内存中的结构如下,其对象本身ThreadLocal
会作为ThreadLocalMap
的key存储。
既然是Map结构,那么会有几个问题:
ThreadLocalMap是如何解决hash冲突的?ThreadLocalMap
是一个简单的Map实现,其没有构造对应的冲突链,而是当遇到冲突时顺延到下一个槽位,也就是常说的开放地址法,具体逻辑可以在java.lang.ThreadLocal.ThreadLocalMap#set
中看到。
ThreadLocalMap的扩容机制是什么?
扩容要提到负载因子,其负载因子计算为threshold = len * 2 / 3
,当元素个数大于该值时会触发扩容,扩容操作把之前元素拷贝进来后替换掉之前的数组。
使用ThreadLocal复用对象
在Java中有一些线程不安全的对象需要被频繁创建,比如StringBuilder
,那么就可以利用ThreadLocal
复用这些对象。
在BigDecimal
中有如下类,其本身是包装了StringBuilder
,并提供重置方法。
1 | static class StringBuilderHelper { |
在使用前需要把该类使用ThreadLocal
包裹
1 | private static final ThreadLocal<StringBuilderHelper> |
利用ThreadLocal
这样设计解决了线程不安全的问题,然后提高对象复用性,尤其是大字符串的拼接会让StringBuilder
不停的扩容,频繁创建对性能影响还是挺大的。
对象池策略
借还策略下的对象池模式也经常被用来解决非线程安全的类在多线程环境下的使用,所谓的借还模式如下所示
1 | public static void main(String[] args) throws Exception { |
上述代码中pool
是一个多线程可以共享的实例,其必须保证对象的借出与归还的原子性,当对象被借出时那么对象就与当前线程绑定了起来,对象池保证了其他线程操作时不会再次获取到该实例,因此对象不存在共享,也就不存在多线程并发问题。
对象池的控制原理
以apache common pool2
为例,其GenericObjectPool
的实现原理主要是ConcurrentMap
与LinkedBlockingDeque(非JDK版本)
,如下图所示:
对象池本质上是一个集生产与消费,且支持可回收的工厂。生产则对应着用户获取对象时,如果当前idleObjects
中不存在则主动去创建对象,消费则对应着Client的borrowObject
操作,可回收则是returnObject
还回池中操作。作为工厂其由责任对生产出的产品个数与消费能力的变化进行调整,因此还需要有一个后台线程做这件事,对应着是org.apache.commons.pool2.impl.BaseGenericObjectPool.Evictor
类定时清理策略。
对应的核心操作解析:
borrowObject操作borrowObject
操作主要是从对象池也就是上述的LinkedBlockingDeque<PooledObject<T>> idleObjects
中取出实体,当实体不存在的时候要主动去创建,
1 | // 取出队首元素,该方法并不会产生阻塞 |
如果上述过程中仍然没有获取到对象,则根据配置选择是否阻塞当前调用,阻塞则使用BlockingDeque
的take操作或者poll(time)操作
1 | if (p == null) { |
returnObject操作returnObject
操作主要是把使用过的对象还回池中,反映到操作上就是把一个对象放入LinkedBlockingDeque<PooledObject<T>> idleObjects
的队首或者队尾,当可用对象过多,则是使用直接销毁对象的策略。
1 | // 最大可用对象数量 |
removeAbandoned操作removeAbandoned
主要应对内存中对象实例进行清理,当Client使用完对象却没有还回,此时该对象就应该被清理掉。
清理策略主要针对被借出的对象,对象被借出时该对象上有对应的时间标记,因此遍历池中所有对象,清除状态为被借出,并且借出时间大于指定时间的对象即可。
1 | // 获取全部对象的迭代器 |
Evictor驱逐线程Evictor
是一个TimerTask
的定时任务,其主要功能是清理可用对象数量,保证idleObjects
中的数量最小可用。Evictor
对应的操作在org.apache.commons.pool2.impl.GenericObjectPool#evict
方法中,其逻辑是遍历idleObjects
中可用对象,使用策略接口EvictionPolicy
判断是否符合销毁条件,符合则销毁,逻辑比较简单。
而EvictionPolicy
的默认策略为对象在idleObjects
的存活时间大于配置的清理时间,并且当前idleObjects
的数量对象大于最小可用对象配置的情况下进行回收。
1 |
|
总结
Thread Specific Storge
模式的本质是不共享数据,从而解决了多线程下竞争的问题,一般情况下对于构造成本比较小的数据直接使用ThreadLocal
,需要时则直接创建一个与当前线程所绑定。构造成本比较大的对象比如各种连接池则使用对象池方式。
参考
- 版权声明: 感谢您的阅读,本文由屈定's Blog版权所有。如若转载,请注明出处。
- 文章标题: 并行设计模式--Thread Specific Storge模式
- 文章链接: https://mrdear.cn/posts/design-patterns-thread-specific-storge.html