ThreadLocal应用

ThreadLocal概念

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

基本使用

/**
 * @Author: 张定辉
 * @CreateDate: 2022/11/30
 * @Description: 使用ThreadLocal确保线程安全问题
 */
public class UseThreadLocalTest {
    private static final Logger log= LoggerFactory.getLogger(UseThreadLocalTest.class.getName());
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    @Accessors(chain = true)
    static class House{
        private ThreadLocal<Integer> saleHouseCount=ThreadLocal.withInitial(()->0);
        private LongAdder longAdder=new LongAdder();

        public void add(){
            saleHouseCount.set(saleHouseCount.get()+1);
            longAdder.increment();
        }

        public Long getSumCount(){
            return longAdder.sum();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        final Random random=new Random();
        House house=new House();
        CountDownLatch latch=new CountDownLatch(5);
        for (int i=0;i<5;i++){
            new Thread(()->{
                int count= random.nextInt(5)+1;
                for(int j=0;j<count;j++){
                    house.add();
                }
                log.info("销售{}卖出的房子数量是:{}",Thread.currentThread().getName(),count);
                latch.countDown();
            },String.valueOf(i)).start();
        }
        latch.await();
        log.info("集团收到的总销售额度是:{}",house.getSumCount());
    }
}

上诉案例中貌似没有问题,但是在阿里的规范中强制要求,ThreadLocal使用完之后必须使用try-catch-finally调用remove方法清除ThreadLocal变量的值,下面将使用代码说明为什么

不加remove方法出现的问题
package com.threadLocal;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;

/**
 * @Author: 张定辉
 * @CreateDate: 2022/11/30
 * @Description: 使用线程池对ThreadLocal进行测试,不使用remove的后果
 */
public class UseThreadPoolTest {
    private static final Logger log= LoggerFactory.getLogger(UseThreadLocalTest.class.getName());
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    @Accessors(chain = true)
    static class House{
        private ThreadLocal<Integer> saleHouseCount=ThreadLocal.withInitial(()->0);
        private LongAdder longAdder=new LongAdder();

        public void add(){
            saleHouseCount.set(saleHouseCount.get()+1);
            longAdder.increment();
        }

        public Long getSumCount(){
            return longAdder.sum();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //使用线程池进行任务的执行
        ThreadPoolExecutor executor= new ThreadPoolExecutor(5,10,100, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));
        executor.setRejectedExecutionHandler((r, executor1) -> log.error("线程池队列已满!"));
        CountDownLatch latch=new CountDownLatch(10);

        House house=new House();
        final Random random=new Random();

