Bootstrap

【JUC】Java锁介绍

文章目录

阿里锁开发规范

【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。说明:使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法(RPC涉及网络通讯,延迟较高)。

乐观锁和悲观锁

悲观锁

  • 认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改
  • synchronized和Lock的实现类都是悲观锁,适合写操作多的场景,先加锁可以保证写操作时数据正确
  • 显示的锁定之后再操作同步资源-----狼性锁

【synchronized】

// 悲观锁的调用方式
public synchronized void m1(){
    // 加锁后的业务逻辑......
}

【Lock】

// 保证多个线程使用的是同一个Lock对象的前提下
ReentrantLock lock = new ReentrantLock();
public void m2() {
    lock.lock();
    try {
        // 操作同步资源
    } finally {
        lock.unlock();
    }
}

乐观锁

  • 认为自己在使用数据的时候不会有别的线程修改数据或资源,不会添加锁
  • Java中使用无锁编程来实现,只是在更新的时候去判断,之前有没有别的线程更新了这个数据
    • 如果这个数据没有被更新,当前线程将自己修改的数据成功写入
    • 如果已经被其他线程更新,则根据不同的实现方式执行不同的操作,比如:放弃修改、重试抢锁等等。判断规则有:
      • 版本号机制Version(获取数据的时候版本是1,修改之后,版本号是2;如果修改的时候发现版本号不再是1,说明已经有其他线程修改了,)
      • 最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
  • 适合读操作多的场景,不加锁的特性能够使其读操作的性能大幅提升,乐观锁则直接去操作同步资源,是一种无锁算法
// 悲观锁的调用方式
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

synchronized 类锁、对象锁

synchronized有三种应用方式

  • 作用于实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁
  • 作用于代码块,对括号里配置的对象加锁
  • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁

锁相关的8种案例演示(对象锁、类锁)

标准访问ab两个线程,请问先打印邮件还是短信?

/**
* 资源类
**/
class Phone {
    public synchronized void sendEmail() {
        System.out.println("------sendEmail");
    }
    public synchronized void sendSMS() {
        System.out.println("------sendSMS");
    }
}

public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();
        try {
            // 保存a线程启动之后再来启动b线程
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone.sendSMS();
        }, "b").start();
    }
}
  • 答:先邮件,后短信 ,因为共用一个对象锁(sendEmail执行完成才能执行sendSMS)
  • 例如两个人去用一台手机,等第一个人用手机发完邮件,第二个人才可以发短信
  • 一个对象里面如果有多个synchronized方法,某一个时刻内,只有一个线程能去调用其中的一个synchronized方法了,其它的线程都只能等待。换句话然某一个时刻内,只能有唯一的一个线程去访问其中一个synchronized方法。锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它synchronized方法

sendEmail钟加入暂停3秒钟,请问先打印邮件还是短信?

/**
* 资源类
**/
class Phone {
    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------sendEmail");
    }
    public synchronized void sendSMS() {
        System.out.println("------sendSMS");
    }
}

public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();
        try {
            // 保存a线程启动之后再来启动b线程
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone.sendSMS();
        }, "b").start();
    }
}
  • 答:先邮件,后短信 共用一个对象锁

添加一个普通的hello方法,然后在b线程中调用,请问先打印普通方法还是邮件?

/**
* 资源类
**/
class Phone {
    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------sendEmail");
    }
    public synchronized void sendSMS() {
        System.out.println("------sendSMS");
    }
    public void hello() {
        System.out.println("------hello");
    }
}

public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();
        try {
            // 保存a线程启动之后再来启动b线程
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone.hello();
        }, "b").start();
    }
}
  • 答:先hello,再邮件
  • 你用手机来发邮件,我借你手机充电线来充电,不发生资源的争抢

有两部手机,请问先打印邮件还是短信?

/**
* 资源类
**/
class Phone {
    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------sendEmail");
    }
    public synchronized void sendSMS() {
        System.out.println("------sendSMS");
    }
    public void hello() {
        System.out.println("------hello");
    }
}

