一文带你搞懂 ThreadLocal 的原理及使用

ThreadLocal 介绍

ThreadLocal 翻译过来是 本地线程,它的作用是为每一个线程创建一个副本,不同线程之间访问不同的副本,他们之间互不干扰。这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。

总结:
1. 线程并发: 在多线程并发的场景下
2. 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
3. 线程隔离: 每个线程的变量都是独立的,不会相互影响

ThreadLocal的使用

1、常见 API
方法声明描述
ThreadLocal()创建 ThreadLocal 对象
public void set( T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量
2、使用

我们先来看一下下面这个例子

    private String content;

    private String getContent() {
        return content;
    }

    private void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        ThreadLocalTest demo = new ThreadLocalTest();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
/**
线程0--->线程1的数据
线程2--->线程2的数据
线程3--->线程2的数据
线程1--->线程3的数据
线程5--->线程5的数据
线程4--->线程4的数据
线程9--->线程8的数据
线程8--->线程8的数据
线程6--->线程6的数据
线程7--->线程7的数据
*/

我们看到不同的线程在操作同一个资源时,并没有互相隔离,那我们如何然线程于它自己的资源绑定或者说隔离呢?我们也许会想到加锁,但是我们今天用 ThreadLocal 来解决这个问题。我们只需要把数据放到 ThreadLocal 对象中,在我们存取时,他会根据不同线程返回属于它的数据。

    ThreadLocal<String> threadLocal = new ThreadLocal<>();

    private String content;

    private String getContent() {
        return threadLocal.get();
    }

    private void setContent(String content) {
        threadLocal.set(content);
    }

    public static void main(String[] args) {
        ThreadLocalTest demo = new ThreadLocalTest();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    demo.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
/**
线程1--->线程1的数据
线程0--->线程0的数据
线程2--->线程2的数据
线程3--->线程3的数据
线程4--->线程4的数据
线程6--->线程6的数据
线程9--->线程9的数据
线程5--->线程5的数据
线程7--->线程7的数据
线程8--->线程8的数据
*/

我们可以看到无论我们怎么运行,输出总是符合我们的设想,那你觉得它的底层实现会是什么呢?我第一想法就是使用Map,key为线程,然后value就是我们传入的值。后面我们再对其进行讨论,那除了这个还有没有其他方法来对资源进行隔离吗?另一个方法就是加锁。我们先来看一下加锁是怎么解决这个问题的。

    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        SynchronizedTest demo02 = new SynchronizedTest();

        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(){
                @Override
                public void run() {
                    synchronized (SynchronizedTest.class){
                        demo02.setContent(Thread.currentThread().getName() + "的数据");
                        String content = demo02.getContent();
                        System.out.println(Thread.currentThread().getName() + "--->" + content);
                    }
                }
            };
            t.setName("线程" + i);
            t.start();
        }
    }

在某个层面上讲,使用 ThreadLocal 来为每个线程创建一个副本(消耗内存空间)是空间换时间,而加锁则是时间换空间(不需要额外内存)。但是加锁的速度会比较慢,后面我们来详细讨论下他们两者的区别吧。

3、ThreadLocal类与synchronized关键字的区别

虽然ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同。

synchronizedThreadLocal
原理同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰。
侧重点多个线程之间访问资源的同步性多线程中让每个线程之间的数据相互隔离

总结: 在刚刚的案例中,虽然使用ThreadLocal和synchronized都能解决问题,但是使用ThreadLocal更为合适,因为这样可以使程序拥有更高的并发性。


ThreadLocal 使用案例

这里我们先构建一个简单的转账场景: 有一个数据表account,里面有两个用户Jack和Rose,用户Jack 给用户Rose 转账。

  • 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 层:
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层代码:
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("转账成功!");
        }
    }
}

引入事务

案例中的转账涉及两个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;
    }
}

所以这里就需要操作事务,来保证转出和转入操作具备原子性,要么同时成功,要么同时失败。

  • JDBC中关于事务的操作的api
Connection接口的方法作用
void setAutoCommit(false)禁用事务自动提交(改为手动)
void commit();提交事务
void rollback();回滚事务
  • 开启事务的注意点
    • 为了保证所有的操作在一个事务中,案例中使用的连接必须是同一个: service层开启事务的connection需要跟dao层访问数据库的connection保持一致
    • 线程并发情况下, 每个线程只能操作各自的 connection

常规的解决方案

基于上面给出的前提, 大家通常想到的解决方案是 :

  • 从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
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类的修改:照常使用
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);
        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,conn);
        JdbcUtils.release(pstm);
    }
}
ThreadLocal 的好处
  • 传递数据 : 保存每个线程绑定的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题

  • 线程隔离 : 各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失


ThreadLocal的设计与源码解析

