synchronized 锁是可重入锁吗?如何验证?

摘要:举例证明 synchronized锁 是可重入锁,并描述可重入锁的实现原理。

综述

  先给大家一个结论:synchronized锁 是可重入锁

  关于什么是可重入锁,通俗来说,当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。或者说,可重入锁是同一个线程重复请求由自己持有的锁对象时,可以请求成功而不会发生死锁。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。可重入锁又称递归锁。

验证可重入

  假设我们现在不知道它是不是一个可重入锁,那我们就应该想方设法来验证它是不是可重入锁?怎么验证呢?看下面的代码!

public class SuperSalesman {

    public int ticketNum = 10;

    public synchronized void superSaleTickets()  {
        ticketNum --;
        System.out.println("父类售票后,剩余票数:" + ticketNum
                + " " + Thread.currentThread().getName());
        try {
            Thread.sleep(30);
        } catch (InterruptedException e) {
            System.out.println("error, " + e);
        }
    }

}

  创建子类:

public class ChildSalesman extends SuperSalesman {

    public static void main(String[] args) {
        ChildSalesman child = new ChildSalesman();
        child.childSaleTickets();
    }

    public synchronized void childSaleTickets() {
        while (ticketNum > 0) {
            ticketNum --;
            System.out.println("子类售票后,余票为:" + ticketNum
                    + " " + Thread.currentThread().getName());
            superSaleTickets(); //允许进入,synchronized的可重入性
        }
    }

    @Override
    public synchronized void superSaleTickets() {
        System.out.println("I am working");
        super.superSaleTickets();
    }
}

  现在运行一下上面的父子类继承代码,我们看一下结果:

子类售票后,余票为:9 main
I am working
父类售票后,剩余票数:8 main
子类售票后,余票为:7 main
I am working
父类售票后,剩余票数:6 main
子类售票后,余票为:5 main
I am working
父类售票后,剩余票数:4 main
子类售票后,余票为:3 main
I am working
父类售票后,剩余票数:2 main
子类售票后,余票为:1 main
I am working
父类售票后,剩余票数:0 main

Process finished with exit code 0

  现在可以验证出 synchronized 是可重入锁了吧!因为这些方法输出了相同的线程名称,表明即使递归调用synchronized修饰的方法,也没有发生死锁,证明其是可重入的。

  下面是多个方法嵌套调用的例子:

public class SyncTest {

    public static void main(String[] args) {
        LockTest lock = new LockTest();
        lock.method1();
    }
}

public class LockTest {
    public synchronized void method1() {
        System.out.println("method1");
        method2();
    }

    public synchronized void method2() {
        System.out.println("method2");
        method3();
    }

    public synchronized void method3() {
        System.out.println("method3");
    }
}

  执行main方法,控制台打印信息如下,说明不会因为之前已经获取过锁还没释放而发生阻塞。即同一线程可执行多个持有同一把锁的方法。

/Library/Java/JavaVirtualMachines/jdk-17.0.2.jdk ...
method1
method2
method3

  可以看到调用的三个方法均得到了执行。我们知道synchronized修饰普通方法时,使用的是对象锁,也就是SuperSalesman对象。三个方法的锁都是SuperSalesman对象。我们在子类中执行childSaleTickets方法时,获取了SuperSalesman对象锁,然后在childSomeString时调用了重写父类的superSaleTickets方法,该方法的锁也是SuperSalesman对象锁,然后在其中调用父类的superSaleTickets方法,该方法的锁也是SuperSalesman对象锁。一个锁多次请求,而且都成功了,所以synchronized是可重入锁。

  所以在 java 内部,同一线程在调用自己类中其它 synchronized 方法/块或调用父类的 synchronized 方法/块都不会阻碍该线程的执行。就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。因为java线程是基于“每个线程(per-thread)”,而不是基于“每次调用(per-invocation)”的(java中线程获得对象锁的操作是以线程为粒度的,per-invocation 互斥体获得对象锁的操作是以每次调用作为粒度的)。

可重入锁的实现原理

  看到这里,你终于明白了 synchronized 是一个可重入锁。但是面试官要再问你,可重入锁的原理是什么?

解释一

  可重入锁实现可重入性原理或机制是:每一把锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这把锁,就可以再次拿到这把锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。

解释二

  通过javap -c SynchronizedLock.class 反编译,来解析synchronized可重入锁原理:synchronized通过monitor计数器实现,当执行monitorenter命令时:判断当前monitor计数器值是否为0,如果为0,则说明当前线程可直接获取当前锁对象;否则,判断当前线程是否和获取锁对象线程是同一个线程。若是同一个线程,则monitor计数器累加1,当前线程能再次获取到锁;若不是同一个线程,则只能等待其它线程释放锁资源。当执行完synchronized锁对象的代码后,就会执行monitorexit命令,此时monitor计数器就减1,直至monitor计数器为0时,说明锁被释放了。

结束语

  如果您觉得本文对您有帮助,请点一下“推荐”按钮,您的【推荐】将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接;否则,楼兰胡杨保留追究法律责任的权利。

Reference

posted @ 2022-04-03 17:00  楼兰胡杨  阅读(2352)  评论(0编辑  收藏  举报