文章目录

Java 线程问题:线程死锁与线程安全

发布于 2026-04-05 23:39:53 · 浏览 14 次 · 评论 0 条

Java 线程问题:线程死锁与线程安全

在 Java 多线程编程中,线程死锁与线程安全是两个最核心、也是最容易被忽视的问题。一旦处理不当,你的程序可能会陷入无限等待,或者出现数据错乱、崩溃等严重故障。这篇文章将用最直白的方式,帮你彻底理解这两个问题的本质,并掌握实际的解决方法。


一、线程死锁:程序永远的"死循环"

1.1 什么是线程死锁

线程死锁指的是两个或多个线程互相持有对方需要的资源,同时又等待对方释放资源,导致所有线程都无法继续执行的状态。想象一个场景:两个人过独木桥,A 刚走到桥中间,B 也走到桥中间,两人都不愿意让步,结果谁也过不去——这就是死锁的直观比喻。

死锁一旦发生,程序中涉及的线程会永远处于阻塞状态,不会自动恢复。你只能通过重启程序来解决问题,这对于生产环境来说是灾难性的。

1.2 死锁产生的四个必要条件

死锁的发生必须同时满足以下四个条件,缺一不可。理解这四个条件是预防死锁的关键:

条件 说明 能否破坏
互斥 资源一次只能被一个线程使用 不能(这是资源本身的特性)
请求与保持 线程持有至少一个资源,同时等待其他资源
不可剥夺 资源在释放前,不能被强行夺走
循环等待 存在一个线程等待环

1.3 死锁代码示例

下面是一个典型的死锁案例。两个线程分别持有不同的锁,同时等待对方持有的锁:

public class DeadlockDemo {

    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("线程1: 持有锁A,等待锁B");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {}
                synchronized (lockB) {
                    System.out.println("线程1: 获得锁B");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("线程2: 持有锁B,等待锁A");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {}
                synchronized (lockA) {
                    System.out.println("线程2: 获得锁A");
                }
            }
        });

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

运行这段代码,你会发现程序永远停在那里,没有任何输出——这就是死锁。线程1持有lockA并等待lockB,线程2持有lockB并等待lockA,形成了一个循环等待链。

1.4 如何检测和预防死锁

检测死锁的方法:

  1. 使用 JConsole 或 jvisualvm:JDK 自带的图形化工具,可以实时查看线程状态,识别死锁。
  2. 使用 jstack 命令:在命令行执行 jstack <pid>,会输出线程堆栈信息,如果存在死锁,会明确显示 "Found one Java-level deadlock"。

预防死锁的策略:

  1. 破坏请求与保持条件:线程必须一次性申请所有需要的资源,要么全给,要么全不给。
  2. 破坏不可剥夺条件:当线程申请新资源失败时,必须释放已持有的所有资源。
  3. 破坏循环等待条件:对所有资源进行编号,线程必须按照编号顺序申请资源,不能反向或跳跃申请。

修改后的无死锁代码(按顺序申请):

public class NoDeadlockDemo {

    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            // 先申请 lockA,再申请 lockB
            synchronized (lockA) {
                System.out.println("线程1: 持有锁A");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockB) {
                    System.out.println("线程1: 获得锁B");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            // 同样先申请 lockA,再申请 lockB
            synchronized (lockA) {
                System.out.println("线程2: 持有锁A");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockB) {
                    System.out.println("线程2: 获得锁B");
                }
            }
        });

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

二、线程安全:多线程环境下的数据正确性

2.1 什么是线程安全

线程安全是指多个线程访问同一个对象或数据时,程序总能产生正确的结果。换句话说,无论操作系统如何调度这些线程,无论它们是同时执行还是交替执行,程序的最终状态都应该是可预期的。

举个反例:假设你有一个银行账户,余额为1000元。两个线程同时从该账户取钱,如果没有同步机制,可能出现以下情况:

初始余额: 1000
线程A读取余额: 1000
线程B读取余额: 1000
线程A计算并写入: 1000 - 500 = 500
线程B计算并写入: 1000 - 300 = 700  (错误!覆盖了线程A的结果)
最终余额: 700  (实际应扣800,余额200)

这就是典型的线程不安全导致的数据竞争

2.2 线程不安全的后果

线程不安全可能表现为以下几种形式:

现象 描述 示例
数据覆盖 一个线程的操作结果被另一个线程覆盖 银行账户余额计算错误
脏读 读取到未提交的数据 事务中的不可重复读
死循环 由于状态不一致,循环条件永远为真 计数器永远无法达到预期值
程序崩溃 异常状态导致程序异常终止 空指针、数组越界

2.3 如何实现线程安全

Java 提供了多种机制来保证线程安全,从简单到复杂依次为:

2.3.1 使用 synchronized 关键字

synchronized 是最基础的同步机制,它可以修饰方法或代码块,确保同一时刻只有一个线程能执行被修饰的代码:

public class Counter {
    private int count = 0;

