多线程总结(三)Synchronized 关键字的底层实现原理(锁升级概念)以及优化
一,前言
前面我们学习了Synchronized 关键字,的用法,学完直接,我们可以来深入了解一下Synchronized的实现原理以及优化
二,实现原理
首先我们要知道对象锁(monitor)机制
Synchronized不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步,那么Synchronized锁对象是存在哪里?
首先涉及一点jvm的知识(jvm我也还没有复习),可能解释的不是特别好,我们知道new出来的对象会放在堆内存中间,在堆内存中申请空间(1.申请空间,2.赋值,3.指向对应的指针),空间有分为三大部分
对象头,实际变量,填充字段(具体的等我复习完jvm再和大家详细分享,这里只需要关注对象头),HotSpot虚拟机的对象头(Object Header)包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,可在32位与64位完成,这里只看64位,官方称它为“Mark Word”。
而我们的锁对象的信息就存放在Mark Word中
如图可以看出,信息中明确有记录了4中锁的标志位(锁升级的概念后面会详细说),而我们Synchronized 对应的重量级锁标志位为10,所有这就是为什么Synchronized 锁的对象而不是代码
多线程再访问Synchronized 的时候必须要从monitor中获取对应的开始与退出指令,而对象的监视器(monitor)锁对象由ObjectMonitor对象实现,而其中有个变量是count,记录被获取数量。
具体逻辑
当多个线程同时访问该方法,那么这些线程会先被放进等待队列,此时这些线程处于blocking状态
当一个线程获取到了实例对象的监视器(monitor)锁,那么就可以进入running状态,执行方法,此时,ObjectMonitor对象的_owner指向当前线程,_count加1表示当前对象锁被一个线程获取
当running状态的线程调用wait()方法,那么当前线程释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1,同时线程进入_WaitSet队列,直到有线程调用notify()方法唤醒该线程,则该线程重新获取monitor对象进入_Owner区
如果当前线程执行完毕,那么也释放monitor对象,进入waiting状态,ObjectMonitor对象的_owner变为null,_count减1
如果再其中当前线程如果调用其他的静态Synchronized 方法,可以嘛?答案是可以这就是锁的重入性,对应同一个线程只需要获取一次,避免死锁
三.锁升级
我们知道Synchronized 对应的直接向OS申请的重量级锁,加锁之后,效率几乎会低100倍,但是再早期的jdk中,没有锁升级概念,所有直接的重量级锁,导致效率很低,但是再之后jdk(6)版本,提出了锁升级概念
首先我们要知道锁有4种状态,无锁状态、偏向锁、轻量级锁和重量级锁。
1.偏向锁。
当单独一个线程进入代码块时候,系统不会对其进行实际上的加锁操作,而是在Mark Word中将标志位改为01,然后记录其对应的线程id,然后当该线程再次访问的时候,进行id比较如果id相同如果一致(还是线程1获取锁对象);如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程,
所以偏向锁有2大特点
1)虽然说锁不会降级,但是偏向锁可以变成无锁
2)偏向锁不会主动释放锁,只是进行id存储与标志位的改变和比较
2.轻量级锁(自旋)
前面说到当偏向锁遇到线程竞争解决不了问题的时候,会变成轻量级锁(自旋)
因为阻塞线程需要CPU从用户态转到内核态,这样效率太低,但是假设一种情况,占用资源的线程马上就释放锁了,那么我们是不是就不用阻塞其他竞争线程了,可以让其进行自旋,等待当前线程的资源释放,线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间),然后使用CAS把对象头中的内容替换为线程1存储的锁记录的地址;
如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。
总结:
1)注意:自旋是需要占用cpu的,所以一般对于自旋会有次数限制,可能一般为10次,如果超出那么升级会重量级锁Synchronized(或者多个线程竞争,比如又来了一个线程3,4)
2)在升级为重量级锁的时候,会将除了当前线程之外的所以线程全部设置为阻塞
3.重量级锁
也就是我们的Synchronized
总结:
1.执行时间长,线程多,高并发用OS锁
2.执行时间短,线程少,用自旋
(附上一张网上找到的神图)
四.锁优化
1.锁合并