并发编程系列——3ThreadLocal核心原理分析

导读:本篇文章讲解 并发编程系列——3ThreadLocal核心原理分析,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

学习目标

  1. ThreadLocal是啥

  2. ThreadLocal是干啥用的

  3. ThreadLocal底层数据结构是什么

  4. ThreadLocal如何解决hash冲突的

  5. java中存在哪几种引用

  6. ThreadLocal中的key是什么引用,为什么要用这种引用

  7. 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个方法可能是分布在不同的类中的。

并发编程系列——3ThreadLocal核心原理分析

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也有局限性,我们来看看阿里规范:

并发编程系列——3ThreadLocal核心原理分析

第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);
}

并发编程系列——3ThreadLocal核心原理分析

//初始化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

并发编程系列——3ThreadLocal核心原理分析

 并发编程系列——3ThreadLocal核心原理分析

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()来清除内存

并发编程系列——3ThreadLocal核心原理分析

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,不管内存空间是否足够,都会回收该对象。

下文预告

  1. 锁的分类

  2. Reentrantlock的使用

  3. Reentrantlock的工作流程

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/76681.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!