线程
线程和进程
进程:正在运行程序的实例
线程:进程中的执行单元,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
:默认策略,直接抛出 RejectedExecutionExceptionCallerRunsPolicy
:由提交任务的线程执行任务DiscardPolicy
:直接丢弃任务DiscardOldestPolicy
:丢弃队列中最旧的任务,然后重新提交新任务
阻塞队列
无界队列
LinkedBlockingQueue
基于链表的阻塞队列
默认容量为 Integer.MAX_VALUE,可以视为无界队列
支持 FIFO(先进先出)顺序
使用两把锁,一把用于控制读操作,另一把用于控制写操作
PriorityBlockingQueue
基于优先级的阻塞队列
默认容量为 Integer.MAX_VALUE,可以视为无界队列
任务必须实现 Comparable 接口,或者传入 Comparator 来定义优先级
有界队列
ArrayBlockingQueue
:
基于数组的阻塞队列
容量固定,创建时需要指定容量
支持 FIFO(先进先出)顺序
使用一把锁来控制对队列的访问,这意味着读写操作都是互斥的
同步队列
SynchronousQueue
:
容量为 0,任务必须立即被线程执行
每个插入操作必须等待一个移除操作,反之亦然
延迟队列
DelayQueue
基于优先级的阻塞队列
任务必须实现 Delayed 接口,定义延迟时间
只有在延迟时间到达后才会被取出执行
核心线程数
高并发,任务时间短: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
。如果任务提交速度过快,线程池会不断创建新线程,最终导致线程数过多,耗尽系统资源
FixedThreadPool
和SingleThreadExecutor
使用 无界队列LinkedBlockingQueue
作为任务队列,最终导致 内存溢出(OOM)
缺乏拒绝策略控制,默认使用 AbortPolicy
作为拒绝策略
并发
synchronized
关键字
底层原理
对象头
Mark Word
:存储对象的哈希码、锁状态等信息。Klass Pointer
:指向对象的类元数据。
monitor
Owner
:当前持有锁的线程。EntryList
:等待锁的线程队列。WaitSet
:调用wait()
后进入等待状态的线程队列。
锁升级
无锁:初始状态,没有线程竞争锁
偏向锁:只有一个线程访问同步代码块,通过在
Mark Word
中记录线程 ID,以后该线程进入同步代码块时,无需加锁轻量级锁:多个线程竞争锁,但竞争不激烈。通过 CAS 操作来尝试获取锁
重量级锁:竞争激烈,JVM 会将锁升级为重量级锁
对比lock
CAS
CAS(Compare-And-Swap,比较并交换)是一种 无锁(Lock-Free) 的并发编程技术,用于实现线程安全的变量更新。它是现代多线程编程的核心机制之一,广泛应用于 Java 的 Atomic
类(如 AtomicInteger
、AtomicReference
)、ConcurrentHashMap
等并发工具中。
CAS 的核心原理
CAS 操作包含 3 个参数:
V(内存值):当前变量在内存中的值
E(期望值):线程认为变量当前应该的值
N(新值):要更新的目标值
CAS 执行流程:
检查当前内存值
V
是否等于E
(期望值)。如果相等,说明没有其他线程修改过,更新为
N
。如果不相等,说明已被其他线程修改,放弃更新(或重试)。
CAS 的特点
✅ 优点
无锁(Lock-Free):不需要
synchronized
或Lock
,减少线程阻塞,提高并发性能。原子性:由 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
)
volatile:
保证了不同线程对这个变量进行操作时的可见性
添加了一个内存屏障,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。禁止进行指令重排序,可以保证代码执行有序性
AQS
Java 并发包里的核心抽象类,用来构建锁和其他同步器,比如锁啊、信号量啊之类的东西,很多高级并发工具(像 ReentrantLock
、Semaphore
、CountDownLatch
)底层都是靠它实现的!🤓
🌟 AQS 的核心原理
AQS 主要靠 state
变量 + FIFO 队列 来管理同步状态,核心结构包括:
同步状态(
state
):用一个volatile int state
来表示资源的占用情况。等待队列(CLH 队列):线程抢不到资源时,就会排队等待,类似去银行取号排队 😆。
独占模式 & 共享模式:
独占模式(Exclusive):一个线程独占,比如
ReentrantLock
🔒。共享模式(Shared):多个线程可以共享,比如
Semaphore
🚦 和CountDownLatch
⏳。
⚙️ AQS 的主要方法
AQS 是个 框架,不是直接拿来用的,需要子类去继承它,然后重写下面这些方法:
它还提供了 acquire()
和 release()
这些封装好的方法,自动帮我们处理 CAS 竞争、排队、等待、唤醒 这些烦人的事情 🤯。
🔄 AQS 的工作流程
以独占模式为例:
线程尝试获取锁:
直接
tryAcquire()
看看能不能拿到锁。成功了就开心用资源 🎉,失败了就进等待队列 😢。
加入等待队列:
队列是 FIFO 的,先进先出,公平排队 🏃♂️🏃♀️。
被唤醒后重试获取锁:
轮到你了就再
tryAcquire()
一次。
释放锁:
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.8 的 ConcurrentHashMap
更轻量,吞吐量更高! 🚀
⚙️ ConcurrentHashMap 的核心机制
1️⃣ 数据结构
在 JDK 1.8 里,ConcurrentHashMap
采用了一个数组 + 链表/红黑树的结构:
java
hash
:存放 key 的哈希值。key
&val
:存储键值对。next
:用于链接下一个节点(链表结构)。当链表长度 超过 8,会转换成红黑树🌳,提高查询效率!
2️⃣ put 操作(写入)
插入数据时:
先计算 key 的 hash 值,找到对应的桶(数组索引)。
如果桶为空,直接 CAS 插入(无锁操作)。
如果桶不为空,进入自旋锁 + CAS 机制:
若无冲突,CAS 方式插入数据。
若 key 存在,直接覆盖。
若 hash 冲突(哈希碰撞),用链表/红黑树处理。
3️⃣ get 操作(读取)
get()
操作是无锁的,只需计算哈希值,然后顺序查找链表或红黑树即可,速度非常快!🚀
4️⃣ 扩容机制
ConcurrentHashMap
在负载因子达到 0.75 时,会自动扩容(2 倍),但扩容是渐进式进行的:
采用 分批次迁移,避免像
HashMap
那样扩容时引发性能抖动 🌊。
🧐 ConcurrentHashMap 和其他 Map 对比
死锁
多个线程互相等待对方释放资源,结果谁都释放不了,程序就这样卡死了!
🔥 死锁的四个必要条件(产生死锁的原因)
要发生死锁,必须满足下面四个条件(一个都不能少):
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
Lock
比 synchronized
更灵活,支持可中断锁、超时锁、非阻塞获取锁,推荐在高并发环境使用!
ThreadLocal
在 Java 并发编程中,多线程操作共享变量时,经常需要用锁来保证线程安全(比如 synchronized
、Lock
等)。但如果我们想让每个线程有自己独立的变量副本,互不影响,该咋办?这时候,ThreadLocal
就是你的好朋友啦!
什么是 ThreadLocal
?
ThreadLocal
是 Java 提供的一种线程本地存储机制,它能为每个线程提供独立的变量副本,线程之间互不干扰。
可以理解成线程的私有小口袋:
每个线程 都有自己的
ThreadLocal
变量副本。不同线程 之间的
ThreadLocal
变量是完全独立的。线程结束后,
ThreadLocal
变量会被回收,避免内存泄漏。
💡 适用于哪些场景?
数据库连接(Connection):每个线程独享一个
Connection
,防止多个线程竞争同一个连接。用户 Session 信息:不同线程存储不同的用户身份信息。
线程安全的对象共享:避免使用
synchronized
,提升并发性能。
🛠 ThreadLocal
的常用方法
🚨 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
变量。
评论