Loading

[Java并发]避免死锁

破坏死锁的四个条件
银行家算法
按顺序请求资源
尽量避免嵌套锁
尝试锁定trylock

避免死锁 是并发编程中的一个重要问题。死锁是指多个线程在等待彼此持有的资源,导致无法继续执行的状态。在 Java 中,死锁通常发生在多线程程序中,尤其是在使用同步块、锁和其他并发机制时。

避免死锁有几种常见的策略和技术,下面详细介绍:

1. 资源申请顺序一致(避免循环等待)

问题: 死锁的一个重要条件是线程在不同的顺序上请求资源,导致循环等待的情况发生。

解决方法: 通过规定资源获取的顺序,使所有线程都按照相同的顺序请求锁或资源,避免产生循环等待。

示例:

class Resource1 {}
class Resource2 {}

public class DeadlockAvoidance {
    private final Resource1 r1 = new Resource1();
    private final Resource2 r2 = new Resource2();

    public void method1() {
        synchronized (r1) {
            synchronized (r2) {
                // 对资源 r1 和 r2 的操作
                System.out.println("Method 1");
            }
        }
    }

    public void method2() {
        synchronized (r1) { // 保持与 method1 相同的锁获取顺序
            synchronized (r2) {
                // 对资源 r1 和 r2 的操作
                System.out.println("Method 2");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockAvoidance da = new DeadlockAvoidance();
        new Thread(da::method1).start();
        new Thread(da::method2).start();
    }
}

解释: 在 method1()method2() 中,两个线程获取资源的顺序是相同的(先 r1,再 r2)。如果每个线程都按照相同的顺序获取资源,循环等待就不会发生,从而避免了死锁。

2. 尝试锁定(使用 tryLock

问题: 当线程获取多个锁时,如果某个锁被占用,线程会一直等待,可能造成死锁。

解决方法: 使用 java.util.concurrent.locks.Lock 接口提供的 tryLock() 方法,它允许线程尝试获取锁。如果锁不可用,线程可以选择不等待,而是执行其他逻辑,避免死锁。

示例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TryLockExample {
    private final Lock lock1 = new ReentrantLock();
    private final Lock lock2 = new ReentrantLock();

    public void tryLockMethod1() {
        try {
            if (lock1.tryLock() && lock2.tryLock()) {
                try {
                    // 对资源的操作
                    System.out.println("Method 1 acquired both locks");
                } finally {
                    lock2.unlock();
                    lock1.unlock();
                }
            } else {
                System.out.println("Method 1 failed to acquire locks");
            }
        } finally {
            // 释放锁的操作
        }
    }

    public void tryLockMethod2() {
        try {
            if (lock2.tryLock() && lock1.tryLock()) {
                try {
                    // 对资源的操作
                    System.out.println("Method 2 acquired both locks");
                } finally {
                    lock1.unlock();
                    lock2.unlock();
                }
            } else {
                System.out.println("Method 2 failed to acquire locks");
            }
        } finally {
            // 释放锁的操作
        }
    }

    public static void main(String[] args) {
        TryLockExample example = new TryLockExample();
        new Thread(example::tryLockMethod1).start();
        new Thread(example::tryLockMethod2).start();
    }
}

解释: 通过 tryLock() 方法,线程尝试获取锁。如果锁不可用,它可以立即返回 false,然后执行其他操作而不是一直等待,从而避免了死锁的发生。

3. 超时获取锁

问题: 在某些情况下,线程可能永远无法获取到锁,导致死锁。

解决方法: 使用带有超时的 tryLock(long timeout, TimeUnit unit) 方法来防止无限期等待锁。线程尝试在给定的时间内获取锁,如果超时,它将放弃对锁的请求,从而减少死锁的可能性。

示例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

public class LockTimeoutExample {
    private final Lock lock1 = new ReentrantLock();
    private final Lock lock2 = new ReentrantLock();

    public void method1() {
        try {
            if (lock1.tryLock(1000, TimeUnit.MILLISECONDS)) {
                try {
                    if (lock2.tryLock(1000, TimeUnit.MILLISECONDS)) {
                        try {
                            // 对资源的操作
                            System.out.println("Method 1 acquired both locks");
                        } finally {
                            lock2.unlock();
                        }
                    } else {
                        System.out.println("Method 1 failed to acquire lock2");
                    }
                } finally {
                    lock1.unlock();
                }
            } else {
                System.out.println("Method 1 failed to acquire lock1");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void method2() {
        try {
            if (lock2.tryLock(1000, TimeUnit.MILLISECONDS)) {
                try {
                    if (lock1.tryLock(1000, TimeUnit.MILLISECONDS)) {
                        try {
                            // 对资源的操作
                            System.out.println("Method 2 acquired both locks");
                        } finally {
                            lock1.unlock();
                        }
                    } else {
                        System.out.println("Method 2 failed to acquire lock1");
                    }
                } finally {
                    lock2.unlock();
                }
            } else {
                System.out.println("Method 2 failed to acquire lock2");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        LockTimeoutExample example = new LockTimeoutExample();
        new Thread(example::method1).start();
        new Thread(example::method2).start();
    }
}

解释: 如果线程无法在指定的超时时间内获取到锁,它将放弃锁的请求,避免死锁的产生。

4. 减少锁的使用范围

问题: 死锁通常发生在锁持有的时间过长或锁的范围过大时。

解决方法: 只在真正需要保护共享资源时获取锁,尽量减少锁的使用范围(即缩小同步块的范围),从而减少产生死锁的概率。

示例:

public class NarrowLockScope {
    private final Object lock = new Object();

    public void doSomething() {
        // 在非关键代码区不使用锁
        System.out.println("Doing something outside lock");

        synchronized (lock) {
            // 仅在真正需要同步时使用锁
            System.out.println("Doing something with lock");
        }
    }
}

解释: 锁只用于保护共享资源的关键部分,减少了持有锁的时间,降低了发生死锁的可能性。

5. 避免嵌套锁

问题: 嵌套锁的使用容易导致死锁。即一个线程在持有锁 A 的同时,试图去获取锁 B,另一线程在持有锁 B 时试图获取锁 A,这可能导致死锁。

解决方法: 避免嵌套锁,尽量保持获取单个锁的原则。

示例:

public class SingleLockExample {
    private final Object lock = new Object();

    public void doSomething() {
        synchronized (lock) {
            // 执行需要同步的操作
            System.out.println("Doing something");
        }
    }
}

解释: 使用一个锁,避免多个线程同时竞争多个锁,避免了嵌套锁带来的死锁问题。

6. 使用高级并发工具

问题: 手动管理锁的使用容易导致编程复杂性增加,且容易引发死锁问题。

解决方法: 使用 Java 提供的高级并发工具类,如 java.util.concurrent 包中的 SemaphoreCountDownLatchCyclicBarrierConcurrentHashMap 等来避免死锁。这些工具类通常经过良好的设计和优化,可以帮助简化并发代码,降低死锁风险。

示例:

Semaphore semaphore = new Semaphore(1);

public void doSomething() {
    try {
        semaphore.acquire(); // 获取信号量
        // 执行需要同步的操作
        System.out.println("Doing something with semaphore");
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        semaphore.release(); // 释放信号量
    }
}

解释: 使用 Semaphore 可以有效管理资源的并发访问,避免传统锁机制引发的死锁问题。

总结

为了避免死锁,可以采取以下策略:

  • 确保资源获取的顺序一致,避免循环等待。
  • 使用 tryLock() 或设置
posted @ 2024-09-28 22:59  Duancf  阅读(149)  评论(0)    收藏  举报