Java多线程 开发中避免死锁的八种方法

文章目录

      • 1. 设置超时时间
      • 2. 多使用JUC包提供的并发类,而不是自己设计锁
      • 3. 尽量降低锁的使用粒度
      • 4. 尽量使用同步方法 而不是同步代码块
      • 5. 给线程起有意义的名字
      • 6. 避免锁的嵌套
      • 7. 分配锁资源之前先看能不能收回来资源
      • 8. 专锁专用

1. 设置超时时间

使用JUC包中的Lock接口提供的tryLock方法.
该方法在获取锁的时候, 可以设置超时时间, 如果超过了这个时间还没拿到这把锁, 那么就可以做其他的事情, 而不是像synchronized如果没有拿到锁会一直等待下去.

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

造成超时的原因有很多种:发生了死锁, 线程进入了死循环, 线程逻辑复杂执行慢.

到了超时时间, 那么就获取锁失败, 就可以做一些记录超过, 例如 打印错误日志, 发送报警邮件,提示运维人员重启服务等等.

如下的代码演示了 使用tryLock 来避免死锁的案例.
线程1 如果拿到了锁1 , 那么就在指定的800毫秒内去尝试拿到锁2, 如果两把锁都拿到了 , 那么就释放这两把锁. 如果在指定的时间内, 没有拿到锁2 , 那么就释放锁1 .

线程2 与线程1相反, 先去尝试拿到锁2, 如果拿到了, 就去在3s内尝试拿到锁1, 如果拿到了, 那么就释放锁1和2, 如果3s内没有拿到锁1, 那么释放锁2 .

package com.thread.deadlock;

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

/**
 * 类名称:TryLockDeadlock
 * 类描述:  使用lock接口提供的trylock 避免死锁
 *
 * @author: https://javaweixin6.blog.csdn.net/
 * 创建时间:2020/9/12 17:23
 * Version 1.0
 */
public class TryLockDeadlock implements Runnable {
     
    int flag = 1;

    //ReentrantLock 为可重入锁
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();

    public static void main(String[] args) {
     
        // 创建两个线程 给出不同的flag  并启动
        TryLockDeadlock r1 = new TryLockDeadlock();
        TryLockDeadlock r2 = new TryLockDeadlock();

        r1.flag = 1 ;
        r2.flag = 0 ;
        new Thread(r1).start();
        new Thread(r2).start();

    }