public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone1.sendEmail();
        }, "a").start();
        try {
            // 保存a线程启动之后再来启动b线程
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone2.sendSMS();
        }, "b").start();
    }
}
  • 答:先短信后邮件,资源没有争抢,不是同一个对象锁

有两个静态同步方法,一步手机, 请问先打印邮件还是短信?

/**
* 资源类
**/
class Phone {
    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------sendEmail");
    }
    public static synchronized void sendSMS() {
        System.out.println("------sendSMS");
    }
}

public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();
        try {
            // 保存a线程启动之后再来启动b线程
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone.sendSMS();
        }, "b").start();
    }
}
  • 答:先邮件后短信 ,共用一个类锁

有两个静态同步方法,两部手机, 请问先打印邮件还是短信?

/**
* 资源类
**/
class Phone {
    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------sendEmail");
    }
    public static synchronized void sendSMS() {
        System.out.println("------sendSMS");
    }
    public void hello() {
        System.out.println("------hello");
    }
}

public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone1.sendEmail();
        }, "a").start();
        try {
            // 保存a线程启动之后再来启动b线程
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone2.sendSMS();
        }, "b").start();
    }
}
  • 答:先邮件后短信,共用一个类锁

有一个静态同步方法 一个普通同步方法,请问先打印邮件还是短信?

/**
* 资源类
**/
class Phone {
    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------sendEmail");
    }
    public synchronized void sendSMS() {
        System.out.println("------sendSMS");
    }
}

public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            phone.sendEmail();
        }, "a").start();
        try {
            // 保存a线程启动之后再来启动b线程
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone.sendSMS();
        }, "b").start();
    }
}
  • 答:先短信后邮件(邮件有延迟) ,一个用类锁一个用对象锁,两个锁不一样,不产生竞争

有一个静态同步方法,一个普通同步方法,两部手机,请问先打印邮件还是短信?

/**
* 资源类
**/
class Phone {
    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("------sendEmail");
    }
    public synchronized void sendSMS() {
        System.out.println("------sendSMS");
    }
    public void hello() {
        System.out.println("------hello");
    }
}

public class Lock8Demo {
    public static void main(String[] args) {
        Phone phone1 = new Phone();
        Phone phone2 = new Phone();
        new Thread(() -> {
            phone1.sendEmail();
        }, "a").start();
        try {
            // 保存a线程启动之后再来启动b线程
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            phone2.sendSMS();
        }, "b").start();
    }
}
  • 答:先短信后邮件,一个类锁一个对象锁

结论

  • 当一个线程试图访问同步代码时它首先必须得到锁,正常退出或抛出异常时必须释放锁
  • 对于普通同步方法,锁的是当前实例对象,通常指this(就是new出来的具体实例对象本身),一个对象的所有普通同步方法用的都是同一把锁(对象锁)。也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁
  • 对于静态同步方法,锁的是当前类的Class对象(唯一模板Class),如Phone.class(类锁)
  • 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞争的
  • 对于同步方法块,锁的是synchronized括号内的对象,传入什么锁什么
  • 一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁

在这里插入图片描述

在这里插入图片描述

类模板存储在方法区,new出来的对象在堆里面

notify方法说明

线程想持有一个锁的话,需要满足如下三个条件之一:

  • 执行一个synchronized实例方法
  • 执行同步代码块
  • 执行静态同步方法

从字节码角度分析synchronized实现

  • javap -c ***.class 文件反编译
  • 需要更多信息:javap -v *.class文件反编译
    • -v -verbose:输出附加信息(包括行号、本地变量表,反汇编等详细信息)
public class LockSyncDemo {
    Object object = new Object();

    public void m1() {
        synchronized (object) {
            System.out.println("----hello synchronized code block");
            throw new RuntimeException("-----exp");
        }
    }

    public synchronized void m2() {
        System.out.println("----hello synchronized m2");
    }

    public static synchronized void m3() {
        System.out.println("----hello static synchronized m3");
    }


