并发编程(一)
CPU 在执行线程时,需要记录当前线程的状态:程序计数器寄存器栈信息CPU状态当 CPU 从线程A切换到线程B时:保存线程A状态恢复线程B状态开始执行线程B这个过程就叫:上下文切换CAS 是一种无锁算法。核心思想:比较内存中的值是否是预期值,如果是,则更新。参数说明V当前内存值A期望值B新值V = B;}else{失败协程:在单线程中实现多个任务的调度。特点:用户态切换切换成本低不依赖操作系统线程
一、并行与并发
1. 并行(Parallelism)
并行指的是:多个任务同时执行,并且互不干扰、不抢占资源。
一般真正的并行需要:
-
多核 CPU
-
多个执行单元
-
多个线程真正同时运行
示例
假设有两个线程:
-
线程A计算用户数据
-
线程B计算订单数据
如果 CPU 有两个核心,那么这两个线程可以同时运行。
public class ParallelDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("线程A执行:" + i);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("线程B执行:" + i);
}
});
t1.start();
t2.start();
}
}
并行特点
| 特点 | 说明 |
|---|---|
| 同时执行 | 真正意义上的同时 |
| 依赖硬件 | 需要多核CPU支持 |
| 效率高 | 能提升程序吞吐量 |
二、并发(Concurrency)
并发指的是:
多个任务在同一时间段内交替执行。
注意:
-
并发不一定是真正同时运行
-
本质是 CPU 在多个线程之间快速切换
-
多个线程会争抢 CPU、内存、锁等资源
示例
单核 CPU 上:
-
线程A执行一会
-
CPU切换到线程B
-
再切换回线程A
因为切换速度极快,所以看起来像“同时执行”。
public class ConcurrencyDemo {
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " -> " + i);
}
};
new Thread(task, "线程1").start();
new Thread(task, "线程2").start();
}
}
运行结果会交替输出。
并发特点
| 特点 | 说明 |
|---|---|
| 资源竞争 | 会争抢CPU、锁、内存 |
| 存在线程切换 | 有上下文切换开销 |
| 复杂性高 | 容易出现线程安全问题 |
三、上下文切换(Context Switch)
1. 什么是上下文切换
CPU 在执行线程时,需要记录当前线程的状态:
-
程序计数器
-
寄存器
-
栈信息
-
CPU状态
当 CPU 从线程A切换到线程B时:
-
保存线程A状态
-
恢复线程B状态
-
开始执行线程B
这个过程就叫:
上下文切换
上下文切换的问题
上下文切换非常耗费性能。
如果线程太多:
-
CPU大量时间都花在线程切换
-
真正执行任务的时间减少
-
程序性能下降
四、减少上下文切换的方法
1. 无锁并发编程
核心思想
避免多个线程竞争同一把锁。
为什么锁会导致性能下降
线程竞争锁时:
-
获取不到锁的线程会阻塞
-
CPU需要切换线程
-
导致上下文切换
常见方案:数据分段
例如:
将数据根据 hash 分段:
int index = userId % 4;
不同线程处理不同的数据段。
示例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NoLockDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 8; i++) {
int userId = i;
pool.execute(() -> {
int segment = userId % 4;
System.out.println(
Thread.currentThread().getName()
+ " 处理分段:" + segment
);
});
}
pool.shutdown();
}
}
优点
-
减少锁竞争
-
提高并发性能
-
降低线程阻塞
2. CAS算法(Compare And Swap)
什么是CAS
CAS 是一种无锁算法。
核心思想:
比较内存中的值是否是预期值,如果是,则更新。
CAS包含三个值:
| 参数 | 说明 |
|---|---|
| V | 当前内存值 |
| A | 期望值 |
| B | 新值 |
执行逻辑:
if(V == A){
V = B;
}else{
失败
}
Java中的CAS
Java 的 Atomic 包底层使用 CAS。
例如:
AtomicInteger
AtomicLong
AtomicReference
示例
import java.util.concurrent.atomic.AtomicInteger;
public class CASDemo {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
count.incrementAndGet();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
count.incrementAndGet();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
输出:
2000
为什么不用 synchronized?
因为 AtomicInteger 内部已经使用 CAS 保证线程安全。
CAS优点
-
无锁
-
性能高
-
不会阻塞线程
CAS缺点(补充)
1. ABA问题
一个值:
A -> B -> A
CAS会认为没有变化。
解决方法:
AtomicStampedReference
2. 自旋开销大
CAS失败后会不断重试。
如果竞争激烈:
-
CPU消耗会增加
3. 使用最少线程
为什么线程不能太多
线程不是越多越好。线程太多会导致:
-
频繁切换
-
内存占用增加
-
CPU压力增大
错误示例
任务只有10个:
Executors.newFixedThreadPool(1000)
这样会产生大量空闲线程。
合理线程数
CPU密集型:
线程数 = CPU核心数 + 1
IO密集型:
线程数 = CPU核心数 * 2
示例:线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
int task = i;
pool.execute(() -> {
System.out.println(
Thread.currentThread().getName()
+ " 执行任务:" + task
);
});
}
pool.shutdown();
}
}
五、协程(Coroutine)
什么是协程
协程:
在单线程中实现多个任务的调度。
特点:
-
用户态切换
-
切换成本低
-
不依赖操作系统线程调度
协程与线程区别
| 对比 | 线程 | 协程 |
|---|---|---|
| 调度者 | 操作系统 | 程序自身 |
| 切换成本 | 高 | 低 |
| 并发数量 | 较少 | 很多 |
| 是否需要CPU切换 | 需要 | 不需要 |
Java中的协程
Java传统协程支持较弱。
但是:
-
CompletableFuture
-
Reactor
-
Loom虚拟线程(JDK21)
都属于协程思想的体现。
示例:CompletableFuture
import java.util.concurrent.CompletableFuture;
public class CoroutineDemo {
public static void main(String[] args) {
CompletableFuture.runAsync(() -> {
System.out.println("异步任务执行");
});
System.out.println("主线程继续执行");
}
}
六、锁(Lock)
1. 为什么需要锁
多个线程同时操作共享资源时:
可能出现数据错误。
例如:
count++
这并不是原子操作。
它会拆分为:
读取
修改
写回
多个线程同时执行会产生线程安全问题。
synchronized 示例
public class SyncDemo {
private static int count = 0;
public synchronized void add() {
count++;
}
}
synchronized作用
-
保证原子性
-
保证可见性
-
保证有序性
七、死锁(DeadLock)
1. 什么是死锁
多个线程互相等待资源。最终:谁也无法继续执行。
死锁示例
public class DeadLockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lockA) {
System.out.println("线程1获取lockA");
synchronized (lockB) {
System.out.println("线程1获取lockB");
}
}
}).start();
new Thread(() -> {
synchronized (lockB) {
System.out.println("线程2获取lockB");
synchronized (lockA) {
System.out.println("线程2获取lockA");
}
}
}).start();
}
}
可能出现:
线程1等待lockB
线程2等待lockA
最终程序卡死。
八、避免死锁的方法
1. 避免同时获取多个锁
尽量减少锁嵌套。
错误:
synchronized(lockA){
synchronized(lockB){
}
}
2. 一个锁只保护一个资源
降低锁复杂度。
3. 使用 tryLock
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockDemo {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
if (lock.tryLock(2, TimeUnit.SECONDS)) {
try {
System.out.println("获取锁成功");
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁失败");
}
}
}
优点
不会无限等待。
4. 按顺序获取锁
例如:
所有线程必须:
先获取A
再获取B
这样可以避免循环等待。
九、Java并发底层原理
Java程序执行流程
Java代码
↓
编译成字节码
↓
类加载器加载到JVM
↓
JVM解释/编译执行
↓
转为机器码
↓
CPU执行
因此:
Java并发并不是完全由Java决定。
还依赖:
-
JVM实现
-
CPU指令
-
操作系统调度
十、volatile关键字
1. volatile的作用
volatile 是轻量级 synchronized。
它主要保证:
可见性
即:
一个线程修改变量后。
其他线程立即可见。
volatile示例
public class VolatileDemo {
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
}
System.out.println("线程结束");
}).start();
Thread.sleep(2000);
flag = false;
}
}
如果不加 volatile:
线程可能永远无法结束。
因为线程会把变量缓存到工作内存中。
十一、JMM(Java内存模型)
1. 主内存与工作内存
Java内存模型规定:
主内存
共享变量存储位置。
工作内存
线程私有。
线程会把变量复制到工作内存。
问题来源
线程修改的是:
自己的工作内存。
其他线程可能看不到。
这就是:
可见性问题
volatile如何解决
volatile会:
-
修改后立即刷新到主内存
-
读取时强制从主内存读取
因此:
所有线程看到的值一致。
十二、volatile与synchronized区别
| 对比 | volatile | synchronized |
|---|---|---|
| 保证可见性 | √ | √ |
| 保证原子性 | × | √ |
| 是否阻塞 | 不阻塞 | 会阻塞 |
| 性能 | 高 | 较低 |
| 使用场景 | 状态标记 | 共享资源修改 |
十三、volatile不能保证原子性
错误示例
public class VolatileAtomicDemo {
private static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
count++;
}
}).start();
}
Thread.sleep(3000);
System.out.println(count);
}
}
理论值:
10000
实际可能:
8732
因为:
count++
不是原子操作。
十四、volatile的使用场景
适合场景
1. 状态标记
volatile boolean shutdown
2. 单例双重检查
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
十五、并发编程核心问题总结
1. 原子性
操作不可分割。
例如:
count++
不是原子操作。
2. 可见性
一个线程修改变量后:
其他线程能否立即看到。
volatile 可以解决。
3. 有序性
程序执行顺序可能被重排序。
volatile 与 synchronized 都可以禁止部分重排序。
十六、补充:线程安全集合
Java提供了一些线程安全容器:
| 集合 | 特点 |
|---|---|
| ConcurrentHashMap | 高并发Map |
| CopyOnWriteArrayList | 读多写少 |
| BlockingQueue | 阻塞队列 |
ConcurrentHashMap 示例
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentMapDemo {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("1", "Java");
map.put("2", "并发编程");
System.out.println(map);
}
}
十七、高频问题
1. synchronized和Lock区别
| synchronized | Lock |
|---|---|
| JVM实现 | JDK实现 |
| 自动释放锁 | 手动释放锁 |
| 功能简单 | 功能丰富 |
| 支持少 | 支持公平锁等 |
2. 什么是线程安全
多个线程同时访问时:
程序结果仍然正确。
3. 什么是原子操作
不可被中断的操作。
例如:
AtomicInteger.incrementAndGet()
十八、总结
并发编程的核心目标:
提高程序运行效率
提高资源利用率
但同时也会带来:
线程安全问题
死锁问题
可见性问题
上下文切换开销
因此:
并发编程最核心的思想是:
在保证线程安全的前提下,提高程序执行效率。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)