学习目标
-
ThreadLocal是啥
-
ThreadLocal是干啥用的
-
ThreadLocal底层数据结构是什么
-
ThreadLocal如何解决hash冲突的
-
java中存在哪几种引用
-
ThreadLocal中的key是什么引用,为什么要用这种引用
-
ThreadLocal怎么解决内存泄漏的
第1章 使用场景
作用:ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全(线程隔离机制)
1.1 场景1
这种场景通常用于保存线程不安全的工具类,典型的需要使用的类就是 SimpleDateFormat。
在这种情况下,每个Thread内都有自己的实例副本,且该副本只能由当前Thread访问到并使用,相当于每个线程内部的本地变量,这也是ThreadLocal命名的含义。因为每个线程独享副本,而不是公用的,所以不存在多线程间共享的问题。
//每个线程都创建了SimpleDateFormat对象
public class ThreadLocalDemo01 {
//这里不会出现线程不安全的问题
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
int s = i;
new Thread(()->{
String data = new ThreadLocalDemo01().date(s);
System.out.println(data);
}).start();
Thread.sleep(100);
}
}
private String date(int seconds){
Date date = new Date(1000*seconds);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
return simpleDateFormat.format(date);
}
}
//每次线程调用也是单独创建SimpleDateFormat对象
public class ThreadLocalDemo02 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int s = i;
threadPool.submit(()->{
String data = new ThreadLocalDemo02().date(s);
System.out.println(data);
});
}
threadPool.shutdown();
}
private String date(int seconds){
Date date = new Date(1000*seconds);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
return simpleDateFormat.format(date);
}
}
//出现线程安全问题了,共享一个SimpleDateFormat对象
public class ThreadLocalDemo03 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int s = i;
threadPool.submit(()->{
String data = new ThreadLocalDemo03().date(s,simpleDateFormat);
System.out.println(data);
});
}
threadPool.shutdown();
}
private String date(int seconds,SimpleDateFormat simpleDateFormat){
Date date = new Date(1000*seconds);
return simpleDateFormat.format(date);
}
}
//使用ThreadLocal做隔离
public class ThreadLocalDemo04 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int s = i;
threadPool.submit(()->{
String data = new ThreadLocalDemo04().date(s);
System.out.println(data);
});
}
threadPool.shutdown();
}
private String date(int seconds){
Date date = new Date(1000*seconds);
SimpleDateFormat dateFormat = dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
在ThreadLocalDemo01中们给每个线程都创建了SimpleDateFormat对象,他们之间互不影响,代码是可以正常执行的。
在ThreadLocalDemo02中可以看出,我们用了一个16线程的线程池,并且给这个线程池提交了1000次任务。每个任务中它做的事情和之前是一样的,还是去执行date方法,并且在这个方法中创建一个simpleDateFormat 对象。刚才所做的就是每个任务都创建了一个 simpleDateFormat 对象,也就是说,1000 个任务对应 1000 个 simpleDateFormat 对象,但是如果任务数巨多怎么办?这么多对象的创建是有开销的,并且在使用完之后的销毁同样是有开销的,同时存在在内存中也是一种内存的浪费。我们可能会想到,要不所有的线程共用一个 simpleDateFormat 对象?但是simpleDateFormat 又不是线程安全的,我们必须做同步,比如使用synchronized加锁。到这里也许就是我们最终的一个解决方法。但是使用synchronized加锁会陷入一种排队的状态,多个线程不能同时工作,这样一来,整体的效率就被大大降低了。
在demo04中引入ThreadLocal,对这种场景,ThreadLocal再合适不过了,ThreadLocal给每个线程维护一个自己的simpleDateFormat对象,这个对象在线程之间是独立的,互相没有关系的。这也就避免了线程安全问题。与此同时,simpleDateFormat对象还不会创造过多,线程池一共只有 16 个线程,所以需要16个对象即可。
1.2 场景2
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。
比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。
public class ThreadLocalDemo05 {
public static void main(String[] args) {
User user = new User("jack");
new Service1().service1(user);
}
}
class Service1 {
public void service1(User user){
//给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
UserContextHolder.holder.set(user);
new Service2().service2();
}
}
class Service2 {
public void service2(){
User user = UserContextHolder.holder.get();
System.out.println("service2拿到的用户:"+user.name);
new Service3().service3();
}
}
class Service3 {
public void service3(){
User user = UserContextHolder.holder.get();
System.out.println("service3拿到的用户:"+user.name);
//在整个流程执行完毕后,一定要执行remove
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
//创建ThreadLocal保存User对象
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name){
this.name = name;
}
}
1.3 局限性
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。但是ThreadLocal也有局限性,我们来看看阿里规范:
第2章 源码解析
2.1 set方法
public void set(T value) {
Thread t = Thread.currentThread();//获取当前执行的线程
ThreadLocalMap map = getMap(t);//获得当前线程的ThreadLocalMap实例,存有根据当前线程的ID为Key的ThreadLocalMap里;
if (map != null)//如果map不为空,说明当前线程已经有了一个ThreadLocalMap实例
map.set(this, value);//直接将当前value设置到ThreadLocalMap中
else
createMap(t, value);//说明当前线程是第一次使用线程本地变量,构造map,跟初始化的方法是一个
}
void createMap(Thread t, T firstValue) {
//this代表当前ThreadLocal实例,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);//threadLocalHashCode
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;//魔数
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
上面的代码都是为了引出下面这个代码,证明key为弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);//key为弱引用
value = v;
}
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table; //得到初始化好的16个Entry格子
int len = tab.length; //得到格子的长度
int i = key.threadLocalHashCode & (len-1); //得到下标值,魔数,但是长度必须是2的N次方
//进行线性探测,解决hash冲突(默认e是tab[i],只要e!=null,都会执行 e = tab[i = nextIndex(i, len)])),直到e为空为止
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
//拿到entry的key的值
ThreadLocal<?> k = e.get();
//如果等于我传进来的key,说明还没有被gc回收,设置成功返回
if (k == key) {
e.value = value;
return;
}
//如果key已经被回收,调用replaceStaleEntry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
2.1.1 replaceStaleEntry
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
//得到默认的Entry数组以及长度
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot; //传进来当前的下标索引
//向前扫描,查找最前的一个无效slot(即key为null的entry),并把下标给到slotToExpunge
for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
//向后扫描不为空的entry
for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
//得到entry的key
ThreadLocal<?> k = e.get();
//找到了key,将其与无效的entry交换
if (k == key) {
e.value = value;
//相互交换,staleSlot为最开始的时候的下标,e为找到的不为空的entry,i为不为空的下标 减少hash冲突,因为我现在的hash是我的staleSlot
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
//进行分段清理
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots((slotToExpunge), len);
//返回
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
//第一次,都找不到,在原有的位置,new Entry
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
2.1.2 expungeStaleEntry
/** * 这个函数是ThreadLocal中核心清理函数,它做的事情很简单: * 就是从staleSlot开始遍历,将无效(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。 * 另外,在过程中还会对非空的entry作rehash。 * 可以说这个函数的作用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等) */
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
//对于还没有被回收的情况,需要做一次rehash。
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
前面分析了set方法第一次初始化ThreadLocalMap的过程,也对ThreadLocalMap的结构有了一个全面的了解。那么接下来看一下map不为空时的执行逻辑
-
根据key的散列哈希计算Entry的数组下标
-
通过线性探索探测从i开始往后一直遍历到数组的最后一个Entry
-
如果map中的key和传入的key相等,表示该数据已经存在,直接覆盖
-
如果map中的key为空,则用新的key、value覆盖,并清理key=null的数据
-
rehash扩容
2.2 get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//初始化
return setInitialValue();
}
2.2.1 getEntry
private Entry getEntry(ThreadLocal<?> key) {
//根据ThreadLocal的Id来获取索引,也就是哈希值
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
//这个ThreadLocal有对应的entry并且未失效(因为是弱引用)
if (e != null && e.get() == key)
return e;
else
//线性探测,所以往后找还是有可能能够找到目标Entry的。e为空的时候没用,还是返回的null,不为空的时候只有一种情况,entryKey失效了
return getEntryAfterMiss(key, i, e);
}
2.2.2 getEntryAfterMiss
//没有获取到e的时候调用该方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
//不断向后探测直到遇到空entry。
while (e != null) {
//得到entry的key,弱引用的调用方式
ThreadLocal<?> k = e.get();
//能拿到ThreadLocal的值
if (k == key)
return e;
if (k == null)
//对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry
expungeStaleEntry(i);
else
//不为空,拿到下一个下标
i = nextIndex(i, len);
//拿到下一个Entry
e = tab[i];
}
return null;
}
//环形,不会超过len
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
2.2.3 setInitialValue
private T setInitialValue() {
//value默认为空
T value = initialValue();
//得到当前线程
Thread t = Thread.currentThread();
//当前线程是否存在ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//如果已经存在ThreadLocalMap,threadLocal为key,value为null赋值给entry
map.set(this, value);
else
//不存在,创建map
createMap(t, value);
return value;
}
至此,整个get方法完成
第3章 相关问题
3.1 为什么Key要弱引用?
假如每个key都强引用指向ThreadLocal的对象,也就是下图虚线那里是个强引用,那么这个ThreadLocal对象就会因为和Entry对象存在强引用关联而无法被GC回收,造成内存泄漏,除非线程结束后,线程被回收了,map也跟着回收。
如果key是强引用,那么当我们执行threadLocal=null时,这个对象还被key关联,无法进行回收,只有当线程结束后,才会取消关联
但是用弱引用,我们就能在GC的时候,回收!
但是如果用的是线程池,那么的话线程就不会结束,只会放在线程池中等待下一个任务,但是这个线程的 map 还是没有被回收,它里面存在value的强引用,所以会导致内存溢出。
所以一般用threadLocal.remove()来清除内存
3.2 Value为什么不用弱引用
是因为不清楚这个Value
除了map
的引用还是否还存在其他引用,如果不存在其他引用,当GC
的时候就会直接将这个Value干掉了,而此时我们的ThreadLocal
还处于使用期间,就会造成Value为null的错误,所以将其设置为强引用
key在,value不在了
3.3 怎么解决hash冲突
1.首先,那个魔数就能保证重复性会低,但是基数必须是2的N次方(举例)
2.用开放寻址法,如果真的查到的下标已经存在数据,就去找下一个,找到一个null的为止,并且是环形查找,因为肯定会有空的,会进行提前扩容
第4章 引用类型
除了基础的数据类型外都是引用类型,那么java根据其生命周期的长短又将引用类型分为强引用、软引用、弱引用、虚引用
4.1强引用
也是我们平时用得最多的,new一个对象就是强引用,例如 Object obj = new Object();
当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象!记住是存活着,不可能是你new一个对象就永远不会被GC回收。如果将引用赋值为null时,你的对象就表明不是存活着,这样就会可以被GC回收了。
-
当内存不足的时候,jvm开始垃圾回收,对于强引用的对象,就算出现OOM也不会回收该对象的。 因此,强引用是造成java内存泄露的主要原因之一。
public static void main(String[] args) {
Object obj=new Object();//这样定义就是一个强引用
Object obj2=obj;//也是一个强引用
obj=null;
System.gc();
//不会被垃圾回收
System.out.println(obj2);
}
4.2 软引用
软引用的生命周期比强引用短一些。软引用是通过SoftReference类实现的。当JVM认为内存空间不足时,就会去试图回收软引用指向的对象
对于只有软引用的对象来说, 当系统内存充足时,不会被回收; 当系统内存不足时,会被回收;
Object obj=new Object();
SoftReference wrf=new SoftReference(obj);
obj=null;
System.out.println("未发生GC之前"+wrf.get());
System.gc();
System.out.println("内存充足,发生GC之后"+wrf.get());
4.3 弱引用
弱引用是通过WeakReference类实现的,它的生命周期比软引用还要短,也是通过get()方法获取对象。在GC的时候,不管内存空间足不足都会回收这个对象,同样也可以配合ReferenceQueue 使用,也同样适用于内存敏感的缓存。ThreadLocal中的key就用到了弱引用。
Object obj=new Object();
WeakReference wrf=new WeakReference(obj);
obj=null;
System.out.println("未发生GC之前"+wrf.get());
System.gc();
System.out.println("内存充足,发生GC之后"+wrf.get());
4.4 虚引用
也称虚引用,是通过PhantomReference类实现的。任何时候可能被GC回收,就像没有引用一样。无法通过虚引用访问对象的任何属性或者函数。那就要问了要它有什么用?虚引用仅仅只是提供了一种确保对象被finalize以后来做某些事情的机制。比如说这个对象被回收之后发一个系统通知啊啥的。
虚引用是必须配合ReferenceQueue 使用的,具体使用方法和上面提到软引用的一样。主要用来跟踪对象被垃圾回收的活动。
如果一个对象只是被弱引用引用者,那么只要发生GC,不管内存空间是否足够,都会回收该对象。
下文预告
-
锁的分类
-
Reentrantlock的使用
-
Reentrantlock的工作流程
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/76681.html