    public static void main(String[] args) {

    }
}

在这里插入图片描述

javap -c
PS D:\Projects\juc_bilibili\target\classes\com\bilibili\juc\locks> javap -c .\LockSyncDemo.class
Compiled from "LockSyncDemo.java"
public class com.bilibili.juc.locks.LockSyncDemo {
  java.lang.Object object;

  public com.bilibili.juc.locks.LockSyncDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: new           #2                  // class java/lang/Object
       8: dup
       9: invokespecial #1                  // Method java/lang/Object."<init>":()V
      12: putfield      #3                  // Field object:Ljava/lang/Object;
      15: return

  public void m1();
    Code:
       0: aload_0
       1: getfield      #3                  // Field object:Ljava/lang/Object;
       4: dup
       5: astore_1
       6: monitorenter
       7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: ldc           #5                  // String ----hello synchronized code block
      12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      15: new           #7                  // class java/lang/RuntimeException
      18: dup
      19: ldc           #8                  // String -----exp
      21: invokespecial #9                  // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
      24: athrow
      25: astore_2
      26: aload_1
      27: monitorexit
      28: aload_2
      29: athrow
    Exception table:
       from    to  target type
           7    28    25   any

  public synchronized void m2();
    Code:
       0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #10                 // String ----hello synchronized m2
       5: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public static synchronized void m3();
    Code:
       0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #11                 // String ----hello static synchronized m3
       5: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public static void main(java.lang.String[]);
    Code:
       0: return
}
javap -v
PS D:\Projects\juc_bilibili\target\classes\com\bilibili\juc\locks> javap -v .\LockSyncDemo.class
Classfile /D:/Projects/juc_bilibili/target/classes/com/bilibili/juc/locks/LockSyncDemo.class
  Last modified 2024年7月16日; size 1125 bytes
  SHA-256 checksum 705394b1f660417439abfb88585f1fc108b5caf625d01bc4fbfa19a8f9aad705
  Compiled from "LockSyncDemo.java"
