更多详情内容请访问:JUC 系列文章导读

1、Synchronized

1.1 Synchronized 关键字

synchronized 关键字解决的是多个线程之间访问资源的同步性,可以保证被他修饰的方法或者代码块在任意时刻只能有一个线程在执行。在 Java 早期版本中,其属于重量级锁

因为监视器锁(monitor)是依赖底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相当比较长的时间,时间成本相对较高。

在 JDK 6 以后从 JVM 层面对 synchronized 进行了较大优化,比如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

目前的话,无论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字

1.2 Synchronized 锁的三种实现

  1. 修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁

    public synchronized void method() {}
  2. 修饰静态方法:给当前类加锁,会作用于类的所有实例对象,进入同步代码前要获得当前 class 锁

    public synchronized static void method() {}
  3. 修饰代码块:指定加锁对象,对给定对象/类加锁

    synchronized(this) {}

1.3 Synchronized 锁的八种实现

  • 情况一:两个线程同时访问一个对象synchronized 方法,同步执行;
  • 情况二:两个线程访问的是两个对象synchronized 方法,并行执行;
  • 情况三:两个线程同时访问一个 synchronized 静态方法,同步执行;
  • 情况四:两个线程同时访问 synchronized 方法非 synchronized 方法,并发执行;
  • 情况五:两个线程同时访问同一个对象不同 synchronized 方法,同步执行;
  • 情况六:两个线程同时访问静态 synchronized 方法非静态 synchronized 方法,并行执行;
  • 情况七:同步方法抛出异常后,JVM 会自动释放锁的情况
  • 情况八:两个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法
    • 仅在没有其他线程直接调用非同步方法的情况下,是线程安全的。
    • 若有其他线程直接调用非同步方法,则是线程不安全的。

测试案例

class Phone {

    public synchronized void sendEmail() {
        System.out.println(Thread.currentThread().getName() + "--正在发送邮件...");
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println(Thread.currentThread().getName() + "--发送邮件over!");
    }

    public synchronized void callTelephone() {
        System.out.println(Thread.currentThread().getName() + "--正在拨打电话中...");
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println(Thread.currentThread().getName() + "--挂断!");
    }

    public static synchronized  void sendQQInfo() {
        System.out.println(Thread.currentThread().getName() + "--静态方法执行中...");
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println(Thread.currentThread().getName() + "--静态方法执行完成!");
    }

    public synchronized void method1() {
        System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法,运行开始");
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("线程名:" + Thread.currentThread().getName() + ",同步方法,运行结束,开始调用普通方法");
        method2();
    }

    public void method2() {
        System.out.println("线程:" + Thread.currentThread().getName() + ",普通方法,运行开始");
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("线程:" + Thread.currentThread().getName() + ",普通方法,运行结束");
    }

    public synchronized void errorMethod() {
        System.out.println("线程名:" + Thread.currentThread().getName() + ",抛出异常,释放锁");
        throw new RuntimeException();
    }
}

public class SynchronizedDemo {

