zoukankan      html  css  js  c++  java
  • ThreadLocal

    相关链接

    一、介绍

    /**
     * This class provides thread-local variables.  These variables differ from
     * their normal counterparts in that each thread that accesses one (via its
     * {@code get} or {@code set} method) has its own, independently initialized
     * copy of the variable.  {@code 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).
     *
     * <p>For example, the class below generates unique identifiers local to each
     * thread.
     * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
     * and remains unchanged on subsequent calls.
     * <pre>
     *
    import java.util.concurrent.atomic.AtomicInteger;
    
     public class ThreadId {
         // Atomic integer containing the next thread ID to be assigned
         private static final AtomicInteger nextId = new AtomicInteger(0);
    
         // Thread local variable containing each thread's ID
         private static final ThreadLocal<Integer> threadId =
             new ThreadLocal<Integer>() {
                 @Override protected Integer initialValue() {
                     return nextId.getAndIncrement();
             }
         };
    
         // Returns the current thread's unique ID, assigning it if necessary
         public static int get() {
             return threadId.get();
         }
     }
    * </pre> * <p>Each thread holds an implicit reference to its copy of a thread-local * variable as long as the thread is alive and the {@code ThreadLocal} * instance is accessible; after a thread goes away, all of its copies of * thread-local instances are subject to garbage collection (unless other * references to these copies exist). * * @author Josh Bloch and Doug Lea * @since 1.2 */
      ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过set和get方法访问)时能
    保证各个线程的变量相对独立与其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用
    于关联线程和线程上下文。
      作用:提供线程内的局部变量,不同线程之间不会互相干扰,这种变量在线程的生命周期内起作用,减少同一个
    线程内多个函数或组件之间一些公共变量传递的复杂度(同一线程,不同组件中传递公共变量)。

     二、基本使用

    2.1 常用方法

    2.2 使用案例

     1 public class Demo1 {
     2     //变量
     3     private String content;
     4 
     5     public void setContent(String content) {
     6         this.content = content;
     7     }
     8 
     9     public String getContent() {
    10         return content;
    11     }
    12 
    13 }
     1 public class Main {
     2     public static void main(String[] args) {
     3         final Demo1 demo = new Demo1();
     4 //        final Demo2 demo = new Demo2();
     5         for(int i=0;i<5;i++){
     6             Thread thread = new Thread(new Runnable() {
     7                 @Override
     8                 public void run() {
     9                     //每个线程存入一个变量过一会取出
    10                     demo.setContent(Thread.currentThread().getName()+"的数据");
    11                     System.out.println("--------------------------");
    12                     System.out.println(Thread.currentThread().getName()+"--->"+demo.getContent());
    13 
    14                 }
    15             },"线程"+i);
    16             thread.start();
    17         }
    18     }
    19 }

    输出:

    --------------------------
    --------------------------
    线程3--->线程4的数据
    --------------------------
    线程2--->线程4的数据
    --------------------------
    线程1--->线程4的数据
    --------------------------
    线程0--->线程3的数据
    线程4--->线程4的数据

    修改Demo1,引入ThreadLocal

     1 public class Demo2 {
     2     //变量
     3     private String content;
     4     ThreadLocal<String> threadLocal = new ThreadLocal<>();
     5     public void setContent(String content) {
     6         threadLocal.set(content);
     7     }
     8 
     9     public String getContent() {
    10         return threadLocal.get();
    11     }
    12 
    13 }

     输出:一一对应

    --------------------------
    --------------------------
    线程2--->线程2的数据
    --------------------------
    --------------------------
    线程4--->线程4的数据
    --------------------------
    线程3--->线程3的数据
    线程0--->线程0的数据
    线程1--->线程1的数据

     三、ThreadLocal & synchronized

    3.1 使用synchronized修饰

     1 public class Main {
     2     public static void main(String[] args) {
     3         final Demo1 demo = new Demo1();
     4 //        final Demo2 demo = new Demo2();
     5         for(int i=0;i<5;i++){
     6             Thread thread = new Thread(new Runnable() {
     7                 @Override
     8                 public void run() {
     9                     //每个线程存入一个变量过一会取出
    10                     synchronized (Demo1.class){
    11                         demo.setContent(Thread.currentThread().getName()+"的数据");
    12                         System.out.println("--------------------------");
    13                         System.out.println(Thread.currentThread().getName()+"--->"+demo.getContent());
    14                     }
    15                 }
    16             },"线程"+i);
    17             thread.start();
    18         }
    19     }
    20 }

    输出:效果相同,虽然实现了线程数据隔离的问题,但是失去了并发性。

    --------------------------
    线程0--->线程0的数据
    --------------------------
    线程4--->线程4的数据
    --------------------------
    线程3--->线程3的数据
    --------------------------
    线程2--->线程2的数据
    --------------------------
    线程1--->线程1的数据

    3.2 区别

      synchronized:同步机制采用以时间换空间的方式,只提供了一份变量,让不同的线程排队访问。侧重多个线程之间访问资源的同步。

      ThreadLocal:采用以空间换时间的方式,为每一个线程提供了一份变量的副本,从而实现同时访问而互不干扰。侧重于多个线程之间的数据隔离。


     四、运用场景

    4.1 转账

      转入转出操作应该为原子性操作,如果之间出现异常,导致转账数据错误,例如Service代码中的ArithmeticException异常导致Jack减少100,而Rose没有增加。

     1 package com.juc.transfer_accounts;
     2 
     3 import com.mchange.v2.c3p0.ComboPooledDataSource;
     4 import java.sql.Connection;
     5 import java.sql.SQLException;
     6 /**
     7  * @author Millet
     8  * @date 2020/4/30 23:17
     9  */
    10 public class JdbcUtils {
    11     //通过标识名来创建相应连接池
    12     static ComboPooledDataSource dataSource=new ComboPooledDataSource();
    13     //从连接池中取用一个连接
    14     public static Connection getConnection() throws SQLException {
    15             return dataSource.getConnection();
    16     }
    17     //释放连接回连接池
    18     public static void release(AutoCloseable... ios){
    19         for(AutoCloseable io : ios){
    20             if(io!=null){
    21                 try {
    22                     io.close();
    23                 } catch (Exception e) {
    24                     e.printStackTrace();
    25                 }
    26             }
    27         }
    28     }
    29     public static void commitAndClose(Connection conn){
    30         if(conn!=null){
    31             try {
    32                 conn.commit();
    33             } catch (SQLException e) {
    34                 e.printStackTrace();
    35             }
    36         }
    37     }
    38     public static void rollbackAndClose(Connection conn){
    39         if(conn != null){
    40             try {
    41                 conn.rollback();
    42                 conn.close();
    43             } catch (SQLException e) {
    44                 e.printStackTrace();
    45             }
    46         }
    47     }
    48     
    49 }
    JdbcUtils
    Dao
     1 package com.juc.transfer_accounts;
     2 
     3 import java.sql.SQLException;
     4 
     5 /**
     6  * @author Millet
     7  * @date 2020/4/30 23:17
     8  */
     9 public class AccountService {
    10     public boolean transfer(String outUser, String inUser, int money){
    11         AccountDao ad = new AccountDao();
    12         try {
    13             ad.out(outUser,money);
    14             //转入转出应该为原子操作,如果两个操作中间出现异常,导致转账数据错误,如下所示
    15             int i = 1/0;
    16             ad.in(inUser,money);
    17         } catch (Exception e) {
    18             e.printStackTrace();
    19             return false;
    20         }
    21         return true;
    22     }
    23 }
    Service
     1 package com.juc.transfer_accounts;
     2 
     3 /**
     4  * @author Millet
     5  * @date 2020/4/30 23:17
     6  */
     7 public class AccountController {
     8     public static void main(String[] args) {
     9         //Jack给Rose转账100
    10         String outUser = "Jack";
    11         String inUser = "Rose";
    12         AccountService as = new AccountService();
    13         boolean res = as.transfer(outUser, inUser, 100);
    14        if(res){
    15            System.out.println("转账成功!");
    16        }else {
    17            System.out.println("转账失败!");
    18        }
    19     }
    20 }
    Controller

    4.2 解决方案

    4.2.1 事务

    使用事务时需要注意
    1.service层和Dao层的连接对象Connection需要保持一致
    2.每个线程的Connection对象必须前后一致,线程隔离
    常见解决方案
    1.传参:将service层的connection对象直接传递到Dao层(就是在Dao层方法上增加一个连接对象参数,此处应
    注意Dao层不能释放连接)

    2.加锁
     1 package com.juc.transfer_accounts;
     2 
     3 import org.springframework.stereotype.Repository;
     4 
     5 import java.sql.Connection;
     6 import java.sql.PreparedStatement;
     7 import java.sql.SQLException;
     8 
     9 /**
    10  * @author Millet
    11  * @date 2020/4/30 23:16
    12  */
    13 public class AccountDao {
    14     /**
    15      * 传参:将service层的connection对象直接传递到Dao层(就是在Dao层方法上增加一个连接对象参数,此处应
    16      * 注意Dao层不能释放连接)
    17      */
    18     //转出
    19     public void out(String outUser, int money,Connection conn) throws SQLException {
    20         String sql = "update account set money = money - ? where name = ?";
    21 //        Connection conn = JdbcUtils.getConnection();
    22         PreparedStatement pstm = conn.prepareStatement(sql);
    23         pstm.setInt(1,money);
    24         pstm.setString(2,outUser);
    25 //        JdbcUtils.release(pstm,conn);
    26     }
    27     //传入
    28     public void in(String inUser, int money, Connection conn) throws SQLException {
    29         String sql = "update account set money = money + ? where name = ?";
    30 //        Connection conn = JdbcUtils.getConnection();
    31         PreparedStatement pstm = conn.prepareStatement(sql);
    32         pstm.setInt(1,money);
    33         pstm.setString(2,inUser);
    34 //        JdbcUtils.release(pstm,conn);
    35     }
    36 }
    Dao 
     1 package com.juc.transfer_accounts;
     2 
     3 import java.sql.Connection;
     4 /**
     5  * @author Millet
     6  * @date 2020/4/30 23:17
     7  */
     8 public class AccountService {
     9     public boolean transfer(String outUser, String inUser, int money){
    10         AccountDao ad = new AccountDao();
    11         Connection conn = null;
    12         /**
    13          * 事务的使用注意点:
    14          *      1.service层和Dao层的连接对象Connection需要保持一致
    15          *      2.每个线程的Connection对象必须前后一致,线程隔离
    16          *  常见解决方案:
    17          *      1.传参:将service层的connection对象直接传递到Dao层
    18          *      2.加锁
    19          *  常规解决方案弊端:
    20          *    1.提高代码耦合度  Service-->Dao
    21          *    2. 降低程序性能  synchronized
    22          */
    23         try {
    24             synchronized (AccountService.class){
    25                 //1.开启事务
    26                 conn = JdbcUtils.getConnection();
    27                 conn.setAutoCommit(false);//关闭自动提交事务
    28 
    29                 ad.out(outUser,money,conn);
    30                 //转入转出应该为原子操作,如果两个操作中间出现异常,导致转账数据错误,如下所示
    31                 int i = 1/0;
    32                 ad.in(inUser,money,conn);
    33 
    34                 JdbcUtils.commitAndClose(conn);
    35             }
    36         } catch (Exception e) {
    37             e.printStackTrace();
    38 
    39             JdbcUtils.rollbackAndClose(conn);
    40             return false;
    41         }
    42         return true;
    43     }
    44 }
    Service

      常规解决方案弊端

       1.提高代码耦合度Service-->Dao

       2. 降低程序性能 synchronized

    4.2.2 ThreadLocal

      Connection创建绑定线程,且提交事务需要解绑,防止内存泄漏。

     1 package com.juc.transfer_accounts;
     2 
     3 import com.mchange.v2.c3p0.ComboPooledDataSource;
     4 import java.sql.Connection;
     5 import java.sql.SQLException;
     6 /**
     7  * @author Millet
     8  * @date 2020/4/30 23:17
     9  */
    10 public class JdbcUtils {
    11     //通过标识名来创建相应连接池
    12     static ComboPooledDataSource dataSource=new ComboPooledDataSource();
    13 
    14     //从连接池中取用一个连接
    15     /**
    16      * 原本:直接从连接池中获取链接
    17      * 现在:
    18      *      1.直接获取当前线程绑定的连接对象
    19      *      2.如果连接对象为空
    20      *          2.1 再去连接池中获取链接
    21      *          2.2 将此链接对象跟当前线程进行绑定
    22      * @return
    23      * @throws SQLException
    24      */
    25     static ThreadLocal<Connection> tl = new ThreadLocal<>();
    26     public static Connection getConnection() throws SQLException {
    27         Connection conn = tl.get();
    28         if(conn == null){
    29             conn = dataSource.getConnection();
    30             tl.set(conn);
    31         }
    32         return conn;
    33 //            return dataSource.getConnection();
    34     }
    35     //释放连接回连接池
    36     public static void release(AutoCloseable... ios){
    37         for(AutoCloseable io : ios){
    38             if(io!=null){
    39                 try {
    40                     io.close();
    41                 } catch (Exception e) {
    42                     e.printStackTrace();
    43                 }
    44             }
    45         }
    46     }
    47     public static void commitAndClose(Connection conn){
    48         if(conn!=null){
    49             try {
    50                 conn.commit();
    51                 //解绑当前线程绑定的连接对象
    52                 tl.remove();
    53             } catch (SQLException e) {
    54                 e.printStackTrace();
    55             }
    56         }
    57     }
    58     public static void rollbackAndClose(Connection conn){
    59         if(conn != null){
    60             try {
    61                 conn.rollback();
    62                 conn.close();
    63                 //解绑当前线程绑定的连接对象
    64                 tl.remove();
    65             } catch (SQLException e) {
    66                 e.printStackTrace();
    67             }
    68         }
    69     }
    70 
    71 }
    JdbcUtils

       Service、Dao直接使用JdbcUtils创建conn,且不使用synchronized和传参

     1 package com.juc.transfer_accounts;
     2 
     3 import org.springframework.stereotype.Repository;
     4 
     5 import java.sql.Connection;
     6 import java.sql.PreparedStatement;
     7 import java.sql.SQLException;
     8 
     9 /**
    10  * @author Millet
    11  * @date 2020/4/30 23:16
    12  */
    13 public class AccountDao {
    14     /**
    15      * 传参:将service层的connection对象直接传递到Dao层(就是在Dao层方法上增加一个连接对象参数,此处应
    16      * 注意Dao层不能释放连接)
    17      */
    18     //转出
    19     public void out(String outUser, int money) throws SQLException {
    20         String sql = "update account set money = money - ? where name = ?";
    21         Connection conn = JdbcUtils.getConnection();
    22         PreparedStatement pstm = conn.prepareStatement(sql);
    23         pstm.setInt(1,money);
    24         pstm.setString(2,outUser);
    25         JdbcUtils.release(pstm,conn);
    26     }
    27     //传入
    28     public void in(String inUser, int money) throws SQLException {
    29         String sql = "update account set money = money + ? where name = ?";
    30         Connection conn = JdbcUtils.getConnection();
    31         PreparedStatement pstm = conn.prepareStatement(sql);
    32         pstm.setInt(1,money);
    33         pstm.setString(2,inUser);
    34         JdbcUtils.release(pstm,conn);
    35     }
    36 }
    Dao
     1 package com.juc.transfer_accounts;
     2 
     3 import java.sql.Connection;
     4 /**
     5  * @author Millet
     6  * @date 2020/4/30 23:17
     7  */
     8 public class AccountService {
     9     public boolean transfer(String outUser, String inUser, int money){
    10         AccountDao ad = new AccountDao();
    11         Connection conn = null;
    12         /**
    13          * 事务的使用注意点:
    14          *      1.service层和Dao层的连接对象Connection需要保持一致
    15          *      2.每个线程的Connection对象必须前后一致,线程隔离
    16          *  常见解决方案:
    17          *      1.传参:将service层的connection对象直接传递到Dao层
    18          *      2.加锁
    19          *  常规解决方案弊端:
    20          *    1.提高代码耦合度  Service-->Dao
    21          *    2. 降低程序性能  synchronized
    22          */
    23         try {
    24 //            synchronized (AccountService.class){
    25                 //1.开启事务
    26                 conn = JdbcUtils.getConnection();
    27                 conn.setAutoCommit(false);//关闭自动提交事务
    28 
    29                 ad.out(outUser,money);
    30                 //转入转出应该为原子操作,如果两个操作中间出现异常,导致转账数据错误,如下所示
    31                 int i = 1/0;
    32                 ad.in(inUser,money);
    33 
    34                 JdbcUtils.commitAndClose(conn);
    35 //            }
    36         } catch (Exception e) {
    37             e.printStackTrace();
    38 
    39             JdbcUtils.rollbackAndClose(conn);
    40             return false;
    41         }
    42         return true;
    43     }
    44 }
    Service

       ThreadLocal好处:

        1. 传递数据:保存每个线程绑定的数据,在需要的地方可以直接获取。

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


     五、ThreadLocal内部结构

    5.1 JDK8设计

      每个Thread维护了一个ThreadLocalMap,这个Map的Key是ThreadLocal实例本身,Value才是真正要存储的值Object,具体过程如下:

    public class Thread implements Runnable {{
        ...
        ThreadLocal.ThreadLocalMap threadLocals = null;
        ...
    }
    1. 每个Thread线程内部都有一个Map(ThreadLocalMap)
    2. Map里面存储ThreadLocal对象(Key)和线程的变量副本(Value)
    3. Thread内部的Mao是由ThreadLocal维护的,由ThreadLocal负责向Map获取和设置线程的变量值
    4. 对于不同的线程,每次获取副本值时,别的线程并不能获取当前线程的副本值,形成了副本的隔离,互
    不干扰。

      JDK8设计优点:

    1. 每个Map存储的Entry数量减少,不再是一个Thread对应一个Entry。
    2. 当Thread销毁的时候,ThreadLocalMao也随之销毁,减少内存的使用。早期ThreadLocalMao是由
    ThreadLocal维护的
    ,Thread销毁并不会使ThreadLocalMap销毁。

    六、核心方法源码

    6.1 set()

     1 /**
     2     * 设置当前线程对应的ThreadLocal的值
     3     */
     4 public void set(T value) {
     5     //获取当前线程对象
     6     Thread t = Thread.currentThread();
     7     //获取Thread维护的ThreadLocalMap对象
     8     ThreadLocalMap map = getMap(t);
     9     if (map != null)
    10         //map存在,则设置Entry
    11         map.set(this, value);
    12     else
    13         //1. 当前线程Thread不存在ThreadLocalMap对象
    14         //2.则调用createMap进行ThreadLocalMap初始化
    15         //3.并将Thread t(当前线程)和value作为第一个entry存放之ThreadLocalMap中
    16         createMap(t, value);
    17 }
    18 /**
    19     * 获取Thread维护的ThreadLocalMap对象,返回当前线程对应维护的ThreadLocalMap
    20     */
    21 ThreadLocalMap getMap(Thread t) {
    22         return t.threadLocals;
    23 }
    24 /** 
    25      * 创建当前线程Thread对应维护的ThreadLocalMap
    26      * @param t 当前线程
    27      * @param  存放到Map的第一个Entry的值
    28      */
    29     void createMap(Thread t, T firstValue) {
    30         t.threadLocals = new ThreadLocalMap(this, firstValue);
    31     }

    6.2 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) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            /*
                初始化:有两种情况执行此方法
                      1.map不存在,表示此线程没有维护的ThreadLocalMap对象
                      2.map存在,entry不存在
    
            */
            return setInitialValue();
        }
    
    /**
         * @return the initial value
         */e
        private T setInitialValue() {
            //获取初始化值,此方法可被重写,如果不重写返回null
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                //如果map存在,设置此实体entry
                map.set(this, value);
            else
                //当前线程Thread不存在ThreadLocalMap对象
                //则调用createMap进行初始化,并将当前线程和value作为第一个entry放入Map
                createMap(t, value);
            return value;
        }

      总结:先获取当前线程的ThreadLoaclMap变量,如果存在则返回值,不存在则创建并返回初始值。

    6.3 initiaValue()

    protected T initialValue() {
            return null;
        }

       这个方法是一个延迟调用的方法,从上面的代码可知,在set方法还未调用之前调用了get方法时才会执行此方法,且只执行一次。这个方法缺省值返回一个null。如果想要除null之外的初始值,可以重写此方法。(该方法是一个protected方法,显然是为了让子类覆盖而设计的)。

    6.4 remove()

    1 public void remove() {
    2          ThreadLocalMap m = getMap(Thread.currentThread());
    3          if (m != null)
    4              m.remove(this);
    5      }

    七、ThreadLocalMap源码分析

    7.1 基本结构

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

     7.1.1 默认属性

            /**
             * 初始容量 必须是2的整次幂
             */
            private static final int INITIAL_CAPACITY = 16;
    
            /**
             * 存放数据
             * 数组长度为2的整次幂
             */
            private Entry[] table;
    
            /**
             * 数组中Entry的个数,可用于判断是否需要扩容
             */
            private int size = 0;
    
            /**
             * 扩容阈值
             */
            private int threshold; // Default to 0

    7.1.2 存储结构-Entry

    /**
             * Entry继承WeakReference,并且用ThreadLocal作为key。
             * 如果key为null(entry.get()==null),意味着key不再被引用
             *  因此这时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;
                }
            }

      Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。

    7.2 弱引用和内存泄漏

    弱引用:只要垃圾回收器发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收他的内存。

      Q1:如果Key使用强引用,那么会出现内存泄漏吗?

    1. 假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了
    2. 但是因为threadLocalMap的Entry强引用了threadLocal, 造成ThreadLocal无法被回收
    3. 在没有手动删除Entry以及CurrentThread依然运行的前提下, 始终有强引用链threadRef → currentThread →
    entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value), 导致Entry内存泄漏 也就是说: ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的

       Q2:如果Key使用弱引用,那么会出现内存泄漏吗?

    1. 假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了
    2. 由于threadLocalMap只持有ThreadLocal的弱引用, 没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收
    ,此时Entry中的key = null 3. 在没有手动删除Entry以及CurrentThread依然运行的前提下, 也存在有强引用链threadRef → currentThread → value, value就
    不会被回收, 而这块value永远不会被访问到了, 导致value内存泄漏 也就是说: ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。

      由上可知,内存泄漏的发生跟 ThreadLocalIMap 中的 key 是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?

      细心的同学会发现,在以上两种内存泄漏的情况中.都有两个前提:
        1 .没有手动侧除这个 Entry
        2 · CurrentThread 依然运行
      第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法翻除对应的 Entry ,就能避免内存泄漏。
      第二点稍微复杂一点,由于ThreodLocalMap 是 Threod 的一个属性,被当前线程所引用所以它的生命周期跟 Thread 一样长。那么在使用完 ThreadLocal 的使用,如果当前Thread 也随之执行结束, ThreadLocalMap 自然也会被 gc 回收,从根源上避免了内存泄漏。

      综上, ThreadLocal 内存泄漏的根源是:由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏

      Q3:那为什么Key要使用弱引用呢?

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

    ​   要避免内存泄漏有两种方式:
    ​     1 .使用完 ThreadLocal ,调用其 remove 方法删除对应的 Entry
        ​ 2 .使用完 ThreadLocal ,当前 Thread 也随之运行结束

    ​   相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的.也就是说,只要记得在使用完ThreadLocal 及时的调用 remove ,无论 key 是强引用还是弱引用都不会有问题.

    ​   事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null (也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么是会对 value 置为 null 的.

    ​   这就意味着使用完 ThreadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收对应value在下一次 ThreadLocaIMap 调用 set/get/remove 中的任一方法的时候会被清除,从而避免内存泄漏

    7.3 Hash冲突的解决

    7.3.1 从ThreadLocal的set()方法入手

      回顾:

    1. 首先获取当前线程,并根据当前线程获取一个Map
    2. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal作为Key),这里调用了ThreadLocalMap的set()方法。
    3.如果Map为空,则给该线程创建Map,并设置初始量(这里调用了ThreadLoacalMap的构造方法)

    7.3.2 构造方法:ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue)

    /**
             * firstKey:本ThreadLocal实例
             * firstValue:要保存的线程本地变量
             */
            ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                //初始化table
                table = new Entry[INITIAL_CAPACITY];
                //计算索引(★★★★★)INITiTAL_CAPACITY初始值是16
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                //设置值
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                //设置阈值,是数组容量的2/3
                setThreshold(INITIAL_CAPACITY);
            }

      分析:firstKey.threadLocalHashCode

    private final int threadLocalHashCode = nextHashCode();
    
    private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
    //原子Integer,适合高并发
    private static AtomicInteger nextHashCode = new AtomicInteger();
    //16进制 特殊的hash值
    private static final int HASH_INCREMENT = 0x61c88647;
    
    public final int getAndAdd(int delta) {
            return unsafe.getAndAddInt(this, valueOffset, delta);
        }

      这里定义了一个AtomicInteger类型nextHashCode ,每次获取当前值并加上HASH_INCREMENT,值为0x61c88647,这个值跟斐波那契数列有关,其主要目的是为了让哈希码能均匀地分布在2的n次方数组里,也就是table中,减小哈希冲突。原来 hash code 从 0 开始不断累加 0x61c88647 生成的.。

      分析:(INITIAL_CAPACITY - 1)

       计算hash的时候采用hashCode&(size-1)的算法,这相当于取模运算hashCode%size的一个更有效的实现。例如50%16 = 50&15,使用位运算比取余更快。所以size必须是2的整次幂。

    7.3.3 ThreadLocalMap的set()方法

     1 private void set(ThreadLocal<?> key, Object value) {
     2             Entry[] tab = table;
     3             int len = tab.length;
     4             //计算索引
     5             int i = key.threadLocalHashCode & (len-1);
     6             //线性探测法查找元素 ★★★★★
     7             for (Entry e = tab[i];
     8                  e != null;
     9                  e = tab[i = nextIndex(i, len)]) {
    10                 //取出Key
    11                 ThreadLocal<?> k = e.get();
    12                 //ThreadLocal对应的值存在,则覆盖之前的值
    13                 if (k == key) {
    14                     e.value = value;
    15                     return;
    16                 }
    17                 //key为null,但是值不为null,说明之前的ThreadLocal对象已经被回收了
    18                 //当前数组中的Entry是一个陈旧的元素
    19                 if (k == null) {
    20                     //用新元素替代陈旧的元素,这个方法进行了不少垃圾清理的动作,防止内存泄漏
    21                     replaceStaleEntry(key, value, i);
    22                     return;
    23                 }
    24             }
    25             //ThreadLocal对应的Key不存在并且没有找到陈旧的值,
    26             //则在空元素位置创建一个新的Entry
    27             tab[i] = new Entry(key, value);
    28             int sz = ++size;
    29 
    30             /**
    31               cleanSomeSlots用于清除那些e.get()==null的元素
    32               这种数据key关联的对象已经被回收,所以这个Entry(table[index])
    33                可以被置为null,如果没有清除任何Entry,并且当前使用量达到了负
    34               载因子所定义(长度的2/3),那么进行rehash操作(执行一次全表的扫描清理 
    35               工作)
    36 
    37             */
    38             if (!cleanSomeSlots(i, sz) && sz >= threshold)
    39                 rehash();
    40         }
    41 //获取环形数组的下一个索引
    42 private static int nextIndex(int i, int len) {
    43             return ((i + 1 < len) ? i + 1 : 0);
    44         }

  • 相关阅读:
    5. Mybatis UPDATE更新,DELETE删除
    3. Mybatis Insert
    4. selectKey语句属性配置细节
    2. Mybatis Select
    uoj#282. 长度测量鸡(构造)
    uoj#276. 【清华集训2016】汽水(分数规划+点分治)
    uoj#275. 【清华集训2016】组合数问题(数位dp)
    uoj#274. 【清华集训2016】温暖会指引我们前行(LCT)
    uoj#273. 【清华集训2016】你的生命已如风中残烛(组合数学)
    uoj#272. 【清华集训2016】石家庄的工人阶级队伍比较坚强(矩阵+三维FWT)
  • 原文地址:https://www.cnblogs.com/qmillet/p/12813385.html
Copyright © 2011-2022 走看看