03-JUC进阶-Java中的锁的解析

一、乐观锁和悲观锁

1. 悲观锁

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

悲观锁的实现方式

  • synchronized 关键字
  • Lock 的实现类都是悲观锁

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

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

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

2. 乐观锁

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

乐观锁的实现方式

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

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

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

二、synchronized 锁的8种情况

下面通过一段代码,演示 synchronized 锁的 8 种情况

  1. 标准访问,先打印短信还是邮件

    class Phone {
    
        public synchronized void sendSMS() throws Exception {
            System.out.println("------sendSMS");
        }
    
        public synchronized void sendEmail() throws Exception {
            System.out.println("------sendEmail");
        }
    }
    
    public class Lock_8 {
        public static void main(String[] args) throws Exception {
            Phone phone = new Phone();
    
            new Thread(() -> {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "AA").start();
    
            Thread.sleep(100);
    
            new Thread(() -> {
                try {
                    phone.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "BB").start();
        }
    }

    打印结果:

    ------sendSMS
    ------sendEmail

    分析:两个方法都加了 synchronized 关键字,因为是同一个对象调用,所有是同一把锁,按照顺序执行。

  2. 停4秒在短信方法内,先打印短信还是邮件

    class Phone {
    
        public synchronized void sendSMS() throws Exception {
            //停留4秒
            TimeUnit.SECONDS.sleep(4);
            System.out.println("------sendSMS");
        }
    
        public synchronized void sendEmail() throws Exception {
            System.out.println("------sendEmail");
        }
    }
    
    public class Lock_8 {
        public static void main(String[] args) throws Exception {
            Phone phone = new Phone();
    
            new Thread(() -> {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "AA").start();
    
            Thread.sleep(100);
    
            new Thread(() -> {
                try {
                    phone.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "BB").start();
        }
    }

    打印结果:

    ------sendSMS
    ------sendEmail

    分析:两个方法都加了 synchronized 关键字,因为还是同一个对象调用,所有还是同一把锁,先拿到锁的方法执行结束后,另一个方法才能执行。

  3. 新增普通的hello方法,是先打短信还是hello

    class Phone {
    
        public synchronized void sendSMS() throws Exception {
            //停留4秒
            TimeUnit.SECONDS.sleep(4);
            System.out.println("------sendSMS");
        }
    
        public synchronized void sendEmail() throws Exception {
            System.out.println("------sendEmail");
        }
    
        public void getHello() {
            System.out.println("------getHello");
        }
    }
    
    public class Lock_8 {
        public static void main(String[] args) throws Exception {
            Phone phone = new Phone();
    
            new Thread(() -> {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "AA").start();
    
            Thread.sleep(100);
    
            new Thread(() -> {
                try {
                    phone.getHello();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "BB").start();
        }
    }

    打印结果:

    ------getHello
    ------sendSMS

    分析:hello方法没有加锁,调用即执行,sendSMS有sleep阻塞。

  4. 现在有两部手机,先打印短信还是邮件

    class Phone {
        public synchronized void sendSMS() throws Exception {
            //停留4秒
            TimeUnit.SECONDS.sleep(4);
            System.out.println("------sendSMS");
        }
    
        public synchronized void sendEmail() throws Exception {
            System.out.println("------sendEmail");
        }
    }
    
    public class Lock_8 {
        public static void main(String[] args) throws Exception {
    
            Phone phone = new Phone();
            Phone phone2 = new Phone();
    
            new Thread(() -> {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "AA").start();
    
            Thread.sleep(100);
    
            new Thread(() -> {
                try {
                    phone2.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "BB").start();
        }
    }

    打印结果:

    ------sendEmail
    ------sendSMS

    分析:synchronized 在方法上,是方法级的锁,由于是两个 phone 对象调用不同的方法,所以 sendSMS() 和 sendEmail() 持有的是不同的锁,互不影响。

  5. 两个静态同步方法,1部手机,先打印短信还是邮件

    class Phone {
        public static synchronized void sendSMS() throws Exception {
            //停留4秒
            TimeUnit.SECONDS.sleep(4);
            System.out.println("------sendSMS");
        }
    
        public static synchronized void sendEmail() throws Exception {
            System.out.println("------sendEmail");
        }
    }
    
    public class Lock_8 {
        public static void main(String[] args) throws Exception {
            Phone phone = new Phone();
            
            new Thread(() -> {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "AA").start();
    
            Thread.sleep(100);
    
            new Thread(() -> {
                try {
                    phone.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "BB").start();
        }
    }

    打印结果:

    ------sendSMS
    ------sendEmail

    分析:synchronized 在静态方法上,是类级别的锁,所以两个方法持有的是同一把锁,要按顺序执行

  6. 两个静态同步方法,2部手机,先打印短信还是邮件

    class Phone {
    
        public static synchronized void sendSMS() throws Exception {
            //停留4秒
            TimeUnit.SECONDS.sleep(4);
            System.out.println("------sendSMS");
        }
    
        public static synchronized void sendEmail() throws Exception {
            System.out.println("------sendEmail");
        }
    }
    
    public class Lock_8 {
        public static void main(String[] args) throws Exception {
    
            Phone phone = new Phone();
            Phone phone2 = new Phone();
    
            new Thread(() -> {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "AA").start();
    
            Thread.sleep(100);
    
            new Thread(() -> {
                try {
                    phone2.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "BB").start();
        }
    }

    打印结果:

    ------sendSMS
    ------sendEmail

    分析:synchronized 在静态方法上,是类级别的锁,所以两个方法持有的是同一把锁,要按顺序执行

  7. 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件

    class Phone {
    
        public static synchronized void sendSMS() throws Exception {
            //停留4秒
            TimeUnit.SECONDS.sleep(4);
            System.out.println("------sendSMS");
        }
    
        public void getHello() {
            System.out.println("------getHello");
        }
    }
    
    public class Lock_8 {
        public static void main(String[] args) throws Exception {
            Phone phone = new Phone();
    
            new Thread(() -> {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "AA").start();
    
            Thread.sleep(100);
    
            new Thread(() -> {
                try {
                    phone.getHello();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "BB").start();
        }
    }

    打印结果:

    ------getHello
    ------sendSMS

    分析:类加锁对普通方法无限制,普通方法调用即执行。

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

    class Phone {
    
        public static synchronized void sendSMS() throws Exception {
            //停留4秒
            TimeUnit.SECONDS.sleep(4);
            System.out.println("------sendSMS");
        }
    
        public void sendEmail() throws Exception {
            System.out.println("------sendEmail");
        }
    }
    
    public class Lock_8 {
        public static void main(String[] args) throws Exception {
            Phone phone = new Phone();
            Phone phone2 = new Phone();
    
            new Thread(() -> {
                try {
                    phone.sendSMS();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "AA").start();
    
            Thread.sleep(100);
    
            new Thread(() -> {
                try {
                    phone2.sendEmail();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "BB").start();
        }
    }

    打印结果:

    ------sendEmail
    ------sendSMS

    分析:类加锁对普通方法无限制,普通方法调用即执行。

总结:

  • synchronized 锁的是方法,则是对象锁,同个对象锁的机制要等待,不同对象锁的机制调用同一个不用等待
  • 加了static则为class锁而不是对象锁
  • 对于同步方法块,锁是 synchronized 括号里配置对象

三、字节码角度分析 synchronized 实现

1. synchronized 同步代码块

1.1 文件反编译技巧

  • 文件反编译 javap -c ***.class 文件反编译,-c表示对代码进行反汇编

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

1.2 synchronized 同步代码块

/**
 * 锁同步代码块
 */
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) {

    }
}

1.3 class 文件反编译

执行命令 javap -c LockSyncDemo.class

public class com.georg.controller.LockSyncDemo {
  java.lang.Object object;

  public com.zhang.admin.controller.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: aload_1
      16: monitorexit        // **注**------退出锁
      17: goto          25
      20: astore_2
      21: aload_1
      22: monitorexit        //**注**-----这里又有一个exit, 目的当出现异常时,保证能够释放锁
      23: aload_2
      24: athrow
      25: return
    Exception table:
       from    to  target type
           7    17    20   any
          20    23    20   any

  public static void main(java.lang.String[]);
    Code:
       0: return
}

总结

  • synchronized 同步代码块,实现使用的是moniterentermoniterexit 指令( moniterexit 可能有两个)
  • 那一定是一个enter两个exit吗?(不一样,如果主动throw一个RuntimeException,发现一个enter,一个exit,还有两个athrow)

2. synchronized 普通同步方法

/**
 * 锁普通的同步方法
 */
public class LockSyncDemo {

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

    public static void main(String[] args) {

    }
}
  • 类似于上述操作,最后调用 javap -v LockSyncDemo.class
.....
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/zhang/admin/controller/LockSyncDemo;
......

总结

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

3. synchronized 静态同步方法

/**
 * 锁静态同步方法
 */
public class LockSyncDemo {

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

    }
}
  • 调用 javap -v LockSyncDemo.class
 ......
 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
......

总结

ACC_STATIC 访问标志区分该方法是否是静态同步方法。

四、反编译解析 synchronized 锁的是什么

1. 管程概念

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

2. 解释为什么任何一个对象都可以成为一个锁

Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。

  • ObjectMonitor.javaObjectMonitor.cppobjectMonitor.hpp

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

objectMonitor.hpp

  • objectMonitor.hpp

    属性 作用
    _owner 指向持有ObjectMonitor对象的线程
    _WaitSet 存放处于wait状态的线程队列
    _EntryList 存放处于等待锁block状态的线程队列
    _recursions 锁的重入次数
    _count 用来记录该线程获取锁的次数

    因此:每个对象天生都带着一个对象监视器

五、关于锁升级

这里只是简单提及,做个了解,后面会再做深入讲解

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

锁升级

六、公平锁和非公平锁

1. 案例演示

1.1 非公平锁

使用 ReentrantLock 实现抢票的案例

class Ticket {
    private int number = 30;
    private Lock 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 = 1; i <= 55; i++) ticket.sale();}, "a").start();
        new Thread(() -> {for (int i = 1; i <= 55; i++) ticket.sale();}, "b").start();
        new Thread(() -> {for (int i = 1; i <= 55; i++) ticket.sale();}, "c").start();
    }
}

运行结果,不同的线程抢到票的几率差距很大,线程执行不公平。

a	 卖出第: 30	 还剩下: 29
a	 卖出第: 29	 还剩下: 28
a	 卖出第: 28	 还剩下: 27
a	 卖出第: 27	 还剩下: 26
c	 卖出第: 26	 还剩下: 25
c	 卖出第: 25	 还剩下: 24
c	 卖出第: 24	 还剩下: 23
c	 卖出第: 23	 还剩下: 22
c	 卖出第: 22	 还剩下: 21
c	 卖出第: 21	 还剩下: 20
c	 卖出第: 20	 还剩下: 19
c	 卖出第: 19	 还剩下: 18
c	 卖出第: 18	 还剩下: 17
c	 卖出第: 17	 还剩下: 16
c	 卖出第: 16	 还剩下: 15
c	 卖出第: 15	 还剩下: 14
c	 卖出第: 14	 还剩下: 13
c	 卖出第: 13	 还剩下: 12
c	 卖出第: 12	 还剩下: 11
c	 卖出第: 11	 还剩下: 10
c	 卖出第: 10	 还剩下: 9
c	 卖出第: 9	 还剩下: 8
c	 卖出第: 8	 还剩下: 7
c	 卖出第: 7	 还剩下: 6
c	 卖出第: 6	 还剩下: 5
c	 卖出第: 5	 还剩下: 4
c	 卖出第: 4	 还剩下: 3
c	 卖出第: 3	 还剩下: 2
c	 卖出第: 2	 还剩下: 1
c	 卖出第: 1	 还剩下: 0

1.2 公平锁

使用 ReentrantLock 实现公平抢票的案例

class Ticket {
    private int number = 30;
    private Lock lock = new ReentrantLock(true); //默认用的是非公平锁,传 true 则为公平锁

    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 = 1; i <= 55; i++) ticket.sale();}, "a").start();
        new Thread(() -> {for (int i = 1; i <= 55; i++) ticket.sale();}, "b").start();
        new Thread(() -> {for (int i = 1; i <= 55; i++) ticket.sale();}, "c").start();
    }
}

执行结果,每个线程都得到了较为公平的执行机会。

a	 卖出第: 30	 还剩下: 29
a	 卖出第: 29	 还剩下: 28
a	 卖出第: 28	 还剩下: 27
a	 卖出第: 27	 还剩下: 26
b	 卖出第: 26	 还剩下: 25
c	 卖出第: 25	 还剩下: 24
a	 卖出第: 24	 还剩下: 23
b	 卖出第: 23	 还剩下: 22
c	 卖出第: 22	 还剩下: 21
a	 卖出第: 21	 还剩下: 20
b	 卖出第: 20	 还剩下: 19
c	 卖出第: 19	 还剩下: 18
a	 卖出第: 18	 还剩下: 17
b	 卖出第: 17	 还剩下: 16
c	 卖出第: 16	 还剩下: 15
a	 卖出第: 15	 还剩下: 14
b	 卖出第: 14	 还剩下: 13
c	 卖出第: 13	 还剩下: 12
a	 卖出第: 12	 还剩下: 11
b	 卖出第: 11	 还剩下: 10
c	 卖出第: 10	 还剩下: 9
a	 卖出第: 9	 还剩下: 8
b	 卖出第: 8	 还剩下: 7
c	 卖出第: 7	 还剩下: 6
a	 卖出第: 6	 还剩下: 5
b	 卖出第: 5	 还剩下: 4
c	 卖出第: 4	 还剩下: 3
a	 卖出第: 3	 还剩下: 2
b	 卖出第: 2	 还剩下: 1
c	 卖出第: 1	 还剩下: 0

2. 公平锁/非公平锁 概念解析

  • 公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似于排队买票,先来的人先买,后来的人再队尾排着,这是公平的—– Lock lock = new ReentrantLock(true)—表示公平锁,先来先得。
  • 非公平锁:是指多个线程获取锁的顺序并不是按照申请的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级反转或者饥饿的状态(某个线程一直得不到锁)—- Lock lock = new ReentrantLock(false)—表示非公平锁,后来的也可能先获得锁,默认为非公平锁。

3. 关于 公平锁/非公平锁 的相关问题

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

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

3.2 使⽤公平锁会有什么问题

公平锁保证了排队的公平性,非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的 “锁饥饿”

3.3 什么时候用公平?什么时候用非公平?

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

七、可重入锁

1. 概念解析

可重入锁又名递归锁

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

  • 如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了这样递归方法也就不能继续执行下去了。所以Java中 ReentrantLocksynchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

2. 可重入锁种类

2.1 隐式锁

隐式锁(即synchronized关键字使用的锁)默认是可重入锁

  • 作用于同步代码块

    static Object objectLock = new Object();
    public static void syncBlock()
    {
        new Thread(() -> {
            synchronized (objectLock) {// lock
                System.out.println("-----外层");
                synchronized (objectLock)
                {
                    System.out.println("-----中层");
                    synchronized (objectLock)
                    {
                        System.out.println("-----内层");
                    }
                }
            }//unlock
        },"t1").start();
    }

    打印结果:

    -----外层
    -----中层
    -----内层
  • 作用于同步方法中

    public class ReEntryLockDemo
    {
        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");
        }
    
        public static void main(String[] args)
        {
            ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();
    
            reEntryLockDemo.m1();
        }
    }
    /**
     * main  -----come in m1
     * -----m2
     * -----m3
     * main  -----end m1
     */
    

    打印结果:

    main  -----come in m1
    -----m2
    -----m3
    main  -----end m1

2.2 显式锁

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

  • ReentrantLock 实现的显示可重入锁
public static void main(String[] args) {
    Lock lock = new ReentrantLock();

    new Thread(() -> {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "\t" + "-----外层");
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + "\t" + "-----内层");
            } finally {
                lock.unlock();
            }
        } finally {
            lock.unlock();
        }
    }, "t1").start();

    new Thread(() -> {
        lock.lock();
        try {
            System.out.println("------22222");
        } finally {
            lock.unlock();
        }
    }, "t2").start();
}

打印结果:

t1	-----外层
t1	-----内层
------22222

3. 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. 死锁及排查

4.1 死锁是什么

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

死锁

4.2 死锁产生的原因

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

4.3 死锁代码案例

public class DeadLockDemo {
    static Object lockA = new Object();
    static Object lockB = new Object();


    public static void main(String[] args) {

        Thread a = new Thread(() -> {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有A锁,期待获得B锁");

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "\t 获得B锁成功");
                }
            }
        }, "a");
        a.start();

        new Thread(() -> {
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "\t" + " 自己持有B锁,期待获得A锁");

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                synchronized (lockA) {
                    System.out.println(Thread.currentThread().getName() + "\t 获得A锁成功");
                }
            }
        }, "b").start();
    }
}

运行结果:

a	 自己持有A锁,期待获得B锁
b	 自己持有B锁,期待获得A锁

程序运行到此没有停止,一直等待着……

4.4 如何排查死锁

4.4.1 命令行

  • jps -l 查看当前进程运行状况
  • jstack 进程编号 查看该进程信息
PS F:\> jps -l
29104 
33360 com.atguigu.juc.locks.DeadLockDemo
11172 org.jetbrains.jps.cmdline.Launcher
27412 org.jetbrains.idea.maven.server.RemoteMavenServer36
30148 org.jetbrains.idea.maven.server.RemoteMavenServer36
45796 sun.tools.jps.Jps

#####################################################################################

PS F:\> jstack 33360
2024-09-21 16:39:01
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.241-b07 mixed mode):

"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x0000000026eea000 nid=0x8df0 in Object.wait() [0x000000002a8ff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000716508ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
        - locked <0x0000000716508ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000028ba8000 nid=0x3cb4 in Object.wait() [0x000000002a7ff000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x0000000716506c00> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x0000000716506c00> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Periodic Task Thread" os_prio=2 tid=0x000000002b8e4000 nid=0x1378 waiting on condition

JNI global references: 317


Found one Java-level deadlock:
=============================
"b":
  waiting to lock monitor 0x0000000028bab9f8 (object 0x00000007166f0fe8, a java.lang.Object),
  which is held by "a"
"a":
  waiting to lock monitor 0x0000000028bae338 (object 0x00000007166f0ff8, a java.lang.Object),
  which is held by "b"

Java stack information for the threads listed above:
===================================================
"b":
        at com.atguigu.juc.locks.DeadLockDemo.lambda$main$1(DeadLockDemo.java:44)
        - waiting to lock <0x00000007166f0fe8> (a java.lang.Object)
        - locked <0x00000007166f0ff8> (a java.lang.Object)
        at com.atguigu.juc.locks.DeadLockDemo$$Lambda$2/1199823423.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"a":
        at com.atguigu.juc.locks.DeadLockDemo.lambda$main$0(DeadLockDemo.java:27)
        - waiting to lock <0x00000007166f0ff8> (a java.lang.Object)
        - locked <0x00000007166f0fe8> (a java.lang.Object)
        at com.atguigu.juc.locks.DeadLockDemo$$Lambda$1/1342443276.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

4.4.2 图形化工具

这里使用JDK 自带的 jconsole 演示,此工具在jdk的安装的 bin目录 下,双击 jconsole.exe 打开即可。

image-20240921164426956

八、总结

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


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 george_95@126.com