public class com.bilibili.juc.locks.LockSyncDemo
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #12                         // com/bilibili/juc/locks/LockSyncDemo
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 5, attributes: 1
Constant pool:
   #1 = Methodref          #2.#36         // java/lang/Object."<init>":()V
   #2 = Class              #37            // java/lang/Object
   #3 = Fieldref           #12.#38        // com/bilibili/juc/locks/LockSyncDemo.object:Ljava/lang/Object;
   #4 = Fieldref           #39.#40        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = String             #41            // ----hello synchronized code block
   #6 = Methodref          #42.#43        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #7 = Class              #44            // java/lang/RuntimeException
   #8 = String             #45            // -----exp
   #9 = Methodref          #7.#46         // java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
  #10 = String             #47            // ----hello synchronized m2
  #11 = String             #48            // ----hello static synchronized m3
  #12 = Class              #49            // com/bilibili/juc/locks/LockSyncDemo
  #13 = Utf8               object
  #14 = Utf8               Ljava/lang/Object;
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/bilibili/juc/locks/LockSyncDemo;
  #22 = Utf8               m1
  #23 = Utf8               StackMapTable
  #24 = Class              #49            // com/bilibili/juc/locks/LockSyncDemo
  #25 = Class              #37            // java/lang/Object
  #26 = Class              #50            // java/lang/Throwable
  #27 = Utf8               m2
  #28 = Utf8               m3
  #29 = Utf8               main
  #30 = Utf8               ([Ljava/lang/String;)V
  #31 = Utf8               args
  #32 = Utf8               [Ljava/lang/String;
  #33 = Utf8               MethodParameters
  #34 = Utf8               SourceFile
  #35 = Utf8               LockSyncDemo.java
  #36 = NameAndType        #15:#16        // "<init>":()V
  #37 = Utf8               java/lang/Object
  #38 = NameAndType        #13:#14        // object:Ljava/lang/Object;
  #39 = Class              #51            // java/lang/System
  #40 = NameAndType        #52:#53        // out:Ljava/io/PrintStream;
  #41 = Utf8               ----hello synchronized code block
  #42 = Class              #54            // java/io/PrintStream
  #43 = NameAndType        #55:#56        // println:(Ljava/lang/String;)V
  #44 = Utf8               java/lang/RuntimeException
  #45 = Utf8               -----exp
  #46 = NameAndType        #15:#56        // "<init>":(Ljava/lang/String;)V
  #47 = Utf8               ----hello synchronized m2
  #48 = Utf8               ----hello static synchronized m3
  #49 = Utf8               com/bilibili/juc/locks/LockSyncDemo
  #50 = Utf8               java/lang/Throwable
  #51 = Utf8               java/lang/System
  #52 = Utf8               out
  #53 = Utf8               Ljava/io/PrintStream;
  #54 = Utf8               java/io/PrintStream
  #55 = Utf8               println
  #56 = Utf8               (Ljava/lang/String;)V
{
  java.lang.Object object;
    descriptor: Ljava/lang/Object;
    flags: (0x0000)

  public com.bilibili.juc.locks.LockSyncDemo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: new           #2                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."<init>":()V
        12: putfield      #3                  // Field object:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 10: 0
        line 11: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  this   Lcom/bilibili/juc/locks/LockSyncDemo;

  public void m1();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: getfield      #3                  // Field object:Ljava/lang/Object;
         4: dup
         5: astore_1
         6: monitorenter
         7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc           #5                  // String ----hello synchronized code block
        12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        15: new           #7                  // class java/lang/RuntimeException
        18: dup
        19: ldc           #8                  // String -----exp
        21: invokespecial #9                  // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
        24: athrow
        25: astore_2
        26: aload_1
        27: monitorexit
        28: aload_2
        29: athrow
      Exception table:
         from    to  target type
             7    28    25   any
      LineNumberTable:
        line 14: 0
        line 15: 7
        line 16: 15
        line 17: 25
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      30     0  this   Lcom/bilibili/juc/locks/LockSyncDemo;
      StackMapTable: number_of_entries = 1
        frame_type = 255 /* full_frame */
          offset_delta = 25
          locals = [ class com/bilibili/juc/locks/LockSyncDemo, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]

  public synchronized void m2();
    descriptor: ()V
    flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #10                 // String ----hello synchronized m2
         5: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 21: 0
        line 22: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/bilibili/juc/locks/LockSyncDemo;

  public static synchronized void m3();
    descriptor: ()V
    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #11                 // String ----hello static synchronized m3
         5: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 25: 0
        line 26: 8

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 31: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "LockSyncDemo.java"

synchronized同步代码块

  • 使用的是monitorenter和monitorexit指令

    • public class LockSyncDemo {
          Object object = new Object();
      
          public void m1() {
              synchronized (object) {
                  System.out.println("----hello synchronized code block");
              }
          }
      
          public static void main(String[] args) {
      
          }
      }
      

在这里插入图片描述

  • 一般是一个monitorenter,两个monitorexit

  • public class LockSyncDemo {
        Object object = new Object();
    
        public void m1() {
            synchronized (object) {
                System.out.println("----hello synchronized code block");
                throw new RuntimeException("-----exp");
            }
        }
    
        public static void main(String[] args) {
    
        }
    }
    
  • 如果直接在同步方法里面抛异常,就只有一个monitorexit(极端情况)

-在这里插入图片描述

synchronized普通同步方法

  • 调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程会将现持有monitor锁,然后再执行该方法,最后在方法完成(无论是否正常结束)时释放monitor
    -在这里插入图片描述

synchronized静态同步方法

  • ACC_STATICACC_SYNCHRONIZED访问标志区分该方法是否是静态同步方法
    在这里插入图片描述

反编译synchronized锁的是什么

  • 管程(Monitors,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制
  • 方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACC SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法。当方法调用时,调用指令将会检查方法的ACC SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。

为什么任何一个对象都可以成为一个锁?

在这里插入图片描述

原因:每个类都继承自Object,每个对象天生都带着一个对象监视器ObjectMonitor,每一个锁住的对象都会和Monitor关联

源码分析

C++源码:ObjectMonitor.java—>ObjectMonitor.cpp—>ObjectMonitor.hpp

总结:指针指向Monitor对象(也称为管程或监视器)的真实地址。每个对象都存在着一个monitor与之关联,当一个monitor被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由OnjectMonitor实现的,其主要的数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现):

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

_owner记录现在哪个线程持有了当前对象

在这里插入图片描述

在这里插入图片描述

公平锁和非公平锁

何为公平锁/非公平锁

  • 公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似于排队买票,先来的人先买,后来的人在队尾排着,是公平的。Lock lock = new ReentrantLock(true)表示公平锁。
  • 非公平锁:是指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级反转或者饥饿的状态(某个线程一直得不到锁)。Lock lock = new ReentrantLock(false)表示非公平锁,后来的线程也可能先获得锁,默认为非公平锁。
/**
 * 资源类,模拟3个售票员卖完50张票
 */
class Ticket {
    private int number = 50;
    /**
     * 非公平锁
     */
    ReentrantLock lock = new ReentrantLock();

    public void sale() {
        lock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
            }
        } finally {
            lock.unlock();
        }
    }
}

