Java多线程编程

15012 字
38 分钟

单纯明快 ———《乱马1/2》

概念理解

进程与线程

进程(Process)

  • 进程是程序的运行实例。它拥有独立的内存空间,系统为每个进程分配资源(如CPU时间、内存等)。多个进程之间是相互独立的,它们通常不能直接访问彼此的内存。

线程(Thread)

  • 线程是进程中的执行单元,一个进程可以包含多个线程。这些线程共享同一个进程的资源(如内存),但可以独立运行,执行不同的任务。相比创建进程,创建线程的开销更小,执行效率更高。

并发与并行

并发

并发是指在同一段时间内,系统可以处理多个任务。这些任务不一定是同时执行的,他们可能在单个CPU核心上通过时间片轮转(Time-slicing)的方式交替进行,给人一种”同时进行”的错觉。

核心思想:

  • 任务交替执行:即使只有一个CPU核心,也能通过快速切换来处理多个任务。
  • 宏观上同时执行:从用户的角度看,多个任务似乎都在进行中。
  • 资源共享:多个任务可能共享同一个物理资源(如一个CPU核心)。

并行

并行是指多个任务在同一时刻真正地同时执行。这需要有多个执行单元(多核CPU)来分别处理不同的任务。

核心思想:

  • 任务同时执行:多个任务在不同的执行单元上同时进行。
  • 多个执行单元:至少需要两个及以上的CPU核心。
  • 提高吞吐量:在相同时间内完成更多的任务。

多线程既可以实现并发,也可以实现并行,目的都是更有效的利用计算资源。大部分情况下我们无需过度关心CPU具体如何调度多线程,我们只要正确的编写多线程代码即可。

java多线程基础

线程的生命周期

1748836633425

  • 新建状态

    • 使用new关键字Thread类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到 start()这个线程
  • 就绪状态

    • 当线程对象调用了 start()方法后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里的线程调度器的调度。
  • 运行状态

    • 如果就绪状态的线程获取了CPU资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态可以变为阻塞状态,就绪状态和死亡状态。
    • 注:run()方法会由JVM自动调用

  • 阻塞状态

    • 如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态,阻塞状态不占用CPU时间。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。分为三种情况:
      • 等待阻塞:运行状态中的线程自己执行了 wait()方法,使线程进入等待阻塞,需要另一个线程明确通知才能唤醒
      • 同步阻塞:线程在获取 synchronized 同步锁失败(因为被其他线程占用),直到获取锁后唤醒。
      • 超时阻塞:通过调用线程的 sleep()join()发出了I/O请求,线程就会进入阻塞状态。当sleep()状态超时,join()等待线程终止或超时,或者I/O处理完毕,线程重新转入就绪状态。
    • sleep(),wait(),join()等方法都是线程自己调用,让自己进入阻塞状态的方法。

  • 死亡状态

    • 一个运行状态的线程完成任务或者其他终止条件发送时,该线程就切换到终止状态。切换到终止状态的线程不再执行任何代码,也不能被再次启动。(如果尝试再次调用已终止线程的 start()方法,会抛出 IllegalThreadStateException)

创建一个线程

创建线程的三种方式:

  • 实现Runnable接口
  • 继承Thread类
  • 通过Callable 和Future 创建线程(此处未讲解)

继承Thread来创建线程

步骤:

  1. 创建一个类并继承 Thread
  2. 重写 Thread类中的 run()方法。run()方法包含了线程要执行的代码
  3. 创建子类的实例
  4. 调用该实例的 start()方法来启动线程。

直接调用 run()方法只会将它作为一个普通的方法执行。而不会启动一个新的线程

class ThreadDemo extends Thread {   // 继承Thread类

   private String threadName;    //线程参数
   
   ThreadDemo( String name) {    // 构造时可以传参
      threadName = name;
      System.out.println("Creating " +  threadName );
   }
   
   @Override
   public void run() {
      System.out.println("Running " +  threadName );
      try {
         for(int i = 4; i > 0; i--) {
            System.out.println("Thread: " + threadName + ", " + i);
            // 让线程睡眠一会
            Thread.sleep(50);
         }
      }catch (InterruptedException e) {
         System.out.println("Thread " +  threadName + " interrupted.");
      }
      System.out.println("Thread " +  threadName + " exiting.");
   }
   
}
 
public class TestThread {
 
