从零起步学习并发编程 || 第二章:多线程与死锁在项目中的应用示例

一、什么是多线程?

多线程就是在同一进程内同时存在多个执行流(线程),每个线程都有自己的执行路径、程序计数器和栈,但共享进程的堆内存与全局资源。

在 Java 中,线程可以通过继承 Thread 或实现 Runnable/Callable(并配合 ExecutorService)来创建和管理。

关键点:

  • 并发(concurrency) ≠ 并行(parallelism):并发是多个任务在重叠的时间段内进展;并行是在同一时刻多个任务同时运行(需要多核才是真正的并行)。

  • 线程可以用来把复杂任务拆成多个可以同时"推进"的子任务(尤其是 I/O 密集型或可并行的计算)。

二、多线程的优势

  • 提高响应性:GUI/服务端能同时处理请求或保持界面不阻塞(例如:一个线程处理 I/O,另一个更新界面)。

  • 更好利用等待时间:I/O 等待(磁盘、网络)期间,CPU 可以切换去执行其它线程,从而提高吞吐。

  • 并行计算(在多核 CPU):把可分解的计算任务分配到多个核心上,能显著缩短完成时间。

  • 资源隔离、结构清晰:把不同职责放到不同线程(worker、监听、定时器)更易于设计清晰的系统。

三、单核 CPU 支持多线程吗?

单核 CPU 可以支持多线程 :通过 时间片轮转(time-slicing),OS 在多个线程之间快速切换,使得从用户角度看多个线程"同时"运行。

并行: 多核 CPU 下,多个线程同时在不同核心上执行(真正的 "同时");
**并发:**单核 CPU 下,操作系统通过 "时间片轮转" 机制,快速切换不同线程的执行(比如每个线程执行 10ms,切换速度极快,肉眼感觉像 "同时")。

例如:

单核 CPU 下,你一边听歌(音频线程),一边写代码(编辑器线程),操作系统会快速在这两个线程间切换,你感觉是同时进行的,但实际上 CPU 同一时刻只执行一个线程。

四、单核 CPU 使用多线程效率一定就高吗?

不一定,甚至可能更低:

  1. 效率高的场景:线程中有大量 "IO 操作"(比如网络请求、文件读写),此时 CPU 会闲置,多线程可以利用闲置时间执行其他线程。比如:

    • 单线程:请求 A(等待网络响应 2 秒)→ 处理 A(0.1 秒)→ 请求 B(等待 2 秒)→ 处理 B(0.1 秒),总耗时 4.2 秒;
    • 多线程:线程 1 请求 A(等待 2 秒),同时线程 2 请求 B(等待 2 秒),之后同时处理,总耗时 2.1 秒。
  2. 效率低的场景:线程都是 "CPU 密集型"(比如大量计算,无 IO 等待),此时多线程会增加 "线程切换开销"(保存线程上下文、恢复上下文),反而比单线程慢。比如:

    • 单线程:计算 100 万次,耗时 1 秒;
    • 多线程:两个线程各计算 50 万次,线程切换耗时 0.2 秒,总耗时 1.2 秒。

五、什么是死锁?

死锁是多线程中最典型的问题,指多个线程互相持有对方需要的资源,且都不释放自己持有的资源,导致所有线程都无法继续执行

经典产生条件(四个必要条件):

  1. 互斥(Mutual exclusion):资源不能被多个线程同时使用(例如互斥锁)。

  2. 占有并等待(Hold and wait):线程已持有至少一个资源并等待获取其他被占用的资源。

  3. 不可剥夺(No preemption):资源一旦分配,不能被强制剥夺,只有线程自己释放。

  4. 循环等待(Circular wait):存在线程环,T1 等待 T2,T2 等待 T3,... 最后等待回 T1。

java 复制代码
public class DeadlockDemo {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("t1 持有 lockA");
                sleep(100); // 模拟工作,确保顺序
                synchronized (lockB) {
                    System.out.println("t1 持有 lockB");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("t2 持有 lockB");
                sleep(100);
                synchronized (lockA) {
                    System.out.println("t2 持有 lockA");
                }
            }
        });

        t1.start();
        t2.start();
    }

    private static void sleep(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
    }
}

