Java 王者修炼手册【并发篇-并发关键字】:从 synchronized 到原子类修炼

33 阅读23分钟

大家好,我是程序员强子。

2000491.jpg

又来刷英雄熟练度咯~今天专攻 Java 并发关键字~

  • 同步锁机制:Synchronized作用的对象,底层实现原理,锁升级过程,synchronized与lock对比

  • 内存语义

    • volatile:volatile的作用,内存屏障与禁止重排序的原理
    • final:并发场景语义、初始化安全性保证、引用类型修饰规则、final 与非 final 方法在多线程调用中的指令重排序差异
  • 原子操作

    • CAS:定义、底层原理、三个操作数的作用,问题以及解决方案
    • Unsafe 类:作用、不推荐直接使用的原因、CAS 实现及其他操作
    • Java 原子类:实现原理,分类,存在的问题以及解决方案
  • 内存模型:定义、作用,核心问题解决等等

  • 线程隔离机制-ThreadLocal:作用、底层实现原理及线程隔离方式,内存泄漏原因,典型应用场景等等

同步锁机制

synchronized 作用对象

synchronized 的核心是 ,但锁的载体可以是对象或类,具体作用位置分为三类

作用在实例方法上(对象锁)

锁的载体是当前对象实例(this) ,每个实例有独立的锁。多线程操作同一个实例时会竞争锁,操作不同实例时互不干扰。

public class SyncDemo {
    // 实例方法:锁是当前对象(this)
    public synchronized void instanceMethod() {
        // 同步代码
    }

    public static void main(String[] args) {
        SyncDemo demo1 = new SyncDemo();
        SyncDemo demo2 = new SyncDemo();

        // 线程1操作demo1,线程2操作demo2:不竞争锁(锁是两个不同对象)
        new Thread(() -> demo1.instanceMethod()).start();
        new Thread(() -> demo2.instanceMethod()).start(); 

        // 线程3和线程4都操作demo1:竞争同一把锁(需排队执行)
        new Thread(() -> demo1.instanceMethod()).start();
        new Thread(() -> demo1.instanceMethod()).start();
    }
}

作用在代码块上(灵活指定锁对象)

通过synchronized(锁对象)手动指定锁,锁对象可以是实例对象(对象锁)或Class 对象(类锁)。

public class SyncDemo {
    private Object lock = new Object(); // 自定义对象锁

    public void blockMethod() {
        // 锁是lock对象(对象锁,每个实例的lock独立)
        synchronized (lock) { 
            // 同步代码
        }

        // 锁是SyncDemo.class(类锁,全局唯一)
        synchronized (SyncDemo.class) {
            // 同步代码
        }
    }
}

作用在静态方法上(类锁)

锁的载体是类的Class 对象(每个类只有一个 Class 对象),无论创建多少实例,所有线程都会竞争这把唯一的类锁。

public class SyncDemo {
    // 静态方法:锁是SyncDemo.class
    public static synchronized void staticMethod() {
        // 同步代码
    }

    public static void main(String[] args) {
        SyncDemo demo1 = new SyncDemo();
        SyncDemo demo2 = new SyncDemo();

        // 线程1调用demo1的静态方法,线程2调用demo2的静态方法:竞争同一把类锁(需排队)
        new Thread(() -> demo1.staticMethod()).start();
        new Thread(() -> demo2.staticMethod()).start();
    }
}

看到这里,会不会跟 强子一样,都会有疑惑:

  • 什么是 Class 对象?
  • Class 对象 存储在jvm 哪里, 实例对象又存在哪里,它们之间有什么不同 ?

什么是 Class 对象?

在 Java 中,每个类(比如User、String)被类加载器(ClassLoader) 加载到 JVM 时,JVM 会为这个类创建一个唯一的 Class 对象

它就像这个类的 元数据身份证,存储了该类的所有静态信息

  • 类的名称、包路径;
  • 类的方法、字段,构造函数、继承关系、实现的接口
  • ....

synchronized 的 类锁本质是以类的 Class 对象为锁对象,这依赖于 Class 对象的 唯一性

Class 对象 存储在jvm 哪里, 实例对象又存在哪里,它们之间有什么不同 ?

Class 对象,存储在方法区。

JDK7 及之前,方法区的实现是 永久代(PermGen) ;JDK8 及之后,永久代被移除,方法区的功能由 元空间(Metaspace) 替代

实例对象,存储在堆内存。

实例对象是通过new关键字创建的具体对象

