Java之Synchronize学习

Synchronize的几种实现

Synchronize有三种实现。

  1. 同步方法,修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。
    不同的修饰类型,代表锁的控制粒度

public class SynchronizeTest {

    // 同步方法 实体锁
    public synchronized void synchronizeMethod1(){
        System.out.println("synchronize 同步方法");
    }

    // 同步代码块 实体锁
    public void synchronizeMethod2(){
        synchronized (this){
            System.out.println("synchronize 同步代码块");
        }
    }

    // 同步静态方法 类锁
    public static synchronized void synchronizeMethod3(){
        System.out.println("synchronize 静态同步方法");
    }

    public static void main(String[] args) {
        SynchronizeTest synchronizeTest1 = new SynchronizeTest();
        SynchronizeTest synchronizeTest2 = new SynchronizeTest();

        new Thread(() -> {
            // 实例锁
            synchronizeTest1.synchronizeMethod1();
            synchronizeTest2.synchronizeMethod1();
        });
        
       new Thread(() -> {
            // 类锁
           synchronizeTest1.synchronizeMethod3();
           synchronizeTest2.synchronizeMethod3();
       });

        new Thread(() -> {
            // 实例锁 同方法1一样的
            synchronizeTest1.synchronizeMethod2();
            synchronizeTest2.synchronizeMethod2();
        });
    }

基础理论

synchronize(lock)是基于lock实例来控制锁的粒度的。
实例在内存中是这样布局的
在这里插入图片描述
所有Java实例都有一个monitor对象,monitor对象用来实现方法同步和代码块同步,monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit是插入到方法结束处和异常处,JVM保证每个monitorenter对必须对应的monitorexit与之配对,任何对象都有一个monitor与之相关,当一个monitor被持有后,它将处于锁定状态,线程执行到monitorenter指令时,将会尝试获取到对象所对应的monitor的所有权,即尝试获取对象的锁。

synchronize锁的升级

在这里插入图片描述

偏向锁的基本原理

当一个线程访问加了同步锁的代码块时,会在对象头中存储当前的线程ID,后续这个线程进入和退出这段加了同步锁的代码时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了。

偏向锁的获取和撤销逻辑
  1. 首先获取锁,对象的Markword,判断是否处于可偏向状态。(biased_lock=1, 且ThreadID为空)
  2. 如果是可偏向状态,则通过CAS操作,把当前线程的ID写入到Markword
    a) 如果写入cas成功,那么Markword会存储相关信息(上图)。表示已经获得了锁对象的偏向锁,接着执行同步代码块。
    b) 如果cas失败,说明其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)才能执行
  3. 如果是已偏向状态,需要检查Markword中存储的ThreadID是否等于当前线程的ThreadID
    a) 如果相等,不需要再次获得锁,可直接执行同步代码块
    b) 如果不想等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁。
偏向锁的撤销

偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为偏向锁并不存在所释放的概念),而是在获取偏向锁的过程中,发现cas失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。
对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程有两种情况:

  1. 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于cas重新偏向当前线程
  2. 如果原获得偏向锁的线程同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块
    在我们的应用开发中,绝大部分情况一定会存在2个以上的线程竞争,那么如果开启偏向锁,反而会提升获取锁的资源消耗,所以可以通过JVM参数UseBiasedLocking来设置开启或者关闭偏向锁
    在这里插入图片描述
轻量级锁的基本原理
轻量级锁的加锁和解锁逻辑

锁升级为轻量级锁之后,对象Markword也会进行相应的变化。升级为轻量级锁的过程:

  1. 线程在自己的栈桢中创建锁记录LockRecord。
  2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。
  3. 将锁记录中的Owner指针指向锁对象
  4. 将锁对象的对象头的Markword替换为指向锁记录的指针
自旋锁

轻量级锁在加锁过程中,用到了自旋锁。 所谓自旋,就是指当前有另一个线程来竞争锁时,这个线程会原地循环等待,而不是把该线给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗CPU的,就相当于在执行一个空的for循环。所以,轻量级锁适应那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能获得锁了。
自旋锁的使用,其实也是有一定的概率背景,在大部分同步代码块执行的时间都很短的。所以通过看似无异议的循环反而能提升锁的性能。
但是自旋锁必须有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗CPU资源。默认情况下自旋的次数是10次,可以通过preBlockSpin来修改

在JDK1.6之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋的过程,直接阻塞线程,避免浪费处理器资源。

轻量级锁的解锁

轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过cas操作把线程栈桢中的LockRecord替换回到锁对象的Markword中,如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁
在这里插入图片描述

重量级锁的基本原理

当轻量级锁膨胀到重量级锁之后,意味着线程只能挂起阻塞来等待被唤醒了
查看一下重量级锁的monitor
在这里插入图片描述

运行以后通过javap 工具查看生成的class文件信息,分析synchronize关键字的实现细节
命令 javap -v Test.class
在这里插入图片描述
加了同步代码块以后,在字节码中会看到一个monitorenter和monitorexit。
每一个Java对象都会与一个监视器monitor关联,可以把它理解成为一把锁当一个线程想要执行一段被synchronize修饰的同步方法或者代码块时,该线程得先获取到synchronize修饰的对象对应monitor。
monitorenter表示去获得 一个对象监视器。monitorexit表示释放monitor监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器。
monitor依赖操作系统的MutexLock(互斥锁)来实现,线程被阻塞后进入内核调度状态,这个会导致系统在用户态和内核态之间来回切换,严重影响锁的性能。

重量级锁的加锁基本流程

在这里插入图片描述
任意线程对Object(Object由synchronize保护)的访问,首先要获得Object的监视器。
如果获取失败,线程进入同步队列,线程状态编程BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞的同步队列中的线程,使其重新尝试对监视器的获取。

回顾竞争机制

假如有一个同步代码块,存在Thread1 、Thread2等多个线程
synchronize (lock){ // TODO }
情况一:只有Thread1会进入临界区
情况二: Thread1和Thread2交替进入临界区,竞争条件不激烈
情况三: 多个线程同事进入临界区,竞争条件激烈

偏向锁
此时当Thread1进入临界区时,JVM会将lockObject的对象头Markword的锁标志位设为“01”,同时会用cas操作会把Thread1的线程ID记录到Markword中,此时进入偏向锁模式。所谓“偏向”,指的是一个锁会偏向有Thread1若接下来没有其他线程进入临界区,则Thread1再出入临界区无需在执行任何同步操作,也就是说,若只有Thread1会进入临界区,实际上只有Thread1初次进入临界区时需要cas操作,以后在出入临界区都不会有同步操作带来开销。
轻量级
偏向锁的场景太过理想化,更多的时候是Thread2也会尝试进入临界区,如果Thread2也进入临界区,但是Thread1还没有执行完同步代码块时,会暂停Thread1并升级到轻量级锁。Thread2通过自旋再次尝试以轻量级锁的方式获取锁
重量级锁
如果Thread1和Thread2正常交替执行,那么轻量级锁基本能满足需求。但是如果Thread1和Thread2同时进入临界区,那么轻量级锁就会膨胀为重量级锁,意味着Thread1线程获得了重量级锁的情况下,Thread2会被阻塞。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值