   public static void main(String args[]) {
      ThreadDemo T1 = new ThreadDemo( "Thread-1");
      T1.start();    // 直接调用Thread的start方法来启动线程
  
      ThreadDemo T2 = new ThreadDemo( "Thread-2");
      T2.start();
   }   
}

Thread 的常用方法

对象方法:

序号方法描述
1public void start()
 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
2public void run()
 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
3public final void setName(String name) 
改变线程名称,使之与参数 name 相同。
4public final void setPriority(int priority) 
更改线程的优先级。
5public final void setDaemon(boolean on) 
将该线程标记为守护线程或用户线程。
6public final void join(long millisec)
等待该线程终止的时间最长为 millisec 毫秒,不会强制杀死子线程。不输入参数会一直等待到子线程结束。
7public void interrupt()
发出中断线程信号
8public final boolean isAlive()
测试线程是否处于活动状态。

守护线程/用户线程:主线程结束后自动终止。

非守护线程:主线程结束后,会继续执行自己的任务,直到代码完毕

类方法:

序号方法描述
1public static void yield()
提示线程调度器:当前线程愿意放弃 CPU 的执行权,让其他同优先级的线程有机会运行。
但不一定100%切换线程。
2public static void sleep(long millisec)
在指定的毫秒数内让当前调用该方法的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
3public static boolean holdsLock(Object x)
当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
4public static Thread currentThread()
返回对当前正在执行的线程对象的引用。
5public static void dumpStack()
将当前线程的堆栈跟踪打印至标准错误流。

Thread 实例启动的线程结束后,对象实例依然存在,它的属性也不会因为线程结束而被清除。线程的终止只是意味着该线程的执行体已完成,但不代表Thread对象本身会被销毁。我们也可以通过Thread实例的属性来传递线程计算的结果。

实现Runnable接口来创建线程

java.lang.Runnable是一个函数式接口,它只有一个抽象方法 run(),这种方式是Java中创建线程的更推荐的方式,它避免了单线程的限制。但是要启动多线程,最终还是需要创建Thread对象用于启动线程。

  • run()方法是线程入口

步骤:

  1. 定义一个类实现Runnable接口
  2. 实现 run()方法
  3. 创建 Runnable接口的实现类实例
  4. Runnable对象作为参数传递给 Thread类的构造器,创建 Thread对象
  5. 调用 Thread对象的 star()t方法
class RunnableDemo implements Runnable {	//还可以继承一个父类,和其他接口,更推荐
   private Thread t;    // 将线程封装到类中,方便管理
   private String threadName;
   private int sleepTime = 50; // 成员变量,亦可作为线程的参数
   
   RunnableDemo( String name) {  //构造函数中传递参数,推荐
      threadName = name;   
      System.out.println("Creating " +  threadName );
   }
   
   @Override
   public void run() {     // 线程实际执行的代码
      System.out.println("Running " +  threadName );
      try {
         for(int i = 4; i > 0; i--) {
            System.out.println("Thread: " + threadName + ", " + i);
            // 让线程睡眠一会
            Thread.sleep(this.sleepTime);
         }
      }catch (InterruptedException e) {
         System.out.println("Thread " +  threadName + " interrupted.");
      }
      System.out.println("Thread " +  threadName + " exiting.");
   }
   
   public void start (int sleepTime) { //对于Thread启动的再次封装,此处也可以传递动态参数
      System.out.println("Starting " +  threadName );
      this.sleepTime = sleepTime;
      if (t == null) {
         t = new Thread (this, threadName);//将Runnable对象作为参数传递给Thread构造器
         t.start ();
      }
   }
}
 
public class TestThread {
 
   public static void main(String args[]) {
      RunnableDemo R1 = new RunnableDemo( "Thread-1");
      R1.start(100);
  
      RunnableDemo R2 = new RunnableDemo( "Thread-2");
      R2.start(10);
   }   
}

运行结果:

Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Running Thread-2
Thread: Thread-1, 4
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.

java8 新方法Lambda 表达式

Java8 引入了Lambda表达式,它提供了一种简介的方式来表示匿名函数,极大地简化了代码,尤其是在处理函数式接口时。而 java.lang.Runnable正是一个函数式接口,这使得Lambda表达式创建多线程变得异常方便和直观。

Lambda表达式基础回顾

Lambda 表达式的语法通常是 (parameters)->expression(parameters) -> { statements ;}

  • parameters:方法的参数列表
  • ->:Lambda操作符,表示将参数传递给方法体
  • expression{statements;}:方法体,可以是一个表达式或一个代码块