总之,一句话总结:Class 对象是 模板,实例对象是 模板造出来的产品

底层实现原理

synchronized 的实现依赖对象头对象监视器(monitor)。

什么是对象头?

对象头(Mark Word),每个 Java 对象都有对象头,其中 Mark Word 存储了对象的锁状态、哈希值、GC 年龄等信息。是理解 synchronized 锁机制垃圾回收等底层原理的关键。

不同锁状态下,Mark Word 的结构不同:

  • 无锁状态:存储对象哈希值;
  • 偏向锁:存储偏向的线程 ID;
  • 轻量级锁:存储指向线程栈中锁记录的指针;
  • 重量级锁:存储指向监视器(monitor)的指针

JVM 处理锁的逻辑就是这样:

  • 先通过标志位确定状态(Mark Word最后3位)
  • 再根据状态解析 Mark Word 里的具体信息
  • 最后执行对应策略(比如直接放行、CAS 竞争、进入阻塞队列等)

什么是对象监视器?

重量级锁的核心是 monitor,每个对象都关联一个 monitor。

monitor 内部有三个关键结构:

  • owner:持有锁的线程;
  • EntryList:等待获取锁的线程队列;
  • WaitSet:调用wait()后等待被唤醒的线程队列

把 monitor 比作 会议室

  • owner是当前使用会议室的人
  • EntryList是门口排队等会议室的人
  • WaitSet是暂时出去(比如去洗手间)但还需要回来继续使用会议室的人,回来后需要重新排队(进入EntryList)

接下来总结一下 monitor 获取锁 和 释放锁的 过程~

线程获取锁(进入同步块)

  • 检查 monitor 的owner是否为null(会议室是否空闲)

    • 若owner为null:当前线程直接成为owner,并将 monitor 的 重入计数器(记录同一线程获取锁的次数)设为 1,成功进入同步块
    • 若owner是当前线程(同一线程再次获取锁,可重入):重入计数器加 1,直接进入同步块(无需竞争)
    • 若owner是其他线程(锁被占用):当前线程进入EntryList队列,进入 阻塞状态,等待被唤醒。
  • 在WaitSet的线程

    • 线程已经获取锁,但调用了wait方法(主动释放锁,暂时退出),会把当前线程放到WaitSet
    • 必须由其他持有锁的线程调用notify()/notifyAll()唤醒
    • 唤醒后进入 EntryList重新竞争锁

关于 wait() / notify()/notifyAll()方法,如有不懂,请查看上期 并发基础中有介绍~

线程释放锁(退出同步块)

  • 将 monitor 的重入计数器减 1

  • 若计数器减为 0(当前线程彻底释放锁)

    • 将owner设为null(会议室空闲)
    • 从EntryList中随机唤醒一个线程(非公平),让它重新竞争锁(重复 获取锁 的过程)

锁升级过程

为了平衡性能与线程安全,synchronized 的锁会根据竞争强度动态升级

过程为:偏向锁 → 轻量级锁 → 重量级锁

单向,不可逆

当一个 Java 对象刚被创建(如new Object()),还没有任何线程尝试获取它的锁时,它处于无锁状态

第一个线程尝试进入同步块时,JVM 会将无锁状态升级为偏向锁。

具体步骤如下:

线程检查当前对象是否无锁状态

  1. 是无锁状态

    1. 尝试用 CAS 操作,将自己的线程 ID写入对象头的 Mark Word,并设置为偏向锁

      1. 如果成功,表示偏向锁创建成功
      2. 如果失败,说明有其他线程也在尝试获取锁(出现了竞争),此时偏向锁会直接升级为轻量级锁
  2. 不是无锁状态

    1. 若对象已处于偏向锁状态

      1. 检查 Mark Word 中的 偏向线程 ID 是否是自己(线程 B)

        1. 如果是,说明是同一线程重入,直接进入同步块

        2. 如果不是,说明出现了 竞争(线程 B 想抢线程 A 的偏向锁)

        3. JVM 会检查线程 A 是否还在持有锁(通过栈信息判断)

          1. 如果发现 A 已释放,此时可能直接 撤销偏向锁(将 Mark Word 重置为无锁状态),让线程 B 通过 CAS 重新标记为偏向锁(即线程 B 成为新的偏向线程),不升级为轻量级锁
          2. 如果发现 A 还没释放,强制撤销偏向锁(暂停线程 A,将其栈中的锁状态更新),将锁升级为轻量级锁
          3. 线程 A 继续持有轻量级锁,线程 B 通过自旋 CAS 尝试竞争轻量级锁
    2. 若对象已处于轻量级锁状态

      1. 尝试用 CAS 操作,将自己栈帧中的 锁记录指针 写入对象的 Mark Word

        1. 若 CAS 成功:获取轻量级锁,进入同步块;
        2. 若 CAS 失败:说明竞争加剧(可能多个线程同时抢锁),若失败则自旋几次;
        3. 若自旋多次仍未获取锁,此时轻量级锁会升级为重量级锁,线程 B 进入EntryList队列阻塞等待
    3. 若对象已处于重量级锁状态

      1. 当前线程会直接进入 monitor 的EntryList队列,处于阻塞状态

