目录
目录
一、并发问题
回顾之前写过的关于多线程并发的博客,并发问题的原因主要有三种:
- CPU缓存导致的可见性问题
- JVM优化导致的CPU指令执行顺序问题
- 线程切换导致的原子性问题
并发问题究其原因,是对线程之间共享对象操作导致的。那么如果特定情况下线程都访问自己的变量,是不是就可以解决线程不安全的问题呢?这个线程的本地变量就是ThreadLocal来实现的。
二、ThreadLocal实现原理
在了解ThreadLocal之前,需要先了解对象引用的概念
准备
public class GroovyDto {
public GroovyDto(String groovyName, Integer size) {
this.groovyName = groovyName;
this.size = size;
}
private String groovyName;
private Integer size;
public String getGroovyName() {
return groovyName;
}
public void setGroovyName(String groovyName) {
this.groovyName = groovyName;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
@Override
public String toString() {
return "GroovyDto{" +
"groovyName='" + groovyName + '\'' +
", size=" + size +
'}';
}
}
创建一个测试类ReferenceDemo
1. 引用(reference )
1.1 强引用(Strong Reference)
Java语言中默认声明的就是强引用,这种引用,只要引用还指向对象,那么对象就会一直存活,例如:
Object obj = new Object();
手动设置为null
obj = null;
这种情况obj变量就只是一个引用,存放在栈中,占两个字节,当方法出栈的时候释放掉。而曾经指向的对象,只要没有其他引用指向它,就会被GC。
测试:
public class ReferenceDemo {
public static void main(String[] args) {
//强引用
GroovyDto groovy = new GroovyDto("hello",123);
//手动置空
groovy = null;
//试图回收这个对象
System.gc();
System.out.println(groovy);
}
}
//output : null
//对象被回收
1.2 软引用(Soft Reference)
这类引用是非必须,但还有用的对象。只存在软引用时,内存足够的情况下进行垃圾回收,软引用对象不会被回收,反之会被回收。
就比如设计一个博客系统,一个用户查看可一篇文章退出后,再次查看这篇文章的时候就没必要去数据库中再次查询,而是直接取内存中的对象。还有网页、图片的缓存。这样有效的减少数据库查询,提高性能。
如果用强引用来缓存,内存占满时,我们找不到合适的机会去做垃圾回收。
测试
public class ReferenceDemo {
public static void main(String[] args) {
//强引用
GroovyDto groovy = new GroovyDto("hello",123);
//软引用
SoftReference<GroovyDto> softRef = new SoftReference<GroovyDto>(groovy);
//手动置空去掉强引用
groovy = null;
//试图回收这个对象
System.gc();
System.out.println(softRef.get());
}
}
//output : GroovyDto{groovyName='hello', size=123}
//对象没被回收
1.3 弱引用(Weak Reference)
GC一旦发现一块内存上是有弱引用,那肯定会被回收,不管空间是否足够。
例如一个电商的优惠券系统,用了一个弱引用的用户List与优惠券Coupon对象绑定
这时候如果一个用户注销掉,使用强引用的情况下,我们就需要解决,Coupon和User解绑的动作,一旦忘记删除,那就会出现问题。
反之如果使用弱引用,在下次垃圾回收的时候List里的弱引用对象就会被回收,实现自动更新的效果。
测试
public class ReferenceDemo {
public static void main(String[] args) {
//强引用
GroovyDto groovy = new GroovyDto("hello",123);
//软引用
WeakReference<GroovyDto> weakRef = new WeakReference<GroovyDto>(groovy);
//手动置空去掉强引用
groovy = null;
//试图回收这个对象
System.gc();
System.out.println(weakRef.get());
}
}
//output : null
//对象被回收
1.4 虚引用
用的不多,不做介绍
2.ThreadLocal原理
每个Thread里面都有一个ThreadLocalMap对象
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
每个ThreadLocalMap对象都包含一个Entry[] 数组(java.lang.ThreadLocalMap中的静态内部类),数组中的key是ThreadLocal的弱引用,value是存储的值
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
value是通过java.lang.ThreadLocal的set方法写入的
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null)
//不为空就将value设置到ThreadLocalMap 中
//this表示ThreadLocal对象
map.set(this, value);
else
//如为空即首次设置ThreadLocalMap 的值
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
//t.threadLocals 指向新创建的ThreadLocalMap
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3. 内存泄漏
ThreadLocal本身不存储值,作为一个key让线程在ThreadLocalMap中取值,这里的key是ThreadLocal对象的弱引用,如果外部没有强引用指向这个ThreadLocal,那么GC的时候该对象就一定会被回收。到时候Entry中就会存在key为null的值,这样的值我们是取不到的。
那么如果这个线程如果是线程池里的线程,或者是个很耗时的线程,那么这个key为null的Entry的value就会一直存在,因为存在一个强引用链,这样就会造成内存泄漏。
4. 避免内存泄漏的方法
主动调用ThreadLocal中的remove方法删除对象的值。算然ThreadLocal中的set和get方法都使用了排空处理,但如果没用到set和get方法依然会造成内存泄漏。所以务必使用ThreadLocal中的remove方法将设置的线程本地变量值删除
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
*ThreadLocalMap中的remove方法
* Remove the entry for key.
*/
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) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
因为是值传递,所以多线程在set同一个对象进ThreadLocal会存在并发问题,所以要确保每次set进去的对象都是一个全新的对象。如果想规避这个方法可以重写set方法,实现对象的深拷贝。
三、总结
- ThreadLocal用于存储线程本地变量,解决线程间变量值共享导致的线程安全问题
- 主动调用remove方法删除对象,避免造成内存泄漏
- 使用static的ThreadLocal延长了ThreadLocal的生命周期会导致内存泄漏
- 分配使用了ThreadLocal,但没有调用get(),set(),remove()方法,会导致内存泄漏
- ThreadLocal默认是值传递,一定要避免多线程共享一个对象,不然会造成线程安全问题