跳至主要內容

Java锁

Jin...大约 20 分钟

Java锁

  1. 悲观锁

  2. 乐观锁

  3. 自旋锁

  4. 可重入锁(递归锁)

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

  6. 公平锁/非公平锁

  7. 死锁

  8. 偏向锁

  9. 轻量锁

  10. 重量锁

  11. 邮戳(票据)锁

1、乐观锁和悲观锁

1.1 悲观锁

  • 悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

  • 悲观锁的实现方式

    • synchronized关键字

    • Lock的实现类都是悲观锁

  • 适合写操作多的场景,先加锁可以保证写操作时数据正确。显示的锁定之后再操作同步资源。

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

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

1.2 乐观锁

  • 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作

  • 乐观锁的实现方式

    1. 版本号机制Version。(只要有人提交了就会修改版本号,可以解决ABA问题)

      • ABA问题:再CAS中想读取一个值A,想把值A变为C,不能保证读取时的A就是赋值时的A,中间可能有个线程将A变为B再变为A。
        • 解决方法:Juc包提供了一个AtomicStampedReference,原子更新带有版本号的引用类型,通过控制版本值的变化来解决ABA问题。
    2. 最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

  • 适合读操作多的场景,不加锁的性能特点能够使其操作的性能大幅提升。

//=============乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

2、锁相关的8钟案例

阿里巴巴代码规范

  • 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

  • 说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。

2.1 案例一

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

class Phone1 { //资源类
    public synchronized void sendEmail() {
        System.out.println("-----发送邮件");
    }

    public synchronized void sendSMS() {
        System.out.println("-----发送短信");
    }

}

public class Demo1 {
    public static void main(String[] args) {
        Phone1 phone = new Phone1();

        new Thread(phone::sendEmail, "a").start();

        //暂停毫秒,保证a线程先启动
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(phone::sendSMS, "b").start();
    }
}
image-20220707214509376
image-20220707214509376

2.2 案例二

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

class Phone2 { //资源类
    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----发送邮件");
    }

    public synchronized void sendSMS() {
        System.out.println("-----发送短信");
    }

}

public class Demo2 {
    public static void main(String[] args) {
        Phone2 phone = new Phone2();

        new Thread(phone::sendEmail, "a").start();

        //暂停毫秒,保证a线程先启动
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(phone::sendSMS, "b").start();
    }
}

打印结果:

image-20220707214958150
image-20220707214958150

2.3 案例三

添加一个普通的hello方法,请问先打印邮件还是hello? hello

class Phone3 { //资源类
    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----发送邮件");
    }

    public synchronized void sendSMS() {
        System.out.println("-----发送短信");
    }

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

}

public class Demo3 {
    public static void main(String[] args) {
        Phone3 phone = new Phone3();

        new Thread(phone::sendEmail, "a").start();

        //暂停毫秒,保证a线程先启动
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(phone::hello, "b").start();
    }
}

打印结果:

image-20220707215435253
image-20220707215435253

2.4 案例四

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

class Phone4 { //资源类
    public synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----发送邮件");
    }

    public synchronized void sendSMS() {
        System.out.println("-----发送短信");
    }

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

}

public class Demo4 {
    public static void main(String[] args) {
        Phone4 phone = new Phone4();
        Phone4 phone2 = new Phone4();

        new Thread(phone::sendEmail, "a").start();

        //暂停毫秒,保证a线程先启动
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(phone2::sendSMS, "b").start();
    }
}

打印结果:

image-20220707215747181
image-20220707215747181

2.5 案例五

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

class Phone5 { //资源类
    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----发送邮件");
    }

    public static synchronized void sendSMS() {
        System.out.println("-----发送短信");
    }

}

public class Demo5 {
    public static void main(String[] args) {
        Phone5 phone = new Phone5();

        new Thread(() -> phone.sendEmail(), "a").start();

        //暂停毫秒,保证a线程先启动
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> phone.sendSMS(), "b").start();
    }
}

打印结果:

image-20220707220253646
image-20220707220253646

2.6 案例六

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

class Phone6 { //资源类
    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----发送邮件");
    }

    public static synchronized void sendSMS() {
        System.out.println("-----发送短信");
    }

}

