头条面试官手把手教学 ThreadLocal

SoWhat:麦叔,最近面别的公司没? ios

麦叔:上次面试失败桑心死我了,我沉淀了一礼拜面头条去了。web

SoWhat:哎呦我去!麦叔你这头条都面上了,面了几轮,手写红黑树没? 面试

麦叔:刚刚两轮,一面红黑树轻松搞定了!面我关于Java的JVM跟并发的时候我看你水的那个JVM系列还有并发系列都过了。最后还问了我点ThreadLocal的问题。 sql

SoWhat:擦,ThreadLocal有啥好问的就是个底层Map啊!而且平常我写数据库事务跟Spring的时候也没见用啊!问那么偏门的干什么他们。数据库

麦叔:擦。。。。你关于ThreadLocal知道的那么点啊?Spring的灵魂除了IOC跟AOP就是ThreadLocal了!数组

SoWhat:真的么,麦叔你给我讲讲要不? 麦叔:好今天让你开开眼。微信

在这里插入图片描述

介绍

咱们看下JDK文档的官方描述:ThreadLocal类用来提供线程内部等局部变量,这种变量在多线程环境下访问(get,set)时能保证各个线程的变量相对独立于其余线程内的变量,ThreadLocal实例一般来讲都是private static类型,用于关联线程的上下文。 ThreadLocal做用:提供线程内部的局部变量,不一样线程之间不会被相互干扰,该变量在线程生命周期内起做用,能够减小同一个线程内多个函数或者组件之间一些公共变量传递的复杂度。多线程

  1. 线程并发:在多线程并发环境下用
  2. 传递数据:经过ThrealLocal在同一个线程,不一样组件中传递公共变量。
  3. 线程隔离:每一个线程内变量是独立的,不会相互影响。

初探使用

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

方法 用途
ThreadLocal 建立ThreadLocal对象
set(T value) 设置当前线程绑定的局部变量
T get() 得到当前线程绑定的局部变量
remove() 移除当前线程绑定的局部变量

ThreadLocal使用编辑器

  1. 先是不用
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();
        }
    }
}

重复执行几回会出现以下结果:2. 用Synchronized

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

多执行几回结果总能正确:3. 用了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也能够实现。

对比 synchronized ThreadLocal
原理 以时间换正确性,不一样线程排队访问 以空间换取准确性,为每个线程都提供了一份变量副本,从而实现访问互不干扰
侧重点 多个线程之间访问资源对同步 多线程中让每一个线程之间的数据相互隔离

再度使用

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

Connection接口方法 做用
setAutoCommit(false) 禁止事务自动提交,默认是自动的
commit() 提交事务
rollback() 回滚事务

代码实现

分析转帐业务,咱们先将业务分4层。

  1. dao层:链接数据库进行数据库的crud。
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);
    }
}
  1. service层:开启跟关闭事务,调用dao层。
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;
    }
}
  1. utils层:数据库链接池的关闭跟获取。
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();
        }
    }
}
  1. web层: 真正的调用入口。

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("转帐成功");
        }
    }
}

注意点

  1. 为了保证因此操做在一个事务中,案例中链接必须是同一个, service层开启事务的 connection须要跟 dao层访问数据库的 connection 保持一致
  2. 线程并发的状况下,每一个线程只能操做各自的 connection。 上述注意点在代码中的体现为service层获取链接开启事务的要跟dao层的链接一致,而且在当前线程只能操做本身的链接。

寻常思路

  1. 传参:将service层connection对象直接传递到dao层,
  2. 加锁 常规代码更改以下:

弊端:

  1. 提升代码耦合度:service层connection对象传递到dao层了。
  2. 下降了程序到性能:由于加锁下降了系统性能。
  3. Spring采用 Threadlocal的方式,来保证单个线程中的数据库操做使用的是同一个数据库链接,同时,采用这种方式可使业务层使用事务时不须要感知并管理connection对象,经过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

ThreadLocal思路

ThreadLocal来实现,核心思想就是servicedao从数据库链接确保用到同一个。utils修改部分代码以下:

    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优点:

1.数据传递:保存每一个线程绑定的数据,在须要的地方直接获取,避免参数传递带来的耦合性。 2. 线程隔离:各个线程之间的数据相互隔离又具备并发性,避免同步加锁带来的性能损失。

底层

误解

不看源码仅仅从咱们使用跟别人告诉咱们的角度去考虑咱们会认为ThreadLocal设计的思路:一个共享的Map,其中每个子线程=Key,该子线程对应存储的ThreadLocal值=Value。JDK早期确实是以下这样设计的,不过如今早已不是!

JDK8中设计

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

  1. 每个Thread线程内部都有一个Map(ThreadLocalMap),一个线程能够有多个TreadLocal来存放不一样类型的对象的,可是他们都将放到你当前线程的ThreadLocalMap里,因此确定要数组来存。
  2. Map里存储ThreadLocal对象为key,线程的变量副本为value。
  3. Thread内部的Map是由ThreadLocal类维护的,由ThreadLocal负责向map获取跟设置线程变量值。
  4. 不一样线程每次获取副本值时,别的线程没法得到当前线程的副本值,造成副本隔离,互不干扰。
在这里插入图片描述

优点

