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

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就获得了这两把锁. 
 
再次运行程序, 此时程序打印如下 . 可以看到线程2两次获取锁1 失败 , 两次获得了CPU的执行权, 可能是由于线程1休眠时间过长导致的. 
线程2重复2次失败获取锁1失败后, 线程1苏醒, 获得了2把锁, 并且释放了两把锁, 线程2之后也获得了2把锁. 

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

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

多用并发集合, 而不是用同步集合. 
例如用 ConcurrentHashMap , 而不是使用下图中 Collections 工具类提供的同步集合. 因为同步集合性能低 

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

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

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

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

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

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

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

6. 避免锁的嵌套

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

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. 专锁专用

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

posted @ 2020-09-30 16:21  小琪琪来啦  阅读(3339)  评论(0编辑  收藏  举报
欢迎大家来到我的博客:武汉雅思 | 武汉托福 | 新航道 | 雅思培训 | dnfsf | 天龙sf | 热血江湖sf | 天龙sf | dnfsf