java中的线程安全问题(五)线程变量ThreadLocal

一、介绍

1、介绍:
package java.lang;
public class ThreadLocal<T>

ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象在不同的 Thread 中有不同的副本,这就不存在多线程间共享的问题

2、与普通变量的区别

每个使用该变量的线程都会初始化一个完全独立的实例副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景

3、与Synchronized的区别

ThreadLocal<T>其实是与线程绑定的一个变量。ThreadLocal和Synchonized都可以用于解决多线程并发访问。区别在于:

(1)Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

(2)Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

4、与volatile的区别

(1)不被volatile修饰的变量在多线程环境下也是共享的,只是线程间不能保证实时同步,用volatile修饰后使的能够实时同步;volatile保证的可见性是指共享变量实时同步,所有线程获取到的都是最新值。

(2)为了避免变量共享,通过threadlocal实现了数据隔离的作用。避免不同线程对该变量被通过对外提供的方法进行修改,导致了某个线程 set 以后,想要 get ,发现和之前 set的值不一致。导致业务处理异常

5、注意事项

(1)内存泄漏:由于ThreadLocal是基于线程生命周期管理内部存储的数据,如果线程结束后没有及时清理ThreadLocal,那么这些数据将会一直占用内存,形成所谓的“内存泄漏”。因此,务必确保在适当的时候调用remove()方法。

ThreadLocal的内存泄漏问题主要是由于ThreadLocalMap的生命周期和当前线程一样长,如果没有手动删除对应的key,会导致内存泄漏。ThreadLocalMap的key是ThreadLocal,它们之间是弱引用关系。当ThreadLocal被垃圾回收时,ThreadLocalMap的key就为null了,对应的value也找不到了,从而导致了内存泄漏。因此,为了避免ThreadLocal的内存泄漏问题,应该适时清除ThreadLocal的引用,避免在匿名内部类和finally块中使用ThreadLocal,合理设置初始值,谨慎使用InheritableThreadLocal等。

(2)线程安全:虽然ThreadLocal内部对每个线程的数据访问是线程安全的,但这并不意味着使用ThreadLocal的对象本身是线程安全的。例如,如果多个线程共享同一个非线程安全对象的ThreadLocal引用,并对其进行修改,那么仍可能引发竞态条件。

(3)过度使用:ThreadLocal并非解决所有并发问题的万能良药,过度依赖可能导致代码难以理解和维护。应根据实际需求合理使用,对于真正的全局共享状态,建议采用同步机制或原子类等手段。

二、方法和原理