public class SaleTicketDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        new Thread(() -> {
            for (int i = 0; i < 55; i++) ticket.sale();
        }, "a").start();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) ticket.sale();
        }, "b").start();
        new Thread(() -> {
            for (int i = 0; i < 55; i++) ticket.sale();
        }, "c").start();
    }
}

票全被a线程卖了,如果是公平锁,会一人卖一部分

在这里插入图片描述

ReentrantLock lock = new ReentrantLock(true);出入参数true,就变成了公平锁,这时候获得锁的线程是比较均匀的

在这里插入图片描述

面试题

  • 为什么会有公平锁/非公平锁的设计?为什么默认非公平?
    • 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分地利用CPU的时间片,尽量减少CPU空间状态时间
    • 使用多线程很重要的考量点是线程切换的开销(公平锁,线程切换频繁),当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得很大,所以就减少了线程的开销。
  • 什么时候用公平?什么时候用非公平?
    • 如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省了很多线程切换的时间,吞吐量自然就上去了;否则就用公平锁。

预埋伏AQS

抽象队列同步器

在这里插入图片描述

在这里插入图片描述

后续深入分析

可重入锁(递归锁)

概念说明

  • 指在同一线程在外层方法获取到锁的时侯,在进入该线程的内层方法会自动获取锁(前提,锁的是同一个对象),不会因为之前已经获取过还没释放而阻塞
  • 所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个**优点是可一定程度避免死锁。**假如不是可重入锁,若有1个由synchronized修饰的递归调用方法,程序第2次调用该方法就被自己阻塞了

在这里插入图片描述

可重入锁种类

隐式锁(即synchronized关键字使用的锁)

  • 在一个synchronized修饰的方法或者代码块的内部调用本类的其他synchronized修饰的方法或者代码块时,永远可以得到锁。

【test1】

public class ReEntryLockDemo {
    public static void main(String[] args) {
        final Object o = new Object();
        
        new Thread(() -> {
            synchronized (o) {
                System.out.println("---------------外层调用");
                synchronized (o) {
                    System.out.println("---------------中层调用");
                    synchronized (o) {
                        System.out.println("---------------内层调用");
                    }
                }
            }
        }, "t1").start();
        
    }
}

输出

---------------外层调用
---------------中层调用
---------------内层调用

【test2】

public class ReEntryLockDemo {
    public synchronized void m1() {
        //指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
        System.out.println(Thread.currentThread().getName() + "\t ----come in");
        m2();
        System.out.println(Thread.currentThread().getName() + "\t ----end m1");
    }

    public synchronized void m2() {
        System.out.println(Thread.currentThread().getName() + "\t ----come in");
        m3();
    }

