> 文档中心 > Java高并发编程实战4,synchronized与Lock底层原理

Java高并发编程实战4,synchronized与Lock底层原理

目录

    • 一、synchronized底层原理
    • 二、反编译synchronized方法
      • 1、定义一个最简单的synchronized方法
      • 2、通过```javap -c SynchronizedTest.class```进行反编译:
      • 3、代码分析
    • 三、偏向锁
    • 四、Lock源码分析
      • 1、Lock锁的方法如下
      • 2、下面分别单独介绍一下Lock中的方法
    • 五、公平锁原理
    • 六、非公平锁
      • Java高并发编程实战系列文章
      • 哪吒精品系列文章

一、synchronized底层原理

synchronized是基于JVM中的Monitor锁实现的,Java1.5之前的synchronized锁性能较低,但是从Java1.6开始,对synchronized锁进行了大量的优化,引入可锁粗话、锁消除、偏向锁、轻量级锁、适应性自旋等技术来提升synchronized的性能。

当synchronized修饰方法时,当前方法会比普通方法在常量池中多一个ACC_SYNCHRONIZED标识符,synchronized修饰方法的核心原理如下图:
在这里插入图片描述

JVM在执行程序时,会根据这个ACC_SYNCHRONIZED标识符完成方法的同步。如果调用了被synchronized修饰的方法,则调用的指令会检查方法是否设置了ACC_SYNCHRONIZED标识符。
如果方法设置了ACC_SYNCHRONIZED标识符,则当前线程先获取monitor对象。同一时刻,只会有一个线程获取monitor对象成功,进入方法体执行方法逻辑。在当前线程释放monitor对象前,其它线程无法获取同一个monitor对象,从而保证了同一时刻只有一个线程进入被synchronized修饰的方法中执行方法体的逻辑。

  • synchronized修饰方法时,不需要JVM编译出的字节码完成加锁操作,是一种隐式的实现方式;
  • synchronized修饰代码块时,是通过编译出的字节码生成的monitorenter和monitorexit指令完成的,在字节码层面是一种显示的实现方式;

二、反编译synchronized方法

1、定义一个最简单的synchronized方法

package com.nezha.thread;public class SynchronizedTest {    public void test(){ synchronized(this){     System.out.println("Java知识,尽在哪吒"); }    }}

2、通过javap -c SynchronizedTest.class进行反编译:

3、代码分析

通过反编译,当synchronized修饰代码块时,会在编译出的字节码中插入monitorenter指令和monitorexit指令。

每个线程都有一个监视器锁monitor,当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时首先会尝试获取monitor的所有权。

  1. 如果monitor计数为零,则线程进入monitor并将monitor的计数设置为1,当前线程就是此monitor的所有者;
  2. 如果线程已经获取了monitor,再进入时,monitor的计数+1;
  3. 如果有其它线程占用monitor,此线程则阻塞,直到monitor的计数为0,当前线程将再次尝试获取monitor;

线程执行monitorexit时,monitor计数会-1,如果-1后monitor的计数为0,则当前线程退出此monitor。其它被阻塞的线程尝试获取当前monitor的所有权。

三、偏向锁

大部分情况下,被添加synchronized锁的代码不会存在多线程竞争的情况,但是会出现同一个线程多次获取同一个synchronized锁的现象,这样很浪费性能,此时偏向锁应运而生。
在这里插入图片描述
如果在同一时刻有且仅有一个线程执行了synchronized修饰的方法,则执行方法的线程不存在与其它线程竞争锁的情况,此时,锁就会变为偏向锁。
当锁进入偏向状态时,对象头中的Mark Word的结构就会进入偏向结构。此时偏向锁标记为1,锁标志位为01,并将当前线程的ID记录在Mark Word中。当前线程如果再次进入此方法,要先检查对象头中的Mark Word中是否存储了自己的线程ID。

  • 如果有,表示当前线程已经获取到锁,当前线程可以进入或退出此方法。
  • 如果没有,则说明有其它线程参与锁竞争并获得了偏向锁,此时当前线程会尝试CAS方式将Mark Word中的线程ID替换为自己的线程ID,替换的结果有两种:
  1. CAS操作成功,表示之前获取到偏向锁的线程已经不存在,Mark Word中的线程ID替换为自己的线程ID;
  2. CAS操作失败,表示之前获取到偏向锁的线程仍然存在,此时会暂停之前获取到偏向锁的线程,将Mark Word中的偏向锁标记为0,锁标志位设置为00,偏向锁升级为轻量级锁。

