码迷,mamicode.com
首页 > 编程语言 > 详细

【原创】Java并发编程系列30 | ThreadLocal

时间:2020-11-24 12:56:29      阅读:12      评论:0      收藏:0      [点我收藏+]

标签:i++   check   限流   com   异常   响应   warnings   image   回收   

【原创】Java并发编程系列30 | ThreadLocal

收录于话题
#java 976 #程序员 2286 #并发编程 240 #进阶架构师 | 并发编程专题 12

★★★建议星标我们★★★
公众号改版后文章乱序推荐,希望你可以点击上方“Java进阶架构师”,点击右上角,将我们设为★“星标”!这样才不会错过每日进阶架构文章呀。

技术图片

2020年Java原创面试题库连载中
【000期】Java最全面试题库思维导图
【001期】JavaSE面试题(一):面向对象
【002期】JavaSE面试题(二):基本数据类型与访问修饰符
【003期】JavaSE面试题(三):JavaSE语法(1)
【004期】JavaSE面试题(四):JavaSE语法(3)
【005期】JavaSE面试题(五):String类
【006期】JavaSE面试题(六):泛型
【007期】JavaSE面试题(七):异常
【008期】JavaSE面试题(八):集合之List
【009期】JavaSE面试题(九):集合之Set
【010期】JavaSE面试题(十):集合之Map
【011期】JavaSE面试题(十一):多线程(1)
【012期】JavaSE面试题(十二):多线程(2)
【013期】JavaSE面试题(十三):多线程(3)
【014期】JavaSE面试题(十四):基本IO流
【015期】JavaSE面试题(十五):网络IO流
【016期】JavaSE面试题(十六):反射
【017期】JavaSE面试题(十七):JVM之内存模型
【018期】JavaSE面试题(十八):JVM之垃圾回收
【020期】JavaSE系列面试题汇总(共18篇)
【019期】JavaWeb面试题(一):JDBC
【021期】JavaWeb面试题(二):HTTP协议
【022期】JavaWeb面试题(三):Cookie和Session
【023期】JavaWeb面试题(四):JSP
【024期】JavaWeb面试题(五):Filter和Listener
【025期】Java工具面试题(一):版本控制工具
【026期】Java工具面试题(二):项目管理工具
【027期】Java设计模式面试题
【028期】JavaWeb系列面试题汇总(共10篇)
【029期】JavaEE面试题(一)Web应用服务器
【030期】JavaEE面试题(二)SpringMVC
【031期】JavaEE面试题(三)Spring(1)
【032期】JavaEE面试题(四)Spring(2)
【033期】JaveEE面试题(五)MyBatis
【034期】JavaEE面试题(六)Hibernate
【035期】JavaEE面试题(七)SpringBoot(1)
更多内容,点击上面蓝字查看

技术图片

线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,而大多数解决线程安全问题的方法都是通过加锁的方式,让同一时间只有一个线程能过访问到共享资源。这篇文章介绍另种解决线程安全的思路——ThreadLocal:
介绍
使用
源码
内存泄露
总结

1. 介绍


线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,大多数解决线程安全问题的方法都是通过加锁的方式,让同一时间只有一个线程能过访问到共享资源。
ThreadLocal提供了另一种解决思路,让每个线程拥有自己私有的内存空间,将线程私有的数据存入这个私有空间内,线程与线程之间相互隔离,这样就不会有线程安全问题。
数据结构
每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。
threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。
技术图片

2. 使用


ThreadLocal核心方法:
get():返回当前线程副本中该ThreadLocal对应的值。
initialValue():返回当前线程副本中的该ThreadLocal对应对应的“初始值”。
remove():移除当前线程副本中该ThreadLocal对应的值。
set(T value):当前线程副本中该ThreadLocal对应的值为value。
使用举例:
每个线程保存一个私有的int值count,5个线程count从0加到10,线程之间互不影响。


