Java-ThreadLocal
Java-ThreadLocal
1. 认识ThreadLocal
ThreadLocal 是 Java 中一种很重要的机制 / 数据处理方式,尤其在并发中,数据是否共有、唯一,会直接影响项目的运行逻辑。一般情况下,防止并发冲突或数据不安全的做法是给对象、方法加同步锁 synchronized
。但同步锁并不是万能的,例如同步锁会降低批量处理的效率,或者当业务需要保证数据的隔离性,使用同步锁则需要在方法内频繁销毁、重建对象,如果数据使用独立的处理模块,还会破坏模块化,提高耦合。为此,JDK 1.2 增加了一个工具类:即 ThreadLocal
。
2. 不使用ThreadLocal的问题
为了更好地理解 ThreadLocal 的设计理念,首先考虑以下两个更普遍一些的场景:
- 一个客户端,需要并发地和服务器交互,并且每个连接都需要持久化(需要保存 Cookie)。
- 一个客户端,需要并发地存取数据库,并且每个连接都可能会提交超过一个操作。
(1)如果按最简单的方式来做,每个线程都维护一套自己的网络请求框架,确实不会导致什么异常,但是第一:实际项目中不可能采用这个方案,第二:这么做简直就是“高耦合低内聚”的代表,第三:重新参考以上两条。项目中,绝大部分情况下,一个连接会话会在一个独立的线程内执行,这个线程需要维护一个仅对自己可见的 Cookie,不仅对其他会话不可见,同时也要确保只能获取到自己的 Cookie。
(2)同样,每个线程都独立维护一套数据库会话管理是不现实的,通常会封装到一个工具类中,从工具类中获取、开启、关闭会话以及提交事务等。假如有一个业务:当修改用户信息时,把这个操作记录保存下来。假如每一次的操作都从连接池获取 Connection,就有可能一个操作执行了另一个可能因为某些原因没有执行,所以一般通过以下方式来获取管理 Connection:
1 | public class DBUtil { |
然后用 Transaction 统一提交事务。到目前为止,如果是单线程做这个操作,那是没问题的,但是如果出现并发的情况呢?如果并发很低并且操作轻量,给这个业务方法上同步锁,也是没有问题的,但是如果并发稍微高一些,就不可能放个同步锁了,这时如果还使用这个方案,很有可能会出现 No operations allowed after connection closed
错误,这是因为连接是共享的,如果后启动的线程 2 先执行完并且关闭了连接,先启动的线程 1 再执行相关操作时连接已经被关闭了。
3. ThreadLocal如何解决问题
ThreadLocal 为每个线程分配了一个独立的资源副本,并在内部通过一个 Table 表来维护每个线程和其拥有的独立资源副本的映射关系,所有的线程共享这个 Table。简单点说,ThreadLocal 中通过 set()
方法存入对象,通过 get()
方法取出对象,且线程存入的对象只有该线程自己可以获取到,,每个线程也只能获取到自己之前存入的对象,如果没有存入则调用 get()
返回的是 null
。
因此针对以上两个场景,用 ThreadLocal 就可以很好地解决痛点。
(1)首先是会话连接的持久化,每个线程在建立连接后,调用 set()
将自己的 Cookie 存入,并在需要的时候调用 get()
获取即可,对于每个会话线程,get()
到的都只是自己的 Cookie。
(2)第二个数据库连接管理,也可以把共用的 Connection 放进 ThreadLocal 中管理,改成如下:
1 | public class DBUtil { |
当一个新线程调用 DBUtil.getConnection()
时,会先判断当前线程是否已经存入了一个连接,如果已经存入则直接获取并返回,否则创建一个新的连接,关闭连接时同理。这样,线程之间的连接都是自己的独立对象,不会互相影响。
4. ThreadLocal和同步锁的比较
当然,ThreadLocal 并不是万能的,相比较同步锁方式,由于每个线程都拥有自己的资源副本,因此消耗的内存也更多,需要根据具体的业务确定方案。详细分析将在之后重新整理一份独立文章。
5. ThreadLocal源码分析
暂未完成