java中对锁的理解

模拟死锁代码:

public class LockLearn {
    
    
    public static void main(String[] args) {
    
    
           deadlock();
    }
    private static void deadlock()
    {
    
    
        Object lock1=new Object();
        Object lock2=new Object();
        //线程1 拥有 lock1 试图获取lock2
        new Thread(()->{
    
    
            synchronized (lock1){
    
    
                System.out.println("获取 lock1 成功");
                try {
    
    
                    TimeUnit.SECONDS.sleep(3);
                }catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                synchronized (lock2){
    
    
                    System.out.println(Thread.currentThread().getName());
                }
            }
        }).start();
        //线程2 拥有 lock2 试图获取lock1
        new Thread(()->{
    
    
            synchronized (lock2){
    
    
                System.out.println("获取 lock2 成功");
                try {
    
    
                    TimeUnit.SECONDS.sleep(3);
                }catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                synchronized (lock1){
    
    
                    System.out.println(Thread.currentThread().getName());
                }
            }
        }).start();

    }

}

线程1拥有lock1的情况下试图拥有lock2,线程2有lock2试图拥有lock1,造成死锁。

悲观锁

数据对外界的修改采取保守策略

它认为线程很容易会把数据修改掉

因此整个数据被修改的过程中都会采取锁定状态

知道一个线程使用完,其他线程才可以继续使用

演示代码:

public class LockLearn2 {
    
    
    public static void main(String[] args) {
    
    
        synchronized (LockLearn2.class)
        {
    
    
            System.out.println("这是一个悲观锁的演示");
        }
    }


}

编译代码

// class version 52.0 (52)
// access flags 0x21
public class com/example/tangtang/boot/launch/JVM/LockLearn2 {
    
    

  // compiled from: LockLearn2.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/example/tangtang/boot/launch/JVM/LockLearn2; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
    // parameter  args
    TRYCATCHBLOCK L0 L1 L2 null
    TRYCATCHBLOCK L2 L3 L2 null
   L4
    LINENUMBER 7 L4
    LDC Lcom/example/tangtang/boot/launch/JVM/LockLearn2;.class
    DUP
    ASTORE 1
    MONITORENTER
   L0
    LINENUMBER 9 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "\u8fd9\u662f\u4e00\u4e2a\u60b2\u89c2\u9501\u7684\u6f14\u793a"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L5
    LINENUMBER 10 L5
    ALOAD 1
    MONITOREXIT
   L1
    GOTO L6
   L2
   FRAME FULL [[Ljava/lang/String; java/lang/Object] [java/lang/Throwable]
    ASTORE 2
    ALOAD 1
    MONITOREXIT
   L3
    ALOAD 2
    ATHROW
   L6
    LINENUMBER 11 L6
   FRAME CHOP 1
    RETURN
   L7
    LOCALVARIABLE args [Ljava/lang/String; L4 L7 0
    MAXSTACK = 2
    MAXLOCALS = 3
}

MONITORENTER上锁

MONITOREXIT 解锁,释放资源

乐观锁

一般情况下数据在修改时不会出现冲突

在数据访问之前不会加锁

只有在数据提交更改时,才会对数据进行检测

java中的乐观锁大部分通过(CAS)compare and swap 比较并交换 操作实现的,CAS 是一个多线程同步的原子指令,CAS 操作包含三个重要的信息,即内存位置、预期原值和新值。如果内存位置的值和预期的原值相等的话,那么就可以把该位置的值更新为新值,否则不做任何修改。

CAS 可能会造成 ABA 的问题,ABA 问0题指的是,线程拿到了最初的预期原值 A,然而在将要进行 CAS 的时候,被其他线程抢占了执行权,把此值从 A 变成了 B,然后其他线程又把此值从 B 变成了 A,然而此时的 A 值已经并非原来的 A 值了,但最初的线程并不知道这个情况,在它进行 CAS 的时候,只对比了预期原值为 A 就进行了修改,这就造成了 ABA 的问题。

以警匪剧为例,假如某人把装了 100W 现金的箱子放在了家里,几分钟之后要拿它去赎人,然而在趁他不注意的时候,进来了一个小偷,用空箱子换走了装满钱的箱子,当某人进来之后看到箱子还是一模一样的,他会以为这就是原来的箱子,就拿着它去赎人了,这种情况肯定有问题,因为箱子已经是空的了,这就是 ABA 的问题。

ABA 的常见处理方式是添加版本号,每次修改之后更新版本号,拿上面的例子来说,假如每次移动箱子之后,箱子的位置就会发生变化,而这个变化的位置就相当于“版本号”,当某人进来之后发现箱子的位置发生了变化就知道有人动了手脚,就会放弃原有的计划,这样就解决了 ABA 的问题。

JDK 在 1.5 时提供了 AtomicStampedReference 类也可以解决 ABA 的问题,此类维护了一个“版本号” Stamp,每次在比较时不止比较当前值还比较版本号,这样就解决了 ABA 的问题。

乐观锁有一个优点,它在提交的时候才进行锁定的,因此不会造成死锁。

可重入锁

递归锁-指的是同一个线程,如果外面的函数拥有此锁之后,内层的函数也可以继续获取该锁

Java中synchronized和ReentrantLock都是可重入锁

可重入锁代码演示:

public class LockLearn3 {
    
    
    public static void main(String[] args) {
    
    
        reentrantA();
    }

    private synchronized static void reentrantA() {
    
    
        System.out.println(Thread.currentThread().getName()+"执行reentrantA");
        reentrantB();
    }

    private synchronized static void reentrantB() {
    
    
        System.out.println(Thread.currentThread().getName()+"执行reentrantB");
    }


}

从结果可以看出 reentrantA 方法和 reentrantB 方法的执行线程都是“main” ,我们调用了 reentrantA 方法,它的方法中嵌套了 reentrantB,如果 synchronized 是不可重入的话,那么线程会被一直堵塞。

可重入锁的实现原理,是在锁内部存储了一个线程标识,用于判断当前的锁属于哪个线程,并且锁的内部维护了一个计数器,当锁空闲时此计数器的值为 0,当被线程占用和重入时分别加 1,当锁被释放时计数器减 1,直到减到 0 时表示此锁为空闲状态。

共享锁和悲观锁

独占锁指的是在任何时候最多只能有一个线程持有该锁,比如 synchronized 就是独占锁,而 ReadWriteLock 读写锁允许同一时间内有多个线程进行读操作,它就属于共享锁。

独占锁可以理解为悲观锁,当每次访问资源时都要加上互斥锁,而共享锁可以理解为乐观锁,它放宽了加锁的条件,允许多线程同时访问该资源。

猜你喜欢

转载自blog.csdn.net/tangshuai96/article/details/111314542