方法名描述
ThreadLocal()创建ThreadLocal对象
public void set( T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public T remove()移除当前线程绑定的局部变量,该方法可以帮助JVM进行GC
protected T initialValue()返回当前线程局部变量的初始值
1、ThreadLocal的set()方法
 public void set(T value) {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
        //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 初始化thradLocalMap 并赋值
            createMap(t, value);
    }

ThreadLocal  set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。

2、核心ThreadLocalMap
  static class ThreadLocalMap {
 
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
 
        
    }

ThreadLocalMap是ThreadLocal的静态内部类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。

//这个是threadlocal 的内部方法
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 
 
    //ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

ThreadLocal与Thread,ThreadLocalMap之间的关系  :

ThreadLocalMap其实是Thread线程的一个属性值,而ThreadLocal是维护ThreadLocalMap

这个属性指的一个工具类。Thread线程可以拥有多个ThreadLocal维护的自己线程独享的共享变量(这个共享变量只是针对自己线程里面共享)

获取ThreadLocalMap:

//该方法直接返回的就是当前线程对象t的一个成员变量threadLocals:
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

 也就是说ThreadLocalMap的引用是作为Thread的一个成员变量,被Thread进行维护的。 

2.1、JDK8 之前

每个ThreadLocal都创建一个ThreadLocalMap,用线程作为ThreadLocalMap的key,要存储的局部变量作为ThreadLocalMap的value,这样就能达到各个线程的局部变量隔离的效果

2.1、JDK8 之后

(1)每个Thread维护一个ThreadLocalMap,这个ThreadLocalMap的key是ThreadLocal实例本身,value才是真正要存储的值Object
(2)每个Thread线程内部都有一个ThreadLocalMap
(3)Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
(4)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值
(5)对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。。

3、 ThreadLocal的get方法

get方法是获取当前线程中threadLocal变量的值

    public T get() {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //3、如果map数据不为空,
        if (map != null) {
            //3.1、获取threalLocalMap中存储的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
        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;
    }
4、ThreadLocal的remove方法
 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

三、使用场景

1、ThreadLocal 的经典使用场景是数据库连接和 session 管理等。

public class DataSourceContextHolder {
    public static final String SQLSERVER_DATASOURCE = "sqlserverDataSource";
    public static final String MYSQL_DATASOURCE = "mysqlDataSource";

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    public static void setDbType(String dbType) {
        contextHolder.set(dbType);
    }

    public static String getDbType() {
        String dbType = ((String) contextHolder.get());
        if(StringUtils.isNotBlank(dbType)){
            return dbType;
        }else{
            return SQLSERVER_DATASOURCE;
        }
    }

    public static void clearDbType() {
        contextHolder.remove();
    }
}
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    public Logger getParentLogger() {
        return super.getParentLogger();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDbType();
    }
}

<bean id ="dataSource" class= "com.iflytek.oss.common.config.DynamicDataSource" >
		<property roleName ="targetDataSources">
			<map key-type ="java.lang.String">
				<entry value-ref ="sqlserverDataSource" key= "sqlserverDataSource"></entry>
				<entry value-ref ="mysqlDataSource" key= "mysqlDataSource"></entry>
			</map>
		</property>
		&lt;!&ndash; 默认使用sqlserver的数据源 &ndash;&gt;
		<property roleName ="defaultTargetDataSource" ref= "sqlserverDataSource"></property>
	</bean>
2、共享变量访问,解决线程不安全问题

ThreadLocal 不是用来解决共享对象的多线程访问问题的,数据实质上是放在每个thread实例引用的threadLocalMap,也就是说每个不同的线程都拥有专属于自己的数据容器(threadLocalMap),彼此不影响。因此threadLocal只适用于 共享对象会造成线程安全 的业务场景。

/**
 * 线程间访问共享变量之间问题
 * */
public class DemoQuestion {
    private String name;
    private int age;

    public static void main(String[] args) {
        DemoQuestion demoQuestion = new DemoQuestion();
        for (int i = 0; i < 5; i++) {
            // int j = i;
            new Thread(() ->{
                // demoQuestion.setAge(j);
                demoQuestion.setName(Thread.currentThread().getName() + "的数据");
                System.out.println("=================");
                System.out.println(Thread.currentThread().getName() + "--->" + demoQuestion.getName());
                // System.out.println(Thread.currentThread().getName() + "--->" + demoQuestion.getAge());
            },"t" + i).start();
        }
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

(1)使用Synchronized 关键字加锁解决方案

/**
 * 使用加锁的方式解决:线程间访问共享变量之间问题
 * 将对共享变量的操作进行加锁,保证其原子性
 * */
public class SolveDemoQuestionBySynchronized {
    private String name;
    private int age;

    public static void main(String[] args) {
        SolveDemoQuestionBySynchronized demoQuestion = new SolveDemoQuestionBySynchronized();
        for (int i = 0; i < 5; i++) {
            // int j = i;
            new Thread(() ->{
                synchronized (SolveDemoQuestionBySynchronized.class){
                    demoQuestion.setName(Thread.currentThread().getName() + "的数据");
                    System.out.println("=================");
                    System.out.println(Thread.currentThread().getName() + "--->" + demoQuestion.getName());
                }
            },"t" + i).start();
        }
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

(2)使用 ThreadLocal 方式解决

public class SolveDemoQuestionByThreadLocal {
    private  ThreadLocal<String> name = new ThreadLocal<>();
    private int age;

    public static void main(String[] args) {
        SolveDemoQuestionByThreadLocal demoQuestion = new SolveDemoQuestionByThreadLocal();
        for (int i = 0; i < 5; i++) {
            new Thread(() ->{
                demoQuestion.setName(Thread.currentThread().getName() + "的数据");
                System.out.println("=================");
                System.out.println(Thread.currentThread().getName() + "--->" + demoQuestion.getName());
            },"t" + i).start();
        }
    }
    public String getName() {
        return name.get();
    }
    private void setName(String content) {
        name.set(content);
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w_t_y_y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值