共享变量引发的问题
java中对全局变量的操作是通过JMM(java内存模型)内存模型实现的,全局变量保存在主存中,但是变量的计算则是在线程的工作内存中。
线程计算的过程如下:
- 从主存中获取变量副本,保存到线程工作内存。
- 对变量操作。
- 把工程内存的变量写入主存。
从这个过程中可以看出,如果多个线程同时对一个变量操作,但是1,2,3如果不是一次性完成(原子性的),那么就会导致不同线程把数据的结果写乱,导致结果不是我们期待的。
具体示例可以参考:
Java并发编程-共享模型之管程(Monitor)(四)
ThreadLocal简介
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证规避多线程访问出现线程不安全的方法;当我们在创建一个变量后,如果每个线程对其进行访问的时候都是线程自己的变量,这样就不会存在线程不安的问题。
ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存的变量,从而规避了线程安全的问题,如下图所示:
总结:ThreadLocal只是一种规避多线程共同访问共享变量的手段,对于多个线程需要使用相同的共享变量是无能为力的。
它的使用场景:不同的线程使用不同的变量,不同的线程之间不会共享变量。
ThreadLocal示例
ThreadLocal简单使用
下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用print方法打印当前本地变量的值。
如果在打印之后调用本地变量的remove方法会删除本地内存中的变量:
public class ThreadLocalTest {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void print(String threadName) {
System.out.println(threadName + " print threadLocal variable:" + threadLocal.get());
threadLocal.remove();
}
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
for(int i = 0; i < 3; i++) {
threadLocal.set("thread1");
print("thread1");
System.out.println("thread1:" + threadLocal.get());
}
});
Thread thread2 = new Thread(() -> {
for(int i = 0; i < 3; i++) {
threadLocal.set("thread2");
print("thread2");
System.out.println("thread2:" + threadLocal.get());
}
});
thread1.start();
thread2.start();
}
}
运行结果:
thread1 print threadLocal variable:thread1
thread1:null
thread1 print threadLocal variable:thread1
thread1:null
thread1 print threadLocal variable:thread1
thread1:null
thread2 print threadLocal variable:thread2
thread2:null
thread2 print threadLocal variable:thread2
thread2:null
thread2 print threadLocal variable:thread2
thread2:null
ThreadLocal为什么只能设置一个Value
public class ThreadLocalTest {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static final ThreadLocal<Integer> threadLocalInt = new ThreadLocal<>();
// 因为ThreadLocal本身是Key,所以value只能是一个
threadLocal.set("abc");
threadLocalInt.set(9);
System.out.println(threadLocal.get());
System.out.println(threadLocalInt.get());
}
}
- 因为ThreadLocal本身是Key,所以value只能是一个;实际上在Thread中存放一个ThreadLocalMap,ThreadLocalMap中是一个Entry数组。
- 每个Entry都是一个以ThreadLocal对象的弱引用为key的条目。
- 可以简单的理解为,一个ThreadLocal对象对应一个Entry,Thread的ThreadLocalMap中可以存放多个Entry。
ThreadLocal的实现原理
每个线程都有一个ThreadLocalMap用于存放线程本地变量
- 从上面的ThreadLocal关系图可知:Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部类ThreadLocalMap可以发现实际上它类似一个HashMap。
- 默认情况下,每个线程中的两个变量都是null。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
- 只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建它们。
- 可以看到每个线程的本地变量不是存放在ThreadLocal实例中,而是存放在线程自己的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,而ThreadLocal实例本身只是一个提供操作接口的工具而已。
通过set方法将value值添加到调用线程的threadLocals中,当调用线程调用get方法时能够从他的threadLocals中取出value值。 - 如果调用线程一直不终止,那么这个本地变量将会一直存放在线程的threadLocals中,GC回收不了造成内存泄漏,因此最后的实践是:不使用本地变量的时候及时调用remove方法将threadLocals中本地变量删除。
ThreadLocal提供的操作方法
set方法
public void set(T value) {
// 获取当前线程(调用者线程)
Thread t = Thread.currentThread();
// 获取当前线程的成员变量threadLocals
ThreadLocalMap map = getMap(t);
// 如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量自身
if (map != null)
map.set(this, value);
else
// 如果map为null,说明首次添加,需要先创建出对应的map
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// creatMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get方法
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的threadLocals变量
ThreadLocalMap map = getMap(t);
// 如果threadLocals变量不为null,就可以在map中查找本地变量的值。
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 执行此处,threadLocals为null,或者threadLocals中的threadLocal实例作为key对应的值为null;
// 调用该方法初始当前线程的threadLocals变量
return setInitialValue();
}
// 如果当前线程的threadLocals不存在创建,并设置本地变量为null,同时返回初始值null。
private T setInitialValue() {
// protected T initialValue() {return null;}
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// 删除当前线程中指定ThreadLocal实例的本地变量
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) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
remove方法判断当前线程的对应的threadLocals变量是否为null,不为null就删除ThreadLocals中以TheadLocal实例作为key对应的Entry。
本地变量使用完毕应该立即删除
如上图:每个线程内部有一个名为threadLocals的成员变量,该变量的类型为ThreadLocal.ThreadLocalMap类型(类似一个HashMap),其中的key为当前定义的ThreadLocal变量的this引用,value是set方法设置的值。
每个线程的本地变量存放在自己的成员变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量就会一直存在(所以可能会导致内存溢出),因此使用完毕需要将其remove掉。
ThreadLocal使用不当的内存泄漏问题
弱引用定义
如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null
ThreadLocalMap内部实现
ThreadLocalMap内部实际上是一个Entry数组(private Entry[] table;),我们先看看Entry这个内部类:
/**
* 是继承自WeakReference的一个类,该类中实际存放的key是
* 指向ThreadLocal的弱引用和与之对应的value值(该value值
* 就是通过ThreadLocal的set方法传递过来的值)
* 由于是弱引用,当get方法返回null的时候意味着key的引用可能被回收
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** value就是和ThreadLocal绑定的 */
Object value;
// k:ThreadLocal的引用,被传递给WeakReference的构造方法
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
public WeakReference(T referent) {
super(referent); / /referent:ThreadLocal的引用
}
// Reference构造方法
Reference(T referent) {
this(referent, null);// referent:ThreadLocal的引用
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
- 从上面的代码中,我们可以看出,当前ThreadLocal的引用k被传递给WeakReference的构造函数,所以ThreadLocalMap中Entry的Key为ThreadLocal的弱引用。
- 当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreaqLocal的弱引用,value就是通过set设置的值。
- 如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal实例的强引用,那么TheadLocal实例对应的Entry是不会被释放的,就会造成内存泄漏。
- 如果ThreadLocal实例没有其他强引用,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量会在gc的时候被回收,但是Entry中threadLocal实例对应的value还存在,这就造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项,因此value由于存在引用不会被释放,entry同样不会被释放)。
总结
ThreadLocalMap中Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLocal对象依赖(强引用),ThreadLocalMap中Entry的ThreadLocal对象就会被回收掉,但是对应的值不会被回收,这个时候Map就可能存在key为null但是value不为null的项导致内存泄漏(因为entry释放不掉),因此使得使用是需要及时调用remove方避免内存泄漏。
参考
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/100269.html