撤销偏向锁的过程:

  1. 选择某个没有执行字节码的时间点,暂停拥有锁的线程;
  2. 遍历整个线程栈,检查是否存在对应的锁记录,如果存在锁记录,则清空锁记录,变为无锁状态。同时将锁记录指向Mark Word中的偏向锁标设置为0,锁标志位设置为01,将其设置为无锁状态,并清除Mark Word中的线程ID;
  3. 将当前锁升级为轻量级锁,并唤醒被暂停的线程;

四、Lock源码分析

synchronized是JVM中提供的内置锁,使用内置锁无法很好地完成一些特定场景下的功能。例如,内置锁不支持响应中断、不支持超时、不支持以非阻塞的方式获取锁。而lock锁是在JDK层面实现的一种比内置锁更灵活的锁,它能弥补synchronized内置锁的不足,他们都通过Java提供的接口来完成加锁和解锁操作。

JDK听过的Lock锁是通过Java提供的接口来手动加锁、解锁的,所以lock是一种显示锁。JDK提供的显示锁位于java.util.concurrent包下,也叫JUC显示锁。

1、Lock锁的方法如下

在这里插入图片描述

2、下面分别单独介绍一下Lock中的方法

(1)void lock();

阻塞模式抢占锁的方法。如果当前线程抢占锁成功,则继续向下执行程序的业务逻辑,否则,当前线程会阻塞,直到其它抢占到锁的线程释放锁后再继续抢占锁。

(2)void lockInterruptibly() throws InterruptedException;

可中断模式抢占锁的方法。当前线程在调用lockInterruptibly()方法抢占锁的过程中,能够响应中断信号,从而能够中断当前线程。

(3)boolean tryLock();

非阻塞模式下抢占锁的方法。当前线程调用tryLock()方法抢占锁时,线程不会阻塞,而会立即返回抢占锁的结果。

(4)boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

在tryLock()的基础上,加上限制抢占锁的时间限制。

(5)void unlock();

释放锁。

(6)Condition newCondition();

创建与当前线程绑定的Condition条件,主要用于线程间以“等待 - 通知”的方式进行通信。

所以,Lock锁支持响应中断、超时和以非阻塞的方式获取锁,全面弥补了JVM中synchronized内置锁的不足。

五、公平锁原理

公平锁,顾名思义,就是争抢锁的时候,大家都是公平的。

每个线程抢占锁的时候,都会检索锁维护的等待队列,如果等待队列为空,或者当前线程是等待队列的第一个线程,则当前线程获取到锁,否则,当前线程加入到等待队列的尾部,然后等待队列中的线程会按先进先出的规则按顺序尝试获取资源。

在这里插入图片描述

六、非公平锁

非公平锁的核心就是抢占锁的所有线程是不公平的,在多线程并发环境中,每个线程在抢占锁的过程中都会先直接尝试抢占锁,如果抢占成功,就继续执行程序的业务逻辑,如果抢占失败,就会进入等待队列中排队。

公平锁和非公平锁的区别是,非公平锁在队列的处理上比公平锁多了一个插队的过程,,如果插队时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。

非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

在这里插入图片描述

Java高并发编程实战系列文章

Java高并发编程实战1,那些年学过的锁

Java高并发编程实战2,原子性、可见性、有序性,傻傻分不清

Java高并发编程实战3,Java内存模型与Java对象结构

哪吒精品系列文章

Java学习路线总结,搬砖工逆袭Java架构师

10万字208道Java经典面试题总结(附答案)

SQL性能优化的21个小技巧

Java基础教程系列

Spring Boot 进阶实战
在这里插入图片描述