    @Override
    public void run() {
     
        for (int i = 0; i < 100; i++) {
     
            if (flag == 1) {
     
                //先获取锁1  再获取锁2

                try {
     
                    //给锁1 800毫秒与获取锁, 如果拿到锁, 返回true, 反之返回false
                    if (lock1.tryLock(800, TimeUnit.MICROSECONDS)) {
     

                        System.out.println("线程1获取到了锁1  ");

                        //随机的休眠
                        Thread.sleep(new Random().nextInt(1000));

                        if (lock2.tryLock(800, TimeUnit.MICROSECONDS)) {
     

                            System.out.println("线程1获取到了锁2  ");
                            System.out.println(" 线程1 成功获取了两把锁   ");
                            //释放两把锁, 退出循环
                            lock2.unlock();
                            lock1.unlock();
                            break;
                        } else {
     
                            System.out.println(" 线程1尝试获取锁2 失败, 已经重试  ");

                            //释放锁1
                            lock1.unlock();

                            //随机的休眠
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
     
                        System.out.println(" 线程1 获取锁1失败, 已重试  ");

                    }
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }

            if (flag == 0) {
     

                //先获取锁2  再获取锁1. 并且尝试获取锁的时间变长 ,改成3s

                try {
     

                    //给锁1 800毫秒与获取锁, 如果拿到锁, 返回true, 反之返回false
                    if (lock2.tryLock(3000, TimeUnit.MICROSECONDS)) {
     

                        System.out.println("线程2获取到了锁2  ");

                        //随机的休眠
                        Thread.sleep(new Random().nextInt(1000));

                        if (lock1.tryLock(3000, TimeUnit.MICROSECONDS)) {
     
                            System.out.println("线程2获取到了锁1  ");
                            System.out.println(" 线程2 成功获取了两把锁   ");
                            //释放两把锁, 退出循环
                            lock1.unlock();
                            lock2.unlock();
                            break;
                        } else {
     
                            System.out.println(" 线程2尝试获取锁1 失败, 已经重试  ");

                            //释放锁2
                            lock2.unlock();

                            //随机的休眠
                            Thread.sleep(new Random().nextInt(1000));
                        }
                    } else {
     
                        System.out.println(" 线程2 获取锁2失败, 已重试  ");

                    }
                } catch (InterruptedException e) {
     
                    e.printStackTrace();
                }
            }
        }
    }
}

运行程序后, 此时打印的情况如下:
线程1和2 ,分别拿到了锁1 和2 . 如果此时是用synchronized加锁的, 那么就会进入死循环的情况 , 因为 此时线程1是要去获取锁2的, 而此时锁2被线程2持有着 , 线程2此时要获取锁1 ,而锁1被线程2持有, 那么就会造成死锁.
而使用trylock后, 如下图打印, 线程1在尝试800ms获取锁2失败后, 释放了锁1, 那么此时锁2就获得了锁1, 线程2获得了两把锁, 释放了这两把锁, 接着线程1就获得了这两把锁.
Java多线程 开发中避免死锁的八种方法_第1张图片
再次运行程序, 此时程序打印如下 . 可以看到线程2两次获取锁1 失败 , 两次获得了CPU的执行权, 可能是由于线程1休眠时间过长导致的.
线程2重复2次失败获取锁1失败后, 线程1苏醒, 获得了2把锁, 并且释放了两把锁, 线程2之后也获得了2把锁.
Java多线程 开发中避免死锁的八种方法_第2张图片

2. 多使用JUC包提供的并发类,而不是自己设计锁

JDK1.5后, 有在JUC包提供并发类, 而不需要自己用wait 和notify来进行线程间的通信操作 , 这些成熟的并发类的已经考虑的场景很完备了.
ConcurrentHashMap ConcurrentLinkedQueue AtomicBoolean 等等
实际应用中java.util.concurrent.atomic 包中提供的类使用广泛, 简单方便, 并且效率比Lock更高.

多用并发集合, 而不是用同步集合.
即使用ConcurrentHashMap , 而不是使用下图中Collections工具类提供的同步集合. 因为同步集合性能低
Java多线程 开发中避免死锁的八种方法_第3张图片

3. 尽量降低锁的使用粒度

尽量降低锁的使用粒度 : 用不同的锁 ,而不是一个锁.
整个类如果使用一个锁来保护的话, 那么效率会很低, 而且有死锁的风险, 很多线程都来用这把锁的话, 就容易造成死锁.
锁的使用范围, 只要能满足业务要求, 越小越好.

4. 尽量使用同步方法 而不是同步代码块

如果能使用同步代码块, 就不要使用同步方法,
好处有两点

  1. 同步方法是把整个方法给加上锁给同步了, 范围较大, 使用同步代码块范围小
  2. 使用同步代码块, 可以自己指定锁的对象, 这样有了锁的控制权, 这样也能避免发生死锁

5. 给线程起有意义的名字

给线程起名字的时候, 便于在测试环境和生产环境排查bug和事故的时候快速定位问题.
一些开源的框架和JDK都遵循了给线程起名字的规范

6. 避免锁的嵌套

如下的文章, 必然发生死锁的例子中, 如下的代码就是锁的嵌套. 拿一个锁, 接着再拿一个锁. 并且使用的还是sleep这种不会释放锁的方式, 即拿到一个锁之后,不会去释放锁.
那么如果获取锁的顺序相反了, 就会造成死锁的发生!
https://javaweixin6.blog.csdn.net/article/details/108460550
Java多线程 开发中避免死锁的八种方法_第4张图片

7. 分配锁资源之前先看能不能收回来资源

分配锁资源之前先看能不能收回来资源: 即在分配给某个线程锁资源之前, 先计算一下如果分配出去了, 会不会造成死锁的情况, 也就是能不能回收得回来, 如果不能回收回来, 那么就会造成死锁, 那就不分配锁资源给这个线程 , 如果能回收回来, 那么就分配资源下去.

此种思想的实现有银行家算法来避免死锁的发生. 可以参考如下的文章
https://blog.csdn.net/u014634576/article/details/52600826

https://mp.weixin.qq.com/s?__biz=MzAwNzczMjk1NQ==&mid=400637315&idx=1&sn=f578bf6de58c1a57df07df310ae1ca1b&scene=1&srcid=0920DQXmm3IeDGyaJxxLz6oZ#wechat_redirect

https://www.cnblogs.com/128-cdy/p/12188340.html

8. 专锁专用

尽量不要几个功能用同一把锁. 来避免锁的冲突, 如果都用同一把锁, 那么就容易造成死锁.

你可能感兴趣的:(Java多线程基础与核心)