我们对ThreadLocal的作用有了一定的认识。现在我们一起来看一下ThreadLocal的内部结构,探究它能够实现线程数据隔离的原理。看看我们之前的猜测是对不对的。其实 JDK 早期的设计就是这样的,但是在 JDK 1.8 后就不是了,后面我们来对比下他们的底层实现吧!

核心结构设计

JDK后面优化了设计方案,现时JDK1.8 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap哈希表,这个哈希表的keyThreadLocal实例本身,value才是真正要存储的值Object

  • 每个Thread线程内部都有一个Map (ThreadLocalMap)
  • Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
  • Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

在这里插入图片描述

这样设计的好处
  • 这样设计之后每个Map存储的Entry数量就会变少,因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。

  • Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。


ThreadLocalMap 结构以及Hash冲突的解决方法
我们先来看一下 ThreadLocal 对外暴露的四个方法
方法声明描述
protected T initialValue()返回当前线程局部变量的初始值
public void set( T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量
Get 方法解析
/**
 * 返回当前线程中保存ThreadLocal的值
 * 如果当前线程没有此ThreadLocal变量,
 * 则它会通过调用{@link #initialValue} 方法进行初始化值
 *
 * @return 返回当前线程对应此ThreadLocal的值
 */
public T get() {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取此线程对象 Thread 中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 如果此map存在
    if (map != null) {
        // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e,如果ThreadLocal为空会对其进行处理
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 找到对应的存储实体 e 
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 获取存储实体 e 对应的 value值
            // 即为我们想要的当前线程对应此ThreadLocal的值
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map不存在,则证明此线程没有维护的ThreadLocalMap对象
    // 调用setInitialValue进行初始化
    return setInitialValue();
}

/**
 * set的变样实现,用于初始化值initialValue,
 * 用于代替防止用户重写set()方法
 *
 * @return the initial value 初始化后的值
 */
private T setInitialValue() {
    // 调用initialValue获取初始化的值
    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)并将此实体entry作为第一个值存放至ThreadLocalMap中
        createMap(t, value);
    // 返回设置的值value
    return 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);
}

1、首先获取当前线程

2、根据当前线程获取一个Map

3、如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到5

4、如果e不为null,则返回e.value,否则转到5

5、Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map


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)并将此实体entry作为第一个值存放至ThreadLocalMap中
            createMap(t, value);
    }

1、首先获取当前线程,并根据当前线程获取一个Map

2、如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)

3、如果Map为空,则给该线程创建 Map,并设置初始值


remove方法解析
   /**
     * 删除当前线程中保存的ThreadLocal对应的实体entry
     */
     public void remove() {
        // 获取当前线程对象中维护的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
         if (m != null)
            // 存在则调用map.remove
            // 以当前ThreadLocal为key删除对应的实体entry
             m.remove(this);
     }

1、首先获取当前线程,并根据当前线程获取一个Map

2、如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry


initialValue方法解析
/**
  * 返回当前线程对应的ThreadLocal的初始值
  
  * 此方法的第一次调用发生在,当线程通过{@link #get}方法访问此线程的ThreadLocal值时
  * 除非线程先调用了 {@link #set}方法,在这种情况下,
  * {@code initialValue} 才不会被这个线程调用。
  * 通常情况下,每个线程最多调用一次这个方法。
  *
  * <p>这个方法仅仅简单的返回null {@code null};
  * 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
  * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法,方法标记为 protected 表明是为了让子类去重写的
  * 通常, 可以通过匿名内部类的方式实现
  *
  * @return 当前ThreadLocal的初始值
  */
protected T initialValue() {
    return null;
}

我们看一下存储数据的Map结构

ThreadLocalMap结构以及Hash冲突的解决方法

ThreadLocalMap 是 ThreadLocal 的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。

在这里插入图片描述
在这里插入图片描述

  • 成员变量
/**
 * 初始容量 —— 必须是2的整次幂
 */
private static final int INITIAL_CAPACITY = 16;

/**
 * 存放数据的table,Entry类的定义在下面分析
 * 同样,数组长度必须是2的冥。
 */
private Entry[] table;

/**
 * 数组里面entrys的个数,可以用于判断table当前使用量是否超过负因子。
 */
private int size = 0;

/**
 * 进行扩容的阈值,表使用量大于它的时候进行扩容。
 */
private int threshold; // Default to 0

/**
 * 阈值设置为长度的2/3
 */
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}
  • 存储结构 - Entry
// 在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了
// 另外,Entry继承WeakReference,使用弱引用,可以将ThreadLocal对象的生命周期和线程生命周期解绑,持有对ThreadLocal的弱引用,可以使得ThreadLocal在没有其他强引用的时候被回收掉,这样可以避免因为线程得不到销毁导致ThreadLocal对象无法被回收

