JUC多并发编程 ThreadLocal

ThreadLocal提供线程局部变量,避免线程安全问题。每个线程有自己的变量副本,通过get和set方法操作。文章介绍了ThreadLocal的使用示例,线程池中的应用,以及底层源码分析,包括ThreadLocalMap和内存管理。还讨论了Java的引用类型如强引用、弱引用、软引用和虚引用,以及它们在内存管理和垃圾回收中的角色。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  • ThreadLocal 提供线程局部变量。这些变量与正常的变量不同, 因为每一个线程在访问 ThreadLocal 实例的时候(通过其 get 或 set 方法) 都有自己的,独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段, 使用它的目的是希望将状态(例如, 用户ID或事务ID) 与线程关联起来。
  • 主要解决了让每个线程绑定自己的值, 通过使用 get() 和 set () 方法, 获取默认值或将其值更改为当前线程所存副本的值,从而避免了线程安全问题

实例编码:

import java.util.Random;
import java.util.concurrent.TimeUnit;

class House{
    int saleCount = 0;
    public synchronized  void saleHouse() {
        ++ saleCount;
    }
    ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(()->0);

    public void saleVolumeByThreadLocal() {
        saleVolume.set(1+saleVolume.get());
    }

}

public class ThreadLocalDemo {
    public static void main(String[] args) {
        House house = new House();
        for (int i = 1; i <= 5; i++) {
            new Thread(()-> {
                int size = new Random().nextInt(5) + 1;
                //System.out.println(size);
                try {
                    for (int j = 1; j <= size; j++) {
                        house.saleHouse();
                        house.saleVolumeByThreadLocal();
                    }
                    System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出:" + house.saleVolume.get());
                } finally {
                    // 避免内存泄露
                    house.saleVolume.remove();
                }
            }, String.valueOf(i)).start();
        }

        try{ TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println(Thread.currentThread().getName() + "\t" + "总计卖出多少套:" + house.saleCount);

    }
}

线程池的使用:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class MyData{
    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->0);
    public void add(){
        threadLocal.set(1+threadLocal.get());
    }
}
public class ThreadLocalDemo2 {
    public static void main(String[] args) {
        MyData myData = new MyData();
        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        try{
            for (int i = 0; i < 10; i++) {
                // 线程池 会复用

                threadPool.submit(()-> {
                    try {
                        Integer beforeInt = myData.threadLocal.get();
                        myData.add();
                        Integer afterInt = myData.threadLocal.get();
                        System.out.println(Thread.currentThread().getName() + "\t" + "beforeInt:" + beforeInt + "\t" + "afterInt:" + afterInt);
                    }finally {
                        myData.threadLocal.remove();
                    }
                });
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }

    }
}

ThreadLocal 底层源码分析

Thread、ThreadLocal 和ThreadLocalMap 关系:

  • threadLocalMap 是一个以 threadLocal 实例的 key,任意对象为 value 的 Entry 对象,当我们为threadLocal 变量赋值,实际上就是以当前 threadLocal 实例为 key, 值为 value 的 Entry 往这个 threadLocalMap 中存放
  • ThreadLocalMap 从字面上就可以看出这时一个保存 ThreadLocal 对象的 map(其实是以 ThreadLocal 为 Key), 不过是经过了两层包装的 ThreadLocal 对象
  • JVM 内部维护了一个线程版的 Map<ThreadLocal,Value>(通过 ThreadLocal 对象的 set 方法, 结果把 ThreadLocal 对象自己当作 Key, 放进了 ThreadLocalMap), 每个线程要用到这个 T 的时候,用当前的线程去 Map里面获取, 通过这样让每个线程都拥有自己的独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量

ThreadLocal 内存泄漏:

public class T1 {
    volatile boolean flag;

