• 初探使用

    使用的时候可以简单的理解为ThreadLocal维护这一个HashMap,其中key = 当前线程,value = 当前线程绑定的局部变量。

    ThreadLocal使用

    public class UserThreadLocal {
        private String str = "";
        public String getStr() {return str;}
        public void setStr(String j) {this.str = j;}
        public static void main(String[] args) {
            UserThreadLocal userThreadLocal = new UserThreadLocal();
            for (int i = 0; i < 5; i++) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        userThreadLocal.setStr(Thread.currentThread().getName() + "的数据");
                        System.out.println(Thread.currentThread().getName() + " 编号 " + userThreadLocal.getStr());
                    }
                });
                thread.setName("线程" + i);
                thread.start();
            }
        }
    }

    重复执行几次会出现如下结果:头条面试官手把手教学 ThreadLocal-LMLPHP2. 用Synchronized

      synchronized (UserThreadLocal.class
      // 唯一区别就是用了同步方法块
       userThreadLocal.setStr(Thread.currentThread().getName() + "的数据");
     System.out.println(Thread.currentThread().getName() + " 编号 " + userThreadLocal.getStr());
      }
     }

    多执行几次结果总能正确:头条面试官手把手教学 ThreadLocal-LMLPHP3. 用了ThreadLocal

    public class UserThreadLocal {
        static ThreadLocal<String> str = new ThreadLocal<>();
        public String getStr() {return str.get();}
        public void setStr(String j) {str.set(j);}
        public static void main(String[] args) {
            UserThreadLocal userThreadLocal = new UserThreadLocal();
            for (int i = 0; i < 5; i++) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        userThreadLocal.setStr(Thread.currentThread().getName() + "的数据");
                        System.out.println(Thread.currentThread().getName() + " 编号 " + userThreadLocal.getStr());
                    }
                });
                thread.setName("线程" + i);
                thread.start();
            }
        }
    }

    重复执行结果如下:头条面试官手把手教学 ThreadLocal-LMLPHP结论: 多个线程同时对同一个共享变量里对一些属性赋值会产生不同步跟数据混乱,加锁通过现在同步使用可以实现有效性,通过ThreadLocal也可以实现。

    再度使用

    数据库转账系统,一定要确保转出转入具备事务性,JDBC中关于事务的API。

    代码实现

    分析转账业务,我们先将业务分4层。

    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 preparedStatement = conn.prepareStatement(sql);
            preparedStatement.setInt(1, money);
            preparedStatement.setString(2, outUser);
            preparedStatement.executeUpdate();
            JdbcUtils.release(preparedStatement, conn);
        }

        public void in(String inUser, int money) throws SQLException {
            String sql = "update account set money = money + ?  where name = ?";
            Connection conn = JdbcUtils.getConnection();//数据库连接池获得连接
            PreparedStatement preparedStatement = conn.prepareStatement(sql);
            preparedStatement.setInt(1, money);
            preparedStatement.setString(2, inUser);
            preparedStatement.executeUpdate();
            JdbcUtils.release(preparedStatement, conn);
        }
    }
    public class AccountService {
        public boolean transfer(String outUser, String inUser, int money) {
            AccountDao ad = new AccountDao(); // service 调用dao层
            Connection conn = null;
            try {
                // 开启事务
                conn = JdbcUtils.getConnection();// 数据库连接池获得连接
                conn.setAutoCommit(false);// 关闭自动提交

                ad.out(outUser, money);//转出
                int i = 1/0;// 此时故意用一个异常来检查数据库的事务性。
                ad.in(inUser, money);//转入
                // 上面这两个要有原子性
                JdbcUtils.commitAndClose(conn);//成功提交
            } catch (SQLException e) {
                e.printStackTrace();
                JdbcUtils.rollbackAndClose(conn);//失败回滚
                return false;
            }
            return true;
        }
    }
    public class JdbcUtils {
        private static final ComboBoxPopupControl 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();
            }
        }
    }

    public class AccountWeb {
        public static void main(String[] args) {
            String outUser = "SoWhat";
            String inUser = "小麦";
            int money = 100;
            AccountService as = new AccountService();
            boolean result =  as.transfer(outUser,inUser,money);
            if(result == false){
                System.out.println("转账失败");
            }
            else{
                System.out.println("转账成功");
            }
        }
    }

    注意点

    寻常思路

    头条面试官手把手教学 ThreadLocal-LMLPHP弊端:

    ThreadLocal思路

    ThreadLocal来实现,核心思想就是servicedao从数据库连接确保用到同一个。头条面试官手把手教学 ThreadLocal-LMLPHPutils修改部分代码如下:

        static ThreadLocal<Connection> tl = new ThreadLocal<>();

        private static final ComboBoxPopupControl ds = new ComboPooledDataSource();

        public static Connection getConnection() throws SQLException {
            Connection conn = tl.get();
            if (conn == null) {
                conn = ds.getConnection();
                tl.set(conn);
            }
            return conn;
        }

        public static void commitAndClose(Connection conn) {
            try {
                if (conn != null) {
                    conn.commit();
                    tl.remove(); //类似IO流操作 用完释放 避免内存泄漏 详情看下面分析
                    conn.close();
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

    ThreadLocal优势:

    底层

    误解

    不看源码仅仅从我们使用跟别人告诉我们的角度去考虑我们会认为ThreadLocal设计的思路:一个共享的Map,其中每一个子线程=Key,该子线程对应存储的ThreadLocal值=Value。JDK早期确实是如下这样设计的,不过现在早已不是!头条面试官手把手教学 ThreadLocal-LMLPHP

    JDK8中设计

    在JDK8中ThreadLocal的设计是:每一个Thread维护一个Map,这个MapkeyThreadLocal对象,value才是真正要存储的object,过程如下:

    优势

    JDK8设计比JDK早期设计的优势,我们可以看到早期跟现在主要的变化就是ThreadThreadLocal调换了位置。

    老版:ThreadLocal维护着一个ThreadLocalMap,由Thread来当做这个map里的key。 新版:Thread维护这一个ThreadLocalMap,由当前的ThreadLocal作为key。

    ThreadLocal核心方法

    ThreadLocal对外暴露的方法有4个:

    set方法:
    // 设置当前线程对应的ThreadLocal值
    public void set(T value) {
        Thread t = Thread.currentThread(); // 获取当前线程对象
        ThreadLocalMap map = getMap(t);
        if (map != null// 判断map是否存在
            map.set(this, value); 
            // 调用map.set 将当前value赋值给当前threadLocal。
        else
            createMap(t, value);
            // 如果当前对象没有ThreadLocalMap 对象。
            // 创建一个对象 赋值给当前线程
    }

    // 获取当前线程对象维护的ThreadLocalMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    // 给传入的线程 配置一个threadlocals
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    执行流程:

    get方法
    public T get() {
        Thread t = Thread.currentThread();//获得当前线程对象
        ThreadLocalMap map = getMap(t);//线程对象对应的map
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);// 以当前threadlocal为key,尝试获得实体
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果当前线程对应map不存在
        // 如果map存在但是当前threadlocal没有关连的entry。
        return setInitialValue();
    }

    // 初始化
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    remove

    首先尝试获取当前线程,然后根据当前线程获得map,从map中尝试删除enrty。

         public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }
    initialValue
       protected T initialValue() {
            return null;
        }

    ThreadLocalMap源码分析

    在分析ThreadLocal重要方法时,可以知道ThreadLocal的操作都是围绕ThreadLocalMap展开的,其中2包含3,1包含2。

    ThreadLocalMap成员变量

    跟HashMap一样的参数,此处不再重复。

    // 跟hashmap类似的一些参数
    private static final int INITIAL_CAPACITY = 16;

    private Entry[] table;

    private int size = 0;

    private int threshold; // Default to 0

    ThreadLocalMap主要函数:

    刚刚说的ThreadLocal中的一些getsetremove方法底层调用的都是下面这几个函数

    set(ThreadLocal,Object)
    remove(ThreadLocal)
    getEntry(ThreadLocal)

    内部类Entry

    // Entry 继承子WeakReference,并且key 必须说ThreadLocal
    // 如果key是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;
        }
    }

    ThreadLocalMap中,用Entry来保存KV结构,同时Entry中的key(Threadlocal)是弱引用,目的是将ThreadLocal对象生命周期跟线程周期解绑。弱引用:

    弱引用跟内存泄漏

    可能有些人认为使用ThreadLocal的过程中发生了内存泄漏跟Entry中使用弱引用key有关,结论是不对的。

    如果Key是强引用

    结论: 强引用无法避免内存泄漏。

    如果key是弱引用

    结论:弱引用也无法避免内存泄漏。

    内存泄漏原因

    上面分析后知道内存泄漏跟强/弱应用无关,内存泄漏的前提有两个。

    结论:ThreadLocal内存泄漏根源是由于ThreadLocalMap生命周期跟Thread一样,如果用完ThreadLocal没有手动删除就回内存泄漏。

    为什么用弱引用

    前面分析后知道内存泄漏跟强弱引用无关,那么为什么还要用弱引用?我们知道避免内存泄漏的方式有两个。

    第一种方法容易实现,第二站不好搞啊!尤其是如果线程是从线程池拿的用完后是要放回线程池的,不会被销毁。

    事实上在ThreadLocalMap中的set/getEntry方法中,我们会对key = null (也就是ThreadLocal为null)进行判定,如果key = null,则系统认为value没用了也会设置为null。

    这意味着当我们使用完毕ThreadLocalThread仍然运行的前提下即使我们忘记调用remove, 弱引用也会比强引用多一层保障,弱引用的ThreadLocal会被收回然后key就是null了,对应的value会在我们下一次调用ThreadLocalset/get/remove任意一个方法的时候都会调用到底层ThreadLocalMap中的对应方法。无用的value会被清除从而避免内存泄漏。对应的具体函数为expungeStaleEntry

    Hash冲突

    构造方法

    我们看下ThreadLocalMap构造方法:

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];//新建table
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //找到位置
        table[i] = new Entry(firstKey, firstValue);//放置新的entry
        size = 1;// 容量初始化
        setThreshold(INITIAL_CAPACITY);// 设置扩容阈值
    }

    threadLocalHashCode = nextHashCode();

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;
    // 避免哈希冲突尽量

    其实构造方法跟位细节运算看HashMap,写过的不再重复。头条面试官手把手教学 ThreadLocal-LMLPHP

    set方法

    流程大致如下:



    private void set(ThreadLocal<?> key, Object value) {
        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)]) { // 开放定值法解决哈希冲突
            ThreadLocal<?> k = e.get();

            if (k == key) {//直接覆盖
                e.value = value;
                return;
            }

            if (k == null) {// 如果key不是空value是空,垃圾清除内存泄漏防止。
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // 如果ThreadLocal对应的key不存在并且没找到旧元素,则在空元素位置创建个新Entry
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

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

    PS:

    如果想共享线程的ThreadLocal数据怎么办?

    使用 InheritableThreadLocal 可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。

    private void test() {    
    final ThreadLocal threadLocal = new InheritableThreadLocal();       
    threadLocal.set("帅得一匹");    
    Thread t = new Thread() {        
        @Override        
        public void run() {            
          super.run();            
          Log.i( "张三帅么 =" + threadLocal.get());        
        }    
      };          
      t.start(); 

    为什么一般用ThreadLocal都要用Static?

    阿里规范有云:

    JDK官方规范有云:头条面试官手把手教学 ThreadLocal-LMLPHP

    参考

    黑马老师讲解


    本文分享自微信公众号 - sowhat1412(sowhat9094)。
    如有侵权,请联系 [email protected] 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

    03-24 18:58