这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
为什么要引入锁升级
在JDK1.6之前,java内置锁synchronized是一个重量级的锁,重量级锁为了将没有获取锁的线程挂起和唤醒都会从用户态切换到内核态,频繁的从用户态到内核态的切换,会很消耗CPU的资源,所以,在JDK1.6之前,我们都称synchronized为重量级锁。
在JDK1.6之后,引入锁升级的概念,即,synchronized不再一开始就是重量级锁,而是根据线程的并发情况,逐渐从无锁,偏向锁,轻量级锁,重量级锁逐次进行升级,力求以最小的代价获取到锁,值得注意的是锁只能升级,不能进行降级。这种设计的目的就是增加获取锁和释放锁的效率。因为锁的升级也是有一定的代价的。
其实做过开发的都知道,我们开发中,使用到的锁大致分为两种,一种称为乐观锁,一种称为悲观锁,因为在不同的场景下,使用不同种类的锁可以提高我们的程序执行效率,降低一定的资源消耗。
比如,在并发比较低的时候,使用乐观锁的开销要比悲观锁的代价要小很多,但是在高并发的场景下使用悲观锁的效率要比乐观锁要小。
因为乐观锁的设计一般通过CAS+自旋的机制完成的,高并发时,可能有些线程会因为一直没有获取到锁,导致一直进行自旋,做了大量没有意义的CPU空转其效果还不如直接将线程挂起。所以,基于此,jdk将synchronized做了这样的优化。
对象锁的几个状态
synchronized锁需要关联一个对象,当作用在静态方法时,锁关联的对象即为类所对应的Class对象,当作用在一个普通方法时,锁关联的对象即为this,当作用在代码块时,锁关联的对象即为我们传入的对象。那么这个关联的对象是如何表征目前的锁是什么锁呢?答案其实在对象的对象头中,有关对象结构的文章可以参考java对象结构
无锁状态
Java 对象刚创建时,还没有任何线程来竞争,说明该对象处于无锁状态(无线程竞争它)这偏向锁标识位是 0、锁状态 01
偏向锁状态
偏向锁是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价,如果处于偏向状态,对象的markWord会存储该偏向线程的线程id,注意这里的线程Id并不是我们通过Thread.currentThreadId(),而是操作系统分配的内置的线程id,当偏向的线程执行同步代码块时,只需要判断对象头中存储的线程id是不是当前线程id就可以,所以代价非常小。
轻量级锁
当有两个线程开始竞争锁对象时,情况发生变化,不在是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,那个线程先占有锁对象,锁对象的MarkWord就指向了那个线程的栈帧的锁记录。
当锁处于偏向锁状态时,另一个线程想获取到该锁时,偏向锁就会升级成轻量级锁,企图获取锁的线程会通过自旋的形式获取锁,JVM不会阻塞抢占锁的线程,避免了用户线程从用户态切换到内核态的消耗。
自旋是消耗CPU资源的,如果一直获取不到锁,线程会一直做没有意义的CPU自旋,所以,需要设定一个最大的自旋等待时间,JVM引入了自适应自旋锁,自旋的时间是不固定的,而是有前一次在该锁上线程自旋时间以及锁的拥有者状态决定,如果线程自旋成功,那么下次自旋的字数会更多,如果自旋失败,则下次的自旋次数就会减少。
重量级锁
如果持有锁的线程执行的时间超过最大等待时间仍没有释放锁,那么就会导致其他正在等待获取锁的线程在最大等待时间还是获取不到锁,此时,自旋就不会一直持续下去,这时,JVM会停止线程自旋,将参与自旋的线程挂起,锁膨胀微重量级锁。
重量级锁会让其他申请的线程直接进入阻塞,因为涉及到用户线程从用户态,切换到内核态,所以性能降低,重量级锁也叫
同步锁这个锁对象的MarkWord会指向一个监视器Monitor对象,该对象用集合的形式来登记和管理排队的线程。