    public static void main(String[] args) {
        ThreadLocal<String> t1 = new ThreadLocal<>();
        t1.set("hello threadlocal");
        t1.get();
    }
}
  • 不在会被使用的对象或者变量占用的内存不能被回收
  • 当 function01 方法执行完毕后,栈帧销毁强引用 t1 也就没有了。但是此线程的 ThreadLocalMap 里某个 entry 的 key 引用还指向这个对象,若这个 key 引用是强引用, 就会导致 key 指向的 ThreadLocal 对象及 v 指向的对象不能被 gc 回收,造成内存泄漏。若这个 key 引用是 弱引用就大概率会减少内存泄漏的问题。就可以使 ThreadLocal 对象在方法执行完毕后顺利被回收且 Entry 的 key 引用指向为 null。
  • 虽然弱引用,保证了 key 指向的 ThreadLocal 对象能被及时回收, 但是 v 指向的 value 对象是需要 ThreadLocalMap 调用 get,set 时发现 key 为 null 时才会去回收整个 entry、value,因此弱引用不能 100% 保证内存不泄漏. 我们要在不使用某个 ThreadLocal 对象后,手动调用 remove 方法来删除它,尤其是在线程池中,不仅仅是内存泄漏的问题,因为线程池中线程是重复使用的,意味着这个线程的 ThreadLocalMap 对象也是重复使用的,如果不手动调用 remove 方法, 那么后面的线程就有可能获取到上个线程遗留下来的 value 值, 造成 bug

 

 

引用类型:

  • Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作

import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

class MyObject {
    @Override
    protected void finalize() throws Throwable {
        // 通常目的实在对象被不可撤销地丢弃之前执行清理操作
        System.out.println("------ invoke finalize method~!!!");


    }
}
public class ReferenceDemo {
    public static void main(String[] args) {
        MyObject myObject = new MyObject();

        ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>();
        PhantomReference<MyObject> phantomReference = new PhantomReference<>(myObject, referenceQueue);

        System.out.println(phantomReference.get());
        List<byte[]> list = new ArrayList<>();
        new Thread(()-> {
            while(true) {
                list.add(new byte[1*1024*1024]);
                try{ TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(phantomReference.get() + "\t" + "list add ok");
            }
        },"t1").start();

        new Thread(()-> {
            while(true) {
                Reference<? extends  MyObject> reference = referenceQueue.poll();
                if (reference != null) {
                    System.out.println("----- 有虚对象回收加入队列");
                    break;
                }
            }
        },"t2").start();
    }
    /**
     * 弱引用
     */
    public static void weak() {
        WeakReference<MyObject> weakReference = new WeakReference<MyObject>(new MyObject());
        System.out.println("gc before 内存够用:" + weakReference.get());

        System.gc();
        try{ TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("gc after 内存够用:" + weakReference.get());
    }
    /**
     * 软引用
     */
    public static void soft() {
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.out.println("---- softReference: "+ softReference.get());

        System.gc();
        try{ TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("gc after 内存够用:" + softReference.get());
        // VM option 启动时配置 -Xms10m -Xmx10m
        try {
            byte[] bytes = new byte[50 * 1024 * 1024];
        }catch (Exception e) {
            e.printStackTrace();
        }finally {

        }
        System.out.println("gc after 内存不够用:" + softReference.get());

    }

    /**
     * 强引用
     */
    public static void strong() {
        MyObject myObject = new MyObject();
        System.out.println("gc before:" + myObject);

        myObject = null;
        System.gc(); // 人工开启GC,一般不用

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

强引用:

  • 当内存不足, JVM 开始垃圾回收,对于强引用的对象, 就算是出现了 OOM 也不会对该对象进行回收
  • 当一个对象被强引用变量引用时, 它处于可达状态,即使该对象以后永远不会被用到,JVM 也不会回收
  • 对于一个普通对象,如果没有其他的引用关系,只要超过了引用的作用域或者显示地将相应(强)引用赋值为null, 一般认为就是可以被垃圾收集的了(还要看具体的策略)

软引用:

  • 是一种相对强引用弱化了一些的引用,需要用 java.lang.ref.SoftReference 来实现, 可以让对象豁免一些垃圾回收
  • 当系统内存充足时它不会被回收,当系统内存不足时它会被回收。
  • 通常用在对内存敏感的程序中,比如高速缓存就用到了软引用,内存够用的时候就保留,不够用就回收

弱引用:

  • 需要用 java.lang.ref.weakReference 类来实现, 它比软引用的生存期更短
  • 对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,都会回收该对象占用的内存

虚引用:

  • 必须和引用队列 ReferenceQueue 联合使用,需要java.lang.ref.PhantomReference 类来实现。虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象。
  • 虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供一种确保对象被 finalize 以后,做某些事情的通知机制。PhantomReference 的 get方法总是返回 null, 因此无法访问对象的引用对象
  • 设置虚引用关联对象的唯一目的, 就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步处理,用来实现比 finalize 机制更灵活的回收操作

总结

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值