    /*
    第1种情况:两个线程同时访问一个对象的 synchronized 方法,同步执行;
    结果:
        A--正在发送邮件...
        (1s)
        A--发送邮件over!
        B--正在发送邮件...
        (1s)
        B--发送邮件over!
     */
    @Test
    public void test01() {
        Phone phone = new Phone();
        new Thread(() -> phone.sendEmail(), "A").start();
        new Thread(() -> phone.sendEmail(), "B").start();
        try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }
    }

    /*
    第2种情况:两个线程访问的是两个对象的 synchronized 方法,并行执行;
    结果:
        A--正在发送邮件...
        B--正在拨打电话中...
        (1s)
        A--发送邮件over!
        (又过了2秒)
        B--挂断!
     */
    @Test
    public void test02() {
        new Thread(() -> new Phone().sendEmail(), "A").start();
        new Thread(() -> new Phone().callTelephone(), "B").start();
        try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }
    }

    /*
    第3种情况:两个线程同时访问一个 synchronized 静态方法,同步执行;
    结果:
        A--静态方法执行中...
        (1S)
        A--静态方法执行完成!
        B--静态方法执行中...
        (1S)
        B--静态方法执行完成!
     */
    @Test
    public void test03() {
        Thread t1 = new Thread(() -> Phone.sendQQInfo(), "A");
        Thread t2 = new Thread(() -> Phone.sendQQInfo(), "B");
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {}
    }

    /*
    第4种情况:两个线程同时访问 **synchronized 方法与非 synchronized 方法,并发执行;
    结果:
        A--静态方法执行中...
        B--正在发送邮件...
        (1s)
        B--发送邮件over!
        A--静态方法执行完成!
     */
    @Test
    public void test04() {
        new Thread(() -> Phone.sendQQInfo(), "A").start();
        new Thread(() -> new Phone().sendEmail(), "B").start();
        try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }
    }

    /*
    第5种情况:两个线程同时访问同一个对象的不同 synchronized 方法,同步执行;
    结果:
        A--正在发送邮件...
        (1s)
        A--发送邮件over!
        B--正在发送邮件...
        (3s)
        B--发送邮件over!
     */
    @Test
    public void test05() {
        Phone phone = new Phone();
        new Thread(() -> phone.sendEmail(), "A").start();
        new Thread(() -> phone.callTelephone(), "B").start();
        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
    }

    /*
    第6种情况:两个线程同时访问静态 synchronized 方法和非静态 synchronized 方法,并行执行;
    结果:
        线程1(访问静态资源)--静态方法执行中...
        线程2(访问非静态方法)--正在发送邮件...
        (1s)
        线程2(访问非静态方法)--发送邮件over!
        线程1(访问静态资源)--静态方法执行完成!
     */
    @Test
    public void test06() {
        Thread t1 = new Thread(() -> Phone.sendQQInfo(), "线程1(访问静态资源)");
        Thread t2 = new Thread(() -> new Phone().sendEmail(), "线程2(访问非静态方法)");
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()){}
    }

    /*
    第7种情况:方法抛异常后,会释放锁;
    结果:
        线程名:线程1,抛出异常,释放锁
        线程2--正在发送邮件...
        Exception in thread "线程1" java.lang.RuntimeException
            at ch06_synchronized锁8种情况.Phone.errorMethod(SynchronizedDemo.java:42)
            at ch06_synchronized锁8种情况.SynchronizedDemo.lambda$test07$12(SynchronizedDemo.java:161)
            at java.lang.Thread.run(Thread.java:748)
        线程2--发送邮件over!
     */
    @Test
    public void test07() {
        Phone phone = new Phone();
        Thread t1 = new Thread(() -> phone.errorMethod(), "线程1");
        Thread t2 = new Thread(() -> phone.sendEmail(), "线程2");
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()){}
    }


    /*
    第8种情况:在 synchronized 方法中调用了普通方法,就不是线程安全的了,synchronized 的作用范围只在 “{}” 内;
    结果:两个线程访问同一个对象中的同步方法,同步方法又调用一个非同步方法,仅在没有其他线程直接调用非同步方法的情况下,是线程安全的。若有其他线程直接调用非同步方法,则是线程不安全的。
     */
    @Test
    public void test08() {
        Phone phone = new Phone();
        Thread t1 = new Thread(() -> phone.method2(), "A");
        Thread t2 = new Thread(() -> phone.method1(), "BB");
        Thread t3 = new Thread(() -> phone.method1(), "CCC");
        t1.start();
        t2.start();
        t3.start();
        while (t1.isAlive() || t2.isAlive() || t3.isAlive()) {}
    }
}

2、从字节码角度分析 Synchronized 实现

2.1 什么是同步指令

Java 虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,更常见的是直接将他称为“锁”)来实现