synchronized与lock对比

两者对比如下:

对比维度synchronizedReentrantLock
可重入性支持(同一线程可重复获取锁)支持
公平性非公平,默认先尝试抢占,不按等待顺序可配置
是否可中断不可中断(获取锁时会一直阻塞)可中断
超时获取锁不支持支持
底层实现JVM 层面实现(依赖对象监视器)JDK 层面实现,基于 AQS 框架

synchronized 作为 Java 内置的同步机制,虽然简单易用,但是存在明显缺陷

无法实现非阻塞式获取锁

synchronized 获取锁时,若锁已被占用,当前线程会直接进入阻塞状态

而 Lock 提供 tryLock() 方法,允许线程 尝试获取锁:若获取成功则执行,失败则立即返回(可做其他处理,如重试、放弃)

不能中断等待锁的线程

synchronized 中,等待锁的线程会一直阻塞,无法被中断,即使调用 interrupt() 也无法唤醒,只能死等

而 Lock 提供 lockInterruptibly() 方法,允许等待锁的线程响应中断

锁的释放时机固定,无法灵活控制

synchronized 的锁释放是 隐式 的:只有当线程退出同步块(或同步方法)时,锁才会自动释放,无法在代码中间主动释放或提前释放

而 Lock 需要显式调用 unlock() 释放锁,可在任意位置释放(需配合 try-finally 确保释放),灵活性更高。

仅支持单一条件队列,无法实现多条件等待

synchronized 中,对象的等待队列(WaitSet)是唯一的:所有调用 wait() 的线程都在同一个队列中,唤醒时只能通过 notify() 随机唤醒一个线程,或 notifyAll() 唤醒所有线程

这样可能导致 惊群效应,多数线程被唤醒后发现条件不满足又继续等待,浪费资源。

而 Lock 可通过newCondition()方法 创建多个 条件队列,每个队列对应不同的等待条件,唤醒时可精准唤醒某一条件队列中的线程,避免惊群效应。

notify() 随机唤醒一个线程是什么意思?

synchronized的WaitSet本质是一个无序的等待队列(内部实现更接近 集合 而非 队列),其存储结构不保证线程的等待顺序

当调用notify()时,JVM 会从WaitSet中随机选择一个线程将其唤醒, 具体选择逻辑由JVM实现决定,可能与线程优先级、等待时间等无关

无法选择锁的公平性

synchronized 实现的是 非公平锁

新线程可能 插队获取刚释放的锁,导致等待时间长的线程一直得不到锁(饥饿问题),且无法配置为公平锁

原理就是随机从EntryList 获取一个线程

而 Lock(如 ReentrantLock)可通过构造函数 new ReentrantLock(true)

明确指定为 公平锁

线程获取锁的顺序严格按等待时间排序,避免饥饿

难以获取锁的状态信息

synchronized 没有提供任何方法查询锁的状态

如锁是否被持有、当前持有锁的线程是谁、等待队列的长度等

而 Lock 提供了 isLocked(是否被持有)、isHeldByCurrentThread(当前线程是否持有)、getQueueLength(等待队列长度)等方法,方便监控和调试

高竞争场景下性能可能较差