/**
 * 每个线程保存一个私有的int值count
 * 5个线程count从0加到10,线程之间互不影响
 */
public class ThreadLocalDemo {
    private static ThreadLocal<Integer> countLocal = new ThreadLocal<Integer>(){
        public Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args){
        for (int i = 1; i <= 5; i++) {
            new Thread("Thread_" + i) {
                public void run() {
                    for (int j = 1; j <= 10; j++) {
                        countLocal.set(countLocal.get() + 1);
                        System.out.println(getName() + ": count=" + countLocal.get());
                    }
                };
            }.start();
        }
    }
}

输出结果如下:

Thread_3: count=1
Thread_5: count=1
Thread_5: count=2
Thread_4: count=1
Thread_4: count=2
Thread_2: count=1
Thread_2: count=2
Thread_2: count=3
Thread_2: count=4
Thread_1: count=1
Thread_2: count=5
Thread_4: count=3
Thread_5: count=3
Thread_5: count=4
Thread_3: count=2
Thread_5: count=5
Thread_5: count=6
Thread_5: count=7
Thread_5: count=8
Thread_4: count=4
Thread_4: count=5
Thread_4: count=6
Thread_2: count=6
Thread_2: count=7
Thread_2: count=8
Thread_2: count=9
Thread_1: count=2
Thread_2: count=10
Thread_4: count=7
Thread_5: count=9
Thread_3: count=3
Thread_3: count=4
Thread_5: count=10
Thread_4: count=8
Thread_1: count=3
Thread_4: count=9
Thread_3: count=5
Thread_4: count=10
Thread_1: count=4
Thread_3: count=6
Thread_1: count=5
Thread_3: count=7
Thread_1: count=6
Thread_3: count=8
Thread_1: count=7
Thread_3: count=9
Thread_1: count=8
Thread_3: count=10
Thread_1: count=9
Thread_1: count=10

可以看到,即使5个线程并发执行,但是每个线程内部的count都是按1-10的顺序相加的。

3. 源码


3.1 数据结构
每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。
threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。

/**
 * 线程局部变量threadLocals为ThreadLocal.ThreadLocalMap类型
 */
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

/**
 * ThreadLocal$ThreadLocalMap 散列表结构
 * key=ThreadLocal value=Object
 */
