ThreadLocal原理讲解

概念介绍

对象中的数据被称为该对象的状态。一个对象中的成员变量静态变量被称为该对象的状态变量。如果一个类的同一个实例被多个线程共享,并存在共享状态,则该实例被称为有状态对象。反之,如果一个类的同一个实例即使被多个线程共享,但不会出现并发问题,则该实例被称为无状态对象

在多线程共享同一个有状态对象时,如果想要保证线程的安全性,一是可以采用锁,二是让每个线程有独立的一份该对象的实例,并且每个线程无法访问其他线程中该对象的实例。这种对象被称为线程特有对象,这种线程被称为对象的持有线程

ThreadLocal类相当于当前线程持对象的代理。多个线程访问同一个ThreadLocal实例时,其实访问的是同一个类,但不同的实例对象,一个线程访问多个ThreadLocal实例时,访问的是不同的对象实例。如果使用了ThreadLocal来修饰对象,这些ThreadLocal实例对于线程来说就像是方法中的局部变量一样,所以ThreadLocal实例也被称为线程局部变量。其示意图如下:
在这里插入图片描述

实战

我们知道java中SimpleDateFormat类是一个线程不安全的类,当多个线程同时访问同一个SimpleDateFormat实例时,就会出现问题,下面同时用20个线程进行测试一下:

public class ThreadLocalDemo {

   private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
   
    public static void main(String[] args) throws ParseException {
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                Date parse = null;
                try {
                    parse = simpleDateFormat.parse("2022-10-21");
                } catch (ParseException e) {
                    e.printStackTrace();
                }
                System.out.println(parse);
            }).start();
        }
    }
}

打印结果如下:
在这里插入图片描述
SimpleDateFormat为什么是线程不安全的,我们跟一下parse源码,一直跟到java.text.CalendarBuilder类下的establish方法,在该方法中依次执行clear和set,所以在多线程环境中,如果a线程还未执行完毕,b线程就clear掉了Calendar对象,并且该Calendar对象还是SimpleDateFormat父类中的成员变量,则就会出现线程安全问题。
在这里插入图片描述
在这里插入图片描述

为了解决上述线程不安全的问题,我们可以采用加锁的方式(使用synchronized来修饰共享变量或者使用可重入锁),但如果想要避免锁的争用,我们可以使用ThreadLocal:

public class ThreadLocalDemo {
    private static ThreadLocal<SimpleDateFormat> sdfThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };

    //以上方法等同于下面的lambda
//    private static ThreadLocal<SimpleDateFormat> sdfThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
    public static void main(String[] args) throws ParseException {
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                Date parse = null;
                try {
                    parse = sdfThreadLocal.get().parse("2022-10-21");
                } catch (ParseException e) {
                    e.printStackTrace();
                }
                System.out.println(parse);
            }).start();
        }
    }
}

下面介绍ThreadLocal中常用的四个方法

方法作用
public T get()获取当前线程的特有对象
public void set(T value)给当前线程的ThreadLocal实例重新关联该线程的特有对象
protected T initialValue()初始状态下当前ThreadLocal实例对应的当前线程特有对象
public void remove()移除当前线程下ThreadLocal实例对应的当前线程特有对象

当一个线程初次执行get方法时,会调用initialValue方法,然后返回当前线程的一个特有对象(之所以上面的例子要在ThreadLocal初始化时执行initialValue方法,因为如果不这样做,则get返回的是个null),以后这个线程无论执行多少次get方法,返回的都是这一个实例,除非该线程在中途执行了set方法,设置了新的对象。java8开始,初始化的时候支持lambda,如上例子所示。

注意:
ThreadLocal因为是线程安全的,所以常作为静态成员变量来使用,如果作为局部变量(或非静态成员变量),虽然也可以保证线程安全性,但是每创建一个线程(或每创建一个对象)都需要创建一个ThreadLocal实例,这样会增加内存的成本。

ThreadLocal带来的问题以及解决方法

数据错乱

