JUC并发—12.ThreadLocal源码分析
大纲
1.ThreadLocal的特点介绍
2.ThreadLocal的使用案例
3.ThreadLocal的内部结构
4.ThreadLocal的核心方法源码
5.ThreadLocalMap的核心方法源码
6.ThreadLocalMap的原理总结
1.ThreadLocal的特点介绍
(1)ThreadLocal的注释说明
(2)ThreadLocal的常用方法
(3)ThreadLocal的使用案例
(4)ThreadLocal类与synchronized关键字对比
(1)ThreadLocal的注释说明
//This class provides thread-local variables. //These variables differ from their normal counterparts in that //each thread that accesses one (via its get or set method) has its own, //independently initialized copy of the variable. //ThreadLocal instances are typically private static fields in classes that //wish to associate state with a thread (e.g., a user ID or Transaction ID). public class ThreadLocal<T> { ... ... }
ThreadLocal类可以提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时,能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常是private static类型,用于关联线程和线程上下文。
由此可知,ThreadLocal的作用是:提供线程内的局部变量,不同的线程之间不会相互干扰。由于这种变量只会在线程的生命周期内起作用,所以可以减少同一个线程内,在多个函数或者组件之间传递数据的复杂度。
ThreadLocal的总结如下:
一.线程并发:适用于多线程并发的场景
二.传递数据:可通过ThreadLocal在同一线程,不同组件中传递公共变量
三.线程隔离:每个线程的变量都是独立的,互相之间不会影响
(2)ThreadLocal的常用方法
ThreadLocal的常用方法如下:
(3)ThreadLocal的使用案例
一.没用ThreadLocal时共享变量在线程间不隔离
//需求:线程隔离 //在多线程并发的场景下, 每个线程中的变量都是相互独立 //线程A:设置(变量1) 获取(变量1) //线程B:设置(变量2) 获取(变量2) public class MyDemo { //变量 private String content; private String getContent() { return content; } private void setContent(String content) { this.content = content; } public static void main(String[] args) { MyDemo demo = new MyDemo(); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { //每个线程: 先存一个变量, 过一会再取出这个变量 demo.setContent(Thread.currentThread().getName() + "的数据"); System.out.println("-----------------------"); System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent()); } }); thread.setName("线程" + i); thread.start();//启动线程 } } }
运行后打印结果如下,从结果可以看出多个线程在访问同一个变量的时候出现了异常,有些线程取出了其他线程的数据,线程之间的数据没有实现隔离。
----------------------- ----------------------- 线程0--->线程3的数据 ----------------------- ----------------------- 线程2--->线程4的数据 ----------------------- 线程4--->线程4的数据 线程3--->线程3的数据 线程1--->线程4的数据
二.使用ThreadLocal实现共享变量在线程间隔离
//需求:线程隔离 //在多线程并发的场景下, 每个线程中的变量都是相互独立 //线程A:设置(变量1) 获取(变量1) //线程B:设置(变量2) 获取(变量2) //ThreadLocal: //1.set(): 将变量绑定到当前线程中 //2.get(): 获取当前线程绑定的变量 public class MyDemo { private static ThreadLocal<String> tl = new ThreadLocal<>(); //变量 private String content; private String getContent() { return tl.get(); } private void setContent(String content) { //变量content绑定到当前线程 tl.set(content); } public static void main(String[] args) { MyDemo demo = new MyDemo(); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { demo.setContent(Thread.currentThread().getName() + "的数据"); System.out.println("-----------------------"); System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent()); } }); thread.setName("线程" + i); thread.start();//启动线程 } } }
运行后打印结果如下,从结果看,很好地解决了多线程之间数据隔离的问题。
----------------------- 线程0--->线程0的数据 ----------------------- 线程1--->线程1的数据 ----------------------- 线程2--->线程2的数据 ----------------------- 线程3--->线程3的数据 ----------------------- 线程4--->线程4的数据
(4)ThreadLocal类与synchronized关键字对比
一.synchronized同步方式
上述线程隔离的效果完全可以通过加synchronized锁来实现。
//需求:线程隔离 //在多线程并发的场景下, 每个线程中的变量都是相互独立 //线程A:设置(变量1) 获取(变量1) //线程B:设置(变量2) 获取(变量2) public class MyDemo { //变量 private String content; private String getContent() { return content; } private void setContent(String content) { this.content = content; } public static void main(String[] args) { MyDemo demo = new MyDemo(); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Runnable() { @Override public void run() { //每个线程: 存一个变量, 过一会再取出这个变量 synchronized (MyDemo.class){ demo.setContent(Thread.currentThread().getName() + "的数据"); System.out.println("-----------------------"); System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent()); } } }); thread.setName("线程" + i); //线程0~4 thread.start(); } } }
运行后打印结果如下:
----------------------- 线程0--->线程0的数据 ----------------------- 线程1--->线程1的数据 ----------------------- 线程2--->线程2的数据 ----------------------- 线程3--->线程3的数据 ----------------------- 线程4--->线程4的数据
从结果可以发现,加锁确实可以解决这个问题。但是这里强调的是多线程数据隔离的问题,并不是多线程共享数据的问题。在这个案例中使用synchronized关键字是不合适的,降低了并发性。
二.ThreadLocal与synchronized的区别
虽然ThreadLocal模式与synchronized关键字,都可以用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不同。
三.总结
虽然使用ThreadLocal和synchronized都能解决线程间数据隔离的问题,但是ThreadLocal更为合适,因为它可以使程序拥有更高的并发性。
2.ThreadLocal的使用案例
(1)转账案例的场景
(2)转账案例引入事务
(3)常规方案解决引入事务后的问题
(4)ThreadLocal方案解决引入事务后的问题
(5)ThreadLocal的应用场景总结
(1)转账案例的场景
有一个数据表account,里面有两个用户Jack和Rose,Jack给Rose转账。案例的实现使用了MySQL数据库、JDBC和C3P0框架,以下是详细的代码。
一.数据准备
--使用数据库 use test; --创建一张账户表 create table account( id int primary key auto_increment, name varchar(20), money double ); -- 初始化数据 insert into account values(null, 'Jack', 1000); insert into account values(null, 'Rose', 0);
二.C3P0配置文件和工具类
<c3p0-config> <!-- 使用默认的配置读取连接池对象 --> <default-config> <!-- 连接参数 --> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property> <property name="user">root</property> <property name="password">123456</property> <!-- 连接池参数 --> <property name="initialPoolSize">5</property> <property name="maxPoolSize">10</property> <property name="checkoutTimeout">3000</property> </default-config> </c3p0-config>
三.JdbcUtils工具类
public class JdbcUtils { //c3p0数据库连接池对象属性 private static final ComboPooledDataSource ds = new ComboPooledDataSource(); //从数据库连接池中获取一个连接 public static Connection getConnection() throws SQLException { return ds.getConnection(); } //释放资源 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(Connection conn) { try { if (conn != null) { //提交事务 conn.commit(); //释放连接 conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } public static void rollbackAndClose(Connection conn) { try { if (conn != null) { //回滚事务 conn.rollback(); //释放连接 conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }
四.Dao层代码:AccountDao
public class AccountDao { public void out(String outUser, 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,outUser); pstm.executeUpdate(); JdbcUtils.release(pstm,conn); } public void in(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); } }
五.Service层代码:AccountService
public class AccountService { public boolean transfer(String outUser, String inUser, int money) { AccountDao ad = new AccountDao(); try { //转出 ad.out(outUser, money); //转入 ad.in(inUser, money); } catch (Exception e) { e.printStackTrace(); return false; } return true; } }
六.Web层代码:AccountWeb
public class AccountWeb { public static void main(String[] args) { //模拟数据 : Jack给Rose转账100 String outUser = "Jack"; String inUser = "Rose"; int money = 100; AccountService as = new AccountService(); boolean result = as.transfer(outUser, inUser, money); if (result == false) { System.out.println("转账失败!"); } else { System.out.println("转账成功!"); } } }
(2)转账案例引入事务
案例中的转账涉及两个DML操作:一个转出,一个转入。这两个操作需要具备原子性,否则可能会出现数据修改异常。所以需要引入事务来保证转出和转入操作具备原子性,也就是要么都同时成功,要么都同时失败。
引入事务改造前:
public class AccountService { public boolean transfer(String outUser, String inUser, int money) { AccountDao ad = new AccountDao(); try { //转出 ad.out(outUser, money); //模拟转账过程中的异常:转出成功,转入失败 int i = 1/0; //转入 ad.in(inUser, money); } catch (Exception e) { e.printStackTrace(); return false; } return true; } }
引入事务改造如下:
public class AccountService { public boolean transfer(String outUser, String inUser, int money) { AccountDao ad = new AccountDao(); Connection conn = null; try { //1.开启事务 conn = JdbcUtils.getConnection(); conn.setAutoCommit(false);//禁用事务自动提交(改为手动) //转出 ad.out(conn, outUser, money); //模拟转账过程中的异常:转出成功,转入失败 int i = 1/0; //转入 ad.in(conn, inUser, money); //2.事务提交 JdbcUtils.commitAndClose(conn); } catch (Exception e) { e.printStackTrace(); //3.失败时事务回滚 JdbcUtils.rollbackAndClose(conn); return false; } return true; } }
JDBC开启事务的注意点:
为了保证所有操作在一个事务中,转账时使用的Connection必须是同一个。Service层开启事务的Connection要和Dao层访问数据库的Connection一致。线程并发情况下,每个线程只能操作各自从JdbcUtils中获取的Connection。
(3)常规方案解决引入事务后的问题
一.常规方案的实现
基于上面给出的前提, 常规方案是:传参 + 加锁。
传参:从Service层将Connection对象向Dao层传递
加锁:防止多个线程并发操作从JdbcUtils中获取的是同一个Connection
AccountService类修改如下:
//事务的使用注意点: //1.Service层和Dao层的连接对象保持一致 //2.每个线程的Connection对象必须前后一致, 线程隔离 //常规的解决方案 //1.传参: 将Service层的Connection对象直接传递到Dao层 //2.加锁: 防止多个线程并发操作从JdbcUtils中获取的同一个Connection //常规解决方案的弊端: //1.代码耦合度高 //2.降低程序性能 public class AccountService { public boolean transfer(String outUser, String inUser, int money) { AccountDao ad = new AccountDao(); //线程并发情况下,为了保证每个线程使用各自的Connection,避免不同的线程修改同一个Connection,故加锁 synchronized (AccountService.class) { Connection conn = null; try { //1.开启事务 conn = JdbcUtils.getConnection();//从线程池中获取一个Connection对象 conn.setAutoCommit(false);//禁用事务自动提交(改为手动) //转出 ad.out(conn, outUser, money); //模拟转账过程中的异常:转出成功,转入失败 int i = 1/0; //转入 ad.in(conn, inUser, money); //2.事务提交 JdbcUtils.commitAndClose(conn); } catch (Exception e) { e.printStackTrace(); //3.失败时事务回滚 JdbcUtils.rollbackAndClose(conn); return false; } return true; } } }
AccountDao类修改如下:
注意:Connection不能在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(); //连接不能在这里释放,Service层中还需要使用 //JdbcUtils.release(pstm,conn); JdbcUtils.release(pstm); } }
二.常规方案的弊端
上述的确按要求解决了问题,但是存在如下弊端:
第一.直接从Service层传递Connection到Dao层,代码耦合度比较高
第二.加锁会降低程序并发性,程序性能下降
(4)ThreadLocal方案解决引入事务后的问题
一.ThreadLocal方案的实现
像这种需要进行数据传递和线程隔离的场景,可以使用ThreadLocal来解决。
JdbcUtils工具类加入ThreadLocal:
public class JdbcUtils { //ThreadLocal对象: 将Connection绑定在当前线程中 private static final ThreadLocal<Connection> tl = new ThreadLocal(); //c3p0数据库连接池对象属性 private static final ComboPooledDataSource ds = new ComboPooledDataSource(); //获取连接 //原来: 直接从连接池中获取连接 //现在: //1.直接获取当前线程绑定的连接对象 //2.如果连接对象是空的,再去连接池中获取连接,并将此连接对象跟当前线程进行绑定 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 { //1.开启事务 conn = JdbcUtils.getConnection(); conn.setAutoCommit(false);//禁用事务自动提交(改为手动) //转出:这里不需要传参conn了 ad.out(outUser, money); //模拟转账过程中的异常:转出成功,转入失败 int i = 1/0; //转入 ad.in(inUser, money); //2.事务提交 JdbcUtils.commitAndClose(conn); } catch (Exception e) { e.printStackTrace(); //3.失败时事务回滚 JdbcUtils.rollbackAndClose(conn); return false; } return true; } }
AccountDao类去掉Connection参数:
public class AccountDao { public void out(String outUser, 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,outUser); pstm.executeUpdate(); JdbcUtils.release(pstm); } public void in(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); } }
二.ThreadLocal方案的好处
从上述可以看到,ThreadLocal方案有两个优势:
优势一:传递数据
保存每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递带来的代码耦合问题。
优势二:线程隔离
各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。
(5)ThreadLocal的典型应用场景
场景一:在TransactionSynchronizationManager类中(这是Spring-JDBC的类),会通过ThreadLocal来保证数据库连接和事务资源的隔离性,从而避免了不同线程之间事务和连接混乱的问题。
场景二:在实际开发中,当用户登录之后,拦截器会获得用户的基本信息。这些信息在后续的方法中会用到,如果设置到HttpServletRequest中,则不是很灵活,而且还依赖服务器对象,这时就可以用ThreadLocal。
3.ThreadLocal的内部结构
(1)早期的设计
(2)现在的设计
(3)现在的设计的优势
(1)早期的设计
如果不去看源码,可能会猜测ThreadLocal是如下这样设计的:
首先每个ThreadLocal都创建一个Map,然后用线程作为Map的key,线程的变量副本作为Map的value,这样就能让各个线程的变量副本实现数据隔离的效果。
这是最简单的设计方法,JDK最早期的ThreadLocal确实是这样设计的。
(2)现在的设计
JDK后面优化了设计方案,在JDK8中ThreadLocal的设计是:
首先让每个Thread创建一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的局部变量。
具体设计如下:
一.每个Thread线程内部都有一个Map(ThreadLocalMap)
二.Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
三.Thread内部的Map由ThreadLocal维护,ThreadLocal会向Map获取和设置线程的变量副本
四.其他线程不能获取当前线程的变量副本,从而实现了数据隔离
(3)现在的设计的优势
优势一:每个Map存储的Entry数量变少,可以降低Hash冲突发生的概率。因为之前的存储数量由Thread的数量决定,现在由ThreadLocal的数量决定。在实际中,ThreadLocal的数量往往要少于Thread的数量。
优势二:当Thread销毁后,对应的ThreadLocalMap也随之销毁。让线程变量副本的生命周期跟随着线程的生命周期,可以减少内存的占用。
4. ThreadLocal的核心方法源码
(1)ThreadLocal的整体设计原理
(2)ThreadLocal的set()方法源码
(3)ThreadLocal的get()方法源码
(4)ThreadLocal的initialValue()方法源码
(1)ThreadLocal的整体设计原理
ThreadLocal为实现多个线程对同一个共享变量进行set操作时线程隔离,每个线程都有一个与ThreadLocal关联的容器来存储共享变量的初始化副本。当线程对变量副本更新时,只更新存储在当前线程关联容器中的数据副本。
如下图示,在每个线程中都会维护一个成员变量ThreadLocalMap。其中key是一个指向ThreadLocal实例的弱引用,而value表示ThreadLocal的初始化值或者当前线程执行set()方法设置的值。
假设定义了3个不同功能的ThreadLocal共享变量,而在Thread1中分别用到了这3个ThreadLocal进行操作,那么这3个ThreadLocal都会存储到Thread1的ThreadLocalMap中。
如果Thread2也想用这3个ThreadLocal共享变量,那么在Thread2中也会维护一个ThreadLocalMap,把这3个ThreadLocal共享变量保存到该ThreadLocalMap中。
如果Thread1想要对local1进行运算,则将local1实例作为key,从Thread1的ThreadLocalMap中获取对应的value值,进行运算即可。
(2)ThreadLocal的set()方法源码
该方法会在当前线程的成员变量ThreadLocalMap中设置一个值。
具体的执行流程如下:
一.首先通过Thread的currentThread()方法获取当前线程。
二.然后通过ThreadLocal的getMap()方法获取当前线程的成员变量ThreadLocalMap。
三.如果获取的ThreadLocalMap为空,于是调用createMap()方法初始化当前线程的成员变量ThreadLocalMap。
四.如果获取的ThreadLocalMap不为空,则更新ThreadLocalMap中key为当前ThreadLocal对象所对应的value。
public class ThreadLocal<T> { ... //在当前线程中设置一个值,并保存在该线程的ThreadLocalMap中 public void set(T value) { //首先通过Thread.currentThread()获取当前线程 Thread t = Thread.currentThread(); //然后获取当前线程的成员变量ThreadLocalMap ThreadLocalMap map = getMap(t); //判断当前线程的成员变量ThreadLocalMap是否为空 if (map != null) { //如果不为空,则调用map.set()更新ThreadLocalMap中key为当前ThreadLocal对象所对应的value map.set(this, value); } else { //如果为空,则调用createMap()方法初始化当前线程的成员变量ThreadLocalMap createMap(t, value); } } //获取线程Thread的成员变量ThreadLocalMap ThreadLocalMap getMap(Thread t) { return t.threadLocals; } //初始化线程Thread的成员变量ThreadLocalMap void createMap(Thread t, T firstValue) { //这里的this是调用此方法的threadLocal对象 //初始化ThreadLocalMap的第一个元素,key为调用此方法的threadLocal对象,value为传入的firstValue t.threadLocals = new ThreadLocalMap(this, firstValue); } ... } public class Thread implements Runnable { ... //每个线程都有一个ThreadLocalMap类型的成员变量,叫threadLocals ThreadLocal.ThreadLocalMap threadLocals = null; ... }
(3)ThreadLocal的get()方法源码
该方法会从当前线程的成员变量ThreadLocalMap中获取一个值。
具体的执行流程如下:
一.首先通过Thread的currentThread()方法获取当前线程。
二.然后通过ThreadLocal的getMap()方法获取当前线程的成员变量ThreadLocalMap。
三.如果获取的ThreadLocalMap不为空,而且key为当前ThreadLocal对象所对应的value值也不为空,则返回该value。
四.否则就调用setInitialValue()方法,初始化当前线程的成员变量ThreadLocalMap,或者初始化ThreadLocalMap中key为当前ThreadLocal对象所对应的value值。
public class ThreadLocal<T> { ... //从当前线程的成员变量ThreadLocalMap中获取一个值 //如果当前线程的成员变量ThreadLocalMap为空,则调用setInitialValue()方法进行初始化 public T get() { //获取当前线程 Thread t = Thread.currentThread(); //获取当前线程的成员变量ThreadLocalMap ThreadLocalMap map = getMap(t); //如果当前线程的成员变量ThreadLocalMap不为空 if (map != null) { //以当前ThreadLocal对象为key, //调用当前线程的成员变量ThreadLocalMap的getEntry()方法,来获取对应的Entry对象 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //如果当前线程的成员变量ThreadLocalMap为空, //或者ThreadLocalMap中key为当前ThreadLocal对象所对应的value为空 //那么就需要进行初始化 return setInitialValue(); } //进行初始化并返回key为当前ThreadLocal对象所对应的value //该方法通过initialValue()方法获取初始值来初始化当前线程的成员变量ThreadLocalMap并赋值 //如下两种情况需要进行初始化: //第一种情况: map不存在,表示当前线程的成员变量ThreadLocalMap还没初始化 //第二种情况: map存在, 但是key为当前ThreadLocal对象所对应的value为空 private T setInitialValue() { //调用initialValue()方法获取初始化的值 T value = initialValue(); //获取当前线程对象 Thread t = Thread.currentThread(); //获取当前线程的成员变量ThreadLocalMap ThreadLocalMap map = getMap(t); //判断当前线程的成员变量ThreadLocalMap是否为空 if (map != null) { //如果不为空,则调用map.set()更新ThreadLocalMap中key为当前ThreadLocal对象所对应的value map.set(this, value); } else { //如果为空,则调用createMap()方法初始化当前线程的成员变量ThreadLocalMap createMap(t, value); } //返回初始化的value return value; } ... }
(4)ThreadLocal的initialValue()方法源码
在set()方法还未调用而先调用get()方法时,就会执行initialValue()方法。该方法会返回当前线程的成员变量ThreadLocalMap中,key为当前ThreadLocal对象所对应的value的初始值。initialValue()方法默认情况下会返回一个null,但可以重写覆盖此方法。
public class ThreadLocal<T> { ... //返回当前线程的成员变量ThreadLocalMap中,key为当前ThreadLocal对象所对应的value的初始值 protected T initialValue() { return null; } ... }
5.ThreadLocalMap的核心方法源码
(1)ThreadLocalMap的初始化方法
(2)ThreadLocalMap的set()方法
(3)ThreadLocalMap的弱引用
(1)ThreadLocalMap的初始化方法
ThreadLocal的createMap()方法会初始化一个ThreadLocalMap集合。
ThreadLocalMap的构造方法主要会进行如下处理:首先初始化一个长度为16的Entry数组,然后通过对firstKey的hashCode进行位运算取模来得到一个数组下标i,接着根据firstKey和firstValue封装一个Entry对象,最后将这个Entry对象保存到Entry数组的下标为i的位置中。
public class ThreadLocal<T> { ... //初始化线程Thread的成员变量ThreadLocalMap void createMap(Thread t, T firstValue) { //这里的this是调用此方法的threadLocal对象 //初始化ThreadLocalMap的第一个元素,key为调用此方法的threadLocal对象,value为传入的firstValue t.threadLocals = new ThreadLocalMap(this, firstValue); } //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 { //The entries in this hash map extend WeakReference, //using its main ref field as the key (which is always a ThreadLocal object). //Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced, //so the entry can be expunged from table. //Such entries are referred to as "stale entries" in the code that follows. static class Entry extends WeakReference<ThreadLocal<?>> { //The value associated with this ThreadLocal. Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } //The initial capacity -- MUST be a power of two. private static final int INITIAL_CAPACITY = 16; //The table, resized as necessary. //table.length MUST always be a power of two. private Entry[] table; //The number of entries in the table. private int size = 0; ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //初始化一个长度为16的Entry数组 table = new Entry[INITIAL_CAPACITY]; //通过对firstKey这个ThreadLocal实例对象的hashCode,进行位运算取模,来得到一个数组下标i int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //将firstKey和firstValue封装成一个Entry对象,保存到数组的下标为i的位置 table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } ... } ... } public class Thread implements Runnable { ... //每个线程都有一个ThreadLocalMap类型的成员变量,叫threadLocals ThreadLocal.ThreadLocalMap threadLocals = null; ... }
(2)ThreadLocalMap的set()方法
在ThreadLocal的set()方法中,如果发现当前线程的成员变量ThreadLocalMap已经初始化了,那么就会调用ThreadLocalMap的set()方法来保存要设置的值。
在ThreadLocalMap的set(key, value)方法中,首先根据ThreadLocal对象的hashCode和数组长度进行位与运算(即取模),来获取set()方法要设置的元素,应该放置在数组的哪个位置(即数组下标i)。
如果数组下标i的位置不存在Entry对象,则直接将key和value封装成一个新的Entry对象然后存储到数组的下标为i的这个位置。
如果数组下标i的位置存在Entry对象,则使用for循环从数组下标i位置开始往后遍历(线性探索解决Hash冲突)。
如果根据key计算出来的数组下标i已经存在其他的value,且该位置的key和要设置的key不同,则继续寻找i + 1的位置进行存储。
如果根据要设置的key找出的数组对应位置的Entry元素的key为null,则调用replaceStaleEntry()方法来进行替换和清理。
因为Entry元素中的key是弱引用,有可能ThreadLocal实例被回收了导致Entry元素中的key为null。
最后统计数组的元素个数,如果元素个数超出阈值则进行扩容。
public class ThreadLocal<T> { ... static class ThreadLocalMap { //The entries in this hash map extend WeakReference, //using its main ref field as the key (which is always a ThreadLocal object). //Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced, //so the entry can be expunged from table. //Such entries are referred to as "stale entries" in the code that follows. static class Entry extends WeakReference<ThreadLocal<?>> { //The value associated with this ThreadLocal. Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } //The initial capacity -- MUST be a power of two. private static final int INITIAL_CAPACITY = 16; //The table, resized as necessary. //table.length MUST always be a power of two. private Entry[] table; //The number of entries in the table. private int size = 0; ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //初始化一个长度为16的Entry数组 table = new Entry[INITIAL_CAPACITY]; //通过对firstKey这个ThreadLocal实例对象的hashCode,进行位运算取模,来得到一个数组下标i int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //将firstKey和firstValue封装成一个Entry对象,保存到数组的下标为i的位置 table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } //Set the value associated with key. //@param key the thread local object //@param value the value to be set private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; //首先根据ThreadLocal对象的hashCode和数组长度进行位与运算(即取模),来获取元素放置的位置(即数组下标) int i = key.threadLocalHashCode & (len-1); //然后从i开始往后遍历到数组最后一个Entry(线性探索) for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //获取Entry元素中的key ThreadLocal<?> k = e.get(); //如果key相等,则覆盖value if (k == key) { e.value = value; return; } //如果key为null,则用新key、value覆盖 //同时清理key = null的陈旧数据(弱引用) if (k == null) { replaceStaleEntry(key, value, i); return; } } //如果数组下标i的位置不存在数据,则直接将key和value封装成Entry对象存储到该位置 tab[i] = new Entry(key, value); int sz = ++size; //如果超过阈值,就需要扩容了,cleanSomeSlots()方法会清理数组中的无效的key if (!cleanSomeSlots(i, sz) && sz >= threshold) { rehash();//扩容 } } private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } ... } ... }
(3)ThreadLocalMap的弱引用
ThreadLocalMap的数组table中的Entry元素的key什么时候会为空?
当线程已经退出 + ThreadLocal已失去了线程的引用 + 垃圾回收时,key便会为空。也就是如果ThreadLocal对象被回收了,那么ThreadLocalMap中的key就会为空。
public class ThreadLocal<T> { ... static class ThreadLocalMap { private Entry[] table; static class Entry extends WeakReference<ThreadLocal<?>> { //The value associated with this ThreadLocal. Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ... } }
在JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。如下代码中,定义了一个Object对象和一个引用了Object对象的弱引用对象。当把object变量设置为null后,也就是object变量不再引用这个Object对象了,那么gc()方法便会对这个Object对象进行回收,尽管它被弱引用对象引用着。
public class WeakReferneceExample { //object变量引用了新创建的Object对象 static Object object = new Object(); public static void main(String[] args) { //objectWeakReference变量引用了新创建的WeakReference对象 //但WeakReference对象是通过传入object变量创建的,所以objectWeakReference变量其实引用了Object对象 WeakReference<Object> objectWeakReference = new WeakReference<>(object); object = null;//object变量不再引用Object对象,但此时objectWeakReference还在引用Object对象 System.gc();//此时垃圾回收,会回收还在被弱引用引用着的Object对象 System.out.println("gc之后" + objectWeakReference.get());//会输出null } }
如果在强引用的情况下,执行gc()方法时,Object对象是不会被回收的。
public class StrongReferenceExample { static Object object = new Object();//object变量引用了新创建的Object对象 public static void main(String[] args) { Object strongRef = object;//strongRef变量也引用新创建的Object对象 object = null;//object变量不再引用Object对象,但此时strongRef变量还在引用新创建的Object对象 System.gc();//此时垃圾回收,不会回收还在被强引用引用着的Object对象 System.out.println("gc之后" + strongRef);//会输出Object对象 } }
6.ThreadLocalMap的原理总结
(1)ThreadLocalMap的基本结构
(2)ThreadLocalMap的成员变量
(3)ThreadLocalMap的存储结构Entry
(4)弱引用和内存泄漏
(5)如果ThreadLocalMap的key使用强引用
(6)如果ThreadLocalMap的key使用弱引用
(7)使用ThreadLocal出现内存泄漏的原因
(8)ThreadLocalMap为什么使用弱引用
(9)使用线性探测法解决Hash冲突
(1)ThreadLocalMap的基本结构
ThreadLocalMap是ThreadLocal的内部类,它没有实现Map接口,而是用独立的方式实现了Map的功能,它内部的Entry也是用独立的方式实现的。
(2)ThreadLocalMap的成员变量
table是一个Entry类型的数组,用于存储数据。
size代表table数组中的元素个数。
threshold代表需要扩容时size的阈值。
public class ThreadLocal<T> { ... static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } //初始容量 —— 必须是2的整次幂 private static final int INITIAL_CAPACITY = 16; //存放数据的table,同样,数组长度必须是2的整次幂 private Entry[] table; //数组里面Entry的个数,可以用于判断table当前使用量是否超过阈值 private int size = 0; //进行数组扩容的阈值 private int threshold; // Default to 0 ... } ... }
(3)ThreadLocalMap的存储结构Entry
在ThreadLocalMap中,也是用Entry来保存K-V结构数据的,不过Entry中的key只能是ThreadLocal类型的对象。
另外Entry继承自WeakReference,也就是key(ThreadLocal对象)是弱引用,使用弱引用的目的是将ThreadLocal对象的生命周期和线程的生命周期进行解绑。
public class ThreadLocal<T> { ... static class ThreadLocalMap { //Entry继承WeakReference,并且用ThreadLocal作为key. //如果key为null(entry.get() == null),意味着key不再被引用,此时Entry也可以从table中清除 static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ... } ... }
(4)弱引用和内存泄漏
有人在使用ThreadLocal的过程中会发现有内存泄漏的情况,就猜测内存泄漏与Entry中使用了弱引用有关,这个理解其实是不对的。
一.内存泄漏和内存溢出
Memory Overflow内存溢出:指的是没有足够的内存提供申请者使用。
Memory Leak内存泄漏:指的是程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等后果,内存泄漏的堆积终将导致内存溢出。
二.弱引用和强引用
强引用:就是最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还活着,垃圾回收器就不会回收这种对象。
弱引用:GC垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会将其回收。
(5)如果ThreadLocalMap的key使用强引用
假设ThreadLocalMap中的数组元素Entry对象的key使用了强引用,此时ThreadLocal的内存图(实线表示强引用)如下:
假设在业务代码中使用完ThreadLocal ,栈中的ThreadLocalRef被回收了。但是因为ThreadLocalMap对象的Entry元素强引用了ThreadLocal对象,那么就可能会造成ThreadLocal对象无法被回收。
在没有手动删除这个Entry对象以及CurrentThread依然运行的情况下,存在这样的强引用链:CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry
于是Entry对象就不会被回收,从而导致Entry的key和value都内存泄露。由此可见:即使Entry对象的key使用强引用, 也无法避免内存泄漏。
(6)如果ThreadLocalMap的key使用弱引用
假设ThreadLocalMap中的数组元素Entry对象的key使用了强引用,此时ThreadLocal的内存图(实线表示强引用,虚线表示弱引用)如下:
假设在业务代码中使用完ThreadLocal ,栈中的ThreadLocalRef被回收了。由于ThreadLocalMap只持有ThreadLocal对象的弱引用,此时没有任何强引用指向Heap堆中的Threadlocal对象,所以ThreadLocal对象就可以顺利被GC回收,此时Entry对象中的key = null。
在没有手动删除这个Entry对象以及CurrentThread依然运行的情况下,存在这样的强引用链:CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry -> Value
于是即使被key弱引用的ThreadLocal对象被GC回收了(key变为null),但被value强引用的对象不会被回收,从而导致Entry的value存在内存泄漏。由此可见:即使Entry对象的key使用了弱引用, 也有可能内存泄漏。
Entry对象的key可使用弱引用,是因为栈中有变量强引用ThreadLocal对象。Entry对象的value就不能使用弱引用了,因为Value对象只有value引用。否则一旦GC回收Value对象后,而ThreadLocal对象没被回收就会有问题。
(7)使用ThreadLocal出现内存泄漏的原因
发生内存泄漏与ThreadLocalMap中的key是否使用弱引用是没有关系的。
发生内存泄漏的的真正原因是:
原因一:没有手动删除Entry对象;
原因二:CurrentThread依然运行;
一.手动删除Entry元素
只要在使用完ThreadLocal后,调用其remove()方法删除对应的Entry元素,则可避免内存泄漏。
二.使用完ThreadLocal后当前线程随之结束
由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期和Thread线程一样长。那么在使用完ThreadLocal,如果当前线程Thread随之执行结束,ThreadLocalMap自然也会被GC回收,这样就能从根源上避免了内存泄漏。
综上可知,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期和Thread线程一样长,如果没有手动删除对应的Entry对象,那么就会导致内存泄漏。
(8)ThreadLocalMap为什么使用弱引用
ThreadLocalMap的数组中的Entry元素的key,无论使用强引用还是弱引用,都无法完全避免内存泄漏,出现内存泄露与使用弱引用是没有关系的。
要完全避免内存泄漏只有两种方式:
一.使用完ThreadLocal后,调用其remove()方法删除对应的Entry元素
二.使用完ThreadLocal后,当前线程也结束运行
相对第一种方式,第二种方式显然更不好控制。特别是在使用线程池的时候,核心线程一般不会随便结束的。也就是说,只要记得在使用完ThreadLocal后及时调用remove()方法删除Entry,那么无论Entry元素的key是强引用还是弱引用都不会出现内存泄露的问题。
那么为什么Entry元素的key要用弱引用呢?
因为在调用ThreadLocal的set()、get()、remove()方法中,会触发调用ThreadLocalMap的set()、getEntry()、remove()方法。这些方法会判断key是否为null,如果为null就设置对应的value也为null。
这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove()方法删除Entry元素,弱引用也比强引用多一层保障。
弱引用的ThreadLocal对象会被回收,那么对应的value在下一次调用set()、getEntry()、remove()方法时就会被清除,从而避免内存泄漏。但如果没有下一次调用set()、getEntry()、remove()中的任一方法,那么还是会存在内存泄露的问题。
public class ThreadLocal<T> { ... static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } //初始容量 —— 必须是2的整次幂 private static final int INITIAL_CAPACITY = 16; //存放数据的table,同样,数组长度必须是2的整次幂 private Entry[] table; //数组里面Entry的个数,可以用于判断table当前使用量是否超过阈值 private int size = 0; //进行数组扩容的阈值 private int threshold; // Default to 0 private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; //首先根据ThreadLocal对象的hashCode和数组长度进行位与运算(即取模),来获取元素放置的位置(即数组下标) int i = key.threadLocalHashCode & (len-1); //然后从i开始往后遍历到数组最后一个Entry(线性探索) for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //获取Entry元素中的key ThreadLocal<?> k = e.get(); //如果key相等,则覆盖value if (k == key) { e.value = value; return; } //如果key为null,则用新key、value覆盖 //同时清理key = null的陈旧数据(弱引用) if (k == null) { replaceStaleEntry(key, value, i); return; } } //如果数组下标i的位置不存在数据,则直接将key和value封装成Entry对象存储到该位置 tab[i] = new Entry(key, value); int sz = ++size; //如果超过阈值,就需要扩容了,cleanSomeSlots()方法会清理数组中的无效的key if (!cleanSomeSlots(i, sz) && sz >= threshold) { rehash();//扩容 } } private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) { return e; } else { return getEntryAfterMiss(key, i, e); } } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) { return e; } if (k == null) { expungeStaleEntry(i);//清理数组中的无效的key } else { i = nextIndex(i, len); } e = tab[i]; } return null; } private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i);//清理数组中的无效的key return; } } } ... } ... }
(9)使用线性探测法解决Hash冲突
ThreadLocalMap是使用线性探测法(开放寻址法)来解决Hash冲突的,该方法一次探测下一个位置,直到有空的位置后插入。若整个空间都找不到有空的位置,则产生溢出。
假设当前table长度为16,如果根据当前key计算出来的Hash值为14。此时table[14]上已经有值,且其key与当前key不一致,则发生了Hash冲突。这时就会将14加1得到15,取table[15]进行判断。如果判断table[15]时还是Hash冲突,那么就会回到0,取table[0]继续判断,直到可以插入为止。
详细介绍后端技术栈的基础内容,包括但不限于:MySQL原理和优化、Redis原理和应用、JVM和G1原理和优化、RocketMQ原理应用及源码、Kafka原理应用及源码、ElasticSearch原理应用及源码、JUC源码、Netty源码、zk源码、Dubbo源码、Spring源码、Spring Boot源码、SCA源码、分布式锁源码、分布式事务、分库分表和TiDB、大型商品系统、大型订单系统等