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
    评论
### 回答1: synchronized关键字的底层实现原理涉及到Java对象头的概念。在Java对象头中,有一个表示锁状态的标志位,它用来标识对象的锁状态。当线程进入一个synchronized方法或代码块时,会尝试获取对象的锁。如果该锁没有被其他线程占用,则该线程会成功获取锁并进入临界区。如果该锁已经被其他线程占用,则该线程会进入阻塞状态,直到锁被释放。当线程执行完synchronized方法或代码块后,会释放锁。 在JVM中,synchronized关键字实现的锁有两种,分别为偏向锁和重量级锁。偏向锁是一种优化机制,它在对象创建时会将锁标志位初始化为偏向模式。当一个线程获取该对象的锁时,会将当前线程的ID记录在对象头中,并将锁标志位设置为偏向模式。以后该线程再次获取该对象的锁时,无需竞争,可以直接获取。重量级锁则是一种比较传统的锁机制,它使用操作系统的互斥量来实现锁。当多个线程竞争同一对象的锁时,会进入阻塞队列,等待锁被释放。 因此,synchronized关键字的底层实现原理就是通过Java对象头中的标志位来实现锁状态的记录和判断,并通过偏向锁和重量级锁来优化锁的竞争。 ### 回答2: synchronized是Java中用来实现线程同步的关键字,它保证了在同一时间只有一个线程可以进入被synchronized修饰的代码块或方法。synchronized底层实现原理涉及到Java对象头、Monitor、线程间通信等。 每个Java对象在内存中都会有一个对象头,对象头中包含了一些元数据字段,其中有一个字段用来记录当前对象的锁信息。当一个线程进入synchronized代码块时,首先会尝试对对象加锁,如果对象的锁信息表明已经被其他线程锁定,则该线程会进入阻塞状态,等待其他线程释放锁。如果对象的锁信息表明还没有被其他线程锁定,则将对象头中的锁信息设置为该线程,并且将一个Monitor关联到该对象上。 Monitor是Java中用来实现监视器锁的机制,它与每个Java对象关联。Monitor内部维护了一个线程等待队列和一个拥有锁的线程。每个Monitor对象只能拥有一个线程,其他线程需要获取锁时只能进入等待队列。当某个线程执行完synchronized代码块或方法时,会释放锁,并且唤醒等待队列中的一个线程来竞争锁。 线程间的通信是通过底层的wait()、notify()和notifyAll()方法实现的。当一个线程执行wait()方法时,它会释放锁并进入阻塞状态,等待其他线程调用notify()或notifyAll()方法来唤醒它。唤醒的线程将进入就绪状态,并与其他线程竞争锁,竞争成功后将继续执行。 总结起来,synchronized底层实现原理是通过Java对象头、Monitor和线程间的通信来实现的。它保证了在同一时间只有一个线程可以进入被synchronized修饰的代码块或方法,避免了多个线程对共享资源的并发访问造成的数据不一致问题。 ### 回答3: synchronized是Java中用于实现线程同步的关键字,可以用于修饰方法或代码块,保证多个线程对同一资源进行访问时的互斥。 synchronized底层实现原理是基于对象的监视器(Monitor)机制。在Java中的每一个对象都会有一个与之关联的Monitor对象,Monitor对象用于同步对共享资源的访问。当一个线程遇到synchronized修饰的代码块或方法时,它首先需要获得对象的Monitor对象的锁。若锁已经被其他线程持有,则该线程会进入阻塞状态,直到锁被释放。当该线程获得锁之后,它就可以执行临界区内的代码了。 当一个线程执行完synchronized代码块或方法后,会释放对Monitor对象的锁,其他处于等待的线程就有机会获得锁,进入临界区执行代码。这样就保证了在任意时刻,只有一个线程可以获得锁,其他线程需要等待,实现了对共享资源的互斥访问。 synchronized通过内置的锁机制来实现线程间的同步,确保了数据的一致性和完整性。它基于底层的Monitor机制利用了操作系统的原子性操作,保证了多线程并发执行时的正确性。但是,在synchronized的机制下,一个线程获得了对象的锁之后,其他线程必须等待,可能会造成线程的阻塞和延迟。此外,在一些特殊情况下,可能会出现死锁的问题,即多个线程相互等待对方释放锁。 总之,synchronized是一种可靠的线程同步机制,通过Monitor对象的锁机制实现对共享资源的互斥访问。它的底层实现原理是基于对象的监视器(Monitor)机制,利用锁和等待队列来控制线程的执行和互斥访问。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值