t1 先锁 lockA 再想要 lockB;t2 先锁 lockB 再想要 lockA ------ 如果两个都在各自的第一把锁后等待第二把锁,就会死锁。

六、如何检测死锁?

  1. 固定锁获取顺序(最简单且常用):对多个资源设定全局顺序,所有线程都按相同顺序获取锁(例如先锁 A 再锁 B),这样就消除了循环等待条件。

  2. 减少锁的持有时间:尽量把同步块变窄,只保护必要的临界区。

  3. 使用 tryLock(带超时)而不是阻塞式获取 (适用于 java.util.concurrent.locks.Lock):

    • 如果一段时间内获取不到锁,就释放已持有的锁、退避并重试,从而避免无限等待。

    • 例子:

      java 复制代码
      import java.util.concurrent.locks.Lock;
      import java.util.concurrent.locks.ReentrantLock;
      import java.util.concurrent.TimeUnit;
      
      Lock la = new ReentrantLock();
      Lock lb = new ReentrantLock();
      
      boolean safeAcquire() throws InterruptedException {
          while (true) {
              boolean aLocked = la.tryLock(50, TimeUnit.MILLISECONDS);
              if (!aLocked) continue;
              try {
                  boolean bLocked = lb.tryLock(50, TimeUnit.MILLISECONDS);
                  if (!bLocked) {
                      // 无法获得第二把锁,释放第一把,稍后重试
                      la.unlock();
                      Thread.sleep(10);
                      continue;
                  }
                  // 两把锁都拿到了
                  return true;
              } finally {
                  // 在使用完成后解锁(调用者负责)
              }
          }
      }
  4. 使用更高级的并发数据结构/算法ConcurrentHashMapBlockingQueue 等并发容器通常内部已避免复杂锁竞争。

  5. 尽可能使用不可变对象或无锁算法:减少需要加锁的场景。

  6. 避免嵌套锁或减少锁的层级:尽量不在持有多个锁时调用外部可阻塞的方法。

  7. 用单线程执行关键段(actor 模型 / 消息队列):把需要串行化的操作发送到单独线程处理(例如事件队列),从而避免锁。

小结

  • 多线程是把多个执行流放入同一进程,共享内存但独立栈/PC。并发 ≠ 并行。

  • 单核 CPU 支持多线程(通过时间片切换),但多线程不总是更快;CPU 密集型任务在单核上多线程可能更慢。

  • 死锁由互斥、占有并等待、不可剥夺、循环等待四条件共同导致。

  • 检测死锁:jstack、JVisualVM、ThreadMXBean.findDeadlockedThreads() 等工具。

  • 避免死锁:统一锁顺序、tryLock(超时)、缩小锁粒度、使用并发容器或单线程处理关键逻辑。

  • 设计和代码审查阶段预防胜过事后抢救。

相关推荐
YangYang9YangYan2 小时前
2026大专大数据技术专业学习数据分析的必要性
大数据·学习·数据分析
m5655bj2 小时前
通过 C# 设置 Word 文档背景颜色、背景图
开发语言·c#·word
张np2 小时前
java进阶-Zookeeper
java·zookeeper·java-zookeeper
long3162 小时前
合并排序 merge sort
java·数据结构·spring boot·算法·排序算法
定偶2 小时前
事务、触发器、存储过程与视图全解析
数据库·oracle
Lisson 32 小时前
VF01修改实际开票数量增强
java·服务器·前端·abap
范纹杉想快点毕业2 小时前
STM32单片机与ZYNQ PS端 中断+状态机+FIFO 综合应用实战文档(初学者版)
linux·数据结构·数据库·算法·mongodb
WZTTMoon2 小时前
【无标题】
java
拓云者也2 小时前
常用的生物信息学数据库以及处理工具
数据库·python·oracle·r语言·bash