当一个接口只包含一个抽象方法时,它被称为函数式接口(Functional Interface)。Runnable接口就是一个典型的函数式接口,它只包含一个run()抽象方法:

@FunctionalInterface
public interface Runnable{
	public abstract void run();
}

使用Lambda表达式创建多线程

在Java8之前,我们通常通过实现 Runnable接口来创建线程,像这样:

Thread oldWayThread = new Thread(new Runnable(){
	@Override
	public void run(){
		System.out.println("传统匿名内部类方式创建的线程。");
	}
});

有了Lambda表达式,你可以将上述代码简化为:

Thread lambdaThread = new Thread(()->{
	System.out.println("Lambda表达式创建的线程!");
});

线程同步

为什么要进行线程同步?

  • 数据不一致:当多个线程同时访问和修改共享数据时,如果没有同步机制,就可能会出现数据读写混乱,导致最终结果错误。
  • 竞态条件:多个线程竞争同一资源,执行顺序不确定,导致结果依赖于特定的执行顺序。
  • 内存可见性:一个线程对于共享变量的修改,可能对另一个线程不可见,因为每个线程都有自己的工作内存,修改可能只存在于工作内存中,而没有及时刷新到主存中。

synchronied关键字

synchronied 是Java中用于实现线程同步的重要关键字,它能够保证在同一时刻,只有一个线程可以执行特定的代码块或方法,从而避免了多线程环境下的数据不一致问题。

作用:

  • 原子性(Atomicity):确保被 synchronized块保护的代码,在同一时间只能被一个线程执行。这意味改代码块的执行是不可中断的,要么全部执行,要么不执行。
  • 可见性(Visibility):当一个线程释放 synchronized锁时,它所做的所有对共享变量的修改都会被刷新到主存中。当另一个线程获取到这个锁时,它会从主存中读取最新的共享变量值。这保证了线程见对于共享变量修改的可见性。
  • 有序性(Ordering):synchronized保证了代码的执行顺序。它会防止编译器和处理器进行指令重排,确保在同步块内的代码按序执行,并且同步块之前的代码先于同步块内部的代码执行,同步块内部的代码先于同步块之后的代码执行。

用法

同步方法

将synchronized 关键字修饰一个方法

  • 锁是当前实例对象 this
  • 当一个线程调用一个 synchronized 实例方法时它必须获取该实例的锁。
  • 其他线程如果也想调用该实例的任何 synchronized实例方法,则必须等待锁释放。
public class MyCounter {
    private int count = 0;

    // 同步实例方法,锁是 MyCounter 实例
    public synchronized void increment() {
        count++;
        System.out.println(Thread.currentThread().getName() + " incremented count to: " + count);
    }

    public synchronized int getCount() {
        return count;
    }
}

Thread-1 正在执行 increment() 方法时,Thread-2 不能执行 getCount() 方法。

修饰静态方法:

  • 锁是当前类的 Class对象(Myclass.class)
  • 当一个线程调用一个 synchronized 静态方法时,它必须先获取该类的Class对象的锁
  • 其他线程如果也想调用该类的任何 synchronized 静态方法,则必须等待锁释放。
public class MyStaticCounter {
    private static int staticCount = 0;

    // 同步静态方法,锁是 MyStaticCounter.class
    public static synchronized void staticIncrement() {
        staticCount++;
        System.out.println(Thread.currentThread().getName() + " incremented staticCount to: " + staticCount);
    }

    public static int getStaticCount() {
        return staticCount;
    }
}

同步代码块