public class Demo6 {
    public static void main(String[] args) {
        Phone6 phone = new Phone6();
        Phone6 phone2 = new Phone6();

        new Thread(() -> phone.sendEmail(), "a").start();

        //暂停毫秒,保证a线程先启动
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> phone2.sendSMS(), "b").start();
    }
}

打印结果:

image-20220707220539416
image-20220707220539416

2.7 案例七

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

class Phone7 { //资源类
    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----发送邮件");
    }

    public synchronized void sendSMS() {
        System.out.println("-----发送短信");
    }

}

public class Demo7 {
    public static void main(String[] args) {
        Phone7 phone = new Phone7();

        new Thread(() -> phone.sendEmail(), "a").start();

        //暂停毫秒,保证a线程先启动
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> phone.sendSMS(), "b").start();
    }
}

打印结果:

image-20220707220909919
image-20220707220909919

2.8 案例八

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

class Phone8 { //资源类
    public static synchronized void sendEmail() {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("-----发送邮件");
    }

    public synchronized void sendSMS() {
        System.out.println("-----发送短信");
    }

}

public class Demo8 {
    public static void main(String[] args) {
        Phone8 phone = new Phone8();
        Phone8 phone2 = new Phone8();

        new Thread(() -> phone.sendEmail(), "a").start();

        //暂停毫秒,保证a线程先启动
        try {
            TimeUnit.MILLISECONDS.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(() -> phone2.sendSMS(), "b").start();
    }
}

打印结果:

image-20220707221137650
image-20220707221137650

2.9 原理

  • 1.2中

    • 一个对象里面如果有多个synchronized方法,某一时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程都只能是等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法,锁的是当前对象this,被锁定后,其它的线程都不能 进入到当前对象的其他synchronized方法
  • 3中

    • hello并未和其他synchronized修饰的方法产生争抢
  • 4 中

    • 锁在两个不同的对象/两个不同的资源上,不产生竞争条件
  • 5.6中static+synchronized - 类锁 phone = new Phone();中 加到了左边的Phone上

    • 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁→实例对象本身。

    • 对于静态同步方法,锁的是当前类的Class对象,如Phone,class唯一的一个模板。

    • 对于同步方法块,锁的是synchronized括号内的对象。synchronized(o)

  • 7.8中一个加了对象锁,一个加了类锁,不产生竞争条件

2.10 3个体现

  • 8种锁的案例实际体现在3个地方-相当于总结
    • 作用域实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁。
    • 作用于代码块,对括号里配置的对象加锁。
    • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁

2.11 字节码角度分析synchronized实现

2.11.1 文件反编译技巧

  • 文件反编译javap -c ***.class文件反编译,-c表示对代码进行反汇编
  • 假如需要更多信息 javap -v ***.class ,-v即-verbose输出附加信息(包括行号、本地变量表、反汇编等详细信息)

2.11.2 synchronized同步代码块

public class SynchronizedClassDemo {

    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) {

    }
}
  • 从target中找到SynchronizedClassDemo.class文件,右键,open in terminal,然后javap -c SynchronizedClassDemo.class
PS D:\Projects\Idea_Projects\Jin\java\target\classes\com\jin\juc\locks\analyse> javap -c .\SynchronizedClassDemo.class
Compiled from "SynchronizedClassDemo.java"score6  
public class com.jin.juc.locks.analyse.SynchronizedClassDemo {
  java.lang.Object object;

  public com.jin.juc.locks.analyse.SynchronizedClassDemo();
    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
}
PS D:\Projects\Idea_Projects\Jin\java\target\classes\com\jin\juc\locks\analyse>
  • 总结

synchronized同步代码块,实现使用的是moniterentermoniterexit指令(moniterexit可能有两个)

2.11.3 synchronized普通同步方法

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

    public static void main(String[] args) {

    }
}
  • 类似于上述操作,最后调用javap -v .\ObjectSyncDemo.class
PS D:\Projects\Idea_Projects\Jin\java\target\classes\com\jin\juc\locks\analyse> javap -v .\ObjectSyncDemo.class
Classfile /D:/Projects/Idea_Projects/Jin/java/target/classes/com/jin/juc/locks/analyse/ObjectSyncDemo.class
  Last modified 2022-7-8; size 705 bytes
  MD5 checksum bf5b93c8122ac3423dde6a22f3b2034f      
  Compiled from "ObjectSyncDemo.java"
