线程

线程和进程

进程:正在运行程序的实例

线程:进程中的执行单元,CPU调度的基本单位

并行和并发

并行:同一时间动手做多件事情的能力

并发:同一时间应对多件事情的能力

创建方式

  • 继承Thread类

  • 实现runable接口

    • run没有返回值

    • run抛出的异常只能在内部,不能向上抛

  • 实现callable接口

    • call会结合future和futuretask方法获取异步执行结果

    • 可以抛出异常

    • 返回结果需要调用FutureTask.get()方法,但是它会阻塞主进程的执行。

  • 线程池创建

线程启动

  • start() :启动线程,调用run()执行run()方法中定义的逻辑代码。只能被调用一次

  • run():封装要被执行的代码,可以多次调用

状态

  • NEW

  • RUNABLE

    • READING

    • RUNNING

  • TERMINATED

  • BLOCKED

  • WAITING

  • TIME_WAITING

顺序执行

  • 调用join()方法

  • 使用 wait() 和 notify()

唤醒

  • notify():只唤醒一个等待的线程

  • notifyAll():唤醒所有等待的线程

等待

wait()

  • Object 的成员方法

  • 可以被 notify 唤醒,不唤醒就一直等下去

  • 获取 wait 对象的锁,执行后会释放对象锁,允许其它线程获得该对象锁

sleep()

  • Thread 的静态方法

  • 等待相应毫秒后醒来,被 interrupt() 中断

  • synchronized 代码块中执行,并不会释放对象锁

停止

退出标志的,还可以通过stop方法强制执行,interrupt方法中断

线程池

核心参数

corePoolSize

保持存活的线程数量,即使它们处于空闲状态。

任务提交到线程池时,如果当前线程数小于 corePoolSize,线程池会创建新线程来执行任务

maximumPoolSize

线程池中允许的最大线程数量。

任务队列已满当前线程数小于 maximumPoolSize 时,线程池会创建新线程来执行任务

keepAliveTime

线程数超过核心线程数时,空闲线程的存活时间。

  • 线程池中的线程数量大于 corePoolSize,且线程空闲时间超过 keepAliveTime,则这些线程会被回收

  • 如果 allowCoreThreadTimeOut 设置为 true,则核心线程也会被回收。

unit

keepAliveTime 的时间单位

workQueue

用于存放等待执行的任务的队列

  • 线程数达到 corePoolSize 时,新任务会被放入任务队列

  • 任务队列已满且线程数未达到 maximumPoolSize,则会创建新线程执行任务。

threadFactory

用于创建新线程的工厂

handler

拒绝策略

  • AbortPolicy默认策略,直接抛出 RejectedExecutionException

  • CallerRunsPolicy:由提交任务的线程执行任务

  • DiscardPolicy直接丢弃任务

  • ​DiscardOldestPolicy丢弃队列中最旧的任务,然后重新提交新任务

阻塞队列

无界队列

LinkedBlockingQueue

  1. 基于链表的阻塞队列

  2. 默认容量为 Integer.MAX_VALUE,可以视为无界队列

  3. 支持 FIFO(先进先出)顺序

  4. 使用两把锁,一把用于控制操作,另一把用于控制操作

PriorityBlockingQueue

  1. 基于优先级的阻塞队列

  2. 默认容量为 Integer.MAX_VALUE,可以视为无界队列

  3. 任务必须实现 Comparable 接口,或者传入 Comparator 来定义优先级

有界队列

ArrayBlockingQueue

  1. 基于数组的阻塞队列

  2. 容量固定,创建时需要指定容量

  3. 支持 FIFO(先进先出)顺序

  4. 使用一把锁来控制对队列的访问,这意味着读写操作都是互斥的

同步队列

​SynchronousQueue

  1. 容量为 0,任务必须立即被线程执行

  2. 每个插入操作必须等待一个移除操作,反之亦然

延迟队列

DelayQueue

  1. 基于优先级的阻塞队列

  2. 任务必须实现 Delayed 接口,定义延迟时间

  3. 只有在延迟时间到达后才会被取出执行

