ThreadLocal详解

在JDK官方文档中描述:ThreadLocal类用来提供线程内部等局部变量,这种变量在多线程环境下访问时能够保证多个线程的变量相对独立于其他线程内的变量,ThreadLocal实例通常设置为private static类型,用于关联线程的上下文。作用:提供线程内部的局部变量,不同线程之间不会相互干扰,该变量是在线程的生命周期内起作用,可以减少同一个线程内多个函数或者组件之间传递公共变量的复杂度,简单来说就是每个线程持有一份当前变量的副本,各个线程之间的副本互不干扰,是典型的用空间换区安全性的做法。

1 线程并发:多线程并发环境下使用
2 传递数据:通过ThreadLocal在同一个线程之间,不同组件传递变量
3 线程隔离:每个线程内变量是独立的不会相互影响

初体验

使用的时候可以把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();
        }
    }
}

重复执行几次,会出现数据错乱的结果:
在这里插入图片描述
可以使用synchronized来使得设置数据和获取数据串行执行,但是这样会降低程序的并发度。

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

使用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也可以保证各个线程获取对应的数据。
结论: 多个线程同时对同一个共享变量里对一些属性赋值会产生不同步跟数据混乱,加锁通过现在同步使用可以实现有效性,通过ThreadLocal也可以实现。
在这里插入图片描述

再度使用

数据库业务的转账功能,一定要保证转入和转出具有事务性,JDBC中关于事务的API
在这里插入图片描述
分析转账业务,可以将业务分为四层。
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);
    }
}

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

3 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();
        }
    }
}

5 controller层:真正的调用入口:

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层的连接一致,并且在当前线程只能操作自己的连接。
在这里插入图片描述

传参:将service层connection对象直接传递到dao层,
加锁 常规代码更改如下
在这里插入图片描述
加锁的弊端:
1 提高代码耦合度:service层connection对象传递到dao层了。
2 降低了程序到性能:因为加锁降低了系统性能。
3 Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

ThreadLocal的思路

ThreadLocal的核心思想就是service和dao层从数据库连接确保使用同一个。
在这里插入图片描述
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底层原理

不看源码仅仅从我们使用跟别人告诉我们的角度去考虑我们会认为ThreadLocal设计的思路:一个共享的Map,其中每一个子线程=Key,该子线程对应存储的ThreadLocal值=Value。JDK早期确实是如下这样设计的,不过现在早已不是!
在这里插入图片描述
JDK8中的设计
在JDK8中ThreadLocal的设计是:每一个Thread维护一个Map,这个Map的key是ThreadLocal对象,value是要存储的object,过程如下:

1 每个Thread线程内部都有一个Map(ThreadLocalMap),一个线程可以有多个ThreadLocal来存放不同类型的对象,但是它们都存放在当前线程的ThreadLocalMap中。
2 Map里存储ThreadLocal对象作为key,线程的变量副本作为value。
3 Thread内部的Map是由ThreadLocal类来维护的,由ThreadLocal负责向map获取和设置线程变量值。
4 不同线程每次获取副本变量时,别的线程无法获得当前线程的副本值
在这里插入图片描述
优势:
JDK8设计比JDK早期设计的优势,我们可以看到早期跟现在主要的变化就是Thread跟ThreadLocal调换了位置。

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

1 每个Map存储的KV数据变少了,以前是线程个数多则ThreadLocal存储的KV数量就多。现在的K是用ThreadLocal实例化对象来当key的,多线程情况下,ThreadLocal实例化的个数一个比线程数更少。
2 以前线程销毁后ThreadLocal这个Map还是存在的,现在当线程Thread销毁时,ThreadLocalMap也会随之销毁,减少内存占用。

ThreadLocal核心方法

在这里插入图片描述
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中删除entry;

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

initialValue

1 如果没有调用set直接get则会调用此方法,该方法只会调用一次
2 返回一个缺省值null
3 如果不想返回null,可以Override覆盖

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类似的一些参数
private static final int INITIAL_CAPACITY = 16;

private Entry[] table;

private int size = 0;

private int threshold; // Default to 2/3

主要方法:
刚刚说的ThreadLocal中的一些get、set、remove方法底层调用的都是下面这几个函数

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

内部类Entry继承WeakReference,并且key必须是ThreadLocal

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

1 强引用:无论堆空间是否充足,都不能进行垃圾回收的引用。
2 软引用:当系统空间不足时,GC会对此类引用进行回收,如果空间依然不足,则会抛出OOM
3 弱引用:无论JVM空间是否充足,下次垃圾回收一定会被回收,只能生存到下一次垃圾回收之前,GC发生时,不管内存够不够,都会被回收。。
4 虚引用: 唯一目的就是用来在对象被GC回收时收到一个系统通知。

内存泄漏问题

内存泄漏和弱引用没有直接关系,并且弱引用在一定程度上可以降低内存泄漏发生的概率。
如果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依然在运行

  • 第一点表明当我们在使用完毕ThreadLocal后,调用其对应的remove方法删除对应的Entry就可以避免内存泄漏。
  • 第二点是由于ThreadLocalMap是CurrentThread的一个属性,被当前线程引用,生命周期跟CurrentThread一样,如果当前线程结束ThreadLocalMap被回收,自然里面的Entry也被回收了,单问题是如果此时的线程不一样会被回收啊!,如果是线程池呢,用完就放回池子里了。
    结论:ThreadLocal内存泄漏根源是由于ThreadLocalMap生命周期跟Thread一样,如果用完ThreadLocal没有手动删除就回内存泄漏。

为什么使用弱引用

既然内存泄漏和使用引用的强弱无关,那么为什么还要使用弱引用呢?
避免内存泄漏的方式有两个:

  • ThreadLocal使用完毕后调用remove方法删除对应的Entry。
  • ThreadLocal使用完毕后,当前的Thread也随之结束。
    第二种方法不太容易实现,尤其是在使用线程池中的线程用完要放回线程池。
    事实上在ThreadLocalMap中的set/getEntry方法中,我们会对key = null (也就是ThreadLocal为null)进行判定,如果key = null,则系统认为value没用了也会设置为null。
    多了一层保障:
    这意味着当我们使用完毕ThreadLocal,Thread仍然运行的前提下即使我们忘记调用remove, 弱引用也会比强引用多一层保障,弱引用的ThreadLocal会被收回然后key就是null了,对应的value会在我们下一次调用ThreadLocal的set/get/remove任意一个方法的时候都会调用到底层ThreadLocalMap中的对应方法。无用的value会被清除从而避免内存泄漏。对应的具体函数为expungeStaleEntry。

Hash冲突

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

在这里插入图片描述

如果想共享ThreadLocal的数据如何处理?

使用InherittableThreadLocal可以实现多个线程访问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修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值