    public synchronized void m3() {
        System.out.println(Thread.currentThread().getName() + "\t ----come in");
    }

    static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();

        new Thread(() -> {
            reEntryLockDemo.m1();
        },"t1").start();
    }
 }

不会死锁,且从头到尾只有一个线程

在这里插入图片描述

synchronized可重入原理分析

在这里插入图片描述

每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针

  • 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1
  • 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么JVM可以将其计数器加1,否则需要等待,直至持有线程释放该锁
  • 当执行monitorexit时,JVM 则需将锁对象的计数器减1。计数器为零代表锁已被释放

显式锁(如ReentrantLock)

显式锁需要人为调用lock方法来获得锁

 /**
 * 注意:加锁几次就需要解锁几次
 * ---------------外层调用
 * ---------------中层调用
 * ---------------内层调用
 */
Lock lock = new ReentrantLock();
new Thread(() -> {
    lock.lock();
    try {
        System.out.println("---------------外层调用");
        lock.lock();
        try {
            System.out.println("---------------中层调用");
            lock.lock();
            try {
                System.out.println("---------------内层调用");
            } finally {
                lock.unlock();
            }
        } finally {
            lock.unlock();
        }
    } finally {
        lock.unlock();
    }
}, "t2").start();

运行结果如下,说明ReentrantLock也是可重入锁

在这里插入图片描述

如果说unlock次数少于lock,最后线程1加的锁就没有完全解开,其他线程获取不到锁

public static void main(String[] args) {
    Lock lock = new ReentrantLock();
    new Thread(() -> {
        lock.lock();
        try {
            System.out.println("---------------外层调用");
            lock.lock();
            try {
                System.out.println("---------------中层调用");
                lock.lock();
                try {
                    System.out.println("---------------内层调用");
                } finally {
//                        lock.unlock();
                }
            } finally {
                lock.unlock();
            }
        } finally {
            lock.unlock();
        }
    }, "t2").start();

    new Thread(() -> {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t ----come in外层调用");
        } finally {
            lock.unlock();
        }
    }, "t2").start();

}

在这里插入图片描述

死锁及排查

概念

死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力干涉,则它们无法再继续推进下去。

产生原因

  • 系统资源不足
  • 进程运行推进顺序不合适
  • 系统资源分配不当

死锁案例

  • 线程A:持有锁A,想要锁B
  • 线程B:持有锁B,想要锁A
  • 两个线程,不再是可重入锁

在这里插入图片描述

public class DeadLockDemo {
    public static void main(String[] args) {
        final Object objectA = new Object();
        final Object objectB = new Object();

        new Thread(() -> {
            synchronized (objectA) {
                System.out.println(Thread.currentThread().getName() + "\t 自己持有A锁,希望获得B锁");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (objectB) {
                    System.out.println(Thread.currentThread().getName() + "\t 成功获得B锁");
                }
            }
        }, "A").start();

        new Thread(() -> {
            synchronized (objectB) {
                System.out.println(Thread.currentThread().getName() + "\t 自己持有B锁,希望获得A锁");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (objectA) {
                    System.out.println(Thread.currentThread().getName() + "\t 成功获得A锁");
                }
            }
        }, "B").start();
    }
}

发生死锁

在这里插入图片描述

如何排查死锁

纯命令

  • jps -l:查出正在运行的进程编号
  • jstack 进程编号:查看正在死锁的进程的信息
PS D:\Projects\juc_bilibili\target\classes\com\bilibili\juc\locks> jps -l
18608 com.bilibili.juc.locks.DeadLockDemo
19680 com.bilibili.juc.locks.ReEntryLockDemo
6132 org.jetbrains.jps.cmdline.Launcher
1144
32040 jdk.jcmd/sun.tools.jps.Jps
29452 org.jetbrains.idea.maven.server.RemoteMavenServer36
PS D:\Projects\juc_bilibili\target\classes\com\bilibili\juc\locks> jstack 18608
2024-07-17 10:05:45
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.361-b09 mixed mode):