核心线程数

  • 高并发,任务时间短:N+1

  • 并发不高,时间长

    • IO密集:2N+1

    • 计算密集:N+1

  • 并发高,时间长

种类

FixedThreadPool

核心线程数 = 最大线程数线程池的大小固定

同时它的任务队列为无界队列

​CachedThreadPool

核心线程数 = 0,最大线程数 = Integer.MAX_VALUE

任务队列为同步队列(SynchronousQueue),任务必须立即执行。同时空闲时间超过 60 秒会被回收。

​SingleThreadExecutor

核心线程数 = 最大线程数 = 1,线程池中只有一个线程。

同时它的任务队列为无界队列,但是需要顺序执行。

ScheduledThreadPool

核心线程数固定,最大线程数 = Integer.MAX_VALUE

任务队列为延迟队列,支持定时任务和周期性任务。

Executors 缺点

使用了 ​默认配置,无法灵活调整参数(如核心线程数、最大线程数、任务队列、拒绝策略等)

CachedThreadPool的 ​最大线程数 设置为 Integer.MAX_VALUE。如果任务提交速度过快,线程池会不断创建新线程,最终导致线程数过多,耗尽系统资源

FixedThreadPoolSingleThreadExecutor使用 ​无界队列​LinkedBlockingQueue作为任务队列,最终导致 ​内存溢出(OOM)

缺乏拒绝策略控制,默认使用 ​AbortPolicy 作为拒绝策略

并发

synchronized关键字

底层原理

对象头

  • Mark Word:存储对象的哈希码、锁状态等信息。

  • Klass Pointer:指向对象的类元数据

monitor

  • Owner :当前持有锁的线程。

  • EntryList等待锁的线程队列。

  • WaitSet调用 wait() 后进入等待状态的线程队列。

锁升级

  • 无锁:初始状态,没有线程竞争

  • 偏向锁:只有一个线程访问同步代码块,通过在 Mark Word 记录线程 ID,以后该线程进入同步代码块时,无需加锁

  • 轻量级锁:多个线程竞争锁,但竞争不激烈。通过 CAS 操作来尝试获取锁

  • 重量级锁:竞争激烈,JVM 会将锁升级为重量级锁

对比lock

对比维度

🔒 synchronized 关键字

🔑 Lock 接口 (如 ReentrantLock)

🔧 实现方式

Java 语言内置关键字

Java 类库实现的接口

⏱️ 锁获取

自动获取和释放

需手动调用 lock()/unlock()

🔓 锁释放

自动释放 (代码块结束/异常)

需在 finally 中手动释放

🚫 可中断性

❌ 不可中断

✅ 可中断 (lockInterruptibly())

⚖️ 公平性

只有非公平锁

配置公平/非公平锁

⏳ 尝试获取

❌ 不支持

tryLock() 支持

⌛ 超时机制

❌ 不支持

tryLock(time,unit) 支持

📊 条件变量

单一条件 (wait()/notify())

多条件 (newCondition())

🚀 性能

JDK 1.6+ 优化后接近

高竞争时更优

📝 代码风格

简洁

需要更多模板代码

🔍 锁状态

❌ 不可查询

✅ 可查询锁状态

CAS

CAS(Compare-And-Swap,比较并交换)是一种 无锁(Lock-Free) 的并发编程技术,用于实现线程安全的变量更新。它是现代多线程编程的核心机制之一,广泛应用于 Java 的 Atomic 类(如 AtomicIntegerAtomicReference)、ConcurrentHashMap 等并发工具中。

CAS 的核心原理

CAS 操作包含 3 个参数

  • V(内存值):当前变量在内存中的值

  • E(期望值):线程认为变量当前应该的值

  • N(新值):要更新的目标值

CAS 执行流程

  1. 检查当前内存值 V 是否等于 E(期望值)。

  2. 如果相等,说明没有其他线程修改过,更新为 N

  3. 如果不相等,说明已被其他线程修改,放弃更新(或重试)。

