java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台(即 JavaEE(j2ee), JavaME(j2me), JavaSE(j2se))的总称。
这篇文章主要介绍了彻底理解 Java 中的 ThreadLocal 的相关资料, 需要的朋友可以参考下
ThreadLocal 是什么早在 JDK 1.2 的版本中就提供 Java.lang.ThreadLocal,ThreadLocal 为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
从线程的角度看,目标变量就象是线程的本地变量,这也是类名中 "Local" 所要表达的意思。
所以,在 Java 中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在 Java 开发者中得到很好的普及。
ThreadLocal 的接口方法ThreadLocal 类接口很简单,只有 4 个方法,我们先来了解一下:
值得一提的是,在 JDK5.0 中,ThreadLocal 已经支持泛型,该类的类名已经变为 ThreadLocal
ThreadLocal 是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在 ThreadLocal 类中有一个 Map,用于存储每一个线程的变量副本,Map 中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:
- package com.test;
- public class TestNum {
- // ①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
- private static ThreadLocal < Integer > seqNum = new ThreadLocal < Integer > () {
- public Integer initialValue() {
- return 0;
- }
- };
- // ②获取下一个序列值
- public int getNextNum() {
- seqNum.set(seqNum.get() + 1);
- return seqNum.get();
- }
- public static void main(String[] args) {
- TestNum sn = new TestNum();
- // ③ 3个线程共享sn,各自产生序列号
- TestClient t1 = new TestClient(sn);
- TestClient t2 = new TestClient(sn);
- TestClient t3 = new TestClient(sn);
- t1.start();
- t2.start();
- t3.start();
- }
- private static class TestClient extends Thread {
- private TestNum sn;
- public TestClient(TestNum sn) {
- this.sn = sn;
- }
- public void run() {
- for (int i = 0; i < 3; i++) {
- // ④每个线程打出3个序列值
- System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn[" + sn.getNextNum() + "]");
- }
- }
- }
- }
通常我们通过匿名内部类的方式定义 ThreadLocal 的子类,提供初始的变量值,如例子中①处所示。TestClient 线程产生一组序列号,在③处,我们生成 3 个 TestClient,它们共享同一个 TestNum 实例。运行以上代码,在控制台上输出以下的结果:
thread[Thread-0] --> sn[1]
thread[Thread-1] --> sn[1]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-0] --> sn[2]
thread[Thread-1] --> sn[3]
thread[Thread-2] --> sn[2]
thread[Thread-0] --> sn[3]
thread[Thread-2] --> sn[3]
考察输出的结果信息,我们发现每个线程所产生的序号虽然都共享同一个 TestNum 实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过 ThreadLocal 为每一个线程提供了单独的副本。
Thread 同步机制的比较ThreadLocal 和线程同步机制相比有什么优势呢?ThreadLocal 和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
而 ThreadLocal 则从另一个角度来解决多线程的并发访问。ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal 提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进 ThreadLocal。
由于 ThreadLocal 中可以持有任何类型的对象,低版本 JDK 所提供的 get() 返回的是 Object 对象,需要强制类型转换。但 JDK 5.0 通过泛型很好的解决了这个问题,在一定程度地简化 ThreadLocal 的使用,代码清单 9 2 就使用了 JDK 5.0 新的 ThreadLocal
概括起来说,对于多线程资源共享的问题,同步机制采用了 "以时间换空间" 的方式,而 ThreadLocal 采用了 "以空间换时间" 的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
spring 使用 ThreadLocal 解决线程安全问题我们知道在一般情况下,只有无状态的 Bean 才可以在多线程环境下共享,在 Spring 中,绝大部分 Bean 都可以声明为 singleton 作用域。就是因为 Spring 对一些 Bean(如 RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder 等)中非线程安全状态采用 ThreadLocal 进行处理,让它们也成为线程安全的状态,因为有状态的 Bean 就可以在多线程中共享了。
一般的 web 应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图 9‑2 所示:
通通透透理解 ThreadLocal
同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以 ThreadLocal 存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。
下面的实例能够体现 Spring 对有状态 Bean 的改造思路:
代码清单 3 TestDao:非线程安全
- package com.test;
- import java.sql.Connection;
- import java.sql.SQLException;
- import java.sql.Statement;
- public class TestDao {
- private Connection conn; // ①一个非线程安全的变量
- public void addTopic() throws SQLException {
- Statement stat = conn.createStatement(); // ②引用非线程安全变量
- // …
- }
- }
由于①处的 conn 是成员变量,因为 addTopic() 方法是非线程安全的,必须在使用时创建一个新 TopicDao 实例(非 singleton)。下面使用 ThreadLocal 对 conn 这个非线程安全的" 状态 " 进行改造:
代码清单 4 TestDao:线程安全
- package com.test;
- import java.sql.Connection;
- import java.sql.SQLException;
- import java.sql.Statement;
- public class TestDaoNew {
- // ①使用ThreadLocal保存Connection变量
- private static ThreadLocal < Connection > connThreadLocal = new ThreadLocal < Connection > ();
- public static Connection getConnection() {
- // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
- // 并将其保存到线程本地变量中。
- if (connThreadLocal.get() == null) {
- Connection conn = getConnection();
- connThreadLocal.set(conn);
- return conn;
- } else {
- return connThreadLocal.get(); // ③直接返回线程本地变量
- }
- }
- public void addTopic() throws SQLException {
- // ④从ThreadLocal中获取线程对应的Connection
- Statement stat = getConnection().createStatement();
- }
- }
不同的线程在使用 TopicDao 时,先判断 connThreadLocal.get() 是否是 null,如果是 null,则说明当前线程还没有对应的 Connection 对象,这时创建一个 Connection 对象并添加到本地线程变量中;如果不为 null,则说明当前的线程已经拥有了 Connection 对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的 Connection,而不会使用其它线程的 Connection。因此,这个 TopicDao 就可以做到 singleton 共享了。
当然,这个例子本身很粗糙,将 Connection 的 ThreadLocal 直接放在 DAO 只能做到本 DAO 的多个方法共享 Connection 时不发生线程安全问题,但无法和其它 DAO 共用同一个 Connection,要做到同一事务多 DAO 共享同一 Connection,必须在一个共同的外部类使用 ThreadLocal 保存 Connection。
ConnectionManager.java
- package com.test;
- import java.sql.Connection;
- import java.sql.DriverManager;
- import java.sql.SQLException;
- public class ConnectionManager {
- private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
- @Override
- protected Connection initialValue() {
- Connection conn = null;
- try {
- conn = DriverManager.getConnection(
- "jdbc:mysql://localhost:3306/test", "username",
- "password");
- } catch (SQLException e) {
- e.printStackTrace();
- }
- return conn;
- }
- };
- public static Connection getConnection() {
- return connectionHolder.get();
- }
- public static void setConnection(Connection conn) {
- connectionHolder.set(conn);
- }
- }
java.lang.ThreadLocal
那么到底 ThreadLocal 类是如何实现这种 "为每个线程提供不同的变量拷贝" 的呢?先来看一下 ThreadLocal 的 set() 方法的源码是如何实现的:
- /**
- * Sets the current thread's copy of this thread-local variable
- * to the specified value. Most subclasses will have no need to
- * override this method, relying solely on the {@link #initialValue}
- * method to set the values of thread-locals.
- *
- * @param value the value to be stored in the current thread's copy of
- * this thread-local.
- */
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- }
在这个方法内部我们看到,首先通过 getMap(Thread t) 方法获取一个和当前线程相关的 ThreadLocalMap,然后将变量的值设置到这个 ThreadLocalMap 对象中,当然如果获取到的 ThreadLocalMap 对象为空,就通过 createMap 方法创建。
线程隔离的秘密,就在于 ThreadLocalMap 这个类。ThreadLocalMap 是 ThreadLocal 类的一个静态内部类,它实现了键值对的设置和获取(对比 Map 对象来理解),每个线程中都有一个独立的 ThreadLocalMap 副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal 类通过操作每一个线程特有的 ThreadLocalMap 副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap 存储的键值对中的键是 this 对象指向的 ThreadLocal 对象,而值就是你所设置的对象了。
为了加深理解,我们接着看上面代码中出现的 getMap 和 createMap 方法的实现:
- /**
- * Get the map associated with a ThreadLocal. Overridden in
- * InheritableThreadLocal.
- *
- * @param t the current thread
- * @return the map
- */
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- /**
- * Create the map associated with a ThreadLocal. Overridden in
- * InheritableThreadLocal.
- *
- * @param t the current thread
- * @param firstValue value for the initial entry of the map
- * @param map the map to store.
- */
- void createMap(Thread t, T firstValue) {
- t.threadLocals = new ThreadLocalMap(this, firstValue);
- }
接下来再看一下 ThreadLocal 类中的 get() 方法:
- /**
- * Returns the value in the current thread's copy of this
- * thread-local variable. If the variable has no value for the
- * current thread, it is first initialized to the value returned
- * by an invocation of the {@link #initialValue} method.
- *
- * @return the current thread's value of this thread-local
- */
- public T get() {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this);
- if (e != null) return (T) e.value;
- }
- return setInitialValue();
- }
再来看 setInitialValue() 方法:
- /**
- * Variant of set() to establish initialValue. Used instead
- * of set() in case user has overridden the set() method.
- *
- * @return the initial value
- */
- private T setInitialValue() {
- T value = initialValue();
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- return value;
- }
获取和当前线程绑定的值时,ThreadLocalMap 对象是以 this 指向的 ThreadLocal 对象为键进行查找的,这当然和前面 set() 方法的代码是相呼应的。
进一步地,我们可以创建不同的 ThreadLocal 实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的 ThreadLocal 对象作为不同键,当然也可以在线程的 ThreadLocalMap 对象中设置不同的值了。通过 ThreadLocal 对象,在多线程中共享一个值和多个值的区别,就像你在一个 HashMap 对象中存储一个键值对和多个键值对一样,仅此而已。
小结ThreadLocal 是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal 比直接使用 synchronized 同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
ConnectionManager.java
后记
- package com.test;
- import java.sql.Connection;
- import java.sql.DriverManager;
- import java.sql.SQLException;
- public class ConnectionManager {
- private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
- @Override
- protected Connection initialValue() {
- Connection conn = null;
- try {
- conn = DriverManager.getConnection(
- "jdbc:mysql://localhost:3306/test", "username",
- "password");
- } catch (SQLException e) {
- e.printStackTrace();
- }
- return conn;
- }
- };
- public static Connection getConnection() {
- return connectionHolder.get();
- }
- public static void setConnection(Connection conn) {
- connectionHolder.set(conn);
- }
- }
看到网友评论的很激烈,甚至关于 ThreadLocalMap 不是 ThreadLocal 里面的,而是 Thread 里面的这种评论都出现了,于是有了这个后记,下面先把 jdk 源码贴上,源码最有说服力了。
- /**
- * ThreadLocalMap is a customized hash map suitable only for
- * maintaining thread local values. No operations are exported
- * outside of the ThreadLocal class. The class is package private to
- * allow declaration of fields in class Thread. To help deal with
- * very large and long-lived usages, the hash table entries use
- * WeakReferences for keys. However, since reference queues are not
- * used, stale entries are guaranteed to be removed only when
- * the table starts running out of space.
- */
- static class ThreadLocalMap {...
- }
源码就是以上,这源码自然是在 ThreadLocal 里面的,有截图为证。
本文是自己在学习 ThreadLocal 的时候,一时兴起,深入看了源码,思考了此类的作用、使用范围,进而联想到对传统的 synchronize 共享变量线程安全的问题进行比较,而总结的博文,总结一句话就是一个是锁机制进行时间换空间,一个是存储拷贝进行空间换时间。
以上所述是小编给大家介绍的 Java 中的 ThreadLocal,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 PHPERZ 网站的支持!
来源: http://www.phperz.com/article/18/0107/356651.html