"DestroyJavaVM" #25 prio=5 os_prio=0 tid=0x000002d62b66b000 nid=0x7ea8 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"B" #24 prio=5 os_prio=0 tid=0x000002d667d07000 nid=0x4c5c waiting for monitor entry [0x000000f9aa8ff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.bilibili.juc.locks.DeadLockDemo.lambda$main$1(DeadLockDemo.java:37)
        - waiting to lock <0x000000066b975db0> (a java.lang.Object)
        - locked <0x000000066b975dc0> (a java.lang.Object)
        at com.bilibili.juc.locks.DeadLockDemo$$Lambda$2/2046562095.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:750)

"A" #23 prio=5 os_prio=0 tid=0x000002d667cfc000 nid=0x276c waiting for monitor entry [0x000000f9aa7ff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.bilibili.juc.locks.DeadLockDemo.lambda$main$0(DeadLockDemo.java:23)
        - waiting to lock <0x000000066b975dc0> (a java.lang.Object)
        - locked <0x000000066b975db0> (a java.lang.Object)
        at com.bilibili.juc.locks.DeadLockDemo$$Lambda$1/1706234378.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:750)

"Service Thread" #22 daemon prio=9 os_prio=0 tid=0x000002d667a19800 nid=0x76ec runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread14" #21 daemon prio=9 os_prio=2 tid=0x000002d66796f000 nid=0x7d54 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread13" #20 daemon prio=9 os_prio=2 tid=0x000002d667973800 nid=0x7b14 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread12" #19 daemon prio=9 os_prio=2 tid=0x000002d667970800 nid=0x6e24 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread11" #18 daemon prio=9 os_prio=2 tid=0x000002d667972000 nid=0x6fcc waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread10" #17 daemon prio=9 os_prio=2 tid=0x000002d66796e000 nid=0x7d40 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread9" #16 daemon prio=9 os_prio=2 tid=0x000002d66796d800 nid=0x4c24 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread8" #15 daemon prio=9 os_prio=2 tid=0x000002d667971000 nid=0x46fc waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread7" #14 daemon prio=9 os_prio=2 tid=0x000002d66795c000 nid=0x7a8c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread6" #13 daemon prio=9 os_prio=2 tid=0x000002d667945000 nid=0x71f4 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread5" #12 daemon prio=9 os_prio=2 tid=0x000002d66792c000 nid=0x7e88 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread4" #11 daemon prio=9 os_prio=2 tid=0x000002d6653bc000 nid=0x6d48 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread3" #10 daemon prio=9 os_prio=2 tid=0x000002d6653af800 nid=0x7878 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x000002d6678fa000 nid=0x7d48 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x000002d6678f9000 nid=0x69ec waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x000002d6678f6800 nid=0x4f14 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x000002d665380000 nid=0x3dac runnable [0x000000f9a95fe000]
   java.lang.Thread.State: RUNNABLE
        at java.net.SocketInputStream.socketRead0(Native Method)
        at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
        at java.net.SocketInputStream.read(SocketInputStream.java:171)
        at java.net.SocketInputStream.read(SocketInputStream.java:141)
        at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
        at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
        at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
        - locked <0x000000066bce6df0> (a java.io.InputStreamReader)
        at java.io.InputStreamReader.read(InputStreamReader.java:184)
        at java.io.BufferedReader.fill(BufferedReader.java:161)
        at java.io.BufferedReader.readLine(BufferedReader.java:324)
        - locked <0x000000066bce6df0> (a java.io.InputStreamReader)
        at java.io.BufferedReader.readLine(BufferedReader.java:389)
        at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:53)