public class com.jin.juc.locks.analyse.ObjectSyncDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#22         // java/lang/Object."<init>":()V
   #2 = Fieldref           #23.#24        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #25            // ------hello synchronized m2
   #4 = Methodref          #26.#27        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #28            // com/jin/juc/locks/analyse/ObjectSyncDemo
   #6 = Class              #29            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/jin/juc/locks/analyse/ObjectSyncDemo;
  #14 = Utf8               m2
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               MethodParameters
  #20 = Utf8               SourceFile
  #21 = Utf8               ObjectSyncDemo.java
  #22 = NameAndType        #7:#8          // "<init>":()V
  #23 = Class              #30            // java/lang/System
  #24 = NameAndType        #31:#32        // out:Ljava/io/PrintStream;
  #25 = Utf8               ------hello synchronized m2
  #26 = Class              #33            // java/io/PrintStream
  #27 = NameAndType        #34:#35        // println:(Ljava/lang/String;)V
  #28 = Utf8               com/jin/juc/locks/analyse/ObjectSyncDemo
  #29 = Utf8               java/lang/Object
  #30 = Utf8               java/lang/System
  #31 = Utf8               out
  #32 = Utf8               Ljava/io/PrintStream;
  #33 = Utf8               java/io/PrintStream
  #34 = Utf8               println
  #35 = Utf8               (Ljava/lang/String;)V
{
  public com.jin.juc.locks.analyse.ObjectSyncDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/jin/juc/locks/analyse/ObjectSyncDemo;

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

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 16: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "ObjectSyncDemo.java"
PS D:\Projects\Idea_Projects\Jin\java\target\classes\com\jin\juc\locks\analyse> 
  • 总结

调用指令将会检查方法的访问标志是否被设置。如果设置了,执行线程会将先持有monitore然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放monitor

2.11.4 synchronized静态同步方法

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

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


    public static void main(String[] args) {

    }
}
  • 和上面一样的操作
PS D:\Projects\Idea_Projects\Jin\java\target\classes\com\jin\juc\locks\analyse> javap -v .\StaticSyncDemo.class
Classfile /D:/Projects/Idea_Projects/Jin/java/target/classes/com/jin/juc/locks/analyse/StaticSyncDemo.class
  Last modified 2022-7-8; size 803 bytes
  MD5 checksum e604c22936a7cd5db424ed52eb36c1ea
  Compiled from "StaticSyncDemo.java"
public class com.jin.juc.locks.analyse.StaticSyncDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#24         // java/lang/Object."<init>":()V
   #2 = Fieldref           #25.#26        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #27            // ------hello synchronized m2
   #4 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #30            // ------hello synchronized m3---static
   #6 = Class              #31            // com/jin/juc/locks/analyse/StaticSyncDemo
   #7 = Class              #32            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/jin/juc/locks/analyse/StaticSyncDemo;
  #15 = Utf8               m2
  #16 = Utf8               m3
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               args
  #20 = Utf8               [Ljava/lang/String;
  #21 = Utf8               MethodParameters
  #22 = Utf8               SourceFile
  #23 = Utf8               StaticSyncDemo.java
  #24 = NameAndType        #8:#9          // "<init>":()V
  #25 = Class              #33            // java/lang/System
  #26 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
  #27 = Utf8               ------hello synchronized m2
  #28 = Class              #36            // java/io/PrintStream
  #29 = NameAndType        #37:#38        // println:(Ljava/lang/String;)V
  #30 = Utf8               ------hello synchronized m3---static
  #31 = Utf8               com/jin/juc/locks/analyse/StaticSyncDemo
  #32 = Utf8               java/lang/Object
  #33 = Utf8               java/lang/System
  #34 = Utf8               out
  #35 = Utf8               Ljava/io/PrintStream;
  #36 = Utf8               java/io/PrintStream
  #37 = Utf8               println
  #38 = Utf8               (Ljava/lang/String;)V
{
  public com.jin.juc.locks.analyse.StaticSyncDemo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/jin/juc/locks/analyse/StaticSyncDemo;

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

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

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 21: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "StaticSyncDemo.java"
PS D:\Projects\Idea_Projects\Jin\java\target\classes\com\jin\juc\locks\analyse> 
  • 总结
    • 访问标志区分该方法是否是静态同步方法。
    • 注意: m3()方法底层:flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED

2.11.5 反编译synchronized锁的是什么

概念-管程
  • 管程概念
    • 管程:Monitor(监视器),也就是我们平时说的锁。监视器锁
    • 信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。 管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
    • 执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管理。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。
大厂:为什么任何一个对象都可以成为一个锁?
  • 溯源
    • Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法
    • ObjectMonitor.javaObjectMonitor.cppobjectMonitor.hpp

ObjectMonitor.cpp中引入了头文件(include)objectMonitor.hpp

140ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //用来记录该线程获取锁的次数
    _waiters      = 0,
    _recursions   = 0;//锁的重入次数
    _object       = NULL;
    _owner        = NULL; //------最重要的----指向持有ObjectMonitor对象的线程,记录哪个线程持有了我
    _WaitSet      = NULL; //存放处于wait状态的线程队列
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;//存放处于等待锁block状态的线程队列
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }
  • 追溯底层可以发现每个对象天生都带着一个对象监视器

