Java ThreadLocal全面解析

一、ThreadLocal是什么?

官方介绍:
 1. This class provides thread-local variables.  These variables differ from
 2. their normal counterparts in that each thread that accesses one (via its
 3. {@code get} or {@code set} method) has its own, independently initialized
 4. copy of the variable.  {@code ThreadLocal} instances are typically private
 5. static fields in classes that wish to associate state with a thread (e.g.,
 6. a user ID or Transaction ID).
官方介绍翻译:

       TheadLocal类提供了一个线程局部变量,这些变量与其他普通变量的不同在于,通过get、set方法访问的线程都有其独立的初始化的变量副本。ThreadLocal实例通常是private static类型的,用于关联线程或线程上下文(比如说用户ID或者交易ID)

ThreadLocal的特点
  1. 多线程情况:ThreadLocal是应用于多线程并发情况下
  2. 传递数据:多线程情况下可以通过ThreadLocal在同一线程的不同组件中传递数据
  3. 线程隔离:每个线程ThreadLocal所存的变量都是独立的,不会相互影响

       通俗的讲就是:就是在一个线程里放一个数据,不管中间执行了什么操作,最终通过get方法即可得到保存进去的数据,适用于各个线程间依赖不通的变量值完成操作的场景。

二、ThreadLocal的应用场景

应用场景1:多线程中保证当前线程中存入的值在取出的时候还是该值。

改造前代码:

public class ThreadLocalDemo {

    private String content;

    public String getContent() {
        return content;
    }

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

    public static void main(String[] args) {
        ThreadLocalDemo tld = new ThreadLocalDemo();
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread() {
                @Override
                public void run() {
                    tld.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println("====================================");
                    String content = tld.getContent();
                    System.out.println(Thread.currentThread().getName() + ":" + content);
                }
            };
            t.setName("线程" + i);
            t.start();
        }
    }
}

运行后期望结果:各自线程获取各自所存储的数据
在这里插入图片描述
运营后实际结果:
在这里插入图片描述
可以看出多线程情况下,在执行Thread run()方法的时候,出现了线程执行抢占的情况,打印结果不匹配。

改造后代码:使用TheadLocal存储线程局部变量,使其达到线程隔离的效果,不受线程间的影响。

public class ThreadLocalDemo {

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

    private String content;

    public String getContent() {
        String s = tl.get();
        return s;
//        return content;
    }

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

    public static void main(String[] args) {
        ThreadLocalDemo tld = new ThreadLocalDemo();

        for (int i = 0; i < 5; i++) {
            Thread t = new Thread() {
                @Override
                public void run() {
                    tld.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println("====================================");
                    String content = tld.getContent();
                    System.out.println(Thread.currentThread().getName() + ":" + content);
                }
            };
            t.setName("线程" + i);
            t.start();
        }
    }
}

运行后的结果(符合预期):
在这里插入图片描述

应用场景2:转账的事务问题

问题描述:银行转账问题,用户A转出的钱需入用户B的账户,即此转账操作需是原子操作。
工程目录:
1.main函数

public static void main(String[] args) throws SQLException {
    String outUser = "Jack";
    String inUser = "Rose";
    int money = 1000;

    AccountService accountService = new AccountService();
    boolean result = accountService.transfer(outUser,inUser,money);
    if (result) {
        System.out.println("转账成功");
    } else {
        System.out.println("转账失败");
    }
}

2.service层

public boolean transfer(String outUser, String inUser, int money) throws SQLException {
   	AccountDAO ad = new AccountDAO();
    Connection conn = JdbcUtils.getConnection();
    conn.setAutoCommit(false);
    try {
        ad.out(outUser,money,conn);
        // 模拟异常
        int i = 1/0;
        ad.in(inUser,money,conn);
        JdbcUtils.commitAndClose(conn);
    } catch (Exception e) {
        e.printStackTrace();
        JdbcUtils.rollbackAndClose(conn);
        return false;
    }
    return true;
}

3.dao层

public void out(String outUser, int money, Connection conn) throws SQLException {
   String sql = "update account set money = money - ? where name = ?";
    PreparedStatement pstm = conn.prepareStatement(sql);
    pstm.setInt(1,money);
    pstm.setString(2,outUser);
    pstm.executeUpdate();
}

public void in(String inUser, int money, Connection conn) throws SQLException {
    String sql = "update account set money = money + ? where name = ?";
    PreparedStatement pstm = conn.prepareStatement(sql);
    pstm.setInt(1,money);
    pstm.setString(2,inUser);
    pstm.executeUpdate();
}

4.JdbcUtils关键代码

private static final ComboPooledDataSource ds = new ComboPooledDataSource();

public static Connection getConnection(){
    return ds.getConnection();
}

这样使用connection的手动提交可以保证数据操作的原子性,但是我们可以看到service与dao层中间的传递,我们将connection对象进行了传递,使用这种传参的形式,使得service层与dao层代码紧耦合。
而且在线程并发的场景下,需要保证service层与dao层的数据连接对象connection不受影响。
所以大家会想到使用synchronized关键字进行加锁:

public boolean transfer(String outUser, String inUser, int money) throws SQLException {
   	AccountDAO ad = new AccountDAO();
    Connection conn = JdbcUtils.getConnection();
    conn.setAutoCommit(false);
    try {
        synchronized (AccountService.class) {
            ad.out(outUser,money,conn);
            // 模拟异常
            int i = 1/0;
            ad.in(inUser,money,conn);
            JdbcUtils.commitAndClose(conn);
        } 
    } catch (Exception e) {
        e.printStackTrace();
        JdbcUtils.rollbackAndClose(conn);
        return false;
    }
    return true;
}