    // synchronized 方法
    public synchronized void increment() {
        count++;
    }

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

synchronized 的特点包括:自动获取锁和释放锁、可重入(同一线程可多次获取同一把锁)、对阻塞线程公平处理。

2.3.2 使用 ReentrantLock

ReentrantLocksynchronized 的增强版,提供了更灵活的锁控制:

import java.util.concurrent.locks.ReentrantLock;

public class AdvancedCounter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock(true); // 公平锁

    public void increment() {
        lock.lock();  // 加锁
        try {
            count++;
        } finally {
            lock.unlock();  // 必须在 finally 中释放
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock 的优势在于:可尝试获取锁(tryLock())、可设置公平锁、支持Condition条件变量。

2.3.3 使用线程安全类

Java 并发包提供了大量线程安全的类,直接使用这些类可以简化开发:

线程安全类 用途 注意事项
AtomicInteger 原子整数操作 适用于单一变量的原子操作
ConcurrentHashMap 高性能并发 Map 不能锁定整个Map时使用
CopyOnWriteArrayList 读多写少的列表 写操作会复制整个数组
BlockingQueue 阻塞队列 适用于生产者-消费者模式
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicDemo {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();  // 原子操作,无需同步
    }

    public int getCount() {
        return count.get();
    }
}

2.3.4 使用 ThreadLocal

ThreadLocal 为每个线程提供独立的变量副本,各线程之间互不影响:

public class ThreadLocalDemo {
    private static final ThreadLocal<Integer> count = ThreadLocal.withInitial(() -> 0);

    public void increment() {
        count.set(count.get() + 1);
    }

    public int getCount() {
        return count.get();
    }
}

ThreadLocal 适用于:保存线程私有的配置信息、实现线程级别的单例模式、避免在方法间传递参数。

2.4 线程安全设计原则

在实际开发中,遵循以下原则可以有效避免线程安全问题:

  1. 最小化共享:尽量不要在多线程间共享可修改的状态,使用不可变对象或局部变量。
  2. 明确的可变边界:明确区分哪些状态是线程共享的,哪些是线程私有的。
  3. 优先使用高级并发工具:优先使用 ExecutorServiceConcurrentHashMapBlockingQueue 等并发工具类,而不是直接使用低级的 synchronized
  4. 保持同步域小而简单:同步代码块应尽可能小,逻辑应尽可能简单,避免在同步块内调用可能阻塞的方法。

三、实战:综合案例

下面是一个综合运用线程安全和死锁避免策略的完整示例——模拟一个简单的转账系统:

import java.util.concurrent.locks.ReentrantLock;
import java.util.HashMap;
import java.util.Map;

public class BankTransferSystem {

    static class Account {
        private final String id;
        private double balance;
        private final ReentrantLock lock = new ReentrantLock();

        Account(String id, double balance) {
            this.id = id;
            this.balance = balance;
        }

        boolean transferTo(Account target, double amount) {
            // 获取两把锁的顺序:按 id 排序,避免死锁
            Account first = this.id.compareTo(target.id) < 0 ? this : target;
            Account second = this.id.compareTo(target.id) < 0 ? target : this;

            first.lock.lock();
            second.lock.lock();

            try {
                if (this.balance < amount) {
                    System.out.println("余额不足,转账失败");
                    return false;
                }

                this.balance -= amount;
                target.balance += amount;

                System.out.println("从 " + this.id + " 转账 " + amount 
                    + " 到 " + target.id + " 成功");
                System.out.println(this.id + " 余额: " + this.balance 
                    + ", " + target.id + " 余额: " + target.balance);
                return true;

            } finally {
                second.lock.unlock();
                first.lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Map<String, Account> accounts = new HashMap<>();
        accounts.put("A", new Account("A", 1000));
        accounts.put("B", new Account("B", 1000));
        accounts.put("C", new Account("C", 1000));

        Runnable transferTask = () -> {
            String[] ids = {"A", "B", "C"};
            for (int i = 0; i < 10; i++) {
                String from = ids[(int)(Math.random() * 3)];
                String to = ids[(int)(Math.random() * 3)];
                while (from.equals(to)) {
                    to = ids[(int)(Math.random() * 3)];
                }
                double amount = Math.random() * 200 + 50;
                accounts.get(from).transferTo(accounts.get(to), amount);
            }
        };

        Thread[] threads = new Thread[5];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(transferTask);
            threads[i].start();
        }

        for (Thread t : threads) {
            try {
                t.join();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        System.out.println("所有转账完成");
    }
}

这个示例展示了几个关键点:按照固定顺序获取锁来避免死锁、使用细粒度锁减少锁竞争、确保锁一定在 finally 块中释放。


四、关键要点回顾

  • 死锁的四个必要条件是互斥、请求与保持、不可剥夺、循环等待,预防死锁需要破坏其中至少一个条件
  • 按固定顺序申请资源是避免死锁最简单有效的方法
  • 线程安全保证多线程环境下数据的正确性,核心是控制对共享资源的并发访问
  • 根据场景选择合适的同步机制:简单场景用 synchronized,复杂场景用 ReentrantLock,性能敏感场景考虑无锁算法

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文