如果存在同一个线程处理多个任务,但是这些任务都用到了同一个ThreadLocal实例,该实例就变成了这些任务的”共享变量“,但如果该线程特有对象是有状态对象,则下一个任务是可以看到上一个任务修改的数据,从而导致了数据错乱。要想解决该问题,可以在每个任务执行前重新关联一个线程特有对象(使用threadLocal.set()或者threadLocal.remove()方法),但是这样做,其实失去了ThreadLocal的意义,线程特有对象又退化成了任务特有对象。下面举一个例子来说明使用ThreadLocal时如何避免数据错乱。

public abstract class AbsParentTask {

    protected static ThreadLocal<HashMap<String, String>> map = ThreadLocal.withInitial(HashMap::new);

    protected void clear() {
        map.get().clear();
    }

    protected abstract void doSomething();

    protected  HashMap<String, String> getValue(){
        return map.get();
    }

}

public class SonTask1 extends AbsParentTask{

    @Override
    protected void doSomething() {
        map.get().put("key1", "task1");
        //模拟doSomething...
        System.out.println(map.get());
    }
}

public class SonTask2 extends AbsParentTask {
    @Override
    protected void doSomething() {
        //模拟doSomething...但在使用map之前先清空,否则会拿到其他任务的数据
        clear();
        System.out.println(map.get());
    }
}

public class Client {
    public static void main(String[] args) {
        AbsParentTask son1 = new SonTask1();
        AbsParentTask son2 = new SonTask2();

        son1.doSomething();
        son2.doSomething();
    }
}

多线程环境下使用ThreadLocal可以很好的避免数据不一致问题。但是多线程+每个线程有多个任务情况时,要注意数据错乱的情况。

内存泄露

内存泄露是指由于对象永远无法被垃圾回收导致其占用的java虚拟机内存无法被释放。
在此之前先看一下ThreadLocal的内部实现机制。每一个线程(Thread)内部都维护着一个ThreadLocalMap,其类似一个HashMap,里面放有多个entry条目,该线程就被称为这些条目的属主线程,entry的key是一个ThreadLocal实例,value是线程的特有对象,因此,entry的作用是为该线程与ThreadlLocal实例建立联系,再将ThreadLocal实例和线程特有对象建立联系。entry对key的引用是弱引用,当没有地方使用key时,该key的实例会被垃圾回收,进而将key置为null,这个entry就变成了无效entry。entry对value的使用是强引用,即任何时候都不会被垃圾回收,那就会存在key为null,但是value不为null的情况,但是通过为null的key,也获取不到value,该value就无法被垃圾回收,从而导致了内存泄露。
下面是ThreadLocal的内部实现:
在这里插入图片描述
要想解决ThreadLocal内存泄露的问题,我们可以使用threadLocal.remove()方法。下面简单分析一下remove源码:

  public void remove() {
  		//获取当前线程的ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
         	//移除当前ThreadLocal实例以及对象
             m.remove(this);
     }

m.remove(this)方法:

 private void remove(ThreadLocal<?> key) {
            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)]) {
                if (e.get() == key) {
                	//移除key,也就是当前ThreadLocal实例
                    e.clear();
                    //移除value,也就是当前线程特有对象
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

expungeStaleEntry(i)方法:

 private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            //将value置为null
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            ......
}

entry的key之所以使用弱引用,是因为当线程特有对象被删除后,不用处理该ThreadLcoal实例,它也会被垃圾回收。如果使用强引用,则默认不会被垃圾回收,不强制回收会导致内存泄露。而entry的value,因为放的是对象,所以肯定是强引用的。

小总结

ThreadLocal相当于一个保姆,它替线程来管理线程的特有对象。与他起到同样的作用的还有锁,这二者有时可以作为替代关系。需要注意的是使用TheadLocal时尽量声明为static成员变量,以减少内存开销,同时也要避免内存泄露,记得remove。
最后感觉map是万能的,很多看似高深的技术都是通过map做映射来实现的,比如spring的三级缓存,哈哈哈。。。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值