synchronized底层实现原理与ReentrantLock初步理解

前言:在慕课网上学习剑指Java面试-Offer直通车时所做的笔记,供本人复习之用,比较难,我也没大懂,只记录大概意思以后有接触了再改,想要详细解说的不建议看这篇博客.

目录

第一章 对象在内存中的布局

第二章 Monitor

2.1 Monitor在字节码中的表示

第三章 锁的优化

3.1 自旋锁与自适应自旋锁

3.1.1 自旋锁

3.1.2 自适应自旋锁

3.2 锁消除

3.3 锁粗化

第四章 synchronized的四种状态

4.1 偏向锁

4.2 轻量级锁

第五章 Synchronized和ReentrantLock的区别

5.1 ReentrantLock公平性设置

5.2 wait/notify/notifyAll对象化


第一章 对象在内存中的布局

HotSpot 对象在内存中的布局分为三块区域,对象头,实例数据,对齐填充,我们在这里主要讲解对象头.

synchronized使用的锁对象是存储在java对象头里的,其主要结构是由Mark Word和Class Metadata Address组成,Class Metadata Address是指向类元数据的指针,虚拟机通过这个指针确认其是哪个对象的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键.

 Mark Word:

 

第二章 Monitor

上面介绍了java对象头,下面我们介绍Monitor.

Monitor:每个对象天生自带了一把看不见的锁,叫做内部锁或者Monitor锁.

Monitor也称为管程或者监视器锁,我们可以把它理解为一个同步工具,也可以描述为一种同步机制,通常它被描述为一个对象.

这里我们拿重量级锁进行分析,锁的标识位为10,指针指向的是monitor对象的起始地址,每个对象都存在着一个monitor与之关联,monitor存在于对象的对象头中,对象与monitor之间有多存在多种实现方式,如monitor可以与对象一起存在销毁,或当线程试图获取对象锁时,自动生成.当monitor被某个线程持有后,它便处于锁定状态.

在java虚拟机HotSpot中monitor由ObjectMonitor来实现,位于HotSpot虚拟机源码objectMonitor里,通过C++来实现.

        ObjectMonitor中有两个队列,一个是WaitSet一个是EntryList,可以与等待池与锁池联系起来,他们就是用来保存ObjectWaiter的对象列表,每个对象锁的线程都会被封装成ObjectWaiter来保存到里面,owner指向持有ObjectMonitor的线程.

        当多个线程访问同一段同步代码的时候,首先会进入到EntryList集合中,当线程获取到对象的Monitor之后,就进入到Object区域,并把Monitor中的Owner变量设置为当前线程,同时Monitor中的count就会加1,如果线程调用wait方法将会释放当前持有的Monitor,owner会被恢复成null,count也会被减1,同时该线程ObjectWaiter实例就会进入到waitSet集合等待被唤醒,若当前线程执行完毕,它也将释放monitor锁,并复位对应变量的值,以便其它线程进入获取monitor锁.

Monitor锁的竞争,获取与释放.

 

2.1 Monitor在字节码中的表示

java代码:

对应的字节码:

syncsTask方法字节代码:

可以分析出,同步语句块的实现依赖的是monitorenter与monitorexit指令.

monitorenter指向代码块的开始位置,首先获取printStream这个类,传入参数,执行方法.

monitorexit指明同步代码块的结束位置.

当执行monitorenter指令时,当前线程将试图获取对象锁即Object(没听清,音译)所对应的持有权,当Object的Monitor进入计数器的count为0时,线程就可以成功的获取到Monitor,并将计数器设置为1表示取锁成功,如果我们当前线程在之前已经拥有了objectMonitor的持有权,它可以重入这个monitor.假如其它线程已经先于当前线程拥有ObjectMonitor的所有权,那么当前线程将会被阻塞在这里,直到持有该锁的线程执行完毕,即monitorexit被执行,执行线程将释放Monitor锁,并设置计数器为0,其它线程将有机会持有Monitor.

为了保证该方法异常时也能正确执行,编译器会自动生成一个异常处理器,包含另一个monitorexit方法.

