并发编程(高并发、多线程) 第三章

并发容器

  • 1.ThreadLocal
    • 1.1 ThreadLocal是什么?(难度:★★ 频率:★★★)
    • 1.2 为什么要使用ThreadLocal(难度:★★ 频率:★★★)
    • 1.3 ThreadLocal内部结构和原理(难度:★★ 频率:★★★)
    • 1.4 ThreadLocal导致内存泄漏(难度:★★ 频率:★★★)
      • 1.4.1 内存泄漏和内存溢出的区别
      • 1.4.2 强引用和弱引用的区别
      • 1.4.3 ThreadLocal造成内存泄漏的原因?
      • 1.4.4 哪些情况下, ThreadLocal会导致内存泄漏?

1.ThreadLocal

1.1 ThreadLocal是什么?(难度:★★ 频率:★★★)

ThreadLocal即线程本地变量, 是一种线程隔离机制, 如果你创建了一个ThreadLocal变量, 多个线程操作这个变量的时候, 等同于操作自己本地内存中的变量, 从而起到线程隔离的作用, 避免并发环境下的线程安全问题

public class Demo {
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();
 
    static void print(String str){
        System.out.println(str + ":" + threadLocal.get());
    }
 
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("abc");
                print("thread1 variable");
            }
        });
 
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocal.set("def");
                print("thread2 variable");
            }
        });
 
        thread1.start();
        thread2.start();
    }
}

1.2 为什么要使用ThreadLocal(难度:★★ 频率:★★★)

并发场景下, 会存在多个线程同时修改一个共享变量的情况, 这就会导致线程安全问题

为了解决线程安全问题, 可以使用加锁的方式, 例如使用synchronized或者Lock, 但是加锁的方式可能会导致系统变慢, 示意图如下:

并发编程(高并发、多线程) 第三章_第1张图片
还有另外一种方案,就是使用空间换时间的方式,即使用ThreadLocal。使用ThreadLocal类访问共享变量时,会在每个线程的本地,都保存一份共享变量的拷贝副本。多线程对共享变量修改时,实际上操作的是这个变量副本,从而保证线性安全。
并发编程(高并发、多线程) 第三章_第2张图片
ThreadLocal与Synchronized对比
虽然ThreadLocal与synchronized都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同

  1. 每个Thread线程内部都有一个Map(ThreadLocalMap)
  2. Map中存储了ThreadLocal对象(key)和变量副本(value)
  3. Thread内部的Map是ThreadLocal维护的, 由ThreadLocal负责向map中获取和设置线程的变量值
  4. 对于不同的线程, 每个获取副本值时, 别的线程并不能获取到当前线程的副本值, 形成了线程的隔离, 互不干扰
Synchronized ThreadLocal
原理 同步机制采用时间换空间的方法, 只提供了一份变量, 让不同的线程排队访问 ThreadLocal采用空间换时间的方式, 为每一个线程提供了一份变量的副本, 从而实现同时访问, 互不干扰
侧重点 多个线程之间访问资源的同步 多线程中让每个线程之间数据相互隔离

1.3 ThreadLocal内部结构和原理(难度:★★ 频率:★★★)

在这里插入图片描述

最初的设计
每个ThreadLocal是自己维护一个ThreadLocalMap, key是当前线程, value是要存储的局部变量, 这样就可以达到各个线程的局部变量隔离的效果

JDK8的设计
每个Thread维护一个ThreadLocalMap, 这个Map的key是ThreadLocal本身, value是要存储的变量. 具体的流程如下

这样设计的优点:

  1. 每个Map存储的Entry数量变少,之前Entry的数量是由线程个数决定的, 线程个数越多, Entry就越多, 而现在是由ThreadLocal决定的, 在实际开发中, ThreadLocal数量往往少于线程数
  2. 当Thread销毁的时候, ThreadLocalMap会随之销毁, 减少内存的使用, 早期的方案中线程执行结束并不会把ThreadLocalMap销毁(垃圾回收)

源码分析

  • set方法
    在这里插入图片描述
  • get方法
    在这里插入图片描述
    在这里插入图片描述

1.4 ThreadLocal导致内存泄漏(难度:★★ 频率:★★★)

1.4.1 内存泄漏和内存溢出的区别

内存溢出 内存泄漏
定义 内存溢出指的是程序在运行过程中申请的内存超过了系统或者进程所能提供的内存大小(结果) 内存泄漏指的是程序中已经不再需要的内存未被释放,造成系统内存的浪费(起因)
原因 通常是由于程序中存在大量的内存申请,而且没有及时释放,导致系统的可用内存被耗尽 内存泄漏通常是由于程序中存在指针或引用,指向了不再使用的内存块,但程序却没有释放这些内存
表现 当内存溢出发生时,程序通常会崩溃,并且系统可能会报告无法分配内存的错误 内存泄漏不会导致程序立即崩溃,但随着时间的推移,系统可用内存会逐渐减少,最终可能导致系统变慢或者崩溃