        for (int i=0;i<10;i++){
            Runnable thread= () -> {
                int count=random.nextInt(5)+1;
                for(int j=0;j<count;j++){
                    house.add();
                }
                log.info("预计销售数量:{},实际销售数量:{}",count,house.getSaleHouseCount().get());
                latch.countDown();
            };
            executor.execute(thread);
        }
        latch.await();
        log.info("集团总销售数量:{}",house.getSumCount());
        executor.shutdown();
    }
}

运行结果如图:使用线程池运行结果图所示

image-20221130223346030
使用线程池运行结果图

通过运行截图可以看到。线程编号5在第一次使用时,预计的和实际的是一样的但是第二次明显可以看到,实际值是预计值和上一次的实际值的相加结果(2+3),这是由于我们没有使用ThreadLocal的remove方法,在线程池中由于线程复用的因素,导致了前一次该线程的运行值带到了下一次来进行累加运算了。在生产环境下这个值有可能会越来越大最终导致我们的内存泄露问题。

正确用法
try{
    /*
    业务逻辑执行
    */
}finally{
    //在finally中调用remove方法
    threadLocal.remove();
}

ThreadLocal源码解读

public void set(T value) {
    set(Thread.currentThread(), value);
}

private void set(Thread t, T value) {
    ThreadLocalMap map = getMap(t);
    if (map == ThreadLocalMap.NOT_SUPPORTED) {
        throw new UnsupportedOperationException();
    }
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

引用关系

案例使用的公共类

static class Student{
        /**
         * 该方法在GC回收这个对象时调用
         */
        @Override
        protected void finalize() throws Throwable {
            log.info("-------即将被GC回收------");
        }
    }
Reference强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 ps:强引用其实也就是我们平时A a = new A()这个意思。

使用案例

private static void strongReference() throws InterruptedException {
    Student student=new Student();
    log.info("GC before:{}",student);
    System.gc();
    log.info("GC after:{}",student);
    student=null;
    System.gc();
    TimeUnit.SECONDS.sleep(3);
}

通过运行结果可以看到,当我们不将变量student置为null时,即使执行了GC回收也不会被回收(没有1执行Student类中的finalize方法),但是当我们student对象置空后,在调用GC就看到执行了Student类中的finalize方法了

SoftReference软引用

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列

使用案例

在运行代码之前我们需要设置一下JVM参数,将内存值调小

-Xms10m -Xmx10m

private static void softReference() throws InterruptedException {
    //修改修改运行参数-Xms10m -Xmx10m
    SoftReference<Student> softReference=new SoftReference<>(new Student());
    System.gc();
    log.info("内存够用:{}",softReference.get());
    //直接new了一个20M的对象
    byte[] bytes=new byte[1024*1024*20];
    log.info("内存不够用:{}",softReference.get());
    TimeUnit.SECONDS.sleep(2);
}

通过结果可以看到,当内存足够时,软引用并不会被GC回收,但是当内存不够的时候就会被GC回收了

WeakRefernce弱引用

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

使用案例

private static void weakReference() throws InterruptedException {
    WeakReference<Student> weakReference=new WeakReference<>(new Student());
    log.info("GC before:{}",weakReference.get());
    System.gc();
    TimeUnit.SECONDS.sleep(3);
    log.info("GC after:{}",weakReference.get());
}

通过结果可以看到,即使我们的内存空间足够大,一旦被GC发现该引用是一个弱引用的话同样会被回收的

PhantomReference虚引用

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue); 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

使用案例

同样需要将JVM内存设置小一点,虽然虚引用并不是强制内存不够时才回收,但是因为他的回收时间是不确定的,所以我们通过这种方式能够更快更直观的看到效果,仅此而已

private static void PhantomReference() {
    ReferenceQueue<Student> referenceQueue=new ReferenceQueue<>();

    PhantomReference<Student> phantomReference=new PhantomReference<>(new Student(),referenceQueue);

    List<byte[]> container=new ArrayList<>(16);
    new Thread(()->{
        try {
            while(true){
                container.add(new byte[1*1024*1024]);
                //调用get方法始终都会返回null,因为本质上来说的话虚引用可以认为就是没有引用
                log.info("内存够用,虚引用的值为: {}",phantomReference.get());
                TimeUnit.MILLISECONDS.sleep(100);
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }).start();

    new Thread(()->{
        while(true){
            Reference<? extends Student> poll = referenceQueue.poll();
            if(poll==null){
                log.info("对象还未被回收");
            }
            else {
                log.info("有虚对象回收被加入队列");
                break;
            }

        }
    }).start();
}
软引用和弱引用的使用场景

假如有一个应用需要读取大量的本地图片,如果每次读取都从磁盘读取则会影响性能,如果一次性全部加载到内存中又可能会造成内存溢出,此时使用软引用可以解决这个问题,设计思路是使用一个HashMap来保存图片路劲和相应的图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效避免了OOM的问题。


面试题

  • Thread、ThreadLocalMap、ThreadLocal的关系

    ThreadLocalMap是ThreadLocal的内部类,ThreadLocalMap中维护着一个弱引用类型的Entry内部类。Thread类中维护着一个ThreadLocalMap的引用。这样每次调用ThreadLocal的set方法时,实际上是将调用set方法的ThreadLocal对象本身作为key,值是用户传递过来的,存储到当前线程中的ThreadLocalMap中,调用get方法也一样,自身作为key然后取到值,正是这样,ThreadLocal能够实现数据隔离,获取当前线程的局部变量,不受其他线程的影响

  • ThreadLocal的key是弱引用,为什么?

    无论ThreadLocalMap中的key使用哪种类型的引用都无法完全避免内存泄露,跟使用弱引用没有关系,要避免内存泄露有两种方式,1.使用完ThreadLocal,调用其remove方法删除对应的Entry,2.使用完ThreadLocal,当前Thread也随之消失。相对来说,第一种方式要比爹日种方式更好控制,使用了remove方法之后无论是强引用还是弱引用都不会有问题,就算忘记了调用remove方法,弱引用比强也比多了一层报障,弱引用会被ThreadLocal回收,对应的value在下一次调用set,get,remove中的任意方法的时候会被清楚,从而避免内存泄露

ThreadLocal内存泄露问题

ThreadLocal中为什么最后要加remove方法

最佳实践

  • 进行初始化时要设置初始值否则会报空指针异常
  • 使用完成后一定要使用try-finally调用remove方法
  • ThreadLocal变量尽量使用static修饰,因为,实现线程隔离的并不是ThreadLocal而是线程中的ThreadLocalMap,既如此,那么使用静态变量的话只需要分配一块内存空间即可
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值