synchronized 关键字用于一个代码块,需要明确指定一个锁对象。只有获取这个锁对象的监视器,才能执行代码块中的内容。

  • 语法:synchronized (lockObject) { // code to be synchronized }
  • lockObject 可以是任何对象(包括this、class对象、或其他自定义对象)
  • 当一个线程进入 synchronized代码块时,它必须先获取 lockObject的锁
  • 只有当该线程释放了 lockObject的锁后,其他线程才能获取该锁并进入该代码块。
  • 这种方式提供了更细粒度的控制,只对需要同步的代码进行锁定,而不是整个方法。
public class MyBlockCounter {
    private int count = 0;
    private final Object lock = new Object(); // 定义一个专门的锁对象

    public void increment() {
        // 同步代码块,锁是 lock 对象
        synchronized (lock) {
            count++;
            System.out.println(Thread.currentThread().getName() + " incremented count to: " + count);
        }
    }

    public int getCount() {
        return count;
    }
}

重入性(Reentrancy)

synchronized 锁是可重入的。这意味着如果一个线程已经持有一个对象的锁,它可以再次进入同一个对象的其他 synchronized 方法或代码块而不会死锁。每次重入,锁的计数器会增加;每次退出,计数器会减少。只有当计数器归零时,锁才真正被释放。

public class ReentrantExample {
    public synchronized void method1() {
        System.out.println("Method 1 entered by " + Thread.currentThread().getName());
        method2(); // 线程已经持有锁,可以再次进入 method2
        System.out.println("Method 1 exited by " + Thread.currentThread().getName());
    }

    public synchronized void method2() {
        System.out.println("Method 2 entered by " + Thread.currentThread().getName());
        // ...
        System.out.println("Method 2 exited by " + Thread.currentThread().getName());
    }
}

volatile 关键字

volatile 是Java 虚拟机提供的一种轻量级的同步机制,它主要用于保证变量的内存可见性(Memory Visibility)和禁止指令重排序。

用法

volatile 关键字只能用于修饰成员变量静态变量,不能修饰局部变量、方法参数或方法。

public class SharedData {
    // 修饰一个基本数据类型
    public volatile boolean flag = false;

    // 修饰一个引用类型
    public volatile MyObject myObject;
}

内存可见性问题

Java内存模型(Java Memory Moedel, JMM)。JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程都有自己的工作内存(Working Memory),工作内存中保存了该线程使用到的变量的副本。线程对变量的所有操作(读、取)都必须在工作内存中进行,而不能直接读写主内存中的变量。

当一个线程修改了工作内存中的变量副本时,它需要将更新后的值刷新到主内存中,而其他线程如果想要读取这个变量,也需要从主内存中重新加载最新值到自己的工作内存中。这就是内存可见性问题。

Volatile解决原理

  1. 当一个 volatile 变量被写入(修改)时:
    • JVM会强制将修改后的值立即从当前的工作内存刷新到主存
  2. 当一个 volatile 变量被读取时:
    • JVM 会强制当前线程从主内存中读取该变量的最新值到自己的工作内存中,而不是使用工作内存中的旧副本

这保证了 volatile 变量的每次读写操作都是对主内存的直接操作,从而确保了 volatile 变量的最新值对所有线程都是可见的。

禁止指令重新排序

编译器和处理器为了优化性能,可能会对指令进行重排序。而 volatile关键字会阻止对其修饰的变量的读写操作与其他普通内存操作之间的重排序。

简单来说,volatile就像一个”屏障”,它之前的操作都必须在它之前运行,它之后的操作都必须在它之后运行。

接下来我们通过一个小demo来看一下 volatile的功能:

public class VisibilityIssueDemo {
    private boolean flag = false; // 普通变量

    public void setFlagTrue() {
        System.out.println(Thread.currentThread().getName() + " is setting flag to true...");
        flag = true; // 修改 flag
        System.out.println(Thread.currentThread().getName() + " flag is now: " + flag);
    }

    public void observeFlag() {
        System.out.println(Thread.currentThread().getName() + " is observing flag...");
        // 线程可能一直在这里循环,因为 flag 的更新对其工作内存不可见
        while (!flag) {
            // 忙等待,不做任何操作,可能会导致优化器认为 !flag 永远为 true,从而进行优化
            //Thread.sleep(1); // 如果加入sleep,会强制刷新工作内存,从而掩盖问题
        }
        System.out.println(Thread.currentThread().getName() + " flag is now true, stopping observation.");
    }

    public static void main(String[] args) throws InterruptedException {
        VisibilityIssueDemo demo = new VisibilityIssueDemo();

        // 线程 A 负责观察 flag
        Thread observerThread = new Thread(demo::observeFlag, "ObserverThread");
        observerThread.start();

        // 给予观察者线程一些时间启动
        Thread.sleep(100);

        // 线程 B 负责修改 flag
        Thread modifierThread = new Thread(demo::setFlagTrue, "ModifierThread");
        modifierThread.start();

        // 等待两个线程结束
        observerThread.join();
        modifierThread.join();

        System.out.println("\n--- Demo Finished ---");
        System.out.println("Final flag value: " + demo.flag);
    }
}

程序运行结果如下: 1750554163225

观察线程卡死一直不结束,就说明在修改线程中对于flag的修改并没有加载到观察线程的工作区中。

添加volatile 指令:

private volatile boolean  flag = false; // volatile变量

程序运行结果如下:

1750554408832

wait()notify()/notifyAll()

wait()notify()notifyAll()Object类中的方法,它们与 synchronized关键字紧密配合,用于实现线程间的协作通信。

  • wait()
    • 使当前线程进入等待(WAITING)状态,并释放它持有的对象锁
    • 线程会一直等待,直到被其他线程调用相同对象的 notify()notifyAll()方法唤醒,或者被中断。
    • 必须在 synchronized代码块或方法中使用,否则会抛出 IllegalMonitorStateException
  • notify()
    • 唤醒一个在相同对象上调用 wait()方法的线程。如果有多个线程在等待,JVM就会选择其中一个进行唤醒,具体哪个线程被唤醒是不确定的。
    • 不会释放锁,而是等待当前的 synchronized块执行完毕后才释放。
    • 必须在 synchronized代码块或方法中使用
  • notifyAll()
    • 唤醒所有在相同对象上调用wait()方法的线程。被唤醒的线程会竞争该对象锁,只有获得锁的线程才能继续执行。
    • 同样不会释放锁,而是等待当前的 synchronized块执行完毕后才释放锁。
    • 必须在 synchronized代码块或方法中使用。

生产者-消费者模式

生产者-消费者模式是多线程编程中一个非常经典的并发设计模式,它用于解决生产者和消费者之间因产生和消费速度不匹配而导致的问题。这个模式的核心思想是引入一个共享的缓冲区(Buffer),作为生产者和消费者之间的桥梁,从而解耦生产者和消费者,提高系统的吞吐量和资源利用率。

模式角色

生产者-消费者模式主要包含以下三个角色:

  1. 生产者:
    • 负责生成数据
    • 缓冲区满时,生产者必须停止生产并等待,直到缓冲区有空间
    • 当缓冲区放入数据后,生产者会通知等待的消费者
  2. 消费者:
    • 负责处理(消费)数据
    • 当缓冲区空时,消费者必须停止消费并等待,直到缓冲区有数据
    • 当从缓冲区取出数据后,消费者会通知等待的生产者
  3. 缓冲区:
    • 一个共享的数据结构,用于存储生产者生产的数据,供消费者消费。
    • 通常是一个队列(Queue)
    • 必须是线程安全的,以免数据混乱

示例代码:

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.ThreadLocalRandom; // 用于随机数生成

class SharedBuffer<T> {
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;
    private final Object lock = new Object(); // 共享的锁对象

    public SharedBuffer(int capacity) {
        this.capacity = capacity;
    }

    // 生产者放入数据
    public void put(T item) throws InterruptedException {
        synchronized (lock) {
            // 使用while防止虚假唤醒
            while (queue.size() == capacity) { // 缓冲区满,生产者等待
                System.out.println(Thread.currentThread().getName() + " -> 缓冲区已满,等待消费...");
                lock.wait(); // 释放锁,进入等待
            }
            queue.offer(item);
            System.out.println(Thread.currentThread().getName() + " -> 生产并放入: " + item + ", 队列大小: " + queue.size());
            lock.notifyAll(); // 通知等待的消费者
        }
    }

    // 消费者取出数据
    public T take() throws InterruptedException {
        synchronized (lock) {
            while (queue.isEmpty()) { // 缓冲区空,消费者等待
                System.out.println(Thread.currentThread().getName() + " -> 缓冲区为空,等待生产...");
                lock.wait(); // 释放锁,进入等待
            }
            T item = queue.poll();
            System.out.println(Thread.currentThread().getName() + " -> 消费并取出: " + item + ", 队列大小: " + queue.size());
            lock.notifyAll(); // 通知等待的生产者
            return item;
        }
    }
}

// 生产者实现
class Producer implements Runnable {
    private final SharedBuffer<Integer> buffer;
    private final int itemsToProduce;

    public Producer(SharedBuffer<Integer> buffer, int itemsToProduce) {
        this.buffer = buffer;
        this.itemsToProduce = itemsToProduce;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < itemsToProduce; i++) {
                Thread.sleep(ThreadLocalRandom.current().nextInt(50, 200)); // 模拟生产时间
                int data = ThreadLocalRandom.current().nextInt(100); // 生产随机数据
                buffer.put(data);
            }
            System.out.println(Thread.currentThread().getName() + " -> 完成生产。");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println(Thread.currentThread().getName() + " -> 生产中断。");
        }
    }
}

