一、并行与并发

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时:

  1. 保存线程A状态

  2. 恢复线程B状态

  3. 开始执行线程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()

十八、总结

并发编程的核心目标:

提高程序运行效率
提高资源利用率

但同时也会带来:

线程安全问题
死锁问题
可见性问题
上下文切换开销

因此:

并发编程最核心的思想是:

在保证线程安全的前提下,提高程序执行效率。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