方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虛拟机可以从方法常量池中的方法表结构中的 ACC_ SYNCHRONIZED 访问标志得知一个方法是否被声明为同步方法。

当方法调用时,调用指令将会检查方法的 ACC_ SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。

如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。

同步一段指令集序列通常是由 Java 语言中的 synchronized 语句块来表示的,Java 虚拟机的指令集中有 monitorentermonitorexit 两条指令来支持 synchronized 关键字的语义,正确实现 synchronized 关键字
需要 Javac 编译器与 Java 虚拟机两者共同协作支持

2.2 反编译

synchronized 关键字底层原理属于 JVM 层面。可以通过 JDK 自带 javap 的命令查看相关的字节码信息:

  1. 首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件
  2. 然后执行 javap -c -s -v -l SynchronizedDemo.class

2.2.1 synchronized 同步代码块

public void method1() {
    synchronized (this) {
        System.out.println("synchronized 代码块");
    }
}

image-20220810042328862

从上面的部分截图可以看出:synchronized 同步语句块的实现使用的是 monitorentermonitorexit 命令,其中:

  • monitorenter 指令指向同步代码块的开始位置

  • monitorexit 指令指向同步代码块的结束位置,释放锁有两种情况:①正常释放;②异常释放

    一定是一个 enter 和两个 exit 吗?不一定,如果方法中直接抛出了异常处理,就是一个 monitorenter 和 monitorexit

2.2.2 synchronized 修饰方法的情况

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

image-20220810120913875

synchronized 修饰的方法并没有 monitorentermonitorexit 指令,取而代之的是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该标识来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。如果是静态同步方法还会多一个 ACC_STATIC 标记

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

2.2.3 synchronized 锁的是什么

image-20220816043251779

阿里开发手册说明:高并发时,同步调用应该去考量锁的性能损耗,能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁

2.3 Monitor 类

monitor 相当于一个对象的钥匙,只有拿到此对象的 monitor,才能访问该对象的同步代码。相反未获得 monitor 的只能阻塞来等待持有 monitor 的线程释放 monitor。monitorenter 和 monitorexit 对应的就是拿钥匙和还钥匙

2.3.1 Monitor 与 Java 对象以及线程如何关联

首先,每一个对象都有一个属于自己的 monitor,其次如果线程未获取到 singal(许可)则线程阻塞。object 可以比作医院的诊室,monitor 就是负责喊病人的护士,线程则是就诊的病人。

通过护士(监视器)的调度,诊室(synchronized 锁住的对象)内只允许进入一个病人(线程),此病人(线程)在当前时间就拥有此诊室(对象)的使用权,也就是获取了许可。病人就诊完毕,则表明归还了诊室的使用权。然后护士再调度下一个等待的病人进入诊室(被阻塞的线程)。

Synchronized 的语义底层是通过一个 monitor 的对象来完成,其实 wait()/notify() 等方法也依赖于 monitor 对象,这就是为什么只有在同步的块或者方法中才能调用 wait()/notify() 等方法,否则会抛出java.lang.IllegalMonitorState Exception的异常的原因

2.3.2 monitorenter

每一个对象都会和一个监视器 monitor 关联。监视器被占用时会被锁住,其他线程无法来获取该 monitor。当执行 monitorenter 指令时,线程试图获取锁也就是获取对象监视器的 monitor 的持有权。

  • 若 monior 的进入数为 0,线程可以进入 monitor,并将 monitor 的进数置为 1。当前线程成为 monitor 的所有者
  • 若线程已拥有 monitor 的所有权,允许它重入 monitor,则进入 monitor 的进入数加 1
  • 若其他线程已经占有 monitor 的所有权,那么当前尝试获取 monitor 的所有权的线程会被阻塞,直到 monitor 的进入数变为 0,才能重新尝试获取 monitor 的所有权

synchronized 的锁对象会关联一个 monitor,这个 monitor 不是我们主动创建的,是 JVM 的线程执行到这个同步代码块,发现锁对象没有 monitor 就会创建 monitor,monitor 内部有两个重要的成员变量

  1. owner:拥有这把锁的线程
  2. recursions:会记录线程拥有锁的次数

