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

发布时间:2024年01月10日

1.ThreadLocal

1.1 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();
    }
}

内部结构和原理
在这里插入图片描述

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

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

这样设计的优点:

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

ThreadLocal与synchronized对比
虽然ThreadLocal与synchronized都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同

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

源码分析

  • set方法

在这里插入图片描述

  • get方法

在这里插入图片描述
在这里插入图片描述

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

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

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

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

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

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

强引用和弱引用的区别

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

总结:

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

哪些情况下, 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();
        }
    }
}
文章来源:https://blog.csdn.net/qq_24099547/article/details/135508178
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。