概念介绍
对象中的数据被称为该对象的状态。一个对象中的成员变量和静态变量被称为该对象的状态变量。如果一个类的同一个实例被多个线程共享,并存在共享状态,则该实例被称为有状态对象。反之,如果一个类的同一个实例即使被多个线程共享,但不会出现并发问题,则该实例被称为无状态对象。
在多线程共享同一个有状态对象时,如果想要保证线程的安全性,一是可以采用锁,二是让每个线程有独立的一份该对象的实例,并且每个线程无法访问其他线程中该对象的实例。这种对象被称为线程特有对象,这种线程被称为对象的持有线程。
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的三级缓存,哈哈哈。。。