虽然 JDK 1.6 对 synchronized 做了优化(偏向锁、轻量级锁

但在高并发、激烈竞争场景下,synchronized 升级为重量级锁后,依赖操作系统的互斥量(mutex) 实现,上下文切换成本较高

而 Lock 的实现(如 ReentrantLock)基于AQS(抽象队列同步器)

通过 CAS 和自旋减少内核态操作,在高竞争下性能往往更优

但Lock 的自旋也可能消耗 CPU

内存语义关键字

volatile

volatile 是轻量级同步机制,核心作用是保证变量的可见性禁止指令重排序,但不保证原子性。

保障多线程并发程序正确性需要满足并发三要素,缺一不可,而volatile 不满足原子性。

那什么是并发三要素呢?

并发三要素

  • 原子性(Atomicity)

    • 指一个操作或一组操作要么 全部执行且中途不被任何线程中断,要么 全部不执行,不存在执行一半的中间状态
  • 可见性(Visibility)

    • 当一个线程修改了共享变量的值后,其他线程能 立即感知 到该变量的最新值,而非读取到自己工作内存中的旧值。
  • 有序性(Ordering)

    • 程序执行的顺序需符合代码的逻辑顺序,避免编译器、处理器为优化性能而进行的 指令重排序 破坏逻辑正确性。

底层实现

依赖 内存屏障(CPU 指令)

内存屏障按功能分 4 类(逻辑层面,不同 CPU 架构实现可能有差异):

  • LoadLoad:禁止屏障前的 读操作 与屏障后的 读操作 重排序(确保前面的读完成后,再执行后面的读);
  • StoreStore:禁止屏障前的 写操作 与屏障后的 写操作 重排序(确保前面的写完成后,再执行后面的写);
  • LoadStore:禁止屏障前的 读操作 与屏障后的 写操作 重排序(确保读完成后,再执行写);
  • StoreLoad:禁止屏障前的 写操作 与屏障后的 读操作 重排序(强制写操作刷新到主内存后,再执行读操作,保证可见性)。

volatile 写操作后必插 StoreLoad 屏障(保证可见性 + 禁止重排序),其他屏障按需插入。

final

final 在并发场景中主要保证初始化安全性,即 构造函数完成前,final 字段的值对其他线程可见。

初始化安全性

final 字段在构造函数中初始化后,JVM 会插入内存屏障,确保其他线程看到的 final 字段一定是初始化完成的值,不会出现 半初始化状态。

核心屏障是StoreStore,禁止屏障前后的 存储操作(写操作) 发生重排序,它是final字段初始化安全性的关键保障

引用类型的 final

final 修饰引用类型时,仅保证引用地址不变,但对象内部的字段可以修改

final 方法与重排序

final 方法不能被重写,但指令重排序规则与非 final 方法一致,核心差异是 不可重写 带来的稳定性

原子操作

CAS

CAS(Compare And Swap,比较并交换)是乐观锁的底层机制,核心思想是 无锁竞争,通过 CPU 指令保证原子性。

三个操作数

  • 内存地址 V(存储变量的位置);
  • 预期值 A(线程认为当前 V 的值);
  • 新值 B(线程想写入 V 的新值)。

CAS 的执行过程是原子性的(由 CPU 的cmpxchg等指令保证,不会被其他线程中断)

JVM 通过 Unsafe 类的 native 方法(如compareAndSwapInt)调用这些指令。

执行过程

  • 读取当前值:线程从内存地址 V 中读取变量的实际当前值,记为 实际值C

  • 比较预期与实际:将 预期值 A实际值C对比:

    • 若 A == C:说明变量未被其他线程修改,符合预期,执行更新, 将新值 B 写入内存地址 V;
    • 若 A != C:说明变量已被其他线程修改,不符合预期,不执行更新(线程通常会重试或放弃);
  • 返回结果:无论是否更新成功,返回操作前的实际值 C(供线程判断是否需要重试)。

存在的问题

当一个线程(线程 1)读取变量值为 A 后

另一个线程(线程 2)将变量改为 B,随后又改回 A

此时线程 1 的 CAS 操作会误认为 变量未被修改 而成功执行

但实际上变量经历了中间变化(A→B→A)。

这种 值相同但经历过修改 的情况,在某些场景下会导致逻辑错误

解决方案

ABA 问题的核心只是通过 值是否相同无法判断变量是否被修改过。

版本号 的本质是给变量的每一次修改增加一个 唯一标识

  • 每次变量被修改时,不仅更新值,还会递增版本号
  • CAS 操作时,不仅比较 变量当前值预期值,还要比较 当前版本号预期版本号
  • 只有值和版本号都匹配时,才认为变量未被修改过,允许更新;否则,即使值相同,只要版本号不匹配,就判定为 已被修改,CAS 失败

java的AtomicStampedReference类是专门为解决 ABA 问题设计的

它将 变量值版本号 绑定在一起,通过同时检查两者来保证 CAS 的正确性。

其 CAS 操作的核心方法是compareAndSet,需要传入 4 个参数,实现 双条件校验

public boolean compareAndSet(
    V expectedReference,  // 预期的变量值
    V newReference,       // 要更新的新变量值
    int expectedStamp,    // 预期的版本号
    int newStamp          // 要更新的新版本号
)

Unsafe

Unsafe 是 sun.misc 包下的工具类,提供直接操作内存、线程、CAS 等底层能力,但因绕过 JVM 安全检查,不推荐直接使用(可能导致 JVM 崩溃)

核心能力

  • CAS 操作:compareAndSwapInt、compareAndSwapLong等,是原子类的底层依赖;
  • 直接操作内存:allocateMemory(分配内存)、freeMemory(释放内存)
  • 线程控制:park(挂起线程)、unpark(唤醒线程),是 LockSupport的底层实现;
  • 绕过构造函数实例化:allocateInstance(Class),直接创建对象而不调用构造函数

原子类

Java 原子类(java.util.concurrent.atomic)基于 CAS 实现,保证变量操作的原子性

常用分类

  • 基本类型:AtomicInteger、AtomicLong(原子更新 int/long);
  • 引用类型:AtomicReference(原子更新对象引用);
  • 数组类型:AtomicIntegerArray(原子更新数组元素);
  • 字段更新器:AtomicIntegerFieldUpdater(原子更新对象的某个字段);
  • 带版本号:AtomicStampedReference(解决 ABA 问题)。

内存模型

定义

Java 内存模型(JMM)是一种抽象的内存模型

它没有直接对应物理硬件,而是通过规范多线程行为,程序中变量的读取和写入行为

JMM 是 逻辑约定,它不直接控制硬件,而是通过内存屏障、锁机制、volatile 关键字等,约束编译器和 CPU 的行为

使多线程操作在抽象模型上符合 主内存 - 工作内存 的交互规则,最终映射到物理内存时保证数据一致性

解决的问题

  • CPU 缓存:每个核心有独立缓存,变量的读写可能先操作缓存而非主内存,导致不同核心看到的变量值不同
  • 指令重排序:编译器或 CPU 为优化性能,会调整指令执行顺序可能破坏多线程的执行逻辑

JMM 通过定义:

  • 变量如何在主内存和线程工作内存之间交互
  • 哪些操作是原子的
  • 操作的执行顺序如何保证
  • ...

等等一系列以上的规则,解决了这些硬件差异导致的 可见性、原子性、有序性 问题

让开发者无需关注底层硬件细节,就能写出跨平台的线程安全代码。

三大核心问题

可见性

一个线程对变量的修改,其他线程能立即看到最新值

解决机制:

  • volatile:通过内存屏障强制刷新主内存,保证读取最新值
  • synchronized:解锁时将变量刷新到主内存,加锁时从主内存加载
  • final:变量初始化后不可修改,且初始化完成后对其他线程可见

原子性

一个操作(或一系列操作)要么全部执行,要么全部不执行,不可被中断

解决方案:

  • synchronized:同步块内的操作被视为原子操作(同一时间只有一个线程执行)
  • CAS 与原子类:通过 CPU 硬件指令保证的原子性;
  • Lock显式锁的lock()/unlock()之间的操作可保证原子性

有序性

程序执行的顺序符合代码的逻辑顺序,避免指令重排序导致的逻辑混乱

解决方案:

  • volatile:通过内存屏障禁止特定指令重排序;
  • synchronized:同步块内的指令不允许被重排序到块外;
  • happens-before规则:通过定义操作间的偏序关系,保证多线程下的执行顺序

什么是 happens-before 规则?常见的 happens-before 关系有哪些?接着往下看

happens-before 规则

定义

多线程环境中,如果操作 A happens-before 操作 B,那么 A 的执行结果对 B 是 可见的,并且 A 的逻辑顺序在 B 之前

不用关心底层的内存屏障CPU 指令重排序等细节,只通过规则就能判断多线程操作的 可见性。 说白了就是协商好的多个规则

符合这个规则的操作,前面的结果后面一定能看到;不符合规则的 ,结果可能混乱(线程不安全)

常见的 happens-before 规则

  • 程序顺序规则:单线程内,按代码顺序,前面的操作 happens-before后面的操作。
  • volatile 规则:对volatile变量的写操作happens-before 后续对该变量的读操作
  • 锁规则:对一个锁的解锁操作 happens-before 后续对该锁的加锁操作
  • 线程启动规则:Thread.start()操作 happens-before 线程内的所有操作。
  • 线程终止规则:线程内的所有操作 happens-before 其他线程检测到该线程终止(如Thread.join()返回、Thread.isAlive()为 false)。
  • 中断规则:线程 A 调用threadB.interrupt() happens-before 线程 B 检测到中断(如Thread.interrupted())。
  • 传递性规则:若 A happens-before B,且 B happens-before C,则 A happens-before C。
  • 对象终结规则:对象的构造函数执行完成 happens-before 其finalize()方法执行。

ThreadLocal

ThreadLocal 用于实现线程本地变量,即每个线程持有变量的独立副本,避免线程安全问题。

底层原理

ThreadLocal 的核心设计是 每个线程持有独立的变量副本

ThreadLocal 类

作为操作入口,提供 get()、set(T value)、remove() 等方法

还存在一个静态内部类:ThreadLocalMap

Thread

Thread有一个ThreadLocalMap类型的字段threadLocals

threadLocalskey是 ThreadLocal类型,value是线程的变量副本

线程访问ThreadLocal.get()时,实际是从自身的ThreadLocalMap中获取 value,不同线程的 Map 相互独立。

ThreadLocalMap

ThreadLocal 的静态内部类,是一个定制化的哈希表(类似 HashMap,但结构更简单),用于存储键值对

  • Key:当前 ThreadLocal 实例(特殊点:以弱引用形式存储);
  • Value:当前线程的变量副本(强引用)

什么是弱引用?强引用,软引用,虚引用又分别是什么?

  • 强引用(Strong Reference) :最常见的引用类型,平时默认创建的引用都是强引用,

    • 只要对象被强引用关联,GC 绝对不会回收该对象
    • 造成内存泄漏的常见原因
  • 软引用(Soft Reference): 强度仅次于强引用,SoftReference 类实现,

    • GC 在内存充足时不会回收它;内存不足时(即将 OOM 前),会主动回收这些对象
    • 使用场景:缓存(如图片缓存、数据缓存), 内存充足时保留缓存提高效率,内存不足时释放缓存避免 OOM
  • 弱引用(Weak Reference): 强度比软引用更弱,WeakReference 类实现

    • 只要发生 GC(无论内存是否充足),都会被回收

    • 使用场景

      • ThreadLocal 的底层(ThreadLocalMap 的 Key 是弱引用):避免 ThreadLocal 实例本身被长期引用导致泄漏
      • 临时缓存(仅需要对象在当前 GC 周期内存在,无需长期保留)
  • 虚引用(Phantom Reference) :最弱的引用类型,几乎不影响对象的生命周期,PhantomReference 类实现

    • 必须配合 ReferenceQueue 使用(否则无意义)
    • 无法通过引用获取对象实例,get() 方法永远返回 null
    • 使用场景:管理直接内存(如 NIO 的 DirectByteBuffer),实现对象销毁前的资源释放

内存泄漏风险

原因

ThreadLocalMap的 key 是ThreadLocal的弱引用,但 value 是强引用

若 key 被回收,value 会成为 孤儿, 无法访问但未释放,长期积累导致内存泄漏

为什么 ThreadLocal 的 Key 要设为弱引用?

假设ThreadLocalMap的 Key 是强引用(而非弱引用),会出现以下问题:

  • 当ThreadLocal实例的强引用被移除(比如局部变量,或者生命周期走完),那这个key 就永远清除不了
  • 导致 ThreadLocalMap一直有数据,而且是key数据越来越多,却永远无法被删除,所以会导致内存泄漏

key现在已经是弱引用,为什么仍然会出现内存泄漏的情况?

Value 的强引用未被清理而导致内存泄漏。

因为key 是弱引用,一来GC就被清除了,失去了key,这些value永远没办法被删除。。

如何避免 ThreadLocal 的内存泄漏?

手动调用remove()方法

总结

今天把并发关键字相关知识点学透了!

synchronized 的作用对象、底层原理和锁升级,还有它和 lock 的对比;

volatile 的内存屏障与禁止重排序、final 的并发语义和初始化安全;

CAS 的底层逻辑、Unsafe 类的门道、Java 原子类的实现与问题;

内存模型的定义与核心解决点,再到 ThreadLocal 的隔离原理和内存泄漏坑

下一场该练并发里的 相关工具啦!

  • AQS 的核心机制、数据结构
  • ReentrantLockReentrantReadWriteLock 的特性、实现原理及锁相关机制
  • 同步工具(CountDownLatch、CyclicBarrier、Semaphore)
  • 线程池核心原理

这些都是并发的核心武器,敬请期待~

熟练度刷不停,知识点吃透稳,下期接着练~