2.3.3 monitorexit

能执行 monitorexit 指令的线程一定是拥有当前对象的 monitor 的所有权的线程

执行 monitorexit 时会将 monitor 的进入数减 1。当 monitor 的进入数减为 0 时,当前线程退出 monitor,不再拥有 monitor 的所有权,此时其他被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权

monitorexit 指令一般出现了两次(如果方法中直接抛出了异常处理就是一个):

  • 第1次为同步正常退出释放锁;
  • 第2次为发生异步退出释放锁

monitorexit 插入在方法结束处和异常处,JVM保证每个 monitorenter 必须有对应的 monitorexit

2.4 Synchronized 底层执行流程

image-20220810170257109

3、锁升级

3.1 Synchronized 锁 Mark Word 锁标志位

image-20220818144513088

3.2 无锁

当一个对象被创建之后,还没有线程进入,这个时候对象处于无锁状态,其 Mark Word 中的信息如上表所示。

3.3 偏向锁

3.3.1 为什么引入偏向锁

因为经过 HotSpot 的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁

3.3.2 偏向锁升级

只在锁第一次被拥有的时候会在 Java 对象头和栈帧中记录下偏向线程的 ID,因为偏向锁不会主动释放锁,因此以后线程再次获取锁的时候,只需要比较当前线程的 ThreadId 和 Java 对象头中的 ID 是否一致

  • 一致:就无需再次获得锁,直接进入同步。无需每次加锁解锁都去 CAS 更新对象头(如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高
  • 不一致:意味着发生了竞争,需要查看 Java 对象头中记录的原线程是否还存活
    • 死亡:那么锁对象被重置为无锁状态,其他线程可以竞争将其设置为偏向锁
    • 存活:立刻查找原线程的栈帧信息,
      • 如果原线程还是需要继续持有这个锁对象:那么暂停当前线程,撤销偏向锁,升级为轻量锁
      • 如果原线程不再使用该锁对象:将锁对象状态设置为无锁状态,重新偏向新的线程

实际上偏向锁在 JDK1.6 之后是默认开启的,但是启动时间有延迟

  • 所以需要添加参数 -XX:BiasedLockingStartupDelay=0 让其在程序启动时立刻启动。
  • 开启偏向锁:-XX:+UseBiasedLocking-XX:BiasedLockingStartupDelay=0
  • 关闭偏向锁:关闭之后程序默认会直接进入轻量级锁状态 -XX:-UseBiasedLocking

3.4 轻量级锁

3.4.1 为什么引入轻量级锁

轻量级锁是为了在线程近乎交替执行同步块时提高性能。其本质就是自旋锁。在没有多线程竞争的前提下,通过 CAS 减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋再阻塞

3.4.2 轻量级锁升级

线程 1 获取轻量级锁时会先把锁对象的对象头 Mark Word 复制一份到线程 1 的栈帧中,创建用于存储锁记录的空间(称为 DisplacedMarkWord),然后使用 CAS 把对象头中的内容替换为线程 1 存储的锁记录(DisplacedMarkWord)的地址;

如果在线程 1 复制对象头的同时(在线程 1 CAS 之前),线程 2 也准备获取锁,复制了对象头到线程 2 的锁记录空间中,但是在线程 2 CAS 的时候,发现线程 1 已经把对象头换了,线程 2 的 CAS 失败,那么线程 2 就尝试使用自旋锁来等待线程 1 释放锁。

但是如果自旋的时间太长也不行,因为自旋是要消耗 CPU 的,因此自旋的次数是有限制的,如果自旋次数到了线程 1 还没有释放锁,或者线程 1 还在执行,线程 2 还在自旋等待,这时又有一个线程 3 过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止 CPU 空转。

为了避免无用的自旋,轻量级锁一旦膨胀为重量级锁就不会再降级为轻量级锁了;偏向锁升级为轻量级锁也不能再降级为偏向锁。一句话就是锁可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态

3.4.3 轻量级锁与偏向锁的区别

  1. 争夺轻量级锁失败时,自旋尝试抢占锁
  2. 轻量级锁每次退出同步块都需要释放锁,而偏向锁是竞争发生时才释放锁

3.5 偏向锁、轻量级锁、重量级锁的优缺点

优点 缺点 适用场景
偏向锁 加锁解锁无需额外的消耗,和非同步方法时间相差纳秒级别 如果竞争的线程多,那么会带来额外的锁撤销的消耗 基本没有线程竞争锁的同步场景
轻量级锁 竞争的线程不会阻塞,使用自旋,提高程序响应速度 如果一直不能获取锁,长时间的自旋会造成 CPU 消耗 适用于少量线程竞争锁对象,且线程持有锁的时间不长,追求响应速度的场景
重量级锁 线程竞争不适用 CPU 自旋,不会导致 CPU 空转消耗 CPU 资源 线程阻塞,响应时间长 很多线程竞争锁,且锁持有时间长,追求吞吐量的场景

3.6 锁升级概览图

image-20220822004018626

4、锁粗化与锁消除

假如方法中首尾相接,前后相邻的都是同一个锁对象,那 JIT 编译器就会把这几个 synchronized 块合并成一个大块,加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能

/**
 * 锁粗化
 * 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个synchronized块合并成一个大块,
 * 加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
 */
public class LockBigDemo {
    static Object objectLock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (objectLock) {
                System.out.println("11111");
            }
            synchronized (objectLock) {
                System.out.println("22222");
            }
            synchronized (objectLock) {
                System.out.println("33333");
            }
        },"a").start();
    }
}