使用synchronized关键字可以解决多线程可能发生的连接connection对象错取得问题,但是通过代码可以看到使用synchronized关键字在进行转账操作时对类对象进行加锁,影响了程序并发性。
至此,我们考虑使用ThreadLocal类,这主要是解决多线程中连接connection对象在线程间得隔离问题。
只需在获取connection对象时,将其保存至TheadLocal局部变量中即可。

	static ThreadLocal<Connection> tl = new ThreadLocal<>();
    private static final ComboPooledDataSource ds = new ComboPooledDataSource();
    public static Connection getConnection(){
        Connection conn = tl.get();
        if (conn == null) {
            conn = ds.getConnection();
            tl.set(conn);
        } 
        return tl.get();
    }

注意在释放connection连接得时候需要将ThreadLocal中存储变量remove掉:tl.remove();
通过上面得ThreadLocal类也可解决synchronized关键字所能解决得问题,而且效率更高。

应用场景3:

       在web项目中,会遇到很多情况下将数据存储到用户的session域中,但是在后面需要获取数据的时候,需要将request对象在controller层传入service层,可能会传入util方法中,或者传入其他相关处理的类中。然而这都是服务器的用户线程中,所以可以将需要的数据放入ThreadLocal中,在需要的地方之间进行获取即可。这种方式用的比较多。

三、ThreadLocal与synchronized的区别

通过上面的应用可以看到ThreadLocal与synchronized关键字均可实现多线程中线程隔离的问题,但是两者之间处理问题的角度和思路不同。

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

四ThreadLocal内部结构

ThreadLocal内部维护的是一个类似Map的ThreadLocalMap数据结构,key为当前对象的Thread对象,值为泛型的Object。
在这里插入图片描述

  1. 每个Thread线程内部都有一个Map(ThreadLocalMap)
  2. Map里面存储ThreadLocal(Key)和变量的副本(value)
  3. Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取或设置线程的变量值
  4. 对于不同的线程,每次获取副本值时,别的线程并不能获取当前线程的副本值, 行程了副本的隔离,互补干扰。
ThreadLocal内部结构设计的优势:
  1. 之后每个Map存储的Entry数量会变少(为什么会变少?)使用原始Map存储的时候是由Thread数量决定的,现在是由ThreadLocal的数量决定的。
  2. 当Thread销毁后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。

五、ThreadLocal源码分析

初始化方法,当用户重写了set()方法时,可使用此方法提代set()为ThreadLocal数据初始化:

    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;
    }

Get方法:

 public T get() {
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if (map != null) {
     // 获取当前线程在ThreadLocalMap中所存储的Entry对象
         ThreadLocalMap.Entry e = map.getEntry(this);
         if (e != null) {
             @SuppressWarnings("unchecked")
             T result = (T)e.value;
             return result;
         }
     }
     return setInitialValue();
 }

Set方法:

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

remove方法:

  public void remove() {
      ThreadLocalMap m = getMap(Thread.currentThread());
      if (m != null)
      // remove此线程在ThreadLocalMap中的Entry
          m.remove(this);
  }

ThreadLocalMap内部类: ThreadLocalMap是ThreadLocal类的内部静态类,这个ThreadLocalMap是ThreadLocal中的关键数据结构。

 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
 // INITIAL_CAPACITY 初始化值与Map中的初始化值一样,为16,超过2/3则扩充
 // 此处是创建一个存储entry对象的数组table[]
      table = new Entry[INITIAL_CAPACITY];
      // 根据ThreadLocal的hash算法值计算当前key在table数组中所处的index值
      int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
      table[i] = new Entry(firstKey, firstValue);
      size = 1;
      setThreshold(INITIAL_CAPACITY);
  }

Entry 对象,继承自WeakReference(弱引用,生命周期只能存活到下次GC前),只是Key是弱引用类型的,Value并非弱引用。

static class Entry extends WeakReference<ThreadLocal<?>> {
   /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

通过Key获取ThreadLocalMap中的Entry对象

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);
}
代码执行的流程
  1. 获取当前线程
  2. 根据当前线程获取一个Mape
  3. 如果Map不为空,则在Map中以ThreadLocal的引用作为Key来在Map中获取对应的value e,否则转至E;
  4. 如果e不能null,则返回e.value,否则转至E
  5. Map为空或e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value为firstKey和firstValue创建一个新的Map。

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

ThreadLocal中的Hash冲突怎么解决

       ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非HashMap中所使用的链表形式,而是采用线性探测的方式,即根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
       ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。

/**
 * Increment i modulo len.
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
 * Decrement i modulo len.
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

       显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率更低。

       所以这里给出的建议是:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

ThreadLocalMap的问题

       由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
       既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
try {
    threadLocal.set(new Session(1, "Misout的博客"));
    // 其它业务逻辑
} finally {
    threadLocal.remove();
}
ThreadLocal代码使用演示
private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();

//获取Session
public static Session getCurrentSession(){
    Session session =  threadLocal.get();
    //判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中
    try {
        if(session ==null&&!session.isOpen()){
            if(sessionFactory==null){
                rbuildSessionFactory();// 创建Hibernate的SessionFactory
            }else{
                session = sessionFactory.openSession();
            }
        }
        threadLocal.set(session);
    } catch (Exception e) {
        // TODO: handle exception
    }

    return session;
}
总结
  • 每个ThreadLocal只能保存一个变量副本,如果想要上线一个线程能够保存多个副本以上,就需要创建多个ThreadLocal。
  • ThreadLocal内部的ThreadLocalMap键为弱引用,会有内存泄漏的风险。
  • 适用于无状态,副本变量独立后不影响业务逻辑的高并发场景。如果如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决,需要另寻解决方案。

以上为个人学习的理解,如有错误,欢迎指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值