2.11.6 熟悉锁升级

  • synchronized必须作用于某个对象中,所以Java在对象的头文件存储了锁的相关信息。锁升级功能主要依赖于 MarkWord 中的锁标志位和释放偏向锁标志位

3、公平锁和非公平锁

3.1、ReentrantLock抢票案例

class Ticket //资源类,模拟3个售票员卖完50张票
{
    private int number = 50;

    Object lockObject = new Object();

    //    ReentrantLock lock = new ReentrantLock();//默认非公平锁
    ReentrantLock lock = new ReentrantLock(true);//公平锁

    public void sale() {
/*        synchronized (lockObject) {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
            }
        }*/
        lock.lock();
        try {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName() + "卖出第:\t" + (number--) + "\t 还剩下:" + number);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } 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();
    }
}

3.1、非公平锁

  • 默认是非公平锁

  • 非公平锁可以插队,买卖票不均匀。

  • 是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或饥饿的状态(某个线程一直得不到锁)

3.2、公平锁

  • ReentrantLock lock = new ReentrantLock(true);

  • 买卖票一开始a占优,后面a b c a b c a b c均匀分布

  • 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的。

3.3、为什么会有公平锁/非公平锁的设计?为什么默认是非公平?

  • 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间
  • 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

3.4、什么时候用公平?什么时候用非公平?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了; 否则那就用公平锁,大家公平使用。

4、可重入锁(又名递归锁)

4.1、可重入锁说明

  • 可重入锁又名递归锁

  • 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

  • 如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。

  • 所以Java中ReentrantLocksynchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

image-20220709170101588
image-20220709170101588

4.2、“可重入锁”详细解释

  • 可:可以
  • 重:再次
  • 入:进入
  • 锁:同步锁
  • 进入什么:进入同步域(即同步代码块/方法或显示锁锁定的代码)
  • 一句话:一个线程中的多个流程可以获取同一把锁,持有这把锁可以再次进入。自己可以获取自己的内部锁。

4.3、可重入锁种类

4.3.1、隐式锁Synchronized

  • synchronized是java中的关键字,默认可重入锁,即隐式锁

    • 在同步块中

      public class ReSynchronizedDemo {
          public static void main(String[] args) {
              final Object objectLockA = new Object();
              new Thread(() -> {
                  synchronized (objectLockA) {
                      System.out.println("-----外层调用");
                      synchronized (objectLockA) {
                          System.out.println("-----中层调用");
                          synchronized (objectLockA) {
                              System.out.println("-----内层调用");
                          }
                      }
                  }
              }, "a").start();
          }
      }
      
      image-20220709170951272
      image-20220709170951272
    • 在同步方法中

      public synchronized void m1() {
              //指的是可重复可递归调用的锁,在外层使用之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁
              System.out.println(Thread.currentThread().getName() + "\t" + "-----come in m1");
              m2();
              System.out.println(Thread.currentThread().getName() + "\t-----end m1");
          }
      
          public synchronized void m2() {
              System.out.println("-----m2");
              m3();
          }
      
          public synchronized void m3() {
              System.out.println("-----m3");
          }
      
          @Test
          public void demo() {
              ReSynchronizedDemo reEntryLockDemo = new ReSynchronizedDemo();
      
              reEntryLockDemo.m1();
          }
      