总体来说,内存溢出是由于申请的内存过多,超出了系统限制,而内存泄漏是因为未能及时释放已经不再使用的内存。

解决内存溢出和内存泄漏的方法通常包括合理管理内存的申请和释放过程,使用合适的数据结构,以及利用内存管理工具进行检测和优化。

需要说明一点: 虽然内存泄漏可能会导致内存溢出,但内存溢出也可能是由于其他原因,例如程序中存在大量的内存申请,但这些内存并没有被泄漏,而是在程序执行期间一直保持被占用状态,最终导致系统内存耗尽。

1.4.2 强引用和弱引用的区别

  • 强引用
    最常见的引用类型, 如果一个对象具有强引用,即使系统面临内存不足的情况,垃圾回收器也不会回收具有强引用的对象
Object obj = new Object(); // 强引用
  • 弱引用
    当垃圾回收器进行扫描时,无论内存是否充足,都会回收只有弱引用的对象。
    弱引用通常用于构建缓存和实现类似的功能,使得在内存不足时,可以更容易地释放一些占用内存较大但仍可以重新计算或重新加载的对象
WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 弱引用

总结:

  1. 强引用可以阻止对象被垃圾回收,只有在没有任何强引用指向对象时,垃圾回收器才会考虑回收该对象。
  2. 弱引用相对较弱,即使还有弱引用指向对象,垃圾回收器仍然可以在需要时回收该对象。
  3. 强引用适合确保对象不被提前回收的场景,而弱引用适合那些在内存紧张时可以被更容易释放的场景。

1.4.3 ThreadLocal造成内存泄漏的原因?

ThreadLocalMap中使用key为ThreadLocal的弱引用, 而value是强引用. 所以, 当ThreadLocal没有被外部强引用时, 在垃圾回收时, key会被清理掉, 而value不会被清理. 这样一来就会导致ThreadLocalMap中就会出现key为null的Entry, 假如我们不做任何措施的话,value永远无法被GC 回收,这个时候就可能会产生内存泄露。

ThreadLocalMap实现中已经考虑了这种情况,在调用set()、get()、remove()方法的时候,会清理掉key为null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

1.4.4 哪些情况下, ThreadLocal会导致内存泄漏?

  1. 长时间存活的线程
public class MyRunnable implements Runnable {
    private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();

    @Override
    public void run() {
        MyObject obj = new MyObject();
        myThreadLocal.set(obj);

        // 执行任务...

        // 如果线程一直存活,myThreadLocal 将一直持有对 obj 的引用,即使任务执行完毕。
    }
}

在这个例子中,即使任务执行完毕,ThreadLocal 对象仍然持有对 MyObject 的引用,而线程的生命周期可能会很长,导致 MyObject 无法被垃圾回收,从而引发内存泄漏。

为了避免这种情况,需要在不再需要 ThreadLocal 存储的对象时,显式调用 remove() 方法来清理 ThreadLocal。这样可以确保 ThreadLocal 对象中的弱引用被正确清理,从而防止内存泄漏。例如:

public class MyRunnable implements Runnable {
    private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();

    @Override
    public void run() {
        try {
            MyObject obj = new MyObject();
            myThreadLocal.set(obj);

            // 执行任务...

        } finally {
            // 清理 ThreadLocal,防止内存泄漏
            myThreadLocal.remove();
        }
    }
}
  1. 使用线程池

如果在使用线程池的情况下,ThreadLocal被设置在某个任务中,而这个任务在线程池中执行完成后线程被放回线程池而不是销毁,那么ThreadLocal可能在下一次任务执行时仍然持有对上次设置的对象的引用。

ExecutorService executorService = Executors.newFixedThreadPool(5);

executorService.submit(() -> {
    MyObject obj = new MyObject();
    myThreadLocal.set(obj);

    // 执行任务...

    // 线程被放回线程池,但 ThreadLocal 可能仍然持有对 obj 的引用。
});

为了避免这类问题,确保在ThreadLocal不再需要时,调用remove()方法清理它所持有的对象引用。这通常在任务执行结束时或者线程即将被销毁时执行。例如:

public class MyRunnable implements Runnable {
    private static ThreadLocal<MyObject> myThreadLocal = new ThreadLocal<>();

    @Override
    public void run() {
        try {
            MyObject obj = new MyObject();
            myThreadLocal.set(obj);

            // 执行任务...

        } finally {
            // 清理 ThreadLocal,防止内存泄漏
            myThreadLocal.remove();
        }
    }
}

你可能感兴趣的:(java,jvm,性能优化)