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

Posted 哪 吒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java高并发编程实战4,synchronized与Lock底层原理相关的知识,希望对你有一定的参考价值。

目录

一、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 不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

七、StampedLock

JDK中提供了StampedLock类,StampedLock在读取共享变量的过程中,允许后面的一个线程获取写锁,并对共享变量进行写操作,它使用乐观读避免数据不一致,在读过写少的高并发环境下,是比ReadWriteLock更快的锁。

StampedLock支持写锁、读锁、乐观锁。

StampedLock与ReadWriteLock的不同之处在于,ReadWriteLock在获取读锁或写锁后,会返回一个Long类型的变量,之后再释放锁时,需要传入这个Long类型的变量。

在ReadWriteLock读取共享变量时,所有对共享变量的写操作都会被阻塞;而StampedLock提供的乐观读在多个线程读取共享变量时,允许一个线程对共享变量进行写操作。

StampedLock锁内部维护了一个线程等待队列,所有获取锁失败的线程都会进入这个等待队列,队列中的每个节点都代表一个线程,同时会在节点中保存一个标志位locked,用于表示当前线程是否获取到锁,true表示获取到锁,false表示未获取到锁。

当某个线程尝试获取锁时,会先获取等待队列尾部的线程作为当前线程的前驱节点,并且判断前驱节点是否已经成功释放锁,如果已经释放锁,则当前线程获取到锁并继续执行。如果前驱节点未释放锁,则当前线程自旋等待。

当某个线程释放锁时,会先将自身节点的locked标记设置为false,队列中后继节点中的线程通过自旋就能检测到当前线程已经释放锁,从而可以获取到锁并继续执行业务逻辑。

八、锁优化

加锁使得原本能够并行执行的操作变得串行化,串行操作会降低程序的性能,CPU对于线程的上下文切换也会降低系统的性能。下面总结一下锁优化的相关方法。

1、缩小锁的范围

将一些不会引起线程安全问题的代码,移出同步代码块,尤其是耗时的IO操作,或者可能引起阻塞的方法,这样能提高程序执行的速度。

2、减小锁的粒度

减小锁的粒度就是缩小锁定的对象,比如将一个大对象拆分成多个小对象,对这些小对象进行加锁,能够提高程序的并行度,提高程序执行的速度。

3、锁分离

锁分离最典型的技术就是读写锁,ReadWriteLock分为写锁和读锁,其中读读不互斥,读写互斥,写写互斥,这样既保证了线程安全,又提高了性能。

4、锁分段

进一步缩小锁的粒度,对一个独立对象的锁进行分解的现象叫做锁分段。锁分段最典型的例子就是ConcurrentHashMap。
ConcurrentHashMap将数据按照不同的数据段进行存储,每个数据段分配一把锁,当某个数据段占有某个数据段的锁访问数据时,其它数据段的锁也能被其它线程抢占到,提高程序的并行度,提高程序性能。

5、锁粗化

如果同一个线程不停的请求、同步、释放同一把锁,则会降低程序的执行性能,此时可以扩大锁的范围,即进行锁粗化处理。


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

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

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

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

哪吒精品系列文章

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

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

SQL性能优化的21个小技巧

Java基础教程系列

Spring Boot 进阶实战

以上是关于Java高并发编程实战4,synchronized与Lock底层原理的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程实战—–synchronized

JAVA的高并发编程

Java并发编程原理与实战四十二:锁与volatile的内存语义

Java并发编程原理与实战十九:AQS 剖析

Java面试系列之并发编程专题-Synchronized灵魂拷问

Java面试系列之并发编程专题-Synchronized灵魂拷问