Synchronized的重入实现机理
  • 回看上方的ObjectMoitor.hpp

    140ObjectMonitor() {
        _header       = NULL;
        _count        = 0; //用来记录该线程获取锁的次数
        _waiters      = 0,
        _recursions   = 0;//锁的重入次数
        _object       = NULL;
        _owner        = NULL; //------最重要的----指向持有ObjectMonitor对象的线程,记录哪个线程持有了我
        _WaitSet      = NULL; //存放处于wait状态的线程队列
        _WaitSetLock  = 0 ;
        _Responsible  = NULL ;
        _succ         = NULL ;
        _cxq          = NULL ;
        FreeNext      = NULL ;
        _EntryList    = NULL ;//存放处于等待锁block状态的线程队列
        _SpinFreq     = 0 ;
        _SpinClock    = 0 ;
        OwnerIsThread = 0 ;
        _previous_owner_tid = 0;
      }
    
  • ObjectMoitor.hpp底层:每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。_count _owner

  • 首次加锁:当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。

  • 重入:在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。

  • 释放锁:当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。

4.3.2、显式锁Lock

  • 显式锁(即Lock)也有ReentrantLock这样的可重入锁

感觉所谓的显式隐式即是指显示/隐式的调用锁

  • 注意:lock unlock成对

        static Lock lock = new ReentrantLock();
    
        @Test
        public void demo1() {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t----come in 外层调用");
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + "\t------come in 内层调用");
                    } finally {
                        lock.unlock();
                    }
                } finally {
                    lock.unlock();
                }
            }, "t1").start();
        }
    
    image-20220709173125553
    image-20220709173125553
  • 假如lock unlock不成对,单线程情况下问题不大,但多线程下出问题

        public static void main(String[] args) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "\t----come in 外层调用");
                    lock.lock();
                    try {
                        System.out.println(Thread.currentThread().getName() + "\t------come in 内层调用");
                    } finally {
                        lock.unlock();
                    }
                } finally {
    //                lock.unlock();//-------------------------不成对|多线程情况
                }
            }, "t1").start();
    
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println("t2 ----外层调用lock");
                } finally {
                    lock.unlock();
                }
            }, "t2").start();
        }
    
image-20220709173837389
image-20220709173837389

5、死锁及排查

5.1、死锁是什么

  • 死锁

    • 是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。

    • a跟b两个资源互相请求对方的资源

  • 死锁产生的原因

    • 系统资源不足

    • 进程运行推进的顺序不合适

    • 资源分配不当

      image-20220709174240932
      image-20220709174240932

5.2、死锁案例

    public static void main(String[] args) {
        Object object1 = new Object();
        Object object2 = new Object();

        new Thread(() -> {
            synchronized (object1) {
                System.out.println(Thread.currentThread().getName() + "\t 持有a锁,想获得b锁");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }//使得线程b也启动
                synchronized (object2) {
                    System.out.println(Thread.currentThread().getName() + "\t 成功获得b锁");
                }
            }
        }, "A").start();

        new Thread(() -> {
            synchronized (object2) {
                System.out.println(Thread.currentThread().getName() + "\t 持有b锁,想获得a锁");
                synchronized (object1) {
                    System.out.println(Thread.currentThread().getName() + "\t 成功获得a锁");
                }
            }
        }, "B").start();
    }
image-20220709175146662
image-20220709175146662

5.3、如何排查死锁

纯命令

  • jps -l 查看当前进程运行状况
  • jstack 进程编号 查看该进程信息
image-20220709175809878
image-20220709175809878

图形化

  • win + r 输入jconsole ,打开图形化工具,打开线程 ,点击 检测死锁

    image-20220709180146218
    image-20220709180146218
image-20220709180226655
image-20220709180226655
image-20220709180432914
image-20220709180432914
image-20220709180453874
image-20220709180453874
贡献者: Jin
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度