51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 3875|回复: 0
打印 上一主题 下一主题

[转贴] 如何手动模拟一个死锁?

[复制链接]
  • TA的每日心情
    无聊
    3 天前
  • 签到天数: 1050 天

    连续签到: 1 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2021-2-22 10:21:42 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
     在并发编程中有两个重要的概念:线程和锁,多线程是一把双刃剑,在提升性能的同时也带来了编码的复杂性,对开发者的要求也提升了一个档次。而锁的出现就是为了保障多线程同时操作一组资源时的数据一致性,当我们在给资源加上锁之后,只有拥有此锁的线程才能操作此资源,而其他线程只能排队等待使用此锁。
      如何手动模拟一个锁?谈谈你对锁的理解
      死锁指的是两个线程同时占有两个资源,同时又在等待对方释放锁资源。如图:

      死锁的代码演示如下:
    1.  package lock;

    2.   import java.util.concurrent.TimeUnit;

    3.   public class TestDeadLock {

    4.       public static void main(String[] args) {

    5.           deadLock();

    6.       }

    7.       private static void deadLock() {

    8.           Object lock1 = new Object();

    9.           Object lock2 = new Object();

    10.           //线程1拥有 lock1 试图获取 lock2

    11.           new Thread(() -> {

    12.               synchronized (lock1) {

    13.                   System.out.println(Thread.currentThread().getName() + "获取lock1");

    14.                   try{

    15.                       TimeUnit.SECONDS.sleep(3);

    16.                       System.out.println(Thread.currentThread().getName() + "等待3秒");

    17.                   } catch (InterruptedException e) {

    18.                       e.printStackTrace();

    19.                   }

    20.                   synchronized (lock2) {

    21.                       System.out.println(Thread.currentThread().getName() + "获取lock2");

    22.                   }

    23.               }

    24.           }, "AAA").start();

    25.           //线程2拥有lock2

    26.           new Thread(() -> {

    27.               synchronized (lock2) {

    28.                   System.out.println(Thread.currentThread().getName() + "获取lock2");

    29.                   try{

    30.                       TimeUnit.SECONDS.sleep(3);

    31.                       System.out.println(Thread.currentThread().getName() + "等待3秒");

    32.                   } catch (InterruptedException e) {

    33.                       e.printStackTrace();

    34.                   }

    35.                   synchronized (lock1) {

    36.                       System.out.println(Thread.currentThread().getName() + "获取lock1");

    37.                   }

    38.               }

    39.           }, "BBB").start();

    40.       }

    41.   }
    复制代码
    以上程序执行结果如下:
    1.  AAA获取lock1

    2.   BBB获取lock2

    3.   BBB等待3秒

    4.   AAA等待3秒
    复制代码

     可以看出当我们使用线程一拥有锁 lock1 的同时试图获取 lock2,而线程二在拥有 lock2 的同时试图获取 lock1,这样就会造成彼此都在等待对方释放资源,于是就形成了死锁。  锁是指在并发编程中,当有多个线程同时操作一个资源时,为了保证数据操作的正确性,我们需要让多线程排队一个一个的操作此资源,而这个过程就是给资源加锁和释放锁的过程,就好像去公共厕所一样,必须一个一个排队使用,并且在使用时需要锁门和开门一样。
      ·什么是乐观锁和悲观锁?它们的应用都有哪些?乐观锁有什么问题?
      悲观锁指的是数据对外界的修改采取的保守策略,它认为线程很容易会把数据修改掉,因此在整个被修改的过程中都会采取上锁的状态,直到一个线程使用完,其他线程才可以继续使用。
      用synchronized来实现悲观锁源码如下:
    1. public class LockExample {

    2.       public static void main(String[] args) {

    3.           synchronized (LockExample.class) {

    4.               System.out.println("lock");

    5.           }

    6.       }

    7.   }
    复制代码
    我们使用反编译工具查到的结果如下:
    1.  Compiled from "LockExample.java"

    2.   public class com.lagou.interview.ext.LockExample {

    3.     public com.lagou.interview.ext.LockExample();

    4.       Code:

    5.          0: aload_0

    6.          1: invokespecial #1                  // Method java/lang/Object."<init>":()V

    7.          4: return

    8.     public static void main(java.lang.String[]);

    9.       Code:

    10.          0: ldc           #2                  // class com/lagou/interview/ext/LockExample

    11.          2: dup

    12.          3: astore_1

    13.          4: monitorenter // 加锁

    14.          5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;

    15.          8: ldc           #4                  // String lock

    16.         10: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

    17.         13: aload_1

    18.         14: monitorexit // 释放锁

    19.         15: goto          23

    20.         18: astore_2

    21.         19: aload_1

    22.         20: monitorexit

    23.         21: aload_2

    24.         22: athrow

    25.         23: return

    26.       Exception table:

    27.          from    to  target type

    28.              5    15    18   any

    29.             18    21    18   any

    30.   }
    复制代码
     可以看出被 synchronized 修饰的代码块,在执行之前先使用 monitorenter 指令加锁,然后在执行结束之后再使用 monitorexit 指令释放锁资源,在整个执行期间此代码都是锁定的状态,这就是典型悲观锁的实现流程。  乐观锁和悲观锁的概念恰好相反,乐观锁认为一般情况下数据在修改时不会出现冲突,所以在数据访问之前不会加锁,只是在数据提交更改时,才会对数据进行检测。
      Java 中的乐观锁大部分都是通过 CAS(Compare And Swap,比较并交换)操作实现的,CAS 是一个多线程同步的原子指令,CAS 操作包含三个重要的信息,即内存位置、预期原值和新值。如果内存位置的值和预期的原值相等的话,那么就可以把该位置的值更新为新值,否则不做任何修改。
      CAS 可能会造成 ABA 的问题,ABA 问题指的是,线程拿到了最初的预期原值 A,然而在将要进行 CAS 的时候,被其他线程抢占了执行权,把此值从 A 变成了 B,然后其他线程又把此值从 B 变成了 A,然而此时的 A 值已经并非原来的 A 值了,但最初的线程并不知道这个情况,在它进行 CAS 的时候,只对比了预期原值为 A 就进行了修改,这就造成了 ABA 的问题。
      以警匪剧为例,假如某人把装了 100W 现金的箱子放在了家里,几分钟之后要拿它去赎人,然而在趁他不注意的时候,进来了一个小偷,用空箱子换走了装满钱的箱子,当某人进来之后看到箱子还是一模一样的,他会以为这就是原来的箱子,就拿着它去赎人了,这种情况肯定有问题,因为箱子已经是空的了,这就是 ABA 的问题。
      ABA 的常见处理方式是添加版本号,每次修改之后更新版本号,拿上面的例子来说,假如每次移动箱子之后,箱子的位置就会发生变化,而这个变化的位置就相当于“版本号”,当某人进来之后发现箱子的位置发生了变化就知道有人动了手脚,就会放弃原有的计划,这样就解决了 ABA 的问题。
      JDK 在 1.5 时提供了 AtomicStampedReference 类也可以解决 ABA 的问题,此类维护了一个“版本号” Stamp,每次在比较时不止比较当前值还比较版本号,这样就解决了 ABA 的问题。
      相关源码如下:
    1.  public class AtomicStampedReference<V> {

    2.       private static class Pair<T> {

    3.           final T reference;

    4.           final int stamp; // “版本号”

    5.           private Pair(T reference, int stamp) {

    6.               this.reference = reference;

    7.               this.stamp = stamp;

    8.           }

    9.           static <T> Pair<T> of(T reference, int stamp) {

    10.               return new Pair<T>(reference, stamp);

    11.           }

    12.       }

    13.       // 比较并设置

    14.       public boolean compareAndSet(V   expectedReference,

    15.                                    V   newReference,

    16.                                    int expectedStamp, // 原版本号

    17.                                    int newStamp) { // 新版本号

    18.           Pair<V> current = pair;

    19.           return

    20.               expectedReference == current.reference &&

    21.               expectedStamp == current.stamp &&

    22.               ((newReference == current.reference &&

    23.                 newStamp == current.stamp) ||

    24.                casPair(current, Pair.of(newReference, newStamp)));

    25.       }

    26.       //.......省略其他源码

    27.   }
    复制代码
    可以看出它在修改时会进行原值比较和版本号比较,当比较成功之后会修改值并修改版本号。  小贴士:乐观锁有一个优点,它在提交的时候才进行锁定的,因此不会造成死锁。
      ·什么是可重入锁?用代码如何实现?它的实现原理是什么?
      可重入锁也叫递归锁,指的是同一个线程,如果外层函数拥有此锁之后,内层的函数也可以继续获取该锁。在Java语言中ReentrantLock和synchronized都是可重入锁。
      用synchronized来演示一下什么是可重入锁:
    1.  package lock;

    2.   public class TestReentrantLock {   

    3.       public static void main(String[] args) {

    4.           reentrantA();

    5.       }

    6.       private static synchronized void reentrantA() {

    7.           System.out.println(Thread.currentThread().getName() + " thread runs reentrantA()");

    8.           reentrantB();

    9.       }

    10.       private static synchronized void reentrantB() {

    11.           System.out.println(Thread.currentThread().getName() + " thread runs reentrantB()");

    12.       }

    13.   }
    复制代码
    程序执行结果如下:
    1. main thread runs reentrantA()

    2.   main thread runs reentrantB()
    复制代码
    从结果可以看出 reentrantA 方法和 reentrantB 方法的执行线程都是“main” ,我们调用了 reentrantA 方法,它的方法中嵌套了 reentrantB,如果 synchronized 是不可重入的话,那么线程会被一直堵塞。  可重入锁的实现原理,是在锁内部存储了一个线程标识,用于判断当前的锁属于哪个线程,并且锁的内部维护了一个计数器,当锁空闲时此计数器的值为 0,当被线程占用和重入时分别加 1,当锁被释放时计数器减 1,直到减到 0 时表示此锁为空闲状态。
      ·什么是共享锁和独占锁?
      只能被单线程持有的锁叫独占锁,可以被多线程持有的锁叫共享锁。
      独占锁指的是在任何时候最多只能有一个线程持有该锁,比如synchronized就是独占锁,而ReadWriteLock读写锁允许同一时间内有多个线程进行操作,它就属于共享锁。
      独占锁可以理解为悲观锁,当每次访问资源时都要加上互斥锁,而共享锁可以理解为乐观锁,它放宽了加锁的条件,允许多线程同时访问该资源。
      共享锁,又称为读锁,获得共享锁之后,可以查看但无法修改和删除数据。排他锁,又称为写锁、独占锁。获准排他锁后,既能读数据,又能修改数据。





    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏1
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-24 06:03 , Processed in 0.064914 second(s), 24 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表