static class Entry extends WeakReference<ThreadLocal> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal k, Object v) {
        super(k);
        value = v;
    }
}
  • 冲突的解决方法

冲突的解决方法有两种:

​ 1、链地址法

​ 2、线性探测法

我们看一下 ThreadLocal 的 Set方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocal.ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocal.ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //初始化table
    table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
    //计算索引  threadLocal 的 hashcode 和 容量的值-1 进行位的与
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //设置值
    table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
    size = 1;
    //设置阈值
    setThreshold(INITIAL_CAPACITY);
}
  • 代码很简单,获取当前线程,并获取当前线程的ThreadLocalMap实例(从getMap(Thread t)中很容易看出来)。
  • 如果获取到的map实例不为空,调用map.set()方法,否则调用构造函数 ThreadLocal.ThreadLocalMap(this, firstValue)实例化map。
  • ThreadLocalMap的实例化方法是初始化Map,然后再计算索引再设置值。

可以看出来线程中的ThreadLocalMap使用的是延迟初始化,在第一次调用get()或者set()方法的时候才会进行初始化。后面我们再来看一下 ThreadLocalMap 的 set 方法

private void set(ThreadLocal<?> key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        //计算索引。
        int i = key.threadLocalHashCode & (len-1);

        /**
         * 根据获取到的索引进行循环,如果当前索引上的table[i]不为空,在没有return的情况下,
         * 就使用nextIndex()获取下一个(上面提到到线性探测法)。
         */
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            //获取新的位置
            ThreadLocal<?> k = e.get();
            //table[i]上key不为空,并且和当前key相同,更新value
            if (k == key) {
                e.value = value;
                return;
            }
            /**
             * table[i]上的key为空,说明被回收了
             * 这个时候说明改table[i]可以重新使用,用新的key-value将其替换,并删除其他无效的entry
             */
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }

里面有hashCode的方法,我们看一下它是怎么计算的

private final int threadLocalHashCode = nextHashCode();
    
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static AtomicInteger nextHashCode =  new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

这里定义了一个 AtomicInteger 类型,每次获取当前值并加上HASH_INCREMENT,HASH_INCREMENT = 0x61c88647,这个值和斐波那契散列有关(这是一种乘数散列法,只不过这个乘数比较特殊,是32位整型上限2^32-1乘以黄金分割比例0.618…的值2654435769,用有符号整型表示就是-1640531527,去掉符号后16进制表示为0x61c88647),其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里, 也就是Entry[] table中,这样做可以尽量避免hash冲突。

接下来再来看看是怎么解决冲突的

    /**
     * 获取环形数组的下一个索引
     */
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }

    /**
     * 获取环形数组的上一个索引
     */
    private static int prevIndex(int i, int len) {
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

ThreadLocalMap 使用开发地址-线性探测法来解决哈希冲突,线性探测法的地址增量di = 1, 2, … 其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。


弱引用和内存泄漏
相关概念
  • 内存泄漏相关概念

    • Memory overflow:内存溢出,没有足够的内存提供申请者使用。
    • Memory leak:内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。I内存泄漏的堆积终将导致内存溢出。
  • 弱引用相关概念

    • 强引用( “Strong” Reference) , 就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾回收器就不会回收这种对象。即使抛出**内存溢出(OOM)**也不会去回收该区域。
    • 弱引用( WeakReference) ,垃圾回收器一旦发现了 只具有弱弓|用的对象,不管当前内存空间足够与否,都会回收它的内存。
为什么使用弱引用

我们不妨假设Key使用强引用:

在这里插入图片描述

而如果使用弱引用呢?

在这里插入图片描述

根据刚才的分析,我们知道了:无论使用 ThreadLocalMap 中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。要避免内存泄漏有两种方式:

  1. 使用完ThreadLocal ,调用其remove方法删除对应的Entry
  2. 使用完ThreadLocal ,当前Thread也随之运行结束相对第一种方式, 第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。

也就是说,只要记得在使用完ThreadLocal及时的调用remove ,无论key是强弓|用还是弱引用都不会有问题。

事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null (也即是ThreadLocal为null )进行判断,如果为nul的话,那么是会对value置为null的。
这就意味着使用完ThreadLocal , CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一-次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

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)
            //重置 i 节点的 key 和 value
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

我们看一下上面的代码,可以看到当拿到的key不等于key时会调用getEntryAfterMiss方法,该方法会对key的位置进行置空,方便GC进行垃圾的回收。iic


总结:
  • ThreadLocal 的概念以及 API 的使用
  • ThreadLocal 的底层方法实现
  • ThreadLocal 的底层数据结构
  • ThreadLocal 中 Key 使用弱引用的原因
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值