从 JIT 角度看相当于无视它,synchronized(o) 不存在了,这个锁对象并没有被共用扩散到其它线程使用,极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用

/**
 * 锁消除
 * 从JIT角度看相当于无视它,synchronized (o)不存在了,这个锁对象并没有被共用扩散到其它线程使用,
 * 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
 */
public class LockClearUPDemo {
    static Object objectLock = new Object();//正常的

    public void m1() {
        // 锁消除,JIT会无视它,synchronized(对象锁)不存在了。不正常的
        Object o = new Object();

        synchronized (o) { System.out.println("-----hello LockClearUPDemo" + "\t" + o.hashCode() + "\t" + objectLock.hashCode()); }
    }

    public static void main(String[] args) {
        LockClearUPDemo demo = new LockClearUPDemo();

        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                demo.m1();
            }, String.valueOf(i)).start();
        }
    }
}

5、悲观锁与乐观锁

5.1 悲观锁

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

  • 悲观锁的实现方式:

    1. synchronized 关键字
    2. Lock 的实现类都是悲观锁
  • 适合写操作多的场景,先加锁可以保证写操作时数据正确。显示的锁定之后再操作同步资源

5.2 乐观锁

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

  • 乐观锁的实现方式:

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

      ABC 问题:在 CAS 中想读取一个值 A,想把值 A 变为 C,不能保证读取时的 A 就是赋值时的 A,中间可能有个线程将 A 变为 B 再变为A

      解决方法:JUC 包提供了一个 AtomicStampedReference,原子更新带有版本号的引用类型,通过控制版本值的变化来解决 ABA 问题

    2. CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现的

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

6、公平锁与非公平锁

6.1 公平锁与非公平锁介绍

  • 公平锁

    1. ReentrantLock lock = new ReentrantLock(true)
    2. 多个线程按照申请锁的顺序来获取锁,例:排队买票
  • 非公平锁

    1. ReentrantLock lock = new ReentrantLock(false) 默认就是非公平锁
    2. 是指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象

6.2 为什么这么设计,为什么默认是非公平

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

6.3 适用场景

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

7、可重入锁(递归锁)

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