static class ThreadLocalMap {
    private Entry[] table;
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

3.2 get()
在理解的ThreadLocal的存储结构之后,再看get()和set()方法就很简单了。
get():
获取当前线程thread。
获取当前线程thread.threadLocals,threadLocals是map结构。
map的key是ThreadLocal类型,获取map中当前threadLocal对应的value值。
如果map=null,就创建map并赋初值。

public T get() {
    // 获取当前线程私有的map thread.threadLocals 
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 获取map的value值
    if (map != null) {
        // map的key是ThreadLocal类型
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map=null,初始化map,下文有讲解
    return setInitialValue();
}

/**
 * 返回ThreadLocalMap类型的thread.threadLocals 
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

/**
 * 初始化value,map=null时会初始化map
 */
private T setInitialValue() {
    T value = initialValue();// 初始值
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);// 将初始值存入map
    else
        createMap(t, value);// map=null时初始化map
    return value;
}

/**
 * 返回map中value的初始值
 * 默认为null,一般需要重写该方法以获得非null值
 */
protected T initialValue() {
    return null;
}

set():
获取当前线程thread。
获取当前线程thread.threadLocals,threadLocals是map结构。
map的key是ThreadLocal类型,设置map中当前threadLocal对应的value值。
如果map=null,就创建map并赋值。


public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

4. 注意问题


4.1 每个线程最好只存一个ThreadLocal

线性探测解决Hash冲突:根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
如下,ThreadLocalMap.set()方法:

private void set(ThreadLocal<?> key, Object value) {
    // 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
    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)]) {
        ThreadLocal<?> k = e.get();
        // key 存在,直接覆盖
        if (k == key) {
            e.value = value;
            return;
        }
        /*
         * key=null而value!=null,因为key是弱引用
         * 用新的key-value将旧的null-value替换掉
         */
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 清除陈旧的Entry(key == null)
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以每个线程最好只存ThreadLocal。
4.2 引发内存泄露
ThreadLocal使用中会有内存泄露问题。
ThreadLocalMap的key是弱引用,而Value是强引用。源码如下:


static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

ThreadLocalMap的key是弱引用,发生GC时弱引用key会被回收;而value是强引用,GC时不会被回收。
所以ThreadLocalMap中就会出现key为null的Entry,因为key为null,这Entry是不能被访问到的。如果当前线程一直没结束的话,一直有这个引用链:Thread --引用--> ThreaLocalMap --引用--> Entry --引用--> value,这个value就无法被回收,导致内存泄露。
解决:
ThreadLocalMap的set()、cleanSomeSlots()等方法中都做了相应处理,检查存在key=null而value!=null的Entry就会删掉;
在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

5. 总结


同步机制 VS ThreadLocal
同步机制是通过控制线程访问共享对象的顺序,类似“时间换空间”,同一时刻共享对象只能被一个线程访问造成整体上响应时间增加,但是对象只占有一份内存。
而ThreadLocal是为每一个线程分配一个该对象,各用各的互不影响。类似“空间换时间”,为每个线程都分配了一份对象,自然而然内存使用率增加,但整体上时间效率要增加很多。
ThreadLocal存储结构
每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。
threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。
注意问题
ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以每个线程最好只存ThreadLocal。
由于ThreadLocalMap的key是弱引用,ThreadLocal使用中会有内存泄露问题。在使用完ThreadLocal之后调用remove方法删除值,可避免内存泄露问题。

并发系列文章汇总


【原创】01|开篇获奖感言
【原创】02|并发编程三大核心问题
【原创】03|重排序-可见性和有序性问题根源
【原创】04|Java 内存模型详解
【原创】05|深入理解 volatile
【原创】06|你不知道的 final
【原创】07|synchronized 原理
【原创】08|synchronized 锁优化
【原创】09|基础干货
【原创】10|线程状态
【原创】11|线程调度
【原创】12|揭秘 CAS
【原创】13|LockSupport
【原创】14|AQS 源码分析
【原创】15|重入锁 ReentrantLock
【原创】16|公平锁与非公平锁
【原创】17|读写锁八讲(上)
【原创】18|读写锁八讲(下)
【原创】19|JDK8新增锁StampedLock
【原创】20|StampedLock源码解析
【原创】21|Condition-Lock的等待通知
【原创】22|倒计时器CountDownLatch
【原创】22|倒计时器CountDownLatch
【原创】23|循环屏障CyclicBarrier
【原创】24|信号量Semaphore
【原创】25|交换器Exchangere
【原创】26|ConcurrentHashMap(上)
【原创】27|ConcurrentHashMap(下)
【原创】28|Copy-On-Write容器

之前,给大家发过三份Java面试宝典,这次新增了一份,目前总共是四份面试宝典,相信在跳槽前一个月按照面试宝典准备准备,基本没大问题。
《java面试宝典5.0》(初中级)
《350道Java面试题:整理自100+公司》(中高级)
《资深java面试宝典-视频版》(资深)
《Java[BAT]面试必备》(资深)
分别适用于初中级,中高级,资深级工程师的面试复习。
内容包含java基础、javaweb、mysql性能优化、JVM、锁、百万并发、消息队列,高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper、数据结构、限流熔断降级等等。
技术图片
获取方式:点“在看”,V信关注上述Java最全面试题库号并回复 【面试】即可领取,更多精彩陆续奉上。

看到这里,证明有所收获
必须点个在看支持呀,喵

【原创】Java并发编程系列30 | ThreadLocal

标签:i++   check   限流   com   异常   响应   warnings   image   回收   

原文地址:https://blog.51cto.com/15009303/2552578

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!