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.wuliyouguocheng.thread;

public class SynchronizedTest {

    public void test(){
        synchronized(this){
            System.out.println("我是果橙呀");
        }
    }
}

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

3、代码分析

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

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

  1.     1. 如果monitor计数为零,则线程进入monitor并将monitor的计数设置为1,当前线程就是此monitor的所有者;
  2.     2. 如果线程已经获取了monitor,再进入时,monitor的计数+1;
  3.     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,偏向锁升级为轻量级锁。

撤销偏向锁的过程:

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

四、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、锁粗化

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


   

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
ReentrantLocksynchronized都是用于实现并发编程中的同步机制,但它们的底层原理和使用方式有所不同。 1. synchronized底层原理synchronizedJava中的关键字,它基于进入和退出监视器对象(monitor)来实现方法同步和代码块同步。在Java对象头中,有一个标志位用于表示对象是否被锁定。当线程进入synchronized代码块时,它会尝试获取对象的锁,如果锁已经被其他线程持有,则该线程会被阻塞,直到锁被释放。当线程退出synchronized代码块时,它会释放对象的锁,使其他线程可以获取锁并执行相应的代码。 2. ReentrantLock底层原理: ReentrantLockJava中的一个类,它使用了一种称为CAS(Compare and Swap)的机制来实现同步。CAS是一种无锁的同步机制,它利用了CPU的原子指令来实现对共享变量的原子操作。ReentrantLock内部维护了一个同步状态变量,通过CAS操作来获取和释放锁。当一个线程尝试获取锁时,如果锁已经被其他线程持有,则该线程会进入等待状态,直到锁被释放。与synchronized不同,ReentrantLock提供了更灵活的锁获取和释放方式,例如可以实现公平锁和可重入锁。 总结: - synchronizedJava中的关键字,基于进入和退出监视器对象来实现同步,而ReentrantLock是一个类,使用CAS机制来实现同步。 - synchronized是隐式锁,不需要手动获取和释放锁,而ReentrantLock是显式锁,需要手动调用lock()方法获取锁,unlock()方法释放锁。 - ReentrantLock相比synchronized更灵活,可以实现公平锁和可重入锁等特性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雾里有果橙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值