说到锁,都会提 synchronized 。这个英文单词儿啥意思呢?翻译成中文就是「同步」的意思java
通常都是使用 synchronized 这个关键字来给一段代码或者一个方法上锁,使得这段代码或者方法,在同一个时刻只能有一个线程来执行它。web
synchronized 相比于 volatile 来讲,用的比较灵活,你能够在方法上使用,能够在静态方法上使用,也能够在代码块上使用。算法
关于 synchronized 这一块大概就说到这里,阿粉今天想着重来讲一下, synchronized 底层是怎么实现的编程
我知道能够利用 synchronized 关键字来给程序进行加锁,可是它具体怎么实现的我不清楚呀,别急,我们先来看个 demo :数组
public class demo { public void synchronizedDemo(Object lock{ synchronized(lock){ lock.hashCode(); } } }
上面是我写的一个 demo ,而后进入到 class 文件所在的目录下,使用 javap -v demo.class
来看一下编译的字节码(在这里我截取了一部分):安全
public void synchronizedDemo(java.lang.Object); descriptor: (Ljava/lang/Object;)V flags: ACC_PUBLIC Code: stack=2, locals=4, args_size=2 0: aload_1 1: dup 2: astore_2 3: monitorenter 4: aload_1 5: invokevirtual #2 // Method java/lang/Object.hashCode:()I 8: pop 9: aload_2 10: monitorexit 11: goto 19 14: astore_3 15: aload_2 16: monitorexit 17: aload_3 18: athrow 19: return Exception table: from to target type 4 11 14 any 14 17 14 any
应该可以看到当程序声明 synchronized 代码块时,编译成的字节码会包含 monitorenter
和 monitorexit
指令,这两种指令会消耗操做数栈上的一个引用类型的元素(也就是 synchronized 关键字括号里面的引用),做为所要加锁解锁的锁对象。若是看的比较仔细的话,上面有一个 monitorenter
指令和两个 monitorexit
指令,这是 Java 虚拟机为了确保得到的锁不论是在正常执行路径,仍是在异常执行路径上都可以解锁。多线程
关于 monitorenter
和 monitorexit
,能够理解为每一个锁对象拥有一个锁计数器和一个指向持有该锁的线程指针:并发
为何采用这种方式呢?是为了容许同一个线程重复获取同一把锁。好比,一个 Java 类中拥有好多个 synchronized 方法,那这些方法之间的相互调用,不论是直接的仍是间接的,都会涉及到对同一把锁的重复加锁操做。这样去设计的话,就能够避免这种状况。函数
在 Java 多线程中,全部的锁都是基于对象的。也就是说, Java 中的每个对象均可以做为一个锁。你可能会有疑惑,不对呀,不是还有类锁嘛。可是 class 对象也是特殊的 Java 对象,因此呢,在 Java 中全部的锁都是基于对象的性能
在 Java6 以前,全部的锁都是"重量级"锁,重量级锁会带来一个问题,就是若是程序频繁得到锁释放锁,就会致使性能的极大消耗。为了优化这个问题,引入了"偏向锁"和"轻量级锁"的概念。因此在 Java6 及其之后的版本,一个对象有 4 种锁状态:无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态。
在 4 种锁状态中,无锁状态应该比较好理解,无锁就是没有锁,任何线程均可以尝试修改,因此这里就一笔带过了。
随着竞争状况的出现,锁的升级很是容易发生,可是若是想要让锁降级,条件很是苛刻,有种你想来能够,可是想走不行的赶脚。
阿粉在这里啰嗦一句:不少文章说,锁若是升级以后是不能降级的,其实在 HotSpot JVM 中,是支持锁降级的锁降级发生在 Stop The World 期间,当 JVM 进入安全点的时候,会检查有没有闲置的锁,若是有就会尝试进行降级
看到 Stop The World 和 安全点 可能有人比较懵,我这里简单说一下,具体还须要读者本身去探索一番.(由于这是 JVM 的内容,这篇文章的重点不是 JVM )
在 Java 虚拟机里面,传统的垃圾回收算法采用的是一种简单粗暴的方式,就是 Stop-the-world ,而这个 Stop-the-world 就是经过安全点( safepoint )机制来实现的,安全点是什么意思呢?就是 Java 程序在执行本地代码时,若是这段代码不访问 Java 对象/调用 Java 方法/返回到原来的 Java 方法,那 Java 虚拟机的堆栈就不会发生改变,这就表明执行的这段本地代码能够做为一个安全点。当 Java 虚拟机收到 Stop-the-world 请求时,它会等全部的线程都到达安全点以后,才容许请求 Stop-the-world 的线程进行独占工做
接下来就介绍一下几种锁和锁升级
在刚开始就说了, Java 的锁都是基于对象的,那是怎么告诉程序我是个锁呢?就不得不来讲, Java 对象头 每一个 Java 对象都有对象头,若是是非数组类型,就用 2 个字宽来存储对象头,若是是数组,就用 3 个字宽来存储对象头。在 32 位处理器中,一个字宽是 32 位;在 64 位处理器中,字宽就是 64 位咯~对象头的内容就是下面这样:
我们主要来看 Mark Word 的内容:
从上面表格中,应该可以看到,是偏向锁时, Mark Word
存储的是偏向锁的线程 ID ;是轻量级锁时, Mark Word
存储的是指向线程栈中 Lock Record
的指针;是重量级锁时, Mark Word
存储的是指向堆中的 monitor
对象的指针
HotSpot 的做者通过大量的研究发现,在大多数状况下,锁不只不存在多线程竞争,并且老是由同一线程屡次得到
基于此,就引入了偏向锁的概念
因此啥是偏向锁呢?用大白话说就是,我如今给锁设置一个变量,当一个线程请求的时候,发现这个锁是 true
,也就是说这个时候没有所谓的资源竞争,那也不用走什么加锁/解锁的流程了,直接拿来用就行。可是若是这个锁是 false
的话,说明存在其余线程竞争资源,那我们再走正规的流程
看一下具体的实现原理:
当一个线程第一次进入同步块时,会在对象头和栈帧中的锁记录中存储锁偏向的线程 ID 。当下次该线程进入这个同步块时,会检查锁的 Mark Word 里面存放的是否是本身的线程 ID。若是是,说明线程已经得到了锁,那么这个线程在进入和退出同步块时,都不须要花费 CAS 操做来加锁和解锁;若是不是,说明有另一个线程来竞争这个偏向锁,这时就会尝试使用 CAS 来替换 Mark Word 里面的线程 ID 为新线程的 ID 。此时会有两种状况:
偏向锁使用了一种等到竞争出现时才释放锁的机制。也就说,若是没有人来和我竞争锁的时候,那么这个锁就是我独有的,当其余线程尝试和我竞争偏向锁时,我会释放这个锁
在偏向锁向轻量级锁升级时,首先会暂停拥有偏向锁的线程,重置偏向锁标识,看起来这个过程挺简单的,可是开销是很大的,由于:
你觉得就是升级一个轻量级锁?too young too simple
偏向锁向轻量级锁升级的过程当中,是很是耗费资源的,若是应用程序中全部的锁一般都处于竞争状态,偏向锁此时就是一个累赘,此时就能够经过 JVM 参数关闭偏向锁: -XX:-UseBiasedLocking=false
,那么程序默认会进入轻量级锁状态
最后,来张图吧~
若是多个线程在不一样时段获取同一把锁,也就是不存在锁竞争的状况,那么 JVM 就会使用轻量级锁来避免线程的阻塞与唤醒
JVM 会为每一个线程在当前线程的栈帧中建立用于存储锁记录的空间,称之为 Displaced Mark Word 。若是一个线程得到锁的时候发现是轻量级锁,就会将锁的 Mark Word 复制到本身的 Displaced Mark Word 中。以后线程会尝试用 CAS 将锁的 Mark Word 替换为指向锁记录的指针。
若是替换成功,当前线程得到锁,那么整个状态仍是 轻量级锁
状态
若是替换失败了呢?说明 Mark Word 被替换成了其余线程的锁记录,那就尝试使用自旋来获取锁.(自旋是说,线程不断地去尝试获取锁,通常都是用循环来实现的)
自旋是耗费 CPU 的,若是一直获取不到锁,线程就会一直自旋, CPU 那么宝贵的资源就这么被白白浪费了
解决这个问题最简单的办法就是指定自旋的次数,好比若是没有替换成功,那就循环 10 次,尚未获取到,那就进入阻塞状态
可是 JDK 采用了一个更加巧妙的方法---适应性自旋。就是说,若是此次线程自旋成功了,那我下次自旋次数更多一些,由于我此次自旋成功,说明我成功的几率仍是挺大的,下次自旋次数就更多一些,那么若是自旋失败了,下次我自旋次数就减小一些,就好比,已经看到了失败的前兆,那我就先溜,而不是非要“不撞南墙不回头”
自旋失败以后,线程就会阻塞,同时锁会升级成重量级锁
在释放锁时,当前线程会使用 CAS 操做将 Displaced Mark Word 中的内容复制到锁的 Mark Word 里面。若是没有发生竞争,这个复制的操做就会成功;若是有其余线程由于自旋屡次致使轻量级锁升级成了重量级锁, CAS 操做就会失败,此时会释放锁同时唤醒被阻塞的过程
一样,来一张图吧:
重量级锁依赖于操做系统的互斥量( mutex )来实现。可是操做系统中线程间状态的转换须要相对比较长的时间(由于操做系统须要从用户态切换到内核态,这个切换成本很高),因此重量级锁效率很低,可是有一点就是,被阻塞的线程是不会消耗 CPU 的
每个对象均可以当作一个锁,那么当多个线程同时请求某个对象锁时,它会怎么处理呢?
对象锁会设置集中状态来区分请求的线程:
Contention List:全部请求锁的线程将被首先放置到该竞争队列Entry List: Contention List 中那些有资格成为候选人的线程被移到 Entry List 中
Wait Set:调用 wait 方法被阻塞的线程会被放置到 Wait Set 中
OnDeck:任什么时候刻最多只能有一个线程正在竞争锁,该线程称为 OnDeck
Owner:得到锁的线程称为 Owner
!Owner:释放锁的线程
当一个线程尝试得到锁时,若是这个锁被占用,就会把该线程封装成一个 ObjectWaiter
对象插入到 Contention List 队列的队首,而后调用 park
函数挂起当前线程
当线程释放锁时,会从 Contention List 或者 Entry List 中挑选一个线程进行唤醒
若是线程在得到锁以后,调用了 Object.wait
方法,就会将该线程放入到 WaitSet 中,当被 Object.notify
唤醒后,会将线程从 WaitSet 移动到 Contention List 或者 Entry List 中。
可是,当调用一个锁对象的 wait
或 notify
方法时,若是当前锁的状态是偏向锁或轻量级锁,则会先膨胀成重量级锁
synchronized 关键字是经过 monitorenter 和 monitorexit 两种指令来保证锁的
当一个线程准备获取共享资源时:
轻量级锁
的状态重量级锁
的状态,此时,自旋的线程进行阻塞,等待以前线程执行完成而且唤醒本身参考:
到这里,整篇文章的内容就算是结束了。