什么是重入:

 重入代码如下:

 

syncTask方法字节代码:

可以看到它并没有monitorenter和monitorexit,字节码也比较短,因为方法集的同步时隐式的,即无需通过字节码指令来控制.

我们可以看到有ACC_SYNCHORONIZED一个访问标志,用来区分一个方法是否是同步方法,当方法调用时此访问标志会被检测,当被设置时线程将会持有monitor,然后再执行方法,最后在方法无论是正常还是异常完成的情况下执行monitorexit.

第三章 锁的优化

为什么会对synchronized嗤之以鼻?

java6之后,synchronized性能得到了极大的提升.

 

3.1 自旋锁与自适应自旋锁

3.1.1 自旋锁

许多情况下,共享数据的锁定状态持续时间短,切换线程不值.

可以让没获取到monitor锁的线程在门外等待一会儿,但不放弃CPU的执行时间.即通过让线程执行忙循环(自旋)等待锁的释放,不让出CPU.不像sleep一样会让出CPU的执行时间.

缺点:若锁被其它线程长时间占用,会白白等很久,会带来许多性能上的开销.所以可以设置如果在一定时间内没有等到锁,就应该挂起线程了.

3.1.2 自适应自旋锁

由于每次线程需要等待的时间不是固定的,所以想设定时间比较合理是很困难的,所以我们需要自适应自旋锁.

自旋的次数不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定.

如果对于某个锁,自旋经常获取到锁,那么就增加等待时间,如果对于某个锁,自旋很少获取到锁,那在以后获取这个锁时可能省略到这个过程,以避免浪费时间.

 

3.2 锁消除

java 虚拟机在JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁.

可以看到appen是加锁的,但是这个锁永远不会发生竞争,sb属于本地变量没有return,所以这个锁可以消除.

 

 

3.3 锁粗化

我们有时会将锁限制在尽量小的范围,这样需要同步的数量尽可能的变小,使等待锁的线程尽可能块的拿到锁.

但是如果一连串系类操作都会同一个对象反复加锁和解锁,甚至加锁操作出现在循环体中,那即使没有锁竞争,频繁的进行互斥锁操作也会导致不必要的性能浪费,此时我们可以矿大枷锁的范围,避免反复加锁和解锁,

如下图所示,JVM会把加锁的范围到循环的外部,使整个操作只需要加锁一次.

 

第四章 synchronized的四种状态

无锁就是没有锁,重量级锁我们刚才说过,所以我们这里主要介绍偏向锁和轻量级锁.

4.1 偏向锁

应用在锁不存在多线程竞争,总是由同一线程多次获得.

不适用于锁竞争比较激烈的多线程场合.

4.2 轻量级锁

偏向锁失败后会膨胀为轻量级锁.

具体过程:

解锁的过程:

锁的内存语义:

总的来说:

 

总结:对象头里有mark word,markword里存储着这个锁的状态,

当多个线程竞争一个锁时,有几种不同的情况:

偏向锁,

轻量级锁,

重量级锁

 

第五章 Synchronized和ReentrantLock的区别

在java5以前,Synchronized是仅有的同步手段,从java5开始,提供了ReentrantLock(再入锁)的实现,它的语义和synchronized基本相同,通过代码直接调用lock方法去获取,代码编写也更加灵活.

ReentrantLock的特点:

查看ReentrantLock源码,进入其lock方法中.

继续点进其acquire方法中,再点进其acquireQueued方法中.

我们发现acquireQueued在类AbstractQueuedSynchronizer中,AbstractQueuedSynchronizer是队列同步器,简称AQS,它是java并发用来构建锁或其它同步组件的基础框架,是JUCpacakge的核心,一般使用AQS的方式是继承.

像ReentrantLock这些子类是通过AQS实现的抽象方法来管理这些同步状态,一种同步结构往往是可以利用其它的结构去实现的,但是对某种同步结构的倾向会导致复杂晦涩的实现逻辑,所以把基础的同步相关的操作抽象在AQS中了,利用AQS为我们提供同步结构实现了范本.