// 消费者实现
class Consumer implements Runnable {
    private final SharedBuffer<Integer> buffer;
    private final int itemsToConsume;

    public Consumer(SharedBuffer<Integer> buffer, int itemsToConsume) {
        this.buffer = buffer;
        this.itemsToConsume = itemsToConsume;
    }

    @Override
    public void run() {
        try {
            for (int i = 0; i < itemsToConsume; i++) {
                Integer data = buffer.take();
                // Process the data...
                Thread.sleep(ThreadLocalRandom.current().nextInt(100, 300)); // 模拟消费时间
            }
            System.out.println(Thread.currentThread().getName() + " -> 完成消费。");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println(Thread.currentThread().getName() + " -> 消费中断。");
        }
    }
}

public class ProducerConsumerSyncDemo {
    public static void main(String[] args) throws InterruptedException {
        SharedBuffer<Integer> buffer = new SharedBuffer<>(5); // 缓冲区容量为5

        // 创建多个生产者和消费者
        Thread producer1 = new Thread(new Producer(buffer, 10), "Producer-1");
        Thread producer2 = new Thread(new Producer(buffer, 8), "Producer-2"); // 另一个生产者

        Thread consumer1 = new Thread(new Consumer(buffer, 9), "Consumer-1");
        Thread consumer2 = new Thread(new Consumer(buffer, 9), "Consumer-2"); // 另一个消费者

        producer1.start();
        producer2.start();
        consumer1.start();
        consumer2.start();

        // 等待所有线程完成
        producer1.join();
        producer2.join();
        consumer1.join();
        consumer2.join();

        System.out.println("主线程:所有生产-消费活动结束。");
    }
}

