Synchronized实现原理与锁升级机制

        并发编程,要求线程之间能够相互配合以完成工作。这就涉及到数据共享和线程协作。

        Java支持多个线程公式访问同一个对象的方法和成员变量,而关键字synchronized的作用则是确保多线程在同一时刻,只能有一个线程访问synchronized所修饰的方法或同步块,确保线程访问的排他性。
        synchronized是内置在对象头的MarkWork字段。对象头的构成在我的"Jvm对象创建过程"博客中有详细说明。因此,它又被称为内置锁

        对象锁和类锁:对象锁是作用于对象实例上,类锁是用于类的静态方法或者类的class对象上。我们知道,每个类都有一个 class 对象,因此每个类也有一个类锁,类锁实际上是作用到类的对象上,其实也是对象锁,和普通的对象锁没有什么区别。

synchronized实现原理

        Synchronized的实现是基于进入和退出Monitor对象来实现方法或代码块的同步。在字节码层面是通过成对的MonitorEnter和MonitorExit指令来实现。

        对同步块来说,MonitorEnter指令插入在同步代码块的开始位置,而monitorExit指令则插入在方法结束和抛出异常的地方,JVM保证MonitorEnter必须有对应的MonitorExit。当代码执行到MonitorEnter时,会尝试获取该对象Monitor的所有权,即尝试获得对象锁,获取Monitor对象时可能遇到如下的情形:

        1、如果Monitor的进入数为0,则该线程进入Monitor,然后将进入数设置为1,该线程即为Monitor的所有者;

        2、如果线程已经占有该Monitor,只是重新进入,则进入Monitor的进入数加1;

        3、如果其他线程已经占用了Monitor,则该线程进入阻塞状态,直到Monitor的进入数为0(线程执行到monitorExit,会对获得的Monitor的进入数减1),再重新尝试获取Monitor的所有权;

        对同步方法来说,没有通过指令MonitorEnter和MonitorExit来实现,而是方法上多了ACC_SYNCHRONIZED标示符。JVM根据该标示符来实现方法的同步。

        当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取Monitor,获取成功之后才能执行方法体,方法执行完后再释放Monitor。在方法执行期间,其他线程无法再获得该Monitor对象。ACC_SYNCHRONIZED实际也是基于MonitorEnter/MonitorExit来实现。

        Synchronized使用的锁是存放在Java对象头的MarkWord里面,MarkWord存储了同步状态、标识、hashcode、GC状态等信心。        

锁升级机制

        传统的协作阻塞式编程,当线程无法获取锁被阻塞,需要进行cpu上下文的切换。具统计cpu执行一条指令是零点几纳秒,而执行一次上下文切换是执行一条指令耗时的上千倍。这种线程竞争锁失败需要进行上文切换进入阻塞状态的锁机制,被称为重量级锁

        为了提高Synchronized的执行效率,Jvm设计了Synchronized的锁升级机制。既是根据需要修改锁状态,逐渐升级锁的类型。锁升级的过程依次是无锁状态偏向锁状态轻量级锁状态、重量级锁状态。锁升级过程是不可逆的,一旦升级则不会再发生回退。    

偏向锁 

        具大数据统计,多数情况下锁的使用并不存在多线程竞争,并且总是由同一个线程获得。因此,引入偏向锁,降低线程首次获得锁的代价。

        偏向锁,既是它会偏向于第一个访问锁的线程。在程序运行中,只有一个线程访问同步锁,不存在多线程争用的情况。则线程不需要触发同步,直接给该线程加一个偏向锁。

        偏向锁获取过程:

        1、 访问MarkWord中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态;

        2、 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入“5”,否则进入“3”;

        3、 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4;

        4、 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

        5、 执行同步代码。

        偏向锁的撤销(上述步骤4),当遇到其他线程竞争偏向锁时,持有偏向锁的线程会释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。撤销偏向锁的时候会导致STW(stop the word)现象。

        偏向锁适用在,始终只有一个线程在执行同步块的情形。在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,引起STW,导致性能下降。

轻量级锁

        当发生锁的竞争时,偏向锁就会升级为轻量级锁。轻量级锁是自旋锁实现。Synchronized升级轻量级锁的设计设计的思想是:如果持有锁的线程能够在很短时间内释放锁,则等待的线程可以不作上下文切换,只是进行数次自旋等一等,持有锁的线程释放锁后即可立即获取锁。因此避免上下文切换的开销(上面提到上下文切换比执行CPU指令的开销要大的多)。

        轻量级锁的加锁过程:

        1、在代码进入同步块的时候,虚拟机首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的MarkWord的拷贝,官方称之为Displaced Mark Word。

        2、拷贝对象头中的Mark Word复制到锁记录中。

        3、虚拟机使用CAS操作尝试将对象的MarkWord更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。

        4、更新动作成功,则该线程就拥有了该对象的锁,并且对象MarkWord的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态

        5、更新操作失败,虚拟机首先会检查对象的MarkWord是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,则可以直接执行同步块。否则说明多个线程竞争锁,那么它就会自旋等待锁,一定次数后(自旋的开销超过进行上下文切换的开销)仍未获得锁对象。重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒他。锁标志的状态值变为“10”,MarkWord中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态,此时锁升级为重量级锁

重量级锁

        线程自旋是需要消耗CPU的,线程不能一直占用CPU做自旋动作。因此,需要设定一个自旋等待的最大时间。当持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,则竞争锁的线程会停止自旋进入阻塞状态,此时升级到重量级锁。

        关于自旋最大次数(自旋最大等待时间)。jdk1.5默认为10次。在1.6引入了适应性自旋锁,即自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为是一个线程上下文切换的时间。

        Synchronized升级到重量级锁阶段后,线程再竞争锁失败,则要执行上下问切换,并进入阻塞状态。等待锁资源被释放后,再由操作系统调起。

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值