AQS内部的数据和方法可以简单拆分为一个volatile数组成员表征状态即state,一个先进先出的等待队列,各种针对CAS的基础操作方法,以及期望各种同步结构去实现的acquire,release方法,利用AQS实现一个同步结构至少要实现两个基本类型的方法,acquire操作,获取资源的独占权,release操作,用来释放对某个资源的独占.

 

5.1 ReentrantLock公平性设置

传入true表示是公平锁.

建议程序确实有公平需要的时候再用,不用会有额外开销.

具体代码:

public class ReentrantLockDemo implements  Runnable{
    private static ReentrantLock lock = new ReentrantLock(true);
    @Override
    public void run(){
        while (true){
            try{
                lock.lock();
                System.out.println(Thread.currentThread().getName() + " get lock");
                Thread.sleep(1000);
            } catch (Exception e){
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo rtld = new ReentrantLockDemo();
        Thread thread1 = new Thread(rtld);
        Thread thread2 = new Thread(rtld);
        thread1.start();
        thread2.start();
    }
}

公平锁输出:

非公平锁输出:

ReentrantLock相比Synchronized因为可以像普通对象一样使用,所以可以利用它来提供各种便利的方法,进行精细的同步操作.甚至可以实现Synchronized难以表达的用例.

5.2 wait/notify/notifyAll对象化

ReentrantLock将Synchronized转变成了可控的对象,是不是也能将前面讲的wait/notify/notifyAll对象化?

位于JUC包中的locks.Condition做到了这一点.Condition最为典型的应用场景就是标准类库中的ArrayBlockingQueue,ArrayBlockingQueue是数组实现的线程安全的,有界的阻塞队列,线程安全是指ArrayBlockingQueue内部通过互斥锁保护竞争资源,其互斥锁是通过ReentrantLock来实现的,实现了多线程对竞争资源的互斥访问,而有界则指的是ArrayBlockingQueue对应的数组是有界限的,阻塞队列是指多线程访问竞争资源时,当竞争资源已被某线程获取时,其它要获取该资源的线程要阻塞等待.

ArrayBlockingQueue与condition是组合的关系,ArrayBlockingQueue内部有两个condition对象,一个是notEmpty,一个是notFull,而且condition依赖于ArrayBlockingQueue存在,通过condition可以实现对ArrayBlockingQueue更精确的访问,

可以看其构造函数,notEmpty与notFull都是同一个lock创建出来的,然后使用在特定的操作中.

notEmpty用在take方法中,用在当count=0的时候,满足当队列为空时,试图take线程的正确行为,应该是等待有新的消息加入到队列才去做返回,如同之前的future,使用notEmpty就可以优雅的实现等待逻辑,take操作的前提是要保证消息入队,只有队列有消息才触发去取走.

看一下入队的方法.可以看到一旦有消息被放入队列当中,count便会++,notEmpty会调用signal函数去通知等待的线程,signal如同之前说的notify,此时非空条件就会满足,take就能取到对应的东西了.

通过signal和await的组合,ArrayBlockingQueue就能优雅的完成了条件判断和通知等待线程,非常顺畅完成了状态流转.

注意到notEmpty是从new condition出来的,发现condition来自于ConditionObject,ConditionObject来源于AbstractQueuedSynchronizer也就是AQS框架,就是将我们的wait,notify,notifyall等操作转换成了相对应的对象,将复杂而晦涩的同步操作转变为直观可控的对象行为.

总结:

针对最后一条我们去找到park方法.

首先找到ReentrantLock类的lock方法,点击进acquire方法中.再点击进acquireQueued方法,

在acquireQueued中找到parkAndCheckInterrupt方法

点击进去,发现其调用的是LockSupport方法,再点进去发现其调用的是U.park方法.

再点进去,最后发现park方法位于unsafe类里,unsafe是一个类似于后门的工具,可以在任意内存位置处读写数据.另外unsafe还支持一些CAS的操作.

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值