虚假唤醒:在多线程编程中,当线程在调用 Object.wait()方法后,即使没有收到 notify()notifyAll()的通知,或者在收到通知时其等待的条件并未满足,线程却从等待状态中意外唤醒的情况。

死锁(Deadlock)

死锁是多线程编程中一个非常常见且难以解决的问题。当多个线程因竞争共享资源而相互等待,导致所有线程都无法继续执行时,就发生了死锁。

死锁的产生条件

  1. 互斥条件:
    • 至少有一个资源是非共享的(一次只能被一个占用)
    • 例如:一个文件、一个打印机或者一个 synchronized锁。如果资源可以共享(如读操作),那么就不会因为这个资源发生死锁。
  2. 请求与保持条件:
    • 一个线程在持有至少一个资源的同时,又去请求获取另一个被其他线程持有的资源,但获取不到,于是它就一直等待下去,同时不释放自己已经持有的资源。
  3. 不可剥夺条件:
    • 已经分配给一个线程的资源,在未使用完毕之前,不能强制性地剥夺,只能由持有该资源的线程主动释放。
  4. 循环等待条件:
    • 存在一个线程等待链,其中A等待线程B持有的资源,线程B等待C持有的资源,…,直到某个线程Z等待线程A持有的资源,形成一个环路。

死锁示例:

public class DeadlockDemo {

    private static final Object lock1 = new Object(); // 第一把锁
    private static final Object lock2 = new Object(); // 第二把锁

    public static void main(String[] args) {

        // 线程 1:尝试先获取 lock1,再获取 lock2
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println(Thread.currentThread().getName() + ": 已获取 lock1");
                try {
                    Thread.sleep(100); // 模拟一些工作,给 Thread 2 机会获取 lock2
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                System.out.println(Thread.currentThread().getName() + ": 尝试获取 lock2...");
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + ": 已获取 lock2");
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 任务完成");
        }, "Thread-A");

        // 线程 2:尝试先获取 lock2,再获取 lock1
        Thread thread2 = new Thread(() -> {
            synchronized (lock2) { // 注意:这里先获取 lock2
                System.out.println(Thread.currentThread().getName() + ": 已获取 lock2");
                try {
                    Thread.sleep(100); // 模拟一些工作,给 Thread 1 机会获取 lock1
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                System.out.println(Thread.currentThread().getName() + ": 尝试获取 lock1...");
                synchronized (lock1) { // 注意:这里尝试获取 lock1
                    System.out.println(Thread.currentThread().getName() + ": 已获取 lock1");
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 任务完成");
        }, "Thread-B");

        thread1.start();
        thread2.start();
    }
}

运行结果:

Thread-A: 已获取 lock1
Thread-B: 已获取 lock2
Thread-A: 尝试获取 lock2...
Thread-B: 尝试获取 lock1..
//程序卡死