JDK8设计比JDK早期设计的优点,咱们能够看到早期跟如今主要的变化就是ThreadThreadLocal调换了位置。

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

  1. 每一个Map存储的KV数据变小了,之前是线程个数多则 ThreadLocal存储的KV数就变多。如今的K是用 ThreadLocal实例化对象来当key的,多线程状况下 ThreadLocal实例化个数通常都比线程数少!
  2. 之前线程销毁后 ThreadLocal这个Map仍是存在的,如今当Thread销毁时候, ThreadLocalMap也会随之销毁,减小内存使用。

ThreadLocal核心方法

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

方法 用途
initialValue() 返回当前线程局部变量初始化值
set(T value) 设置当前线程绑定的局部变量
T get() 得到当前线程绑定的局部变量
remove() 移除当前线程绑定的局部变量
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);
}

执行流程:

  1. 得到当前线程,根据当前线程得到map。
  2. map不为空则将参数设置到map中,当前到Threadlocal做为key。
  3. 若是map为空,给该线程建立map,设置初始值。
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;
}
  1. 先尝试得到当前线程,得到当前线程对应的map。
  2. 若是得到的map不为空,以当前threadlocal为key尝试得到entry。
  3. 若是entry不为空,返回值。
  4. 但凡2跟3 出现没法得到则经过initialValue函数得到初始值,而后给当前线程建立新map。
remove

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

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
initialValue
  1. 若是没有调用set直接get,则会调用此方法,该方法只会被调用一次,
  2. 返回一个缺省值null。
  3. 若是不想返回null,能够Override 进行覆盖。
   protected T initialValue() {
        return null;
    }

ThreadLocalMap源码分析

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

  1. public class ThreadLocal
  2. static class ThreadLocalMap
  3. static class Entry extends WeakReference<ThreadLocal<?>>
在这里插入图片描述

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对象生命周期跟线程周期解绑。弱引用:

WeakReference :一些有用(程度比软引用更低)可是并不是必需,用弱引用关联的对象,只能生存到下一次垃圾回收以前,GC发生时,无论内存够不够,都会被回收。

弱引用跟内存泄漏

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

若是Key是强引用
  1. 若是在业务代码中使用完 ThreadLocal则此时,Stack中的 ThreadLocalRef就会被 回收了。
  2. 可是此时 ThreadLocalMap中的Entry中的Key是强引用 ThreadLocal的,会形成 ThreadLocal实例 没法回收
  3. 若是咱们没有删除Entry而且CurrentThread依然运行的状况下,强引用链以下图红色,会致使Entry内存泄漏。
在这里插入图片描述

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

若是key是弱引用
  1. 若是在业务代码中使用完来 ThreadLocal则此时,Stack中的 ThreadLocalRef就会被 回收了。
  2. 可是此时 ThreadLocalMap中的Entry中的Key是弱引用 ThreadLocal的,会形成 ThreadLocal回收,此时Entry中的key = null。
  3. 可是当咱们没有手动删除Entry以及CurrentThread依然运行的时候仍是存在强引用链,由于 ThreadLocalRef已经被回收了,那么此时的value就没法访问到了,致使value内存泄漏!
在这里插入图片描述

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

内存泄漏缘由

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

  1. ThreadLocalRef用完后 Entry没有手动删除。
  2. ThreadLocalRef用完后 CurrentThread依然在运行ing。
  • 第一点代表当咱们在使用完毕 ThreadLocal后,调用其对应的 remove方法删除对应的 Entry就能够避免内存泄漏。
  • 第二点是因为 ThreadLocalMapCurrentThread的一个属性,被当前线程引用,生命周期跟 CurrentThread同样,若是当前线程结束 ThreadLocalMap被回收,天然里面的Entry也被回收了,单问题是若是此时的线程不同会被回收啊!,若是是线程池呢,用完就放回池子里了。

结论:ThreadLocal内存泄漏根源是因为ThreadLocalMap生命周期跟Thread同样,若是用完ThreadLocal没有手动删除就回内存泄漏。

为何用弱引用

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

  1. ThreadLocal使用完毕后调用 remove方法删除对应的Entry。
  2. ThreadLocal使用完毕后,当前的 Thread也随之结束。

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

事实上在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,写过的再也不重复。

set方法

流程大体以下:

  1. 根据key得到对应的索引i,查找i位置上的Entry
  2. 若是Entry已存在并key也相等则直接进行值的覆盖。
  3. 若是Entry存在,可是key为空,调用 replaceStaleEntry替换key为空的Entry
  4. 若是遇到了 table[i]为null的时候则须要在 table[i]出建立一个新的Entry,而且插入,同时size+1。
  5. 调用 cleanSomeSlots清理key为null的Entry,再 rehash


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:

  1. 每一个ThreadLocal只能保存一个变量 副本,若是想要上线一个线程可以保存多个副本以上,就须要建立多个ThreadLocal。
  2. ThreadLocal内部的ThreadLocalMap键为 引用,会有内存泄漏的风险,用完记得擦屁股。
  3. 适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。若是若是业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,须要另寻解决方案

若是想共享线程的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?

阿里规范有云:

ThreadLocal没法解决共享对象的更新问题,ThreadLocal对象建议使用 static修饰。这个变量是针对一个线程内全部操做共享的,因此设置为静态变量,全部此类实例共享此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,全部此类的对象(只要是这个线程内定义的)均可以操控这个变量。

JDK官方规范有云:

参考

黑马老师讲解


本文分享自微信公众号 - sowhat1412(sowhat9094)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索