CAS 的特点

✅ 优点

  • 无锁(Lock-Free):不需要 synchronizedLock,减少线程阻塞,提高并发性能。

  • 原子性:由 CPU 指令(如 cmpxchg)保证,不会被线程调度打断。

  • 轻量级:相比锁机制,CAS 开销更小。

❌ 缺点

  • ABA 问题:变量可能被其他线程从 A → B → A,CAS 无法感知中间变化(可用 AtomicStampedReference 解决)。

  • 自旋开销:如果竞争激烈,CAS 可能长时间重试,消耗 CPU 资源。

  • 只能保证单个变量的原子性,无法用于复合操作(如 i++ 需要 AtomicInteger 封装)。

CAS 底层实现(CPU 指令)

CAS 依赖 硬件指令(如 x86 的 CMPXCHG)实现原子操作:

  • Unsafe:Java 通过 sun.misc.Unsafe 提供 CAS 操作(但一般不推荐直接使用)。

  • JVM 优化:JIT 编译器会将 CAS 操作转换为最优的 CPU 指令。

CAS vs 锁(synchronized / Lock

对比项

CAS

锁(synchronized / Lock)

实现方式

无锁,CPU 指令支持

基于 JVM 或 AQS 实现

线程阻塞

不阻塞(自旋)

可能阻塞(进入等待队列)

适用场景

低竞争环境

高竞争环境

ABA 问题

存在(需额外处理)

复合操作

不支持(需封装)

支持(临界区代码)

volatile:

  • 保证了不同线程对这个变量进行操作时的可见性

  • 添加了一个内存屏障,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。禁止进行指令重排序,可以保证代码执行有序性

AQS

Java 并发包里的核心抽象类,用来构建锁和其他同步器,比如锁啊、信号量啊之类的东西,很多高级并发工具(像 ReentrantLockSemaphoreCountDownLatch)底层都是靠它实现的!🤓

🌟 AQS 的核心原理

AQS 主要靠 state 变量 + FIFO 队列 来管理同步状态,核心结构包括:

  • 同步状态(state:用一个 volatile int state 来表示资源的占用情况。

  • 等待队列(CLH 队列):线程抢不到资源时,就会排队等待,类似去银行取号排队 😆。

  • 独占模式 & 共享模式

    • 独占模式(Exclusive):一个线程独占,比如 ReentrantLock 🔒。

    • 共享模式(Shared):多个线程可以共享,比如 Semaphore 🚦 和 CountDownLatch ⏳。

⚙️ AQS 的主要方法

AQS 是个 框架,不是直接拿来用的,需要子类去继承它,然后重写下面这些方法:

方法

作用

tryAcquire(int arg)

试图获取独占锁,成功返回 true,失败就进队列

tryRelease(int arg)

释放锁,并唤醒下一个线程

tryAcquireShared(int arg)

试图获取共享锁,返回值 >0 代表成功

tryReleaseShared(int arg)

释放共享锁,让下一个线程继续

它还提供了 acquire()release() 这些封装好的方法,自动帮我们处理 CAS 竞争、排队、等待、唤醒 这些烦人的事情 🤯。

🔄 AQS 的工作流程

以独占模式为例:

  1. 线程尝试获取锁

    • 直接 tryAcquire() 看看能不能拿到锁。

    • 成功了就开心用资源 🎉,失败了就进等待队列 😢。

  2. 加入等待队列

    • 队列是 FIFO 的,先进先出,公平排队 🏃‍♂️🏃‍♀️。

  3. 被唤醒后重试获取锁

    • 轮到你了就再 tryAcquire() 一次。

  4. 释放锁

    • release() 之后会通知下一个线程:“你可以来啦~” 🎤🎶。

🚀 AQS 在实际中的应用

AQS 是 Java 并发的“基石”,很多大佬级的类都用它实现:

1️⃣ ReentrantLock(可重入锁)

  • 独占模式实现。

  • tryAcquire() 用 CAS 修改 state,抢占资源。

  • tryRelease() 释放锁,唤醒等待线程。

2️⃣ ReentrantReadWriteLock(读写锁)

  • 写锁:独占模式,只有一个线程能写。

  • 读锁:共享模式,多个线程可以同时读 📖。

3️⃣ CountDownLatch(倒计时器)

  • 共享模式实现,countDown() 每次 -1,直到 state == 0,所有等待线程一起释放 🚀。

4️⃣ Semaphore(信号量)

  • 共享模式,实现限流。

  • tryAcquireShared() 只有 state > 0 才能获取许可证 🎫。

ConcurrentHashMap

Java 里的并发版 HashMap,用来在多线程环境下安全、高效地存取数据💨。相比于 HashMap(线程不安全)和 Hashtable(效率低下),ConcurrentHashMap 既保证了线程安全,又优化了性能,在高并发场景下表现非常优秀!💪✨

🚀 ConcurrentHashMap 的核心特点

✅ 线程安全

不像 HashMap 在并发下会发生死循环数据丢失等问题,ConcurrentHashMap 通过分段锁(JDK 1.7)或 CAS + 自旋(JDK 1.8)保证线程安全,不需要 synchronized 的全局锁 🛡️。

⚡ 高性能

  • JDK 1.7:采用分段锁(Segment),支持多个线程同时修改不同的分段,提升并发性能📊。

  • JDK 1.8:优化了锁机制,用 CAS + 自旋锁 + 链表/红黑树,进一步提高并发能力🏎️💨。

📈 支持高并发读

ConcurrentHashMap 允许多个线程同时读取数据,所以读取性能非常高 🚀。

🔧 JDK 1.7 vs JDK 1.8 版本

ConcurrentHashMap 在 JDK 1.8 做了重大优化,彻底移除了 Segment 分段锁,实现方式完全不同:

版本

方式

说明

JDK 1.7

Segment + ReentrantLock

数据分成多个 Segment,每个 Segment 有独立的 ReentrantLock

JDK 1.8

CAS + 自旋 + 链表/红黑树

取消 Segment,用 Node + CAS + 自旋锁 替代,碰撞链过长时转换为红黑树

👉 总结:JDK 1.8 的 ConcurrentHashMap 更轻量,吞吐量更高! 🚀

⚙️ ConcurrentHashMap 的核心机制

1️⃣ 数据结构

在 JDK 1.8 里,ConcurrentHashMap 采用了一个数组 + 链表/红黑树的结构:

java
  • hash:存放 key 的哈希值。

  • key & val:存储键值对。

  • next:用于链接下一个节点(链表结构)。

  • 当链表长度 超过 8,会转换成红黑树🌳,提高查询效率!

2️⃣ put 操作(写入)

插入数据时:

  1. 先计算 key 的 hash 值,找到对应的桶(数组索引)

  2. 如果桶为空,直接 CAS 插入(无锁操作)。

  3. 如果桶不为空,进入自旋锁 + CAS 机制

    • 若无冲突,CAS 方式插入数据。

    • 若 key 存在,直接覆盖。

    • 若 hash 冲突(哈希碰撞),用链表/红黑树处理。

3️⃣ get 操作(读取)

get() 操作是无锁的,只需计算哈希值,然后顺序查找链表或红黑树即可,速度非常快!🚀

4️⃣ 扩容机制

ConcurrentHashMap 在负载因子达到 0.75 时,会自动扩容(2 倍),但扩容是渐进式进行的:

  • 采用 分批次迁移,避免像 HashMap 那样扩容时引发性能抖动 🌊。

🧐 ConcurrentHashMap 和其他 Map 对比

Map 类型

线程安全

读性能

写性能

适用场景

HashMap

❌ 否

🚀 高

🐢 低(同步时)

单线程环境

Hashtable

✅ 是(全局锁)

🐌 低

🐌 低

过时,尽量不要用

ConcurrentHashMap

✅ 是(局部锁 + CAS)

🚀 高

⚡ 高

高并发场景

死锁

多个线程互相等待对方释放资源,结果谁都释放不了,程序就这样卡死了

🔥 死锁的四个必要条件(产生死锁的原因)

要发生死锁,必须满足下面四个条件(一个都不能少):

1️⃣ 互斥(Mutual Exclusion)

  • 资源一次只能被一个线程使用,别人想用?抱歉,得等着 ⏳。
    2️⃣ 占有且等待(Hold and Wait)

  • 线程 A 拿着资源 1,同时等着资源 2,而资源 2 被线程 B 占着,线程 B 也在等资源 1… 🤯。
    3️⃣ 不可剥夺(No Preemption)

  • 资源不能被强行抢走,只能等持有者自己释放 🔒。
    4️⃣ 循环等待(Circular Wait)

  • 存在 A → B → C → A 这样的等待循环,谁都不肯放手 🤷。

💡 如果破坏掉其中任何一个条件,就能避免死锁!

🛠 如何避免死锁?

1. 避免循环等待(破坏第 4 条)

规定获取锁的顺序,所有线程都按照相同顺序获取锁,这样就不会形成环状等待。🚀 线程获取锁的顺序一致,就不会互相等待啦!

2. 使用 tryLock()(破坏第 3 条)

ReentrantLock.tryLock() 代替 synchronized,如果拿不到锁,就不等待,直接跳过。💡 这样线程不会一直傻等着,而是尝试获取锁,失败就放弃,避免死锁!

3. 设置超时时间

配合 tryLock(),给锁加一个超时时间,超过时间就放弃,避免死锁。

4. 使用 Lock 代替 synchronized

Locksynchronized 更灵活,支持可中断锁、超时锁、非阻塞获取锁,推荐在高并发环境使用!

ThreadLocal

在 Java 并发编程中,多线程操作共享变量时,经常需要用锁来保证线程安全(比如 synchronizedLock 等)。但如果我们想让每个线程有自己独立的变量副本,互不影响,该咋办?这时候,ThreadLocal 就是你的好朋友啦!

什么是 ThreadLocal

ThreadLocal 是 Java 提供的一种线程本地存储机制,它能为每个线程提供独立的变量副本,线程之间互不干扰。

可以理解成线程的私有小口袋

  • 每个线程 都有自己的 ThreadLocal 变量副本。

  • 不同线程 之间的 ThreadLocal 变量是完全独立的

  • 线程结束后,ThreadLocal 变量会被回收,避免内存泄漏。

💡 适用于哪些场景?

  • 数据库连接(Connection):每个线程独享一个 Connection,防止多个线程竞争同一个连接。

  • 用户 Session 信息:不同线程存储不同的用户身份信息。

  • 线程安全的对象共享:避免使用 synchronized,提升并发性能。

🛠 ThreadLocal 的常用方法

方法

作用

set(T value)

设置当前线程的 ThreadLocal 变量值

get()

获取当前线程的 ThreadLocal 变量值

remove()

清除当前线程的 ThreadLocal 变量,防止内存泄漏

initialValue()

初始化默认值(可用 withInitial() 方法替代)


🚨 ThreadLocal 的坑!💣

❌ 1. ThreadLocal 内存泄漏

ThreadLocal 变量存放在 Thread 类的 ThreadLocalMap,如果不手动清除,可能会造成内存泄漏,特别是在使用线程池时

🛠 解决方案:一定要 remove()

try {
    threadLocalVar.set("一些数据");
    // 业务逻辑
} finally {
    threadLocalVar.remove(); // 防止内存泄漏
}

❌ 2. ThreadLocal 不能用于跨线程共享

ThreadLocal 是线程私有的,不能用于跨线程共享数据!如果你想多个线程共享同一个变量,ThreadLocal 不是正确的选择!🚫

如果要跨线程共享数据,可以用 InheritableThreadLocal

private static final InheritableThreadLocal<String> threadLocalVar = new InheritableThreadLocal<>();

它允许子线程继承父线程的 ThreadLocal 变量。