多线程-ThreadLocal由浅入深--boy next door♂

1.简介

是个啥:

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

做啥的:

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),
主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。

Demo:

需求:不参加总和计算,希望各自分灶吃饭,各凭销售本事提成,按照出单数各自统计

class House{

    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->0);
    public void saleHouse(){
        Integer value = threadLocal.get();
        value++;
        threadLocal.set(value);
    }
}

public class ThreadLocalD {
    public static void main(String[] args) {
        House house = new House();

        new Thread(()->{
            try {
                for (int i = 0; i < 3; i++) {
                    house.saleHouse();
                }
      System.out.println(Thread.currentThread().getName()+"----"+house.threadLocal.get());
            } finally {
                house.threadLocal.remove();
            }
        },"t1").start();

        
        new Thread(()->{
            try {
                for (int i = 0; i < 7; i++) {
                    house.saleHouse();
                }
      System.out.println(Thread.currentThread().getName()+"----"+house.threadLocal.get());
            } finally {
                house.threadLocal.remove();
            }
        },"t2").start();

    }
}

总结:

1.每个 Thread 内有自己的实例副本且该副本只由当前线程自己使用
2.其它 Thread 不可访问,那就不存在多线程间共享的问题
3.统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立

拢共来说:如何线程不争抢:1.锁住资源(synchr、lock) 2.人手一份,大家各自安好,没必要抢夺

2.由ThreadLocal规范所引出的:

2.1非线程安全的SimpleDateFormat:

SimpleDateFormat中的日期格式不是同步的。推荐(建议)为每个线程创建独立的格式实例。如果多个线程同时访问一个格式,则它必须保持外部同步。

写时间工具类,一般写成静态的成员变量,殊不知,此种写法的多线程下的危险性!几乎调用一次就报一次不同的错误,很难根据日志信息排查到具体问题!即使不异常,时间显示也会乱七八糟!!!

2.1.1解决方案1:

public class DateUtils
{
    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    /**
     * 模拟并发环境下使用SimpleDateFormat的parse方法将字符串转换成Date对象
     */
    public static Date parseDate(String stringDate)throws Exception
    {
        return sdf.parse(stringDate);
    }

    public static void main(String[] args) throws Exception
    {
        for (int i = 1; i <=30; i++) {
            new Thread(() -> {
                try {
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    System.out.println(sdf.parse("2020-11-11 11:11:11"));
                    sdf = null;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
}

缺点:每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收。

★2.1.2解决方式2–ThreadLocal:

线程本地变量存储:

public class DateUtils {

    public static final ThreadLocal<SimpleDateFormat> sdfThreadLocal =ThreadLocal.withInitial(
            ()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static Date parseTL(String stringDate) throws ParseException {
        return sdfThreadLocal.get().parse(stringDate);
    }


    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
                    new Thread(()->{
                        try {
                            System.out.println(DateUtils.parseTL("2020-11-11 11:11:11"));
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }finally {
                            sdfThreadLocal.remove();
                        }
                    },String.valueOf(i)).start();
                }
    }
}

结果:

Wed Nov 11 11:11:11 CST 2020
Wed Nov 11 11:11:11 CST 2020
Wed Nov 11 11:11:11 CST 2020
Wed Nov 11 11:11:11 CST 2020
Wed Nov 11 11:11:11 CST 2020
Wed Nov 11 11:11:11 CST 2020......

2.1.3解决方式3–加锁/第三方时间库

★2.1.4解决方式4–DateTimeFormatter的API:

public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static String format(LocalDateTime localDateTime)
    {
        return DATE_TIME_FORMAT.format(localDateTime);
    }

    public static LocalDateTime parse(String dateString)
    {

        return LocalDateTime.parse(dateString,DATE_TIME_FORMAT);

3.ThreadLocal内存泄漏:

3.1强引用:

class MyObject{
    @Override
    protected void finalize() throws Throwable {
        System.out.println("---finalize was invoked");
    }
}

public class ReferenceD {

    public static void main(String[] args) {
        MyObject myObject = new MyObject();
        System.out.println("gc--before: "+myObject);
 ★★★对象没有引用后,gc()才生效,也就起到一个留遗言的作用
        myObject = null;
        System.gc();  ★★★ 手动gc()在对象断开引用才执行gc
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("gc--after: "+myObject);
    }
}

结果:

gc--before: org.ice.juc.ThreadLocal.MyObject@1540e19d
---finalize was invoked
gc--after: null

3.2软引用:

对于只有软引用的对象来说,
系统内存充足时它不会被回收,
系统内存不足时它会被回收

3.3 弱引用:

对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。

3.3.1:软、弱引用适用场景:

假如有一个应用需要读取大量的本地图片:

 *    如果每次读取图片都从硬盘读取则会严重影响性能,
 *    如果一次性全部加载到内存中又可能造成内存溢出。

此时使用软引用可以解决这个问题。

设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

Map<String, SoftReference> imageCache = new HashMap<String, SoftReference>();

3.4 虚引用(开发阶段用不到):

虚引用需要java.lang.ref.PhantomReference类来实现。

顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,
它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 (ReferenceQueue)联合使用

虚引用的主要作用是跟踪对象被垃圾回收的状态。 仅仅是提供了一种确保对象被 finalize以后,做某些事情的机制。 PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。

其意义在于:说明一个对象已经进入finalization阶段,可以被gc回收,用来实现比finalization机制更灵活的回收操作。

换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理

多线程-ThreadLocal由浅入深--boy next door♂

★3.5:避免内存泄漏:

3.5.1为啥源码用弱引用:

当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用大概率减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。
多线程-ThreadLocal由浅入深--boy next door♂

3.5.2:key为null,值还在,线程未结束的情况:

1 当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

2当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。

3 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心

3.5.3:如何完全保证不泄露:

我们调用get,set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存。
我们在代码中要做的就是及时调用remove()

总结:

1.threadLocal并不解决线程间共享数据的问题
2.适用于变量在线程间隔离且在方法间共享的场景
3.通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
4.每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
5.ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
6.都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法(remove、get、set调用的)回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法

上一篇:.NET使用AutoResetEvent实现多线程打印奇偶数


下一篇:MYSQL 数据库