码迷,mamicode.com
首页 > 其他好文 > 详细

ThreadLocal

时间:2020-05-02 00:12:20      阅读:61      评论:0      收藏:0      [点我收藏+]

标签:线程   操作   同步机制   first   param   两种   use   运算   jdk   

一、介绍

/**
 * 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         }

技术图片

 

ThreadLocal

标签:线程   操作   同步机制   first   param   两种   use   运算   jdk   

原文地址:https://www.cnblogs.com/qmillet/p/12813385.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!