"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x000002d6651b5000 nid=0x68a8 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000002d66515e800 nid=0x7158 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000002d66282d800 nid=0x7934 in Object.wait() [0x000000f9a92ff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000066b788f08> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:150)
        - locked <0x000000066b788f08> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:171)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:188)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000002d66514c000 nid=0x7c58 in Object.wait() [0x000000f9a91ff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000066b786ba0> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x000000066b786ba0> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=2 tid=0x000002d62b714800 nid=0x6b14 runnable

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x000002d62b680000 nid=0x79b8 runnable

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x000002d62b681800 nid=0x7ef0 runnable

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000002d62b683000 nid=0x7894 runnable

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x000002d62b685800 nid=0x2824 runnable

"GC task thread#4 (ParallelGC)" os_prio=0 tid=0x000002d62b687800 nid=0x2bb4 runnable

"GC task thread#5 (ParallelGC)" os_prio=0 tid=0x000002d62b688800 nid=0x7c44 runnable

"GC task thread#6 (ParallelGC)" os_prio=0 tid=0x000002d62b68b800 nid=0x79b4 runnable

"GC task thread#7 (ParallelGC)" os_prio=0 tid=0x000002d62b68c000 nid=0x26f0 runnable

"GC task thread#8 (ParallelGC)" os_prio=0 tid=0x000002d62b68d000 nid=0x7c94 runnable

"GC task thread#9 (ParallelGC)" os_prio=0 tid=0x000002d62b68e000 nid=0x7fdc runnable

"GC task thread#10 (ParallelGC)" os_prio=0 tid=0x000002d62b68f800 nid=0x7edc runnable

"GC task thread#11 (ParallelGC)" os_prio=0 tid=0x000002d62b693800 nid=0x3814 runnable

"GC task thread#12 (ParallelGC)" os_prio=0 tid=0x000002d62b694800 nid=0x1a4c runnable

"GC task thread#13 (ParallelGC)" os_prio=0 tid=0x000002d62b697800 nid=0x790c runnable

"GC task thread#14 (ParallelGC)" os_prio=0 tid=0x000002d62b698800 nid=0x7d64 runnable

"GC task thread#15 (ParallelGC)" os_prio=0 tid=0x000002d62b699800 nid=0x3cb4 runnable

"GC task thread#16 (ParallelGC)" os_prio=0 tid=0x000002d62b69c000 nid=0x7fb0 runnable

"GC task thread#17 (ParallelGC)" os_prio=0 tid=0x000002d62b6a0800 nid=0x79ec runnable

"GC task thread#18 (ParallelGC)" os_prio=0 tid=0x000002d62b69f800 nid=0x7020 runnable

"A":
  waiting to lock monitor 0x000002d66282d418 (object 0x000000066b975dc0, a java.lang.Object),
  which is held by "B"

Java stack information for the threads listed above:
===================================================
"B":
        at com.bilibili.juc.locks.DeadLockDemo.lambda$main$1(DeadLockDemo.java:37)
        - waiting to lock <0x000000066b975db0> (a java.lang.Object)
        - locked <0x000000066b975dc0> (a java.lang.Object)
        at com.bilibili.juc.locks.DeadLockDemo$$Lambda$2/2046562095.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:750)
"A":
        at com.bilibili.juc.locks.DeadLockDemo.lambda$main$0(DeadLockDemo.java:23)
        - waiting to lock <0x000000066b975dc0> (a java.lang.Object)
        - locked <0x000000066b975db0> (a java.lang.Object)
        at com.bilibili.juc.locks.DeadLockDemo$$Lambda$1/1706234378.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:750)

Found 1 deadlock.

在这里插入图片描述

图形化

  • jconsole:cmd执行

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

写锁(独占锁)/读锁(共享锁)

深度源码分析见后面

自旋锁spinLock

深度源码分析见后面

无锁->独占锁->读写锁->邮戳锁

深度源码分析见后面

无锁->偏向锁->轻量锁->重量锁

深度源码分析见后面

文章说明

该文章是本人学习 尚硅谷 的学习笔记,文章中大部分内容来源于 尚硅谷 的视频尚硅谷JUC并发编程(对标阿里P6-P7),也有部分内容来自于自己的思考,发布文章是想帮助其他学习的人更方便地整理自己的笔记或者直接通过文章学习相关知识,如有侵权请联系删除,最后对 尚硅谷 的优质课程表示感谢。

;