JUC编程
并发编程三大特性
原子性
原子性的定义:原子性是指一个操作(多条指令)是不可分割的。 在一个线程在执行某一段指令时,其他的线程如果也想执行,需要等待前一个线程执行完毕后才能执行。
例如 i++,在代码中只有一行代码,看着是只有一个操作,但是对有CPU来说这是三个操作,1. 将i取出放入cpu,2.对i进行加1操作 3.将结果返回
原子性可以解决线程安全问题。在多个线程在同时对一个共享资源(共享变量)进行操作时,出现的问题。
1 | |
可见性
在多线程环境下,每个线程可能会将变量缓存到自己的工作内存(即 CPU 缓存)中,而不是直接从主内存读取和写入。
线程 A 修改了变量 x,但线程 B 仍然看到旧值,因为它从自己的工作内存中读到了旧的副本,而不是主内存中的最新值。
实现可见性的三种方式:
1.volatile关键字
volatile关键字是专门用来实现可见性的,不加volatile则永远不会输出 t1线程结束!
1 | |
- synchronized 重量级锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(flag){
// 这里的println操作中,涉及到了synchronized操作,间接实现了可见性
System.out.println(1);
}
System.out.println("t1线程结束!");
});
t1.start();
Thread.sleep(100);
flag = false;
System.out.println("main线程将flag改为false");
} - lock (lock的底层是volatile)
有序性
有序性是为了防止多线程中的指令重排序造成的不一样的结果
什么是指令重排序
- 指令重排序是指编译器或处理器为了提升性能,在不改变单线程语义的前提下,重新调整指令的执行顺序。
1 | |
即使你写的顺序是 a→b→x,编译器或 CPU 可能会先执行 b = 2,然后再执行 a = 1,只要单线程运行的结果是正确的,这种调整就是允许的。
- 指令重排序的三种类型:
| 类型 | 描述 | |
|---|---|---|
| 编译器重排序 | 编译器在生成字节码时进行的重排序 | |
| JVM 重排序 | JVM 将字节码翻译为机器码时进行的重排序 | |
| CPU 重排序 | CPU 在执行时进行的乱序执行和写缓冲等优化 |
- 指令重排序的危害
在单线程中,重排序是无害的。但在多线程环境下,如果没有适当的同步机制,指令重排序会导致难以察觉的并发错误。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//双重检查锁(DCL)中的重排序问题
class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
//第二次判断是为了防止以下情况:A线程拿到锁了,并且new了对象,而B线程此时正好在 synchronized 获取锁这一行代码,A释放锁后,B会获取到锁,如果不加第二次判断,B线程也会new一个对象
if (instance == null) {
instance = new Singleton(); // 这行会被重排序!
}
}
}
return instance;
}
}
instance = new Singleton() 在JVM和CPU中分为以下三步
- 分配内存
- 将引用赋值给 instance
- 调用构造函数初始化对象
这三个操作是可能出现指令重排序的情况,可能就会造成,test != null,但是还没用执行第二步的初始化属性,导致其他线程拿着一个还未初始化完成的,或者说一个半成品对象去操作,这会带来一些线程安全的问题。
例如:
1 | |
new一个新线程的方法
1. 继承 Thread 类
1 | |
2.实现 Runnable 接口
需要重写run方法
1 | |
- 用匿名内部类简化
1
2
3
4
5
6
7
8
9
10
11public class RunnableDemo {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程运行中:" + Thread.currentThread().getName());
}
});
thread.start();
}
}3. 使用 Callable 接口 + FutureTask
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "线程返回结果:" + Thread.currentThread().getName();
}
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread t1 = new Thread(futureTask);
t1.start();
// 获取线程执行结果
String result = futureTask.get();
System.out.println("线程执行结果:" + result);
}
}线程中断
线程中断(Thread Interruption)是 Java 提供的一种优雅终止线程的机制,它并不会强制终止线程,而是向线程发送一个“中断请求”信号,由线程自己决定何时响应、如何响应。
核心方法:
| 方法 | 作用 |
|---|---|
interrupt() | 向目标线程发出中断请求 |
isInterrupted() | 判断线程是否被中断(不清除中断状态) |
interrupted()(静态方法) | 判断当前线程是否中断,同时清除中断状态 |
1 | |
这个例子展示了一个线程中断的情况,当主线程执行了 worker.interrupt(),就会将 Thread.currentThread().isInterrupted() 设置为true,这样可以进行判断手动停止线程,如果该线程没有sleep,wait等阻塞的话,进行线程中断的话,不会有任何影响,直到该线程执行到代码 Thread.currentThread().isInterrupted()的时候会为true,可手动停止代码,如果线程处于sleep,wait的阻塞状态会立刻抛出异常
锁的分类
悲观锁&乐观锁
Java中的悲观锁,比如synchronized,ReentrantLock,ReentrantReadWriteLock。
Java中的乐观锁,采用的CAS操作,CompareAndSwap(比较和交换),CAS是基于CPU原语实现的。
悲观锁: 悲观锁在获取不到锁资源后,会将当前线程挂起(BLOCKED,WAITING,TIMED_WAITING),线程挂起这个事情,不是JVM层面能解决的问题。需要操作系统来完成这个事情。那就需要涉及到用户态和内核态之间的切换,这种切换,会影响一定效率。
乐观锁: 乐观锁不涉及线程挂起,不涉及用户态和内核态之间的切换。如果长时间执行乐观锁的机制,但是一直无法成功,会浪费一些CPU性能,都在做尝试,但是一直没成功。
如果可以,最好是在尝试几次就能成功的场景,使用乐观锁实现。如果这个锁资源获取需要一定的时间,最好使用悲观锁。
公平锁&&非公平锁
公平锁:用于在多线程环境中控制资源的访问顺序。它的核心目的是保证线程获取锁的顺序是公平的,也就是说,先请求锁的线程应当先获得锁,避免线程“饿死”。
非公平锁:是一种允许线程在任何时刻尝试获取锁的机制,不保证先请求的线程一定先获得锁。这可能会导致某些线程“插队”,从而提高整体性能
在Java中,synchronized只支持非公平锁。
ReentrantLock和ReentrantReadWriteLock既支持公平锁也支持非公平锁,可以通过有参构造传递boolean的值类决定是否公平(默认非公平锁)。
1 | |
可重入锁&&不可重入锁
- 可重入锁:同一个线程在外层方法获取锁之后,能在内层方法中自动获得锁的代码执行权。**不会因为再次获取锁而被阻塞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public class ReentrantExample {
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
System.out.println("outer");
inner(); // 同一个线程再次加锁
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock(); // 如果是不可重入锁,这里会死锁
try {
System.out.println("inner");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
new ReentrantExample().outer();
}
}可重入锁中会有一个计数器,记录了获取锁的次数,获取几次锁就需要释放几次锁 Java 中的 synchronized 和 ReentrantLock 都是可重入锁
- 不可重入锁:同一个线程如果尝试再次获取自己已经持有的锁,会被阻塞或死锁。
1
2
3
4
5
6
7
8
9
10
11
12Lock lock = new NonReentrantLock();
void outer() {
lock.lock();
inner(); // 会死锁
lock.unlock();
}
void inner() {
lock.lock(); // 死锁,因为当前线程已经持有锁
lock.unlock();
}
| 特性 | 可重入锁 (synchronized, ReentrantLock) | 不可重入锁(需要自己实现) |
|---|---|---|
| 重入能力 | 支持 | 不支持 |
| 死锁风险 | 低(递归调用安全) | 高(递归调用可能死锁) |
| 实现复杂度 | 较高,需维护线程状态和计数器 | 简单 |
| 典型应用 | synchronized、ReentrantLock | 自定义锁、嵌入式系统 |
互斥锁&共享锁
互斥锁:同一时间只允许一个线程/事务持有该锁并访问资源,其他线程必须等待锁释放才能继续。也称写锁
特点:也叫 写锁(Write Lock)
独占访问:一个线程获取后,其它线程必须阻塞
适用于修改资源的场景,确保数据一致性
1
2
3
4
5
6
7
8ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 修改资源(独占访问)
} finally {
lock.unlock();
}1
2SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
//FOR UPDATE 是一种 互斥锁(排它锁),阻止其他事务修改或读取该记录(视隔离级别而定)。
共享锁:允许多个线程/事务同时持有锁来读取资源,但不允许写入。
特点:也叫 读锁(Read Lock)
多个线程可并发读取,但不能写入
旦某线程申请了写锁,读写都需等待写锁释放
1 | |
1 | |
| 特性 | 互斥锁(写锁) | 共享锁(读锁) |
|---|---|---|
| 是否独占 | ✅ 是 | ❌ 否(可共享) |
| 读操作 | ❌ 阻塞其他线程读取 | ✅ 允许多个线程读取 |
| 写操作 | ✅ 允许写 | ❌ 禁止写 |
| 典型用途 | 数据写入、修改 | 数据读取 |
| 性能 | 较低(锁粒度粗) | 较高(读多写少场景) |
CAS
CAS 是一种无锁(lock-free)并发编程技术,用于实现线程安全的操作。它是 CPU 层面支持的原子操作指令,Java 在底层通过调用它来实现原子变量更新(如 AtomicInteger、AtomicReference 等)。
- 为什么称CAS是一种无锁的并发编程假设我们有一个变量 V,预期值为 E,新值为 N,那么:
CAS 更像是一种编程思想,核心思想是:
当且仅当预期值与内存中的当前值一致时,才更新为新值。否则什么都不做。
这是一种乐观锁策略:认为并发很少发生,所以先尝试修改,失败了再重试。如果变量当前值和期望值相等,就将其更新为新值;1
2
3
4
5
6if (V == E) {
V = N;
return true;
} else {
return false;
}
否则,表示有其他线程已经修改了它,更新失败。
- CAS的优点
| 优点 | 描述 |
|---|---|
| ✅ 无锁机制 | 避免传统锁带来的上下文切换开销,提高并发性能 |
| ✅ 原子性强 | 硬件保证,天然线程安全 |
| ✅ 效率高 | 没有加锁阻塞,线程不会被挂起 |
- CAS的缺点
❌ 1. ABA 问题
A 线程看到变量从 A → B → A,CAS 会认为没有变化,但实际上已经变化过;
解决办法:使用 AtomicStampedReference 或 AtomicMarkableReference,引入版本号或标记位。
❌ 2. 自旋消耗 CPU(会进行while操作)
在高并发冲突严重时,线程可能会不断地自旋重试,浪费 CPU。
❌ 3. 只能操作一个变量
不能实现多个变量的原子更新;
解决办法:使用加锁或 AtomicReferenceFieldUpdater 等更复杂的机制。
- Java中提供了一个类,Unsafe类,Unsafe中提供了CAS的操作方法
synchronized
synchronized的锁是基于对象实现的。在Java中,Object类中提供了wait和notify之类的,做一个锁的操作。而Java中所有的类都是继承Object的,所以所有对象都可以作为一把锁。
synchronized的使用
- 对象锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class LockTest {
public static int count = 0;
/**
* 这个synchronized方法是基于LockTest的对象,作为一把锁
*/
public synchronized void increment(){
LockTest.count++;
}
/**
* 此时调用test.increment()方法时,底层使用的就是test对象作为锁。
* @param args
*/
public static void main(String[] args) {
LockTest test = new LockTest();
test.increment();
}
} 类锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class LockTest {
public static int count = 0;
/**
* 这个synchronized的static方法是基于LockTest.class,作为一把锁
*/
public static synchronized void increment(){
LockTest.count++;
}
/**
* 此时这种方式,就是基于LockTest.class作为锁,全局就一把
* @param args
*/
public static void main(String[] args) {
LockTest.increment();
LockTest.increment();
}
}static 强调的是“附属于类”,适合设计那些无需实例就能使用、且由所有对象共享的成员;例如例子中的 public static int count = 0,这个count在类加载的时候就会存在了,所有的实例化对象(例如 LockTest test = new LockTest())都能访问这个count,而不是每个对象都有一个属于自己的count,static用来修饰方法也是一样的
同步代码块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class LockTest {
public static int count = 0;
/**
* synchronized (对象),此时这个对象就是当前锁资源
*/
public static void increment(){
synchronized (LockTest.class){
count++;
}
}
/**
* synchronized (对象),此时这个对象就是当前锁资源
*/
public static void decrement(){
synchronized (LockTest.class){
count--;
}
}
}synchronized的优化
- 锁消除:锁消除就是编译器的一个优化的技术(JIT),用于消除不必要的同步锁操作,提升程序的执行效率。
- 锁膨胀/粗化:锁膨胀也是编译器JIT做的一个优化手段,用于扩大锁的范围,从而避免频繁的加锁释放锁操作,从而提升程序的性能。
- 锁升级:锁升级是指 随着锁竞争的加剧,JVM 自动将锁从低级别升级到高级别
1
无锁 → 偏向锁 → 轻量级锁 → 重量级锁锁升级是 不可逆的,一旦升级,不能降级。例如,一旦进入重量级锁,就不会退回轻量级锁。
1.无锁(No Lock)
对象没有被任何线程访问。
普通的非同步对象处于此状态。
- 偏向锁
当只有一个线程访问对象同步块时,为了减少无竞争情况下的开销,会将锁偏向某个线程。
适用于线程偏向性强的场景(即,大多数时间都是一个线程访问该对象)。
- 轻量级锁
有多个线程交替访问(无实际竞争),通过使用 CAS 操作避免重量级的线程阻塞和唤醒。
在同步块进入前尝试使用 CAS 抢占锁。
- 重量级锁
多个线程同时访问某个对象时发生了实际竞争,系统不得不使用操作系统的互斥量机制。
会造成线程阻塞,性能开销较大。
| 升级路径 | 触发条件说明 |
|---|---|
| 无锁 → 偏向锁 | 对象第一次被线程访问,JVM 开启偏向锁,且没有竞争 |
| 偏向锁 → 轻量级锁 | 另一个线程尝试访问被偏向的对象(产生潜在竞争) |
| 轻量级锁 → 重量级锁 | 多线程并发争用同一锁对象,CAS 自旋失败 |
ReentrantLock
ReentrantLock 也是互斥锁
ReentrantLock在加锁方面就提供了4中方式:
lock()死等的方式
lockInterruptibly()死等的方式,但是在死等时,允许中断
tryLock()蜻蜓点水,尝试一下,拿到返回true,没拿到返回false。
tryLock(time,unit)尝试time.unit时间,拿到返回true,没拿到返回false,同时在time.unit时间内,允许中断。
1 | |
公平锁&非公平锁
非公平锁(默认):多个线程抢锁,没有顺序,性能高;
公平锁(可选):按照请求顺序排队,防止“饥饿”,但性能稍差。
1 | |
- 非公平锁的加锁流程:
- 调用 lock() 方法
↓ - 先尝试直接 CAS 修改 state = 0 → 1
- 成功:抢锁成功,设置当前线程为持有者,直接进入临界区
- 失败:进入 AQS.acquire()
↓
- tryAcquire() 再尝试获取锁:
- state == 0:再直接 CAS 抢锁(允许插队)
- 否则如果当前线程是持有锁线程 → 支持重入 → state++
- 否则 → 抢锁失败,准备排队
↓
- addWaiter(Node.EXCLUSIVE)
- 把当前线程封装成 Node 加入 AQS 队列尾部
↓
- 把当前线程封装成 Node 加入 AQS 队列尾部
- acquireQueued()
- 若自己是 head.next → 继续尝试抢锁
- 否则挂起(LockSupport.park)
↓
- 等待持有锁的线程 unlock(),调用 unparkSuccessor() 唤醒下一个
- 公平锁的加锁流程:
- 调用 lock() 方法
↓ - 直接调用 AQS.acquire()
↓ - tryAcquire()
- state == 0 && !hasQueuedPredecessors():
→ 若前面没有排队线程 → 尝试 CAS 抢锁 - 否则:
- 当前线程是否是持有者? → 是:重入
- 否:抢锁失败,准备排队
↓
- state == 0 && !hasQueuedPredecessors():
- addWaiter(Node.EXCLUSIVE)
- 把当前线程封装成 Node 加入 AQS 队列尾部
↓
- 把当前线程封装成 Node 加入 AQS 队列尾部
- acquireQueued()
- 若自己是 head.next → 再次尝试 tryAcquire()
- 否则挂起(LockSupport.park)
↓
- 等待前驱线程释放锁,unpark 唤醒当前线程
- 流程对比:
| 步骤 | 非公平锁 | 公平锁 |
|---|---|---|
| 1 | lock() → 先直接 CAS 抢锁 | lock() → 直接进入 acquire() |
| 2 | tryAcquire() 中直接 CAS 抢锁 | tryAcquire() 中先判断是否排队再抢锁 |
| 3 | 插队?✔️ 支持 | ❌ 不支持,必须排队 |
| 4 | 加入 AQS 队列:Node(线程) | 加入 AQS 队列:Node(线程) |
| 5 | 是 head.next?尝试获取锁 | 是 head.next?判断排队情况再尝试获取锁 |
| 6 | 否则 LockSupport.park 阻塞线程 | 否则 LockSupport.park 阻塞线程 |
| 7 | 唤醒下一个等待节点 | 唤醒下一个等待节点 |
| 项目 | 非公平锁 | 公平锁 |
|---|---|---|
| lock() 方法 | 先尝试直接 CAS 抢锁,失败才排队 | 直接调用 acquire(),不抢锁 |
| tryAcquire() 方法中逻辑 | state == 0 → 直接 CAS 抢锁 | state == 0 → 先检查队列是否有前驱再决定是否抢锁 |
| 抢锁行为 | 不管队列有没有线程,优先尝试抢锁 | 若前面有等待线程,自己不能插队 |
| 性能 | 更快,但可能导致线程饥饿 | 更公平,线程获取锁的顺序更可控,但吞吐量略低 |
ReentrantLock 与 synchronized 的区别
| 对比项 | synchronized | ReentrantLock |
|---|---|---|
| 锁机制类型 | 隐式锁,基于 JVM | 显式锁,由程序员控制 |
| 解锁方式 | 自动释放(方法/代码块执行完) | 需手动 unlock(),否则可能死锁 |
| 是否可中断 | 不可中断 | 可通过 lockInterruptibly() 实现中断响应 |
| 是否可限时尝试加锁 | 不支持 | 支持 tryLock(timeout) 设定等待时间 |
| 是否公平锁 | 不支持,默认非公平 | 支持公平锁和非公平锁构造方式 |
| 性能表现 | 较轻量,适合简单同步场景 | 功能强大,适合复杂并发控制 |
线程池
线程池通过重复利用固定数量的线程来执行大量任务,可以大幅降低线程创建和销毁的开销,提升系统性能和响应速度。
线程池的核心参数
1 | |
- corePoolSize(核心线程数)
指线程池中保留的最小线程数。
即使这些线程处于空闲状态,也不会被回收,始终保留。
当有任务来时,先用核心线程执行。
- maximumPoolSize(最大线程数)
线程池中允许存在的最大线程数量。
当任务太多,队列满了,就会创建非核心线程来执行任务,直到达到最大线程数。
超过该值,就会触发拒绝策略。
- ③ keepAliveTime(非核心线程空闲存活时间)
非核心线程在空闲状态下最多保持多长时间不被销毁。
超过这个时间未被使用,线程会被自动回收。
⚠️ 注意:
默认情况下只对非核心线程有效;
但可以使用 allowCoreThreadTimeOut(true) 让核心线程也超时回收。
- ④ unit(时间单位)
keepAliveTime 的时间单位,使用 TimeUnit 枚举,如:
- TimeUnit.SECONDS
- TimeUnit.MILLISECONDS 等
- ⑤ workQueue(任务队列)
存放提交但尚未执行的任务。
队列种类影响线程池的行为模型。
| 队列类型 | 特点说明 |
|---|---|
ArrayBlockingQueue | 有界阻塞队列,FIFO,常用于限制任务数量 |
LinkedBlockingQueue | 无界队列,任务会无限堆积,容易 OOM(⚠️默认Executors使用) |
SynchronousQueue | 直接交接,不存储任务(用于 CachedThreadPool) |
PriorityBlockingQueue | 支持任务优先级排序 |
- ⑥ threadFactory(线程工厂)
用于创建线程,可以自定义线程名称、是否守护线程、线程组等。
便于线程监控和调试。
默认实现是 Executors.defaultThreadFactory(),你也可以自定义:
1 | |
⑦ RejectedExecutionHandler(拒绝策略)
当线程池 线程数达到最大,任务队列满时,执行失败任务的处理策略。
四种内置策略:
| 策略类 | 行为说明 |
|---|---|
AbortPolicy(默认) | 抛出 RejectedExecutionException 异常 |
CallerRunsPolicy | 谁提交任务,谁就自己去执行(同步) |
DiscardPolicy | 直接丢弃任务,不抛异常 |
DiscardOldestPolicy | 丢弃队列中最老的任务,尝试提交当前任务 |
线程池的简单使用
1 | |
🧾 解释说明:
核心线程:2 个 → 最多同时有 2 个线程在工作
队列容量:2 → 除了正在执行的任务外,还能再缓冲两个任务
最大线程:4 → 如果队列满了,最多还能额外启动 2 个线程
提交 5 个任务:
任务 1 和 2 → 被核心线程直接执行
任务 3 和 4 → 被放入等待队列
任务 5 → 队列已满,创建新线程执行
超过最大线程 + 队列容量时(第 7 个以上任务)→ 触发拒绝策略
submit() 和 execute() 的区别
submit()可以获取到现成的返回值,execute() 不能获取到线程的返回值
| 方法名 | 返回值 | 是否能获取任务执行结果 | 是否能捕获异常 |
|---|---|---|---|
execute() | void | ❌ 不能获取 | ❌ 无法捕获异常(除非 try-catch) |
submit() | Future<?> | ✅ 可以获取 | ✅ 能捕获异常通过 Future.get() 抛出 |
1 | |
结束语
多线程的知识先到此结束,其实很早之前学过一遍了,但是忘的一干二净了,所以来复习一遍,其实还有很多多线程的知识没有学完,等后面有时间了或者遇到没见过的多线程问题会继续补充