如果是 1 个有 synchronized 修饰的递归调用方法,程序第 2 次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以 Java 中 ReentrantLock 和 synchronized 都是可重入锁,可重入锁的一个优点是可一定程度避免锁。

7.1 隐式锁 Synchronized

Synchronized 的重入实现机理:

  • ObjectMoitor.hpp 底层:每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针_count_owner
  • 首次加锁:当执行 monitorenter 时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加 1。
  • 重入:在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加 1,否则需要等待,直至持有线程释放该锁。
  • 释放锁:当执行 monitorexit 时,Java 虚拟机则需将锁对象的计数器减 1。计数器为零代表锁已被释放

public void method1() {
    new Thread(() -> {
        synchronized (obj) {
            System.out.println("synchronized锁——外层调用");
            synchronized (obj) {
                System.out.println("synchronized锁——中层调用");
                synchronized (obj) {
                    System.out.println("synchronized锁——内层调用");
                }
            }
        }
    }, "t1").start();
}

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(Thread.currentThread().getName()+"\t"+"-----come in m2");
    m3();
    System.out.println(Thread.currentThread().getName()+"\t-----end m2");
}

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

7.2 显式锁 Lock

public void test03() {
    Lock lock = new ReentrantLock();
    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();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }, "t1").start();
}

8、死锁的排查

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 进程编号 查看该进程信息

image-20220810165422989

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

image-20220810165604456

9、双重校验锁实现对象单例(线程安全)

面试中面试官经常会说:“单例模式了解吗?解释一下双重检验锁方式实现单例模式的原理”

9.1 最基础实现

这段代码简单明了,而且使用了懒加载模式,但是却存在致命的问题。当有多个线程并行调用 getInstance() 的时候,就会创建多个实例。

public class Singleton {
    private static Singleton instance;
    private Singleton (){}

    public static Singleton getInstance() {
     if (instance == null) {
         instance = new Singleton();
     }
     return instance;
    }
}

虽然做到了线程安全,并且解决了多实例的问题,但是它并不高效。因为在任何时候只能有一个线程调用 getInstance() 方法。

public class Singleton {
    private static Singleton instance;
    private Singleton (){}

    public static synchronized Singleton getInstance() {//封死了
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

9.2 双重检验锁

这就引出了双重检验锁。双重检验锁模式(double checked locking pattern),是以种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。

为什么在同步块内还要再检验一次? 因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

public class Singleton {
    private volatile static Singleton uniqueInstance;
    private Singleton() {}

    public static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {//Single Checked
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {//Double Checked
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

9.3 为什么要判断两次

因为线程 1 和线程 2 在 i=99 的时候,同时判断了一次,都进到 for 循环里,此时线程 1 进入同步代码块,线程 2 进入阻塞队列,当线程 1 跑出代码块后,线程 2 进入同步代码块,线程 1 对 i 进行 +1 操作后,i 变成了 100,所以线程 2 就输出了 100,所以要在同步代码块中再加一次判断,判断 i 的值

public class SynchronizedTest {

    class Data {
        private ArrayList<Integer> arr = new ArrayList<>();

        public void insert() {
            for (int i = 0; i < 100; i++) {
                synchronized (this) {
                    if (i < 100) {
                        if (!arr.contains(i)) {
                            System.out.println(Thread.currentThread().getName() + " 正在插入" + i);
                            arr.add(i);
                        }
                    }
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        Data data = synchronizedTest.new Data();
        new Thread(() ->{ data.insert(); }).start();
        new Thread(() ->{ data.insert(); }).start();
        //Thread.currentThread().sleep(3000);
    }
}

END

本文作者:
文章标题:Synchronized 与锁升级
本文地址:https://www.pendulumye.com/juc/502.html
版权说明:若无注明,本文皆PendulumYe原创,转载请保留文章出处。
最后修改:2022 年 08 月 30 日
千山万水总是情,给个一毛行不行💋