剑指JUC原理-15.ThreadLocal(上):https://developer.aliyun.com/article/1413653
常规解决方案
常规方案的实现
基于上面给出的前提, 大家通常想到的解决方案是 :
- 传参: 从service层将connection对象向dao层传递
- 加锁
以下是代码实现修改的部分:
AccountService 类
public class AccountService { public boolean transfer(String outUser, String inUser, int money) { AccountDao ad = new AccountDao(); //线程并发情况下,为了保证每个线程使用各自的connection,故加锁 synchronized (AccountService.class) { Connection conn = null; try { conn = JdbcUtils.getConnection(); //开启事务 conn.setAutoCommit(false); // 转出 ad.out(conn, outUser, money); // 模拟转账过程中的异常 // int i = 1/0; // 转入 ad.in(conn, inUser, money); //事务提交 JdbcUtils.commitAndClose(conn); } catch (Exception e) { e.printStackTrace(); //事务回滚 JdbcUtils.rollbackAndClose(conn); return false; } return true; } } }
AccountDao 类 (这里需要注意的是: connection不能在dao层释放,要在service层,不然在dao层释放,service层就无法使用了)
public class AccountDao { public void out(Connection conn, String outUser, int money) throws SQLException{ String sql = "update account set money = money - ? where name = ?"; //注释从连接池获取连接的代码,使用从service中传递过来的connection // Connection conn = JdbcUtils.getConnection(); PreparedStatement pstm = conn.prepareStatement(sql); pstm.setInt(1,money); pstm.setString(2,outUser); pstm.executeUpdate(); //连接不能在这里释放,service层中还需要使用 // JdbcUtils.release(pstm,conn); JdbcUtils.release(pstm); } public void in(Connection conn, String inUser, int money) throws SQLException { String sql = "update account set money = money + ? where name = ?"; // Connection conn = JdbcUtils.getConnection(); PreparedStatement pstm = conn.prepareStatement(sql); pstm.setInt(1,money); pstm.setString(2,inUser); pstm.executeUpdate(); // JdbcUtils.release(pstm,conn); JdbcUtils.release(pstm); } }
常规方案的弊端
上述方式我们看到的确按要求解决了问题,但是仔细观察,会发现这样实现的弊端:
- 直接从service层传递connection到dao层, 造成代码耦合度提高
- 加锁会造成线程失去并发性,程序性能降低
ThreadLocal解决方案
ThreadLocal方案的实现
像这种需要在项目中进行数据传递和线程隔离的场景,我们不妨用ThreadLocal来解决:
工具类的修改: 加入ThreadLocal
public class JdbcUtils { //ThreadLocal对象 : 将connection绑定在当前线程中 private static final ThreadLocal<Connection> tl = new ThreadLocal(); // c3p0 数据库连接池对象属性 private static final ComboPooledDataSource ds = new ComboPooledDataSource(); // 获取连接 public static Connection getConnection() throws SQLException { //取出当前线程绑定的connection对象 Connection conn = tl.get(); if (conn == null) { //如果没有,则从连接池中取出 conn = ds.getConnection(); //再将connection对象绑定到当前线程中 tl.set(conn); } return conn; } //释放资源 public static void release(AutoCloseable... ios) { for (AutoCloseable io : ios) { if (io != null) { try { io.close(); } catch (Exception e) { e.printStackTrace(); } } } } public static void commitAndClose() { try { Connection conn = getConnection(); //提交事务 conn.commit(); //解除绑定 及时释放 tl.remove(); //释放连接 conn.close(); } catch (SQLException e) { e.printStackTrace(); } } public static void rollbackAndClose() { try { Connection conn = getConnection(); //回滚事务 conn.rollback(); //解除绑定 及时释放 tl.remove(); //释放连接 conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }
AccountService类的修改:不需要传递connection对象
public class AccountService { public boolean transfer(String outUser, String inUser, int money) { AccountDao ad = new AccountDao(); try { Connection conn = JdbcUtils.getConnection(); //开启事务 conn.setAutoCommit(false); // 转出 : 这里不需要传参了 ! ad.out(outUser, money); // 模拟转账过程中的异常 // int i = 1 / 0; // 转入 ad.in(inUser, money); //事务提交 JdbcUtils.commitAndClose(); } catch (Exception e) { e.printStackTrace(); //事务回滚 JdbcUtils.rollbackAndClose(); return false; } return true; } }
AccountDao类的修改:照常使用
ThreadLocal方案的好处
从上述的案例中我们可以看到, 在一些特定场景下,ThreadLocal方案有两个突出的优势:
传递数据 : 保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题
线程隔离 : 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失
ThreadLocal的内部结构
通过以上的学习,我们对ThreadLocal的作用有了一定的认识。现在我们一起来看一下ThreadLocal的内部结构,探究它能够实现线程数据隔离的原理。
常见的误解
如果我们不去看源代码的话,可能会猜测ThreadLocal是这样子设计的:每个ThreadLocal都创建一个Map,然后用线程作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal 确实是这样设计的,但现在早已不是了。
现在的设计
但是,JDK后面优化了设计方案,在JDK8中 ThreadLocal
的设计是:每个Thread
维护一个ThreadLocalMap
,这个Map的key
是ThreadLocal
实例本身,value
才是真正要存储的值Object
。
具体的过程是这样的:
- 每个Thread线程内部都有一个Map (ThreadLocalMap)
- Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
这样设计的好处
这个设计与我们一开始说的设计刚好相反,这样设计有如下两个优势:
这样设计之后每个Map存储的Entry数量就会变少。因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际运用当中,往往ThreadLocal的数量要少于Thread的数量。
当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。
ThreadLocal的核心方法源码
基于ThreadLocal的内部结构,我们继续分析它的核心方法源码,更深入的了解其操作原理。
除了构造方法之外, ThreadLocal对外暴露的方法有以下4个:
方法声明 | 描述 |
protected T initialValue() | 返回当前线程局部变量的初始值 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
以下是这4个方法的详细源码分析(为了保证思路清晰, ThreadLocalMap部分暂时不展开,下一个知识点详解)
set方法
源码和对应的中文注释
/** * 设置当前线程对应的ThreadLocal的值 * * @param value 将要保存在当前线程对应的ThreadLocal的值 */ public void set(T value) { // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取此线程对象中维护的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 判断map是否存在 if (map != null) // 存在则调用map.set设置此实体entry map.set(this, value); else // 1)当前线程Thread 不存在ThreadLocalMap对象 // 2)则调用createMap进行ThreadLocalMap对象的初始化 // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中 createMap(t, value); } /** * 获取当前线程Thread对应维护的ThreadLocalMap * * @param t the current thread 当前线程 * @return the map 对应维护的ThreadLocalMap */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; } /** *创建当前线程Thread对应维护的ThreadLocalMap * * @param t 当前线程 * @param firstValue 存放到map中第一个entry的值 */ void createMap(Thread t, T firstValue) { //这里的this是调用此方法的threadLocal t.threadLocals = new ThreadLocalMap(this, firstValue); }
代码执行流程
- 首先获取当前线程,并根据当前线程获取一个Map
- 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
- 如果Map为空,则给该线程创建 Map,并设置初始值
get方法
源码和对应的中文注释
/** * 返回当前线程中保存ThreadLocal的值 * 如果当前线程没有此ThreadLocal变量, * 则它会通过调用{@link #initialValue} 方法进行初始化值 * * @return 返回当前线程对应此ThreadLocal的值 */ public T get() { // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取此线程对象中维护的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 如果此map存在 if (map != null) { // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e ThreadLocalMap.Entry e = map.getEntry(this); // 对e进行判空 if (e != null) { @SuppressWarnings("unchecked") // 获取存储实体 e 对应的 value值 // 即为我们想要的当前线程对应此ThreadLocal的值 T result = (T)e.value; return result; } } /* 初始化 : 有两种情况有执行当前代码 第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象 第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry */ return setInitialValue(); } /** * 初始化 * * @return the initial value 初始化后的值 */ private T setInitialValue() { // 调用initialValue获取初始化的值 // 此方法可以被子类重写, 如果不重写默认返回null T value = initialValue(); // 获取当前线程对象 Thread t = Thread.currentThread(); // 获取此线程对象中维护的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 判断map是否存在 if (map != null) // 存在则调用map.set设置此实体entry map.set(this, value); else // 1)当前线程Thread 不存在ThreadLocalMap对象 // 2)则调用createMap进行ThreadLocalMap对象的初始化 // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中 createMap(t, value); // 返回设置的值value return value; }
代码执行流程
- 首先获取当前线程, 根据当前线程获取一个Map
- 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D
- 如果e不为null,则返回e.value,否则转到D
- Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。
remove方法
源码和对应的中文注释
/** * 删除当前线程中保存的ThreadLocal对应的实体entry */ public void remove() { // 获取当前线程对象中维护的ThreadLocalMap对象 ThreadLocalMap m = getMap(Thread.currentThread()); // 如果此map存在 if (m != null) // 存在则调用map.remove // 以当前ThreadLocal为key删除对应的实体entry m.remove(this); }
代码执行流程
- 首先获取当前线程,并根据当前线程获取一个Map
- 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry
initialValue方法
/** * 返回当前线程对应的ThreadLocal的初始值 * 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时 * 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。 * 通常情况下,每个线程最多调用一次这个方法。 * * <p>这个方法仅仅简单的返回null {@code null}; * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值, * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法 * 通常, 可以通过匿名内部类的方式实现 * * @return 当前ThreadLocal的初始值 */ protected T initialValue() { return null; }
此方法的作用是 返回该线程局部变量的初始值。
- 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
- 这个方法缺省实现直接返回一个
null
。 - 如果想要一个除null之外的初始值,可以重写此方法。(备注: 该方法是一个
protected
的方法,显然是为了让子类覆盖而设计的)
剑指JUC原理-15.ThreadLocal(下):https://developer.aliyun.com/article/1413657