同步锁优化jdk1.6对锁的实现引入了大量优化,如自旋锁、自适应自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术,降低锁操作的开销。锁主要存在四种状态,分别是:无锁->偏向锁->轻量级锁->重量级锁,并且会随着激烈的竞争逐步升级。请注意,锁可以升级但不能降级。这个策略是为了提高获取和释放锁的效率。锁优化偏向锁偏向锁是Java6之后新增的一种锁,是一种针对加锁操作的优化方式。经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而是总是被同一个线程多次获取,所以为了降低同一个线程获取锁的成本(其中涉及到一些CAS操作,比较耗时),引入了偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,则锁进入偏向模式。这时MarkWord的结构也变成了偏向锁结构。当线程再次请求锁时,不需要进行任何同步操作,即获取锁的过程,省去了很多与锁申请相关的操作,从而提高了程序的性能。因此,对于没有锁竞争的场合,偏向锁有很好的优化效果。毕竟同一个线程很有可能连续多次申请同一个锁。但是对于锁竞争比较激烈的情况,偏向锁是无效的,因为这种情况下很有可能每次申请锁的线程都不一样,所以这种情况下不应该使用偏向锁,否则收益会很大得不偿失,需要注意的是,偏向锁失效后,不会马上扩容为重量级锁,而是先升级为轻量级锁。下面我们继续了解轻量级锁。引入偏向锁的主要目的是在没有多线程竞争的情况下,尽量减少不必要的轻量级锁执行路径。上文提到,轻量级锁的加锁和解锁操作需要依赖多条CAS原子指令。那么偏向锁是如何减少不必要的CAS操作的呢?大家可以看Mark作品的结构来理解。只需要查看是否是偏向锁,锁ID和ThreadID就可以获取锁。检查MarkWord是否为偏向状态,即是否为偏向锁1,锁标志为01;如果是偏向状态,则测试线程ID是否为当前线程ID,如果是,则转步骤(5),否则转步骤(3);如果线程ID不是当前线程ID,则使用CAS操作竞争锁,如果竞争成功,则将MarkWord的线程ID替换为当前线程ID,否则执行线程(4);如果CAS竞争锁失败,证明存在多线程竞争情况。当到达全局安全点时,挂起获取偏向锁的线程,并将偏向锁升级为轻量级锁,然后阻塞在安全点的线程继续执行同步代码块;同步代码块的执行释放了锁。偏向锁的释放采用只有竞争才会释放锁的机制。该线程不会主动释放偏向锁,需要等待其他线程竞争。偏向锁的取消需要等待全局安全点(这个时间点是没有执行代码的时候)。步骤如下:挂起拥有偏向锁的线程,判断锁对象是否还处于锁定状态;撤销偏向状态,返回无锁状态(01)或轻量级锁状态;如果轻量级锁与偏向锁失效,虚拟机不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化方法(1.6后添加),此时MarkWord的结构也成为一种轻量级的锁结构。轻量级锁提高程序性能的基础是“对于大多数锁来说,在整个同步周期内是没有竞争的”。请注意,这是经验数据。需要理解的是,适合轻量级锁的场景是线程交替执行同步块的场合。如果存在同时访问同一个锁的场合,会导致轻量级锁扩展为重量级锁。引入轻量级锁的主要目的是在不存在多线程竞争的前提下,降低传统重量级锁使用操作系统互斥锁的性能消耗。当关闭偏向锁功能或多个线程竞争偏向锁,偏向锁升级为轻量级锁时,会尝试获取轻量级锁。获取锁判断当前对象是否处于无锁状态(hashcode,0,01)。如果是,JVM会先在当前线程的栈帧中创建一个名为锁记录(LockRecord)的空间来存放当前的锁对象。对象的MarkWord副本(官方给这个副本加了Displaced前缀,即DisplacedMarkWord);否则执行步骤(3);JVM使用CAS操作尝试更新对象的MarkWord指向LockRecord,如果成功则表明锁已竞争,则将锁标志更改为00(表示该对象处于轻量级锁状态),并执行同步操作;如果失败,执行步骤(3);判断当前对象的MarkWord是否指向当前线程帧的栈,如果是,说明当前线程已经持有当前对象的锁,然后直接执行同步代码块;否则,只能说明这个锁对象已经被其他线程抢占了。这时候就需要将轻量级锁扩展为重量级锁。锁标志变为10,等待线程将进入阻塞状态;锁的释放也是通过轻量级锁的CAS操作来进行的。主要步骤如下:取出获取的轻量级锁,保存在DisplacedMarkWord数据中;使用CAS操作替换当前对象MarkWord中提取的数据。如果成功,则表示释放锁成功,否则执行(3);如果CAS操作替换失败,说明有其他线程在尝试获取锁,你需要在释放锁的同时唤醒挂起的线程。对于轻量级锁,性能提升的基础是“对于大多数锁来说,在整个生命周期中不会存在竞争”。如果这个基础被打破,除了互斥的开销之外,还会有额外的CAS操作,所以在多线程竞争的情况下,轻量级锁比重量级锁慢;自旋锁轻量级锁失效后,虚拟机实际上不会在操作系统层面挂起线程,进行称为自旋锁的优化。这是基于这样的事实,在大多数情况下,线程不会持有锁太久。如果直接在操作系统层面挂起线程,可能得不偿失。毕竟操作系统在线程之间切换的时候需要从用户态切换到线程。在核心状态下,这些状态之间的转换需要比较长的时间,时间成本比较高,所以自旋锁假设当前线程可以在近期获得锁,所以虚拟机会让线程当前想获取锁的做几个空循环(这就是为什么叫自旋),一般不会花太长时间,可能50个循环或者100个循环,几次循环后,如果拿到锁,就进入关键部分顺利。如果无法获得锁,线程将在操作系统层面被挂起。这就是自旋锁的优化方法,这种方法确实可以提高效率。最后没办法,只能升级为重量级锁。阻塞和唤醒线程需要CPU从用户态切换到核心态。频繁的阻塞和唤醒对于CPU来说是一个沉重的工作负载,必然会给系统的并发性能带来很大的压力。同时我们发现,在很多应用中,对象锁的锁状态只会持续很短的时间,为了这短时间频繁的阻塞和唤醒线程是不值得的。所以引入自旋锁。什么是自旋锁?所谓自旋锁就是让线程先等待一段时间,而不是立即挂起,看持有锁的线程会不会很快释放锁。为什么要等?只是执行一个无意义的循环(自旋),类似于CAS。自旋等待不能代替阻塞,更不用说对处理器数量的要求了(多核,现在好像没有单核处理器了),虽然可以避免线程切换带来的开销,但是占用处理器时间.如果持有锁的线程快速释放锁,自旋的效率是非常好的。相反,自旋线程会白白消耗处理资源,不会做任何有意义的工作。如果不去厕所拉屎,会导致性能的浪费。因此,旋转等待时间(旋转次数)必须有一个限制。如果自旋超过了定义的时间,仍然没有获取到锁,就应该挂起。自旋锁在JDK1.4.2引入,默认关闭,但可以通过-XX:+UseSpinning开启,JDK1.6默认开启。同时默认旋转次数为10次,可以通过参数-XX:PreBlockSpin进行调整;如果通过参数-XX:preBlockSpin来调整自旋锁的自旋次数,会带来很多不便。如果我把参数调成10,但是系统中有很多线程在你刚退出的时候就释放了锁(多自旋一两次就可以获得锁),你是不是很尴尬?于是JDK1.6引入了自适应自旋锁来让虚拟机越来越智能。自适应自旋锁JDK1.6引入了一种更智能的自旋锁,即自适应自旋锁。所谓自适应,就是自旋的次数不再是固定的,它是由同一把锁上一次自旋的时间和锁拥有者的状态决定的。它是怎么做到的?如果线程自旋成功,那么下次自旋的次数会更多,因为虚拟机认为自从上次成功之后,很有可能这次自旋会再次成功,所以会让自旋一直等待.更多次。反之,如果对于某个锁,只有很少的自旋能够成功,那么以后需要锁的时候就会减少甚至省略自旋的次数,以免浪费处理器资源。有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁状态的预测会越来越准确,虚拟机也会越来越智能。锁消除锁消除是虚拟机的另一种锁优化。这个优化更彻底。Java虚拟机在JIT编译时(可以简单理解为某段代码即将第一次执行时编译,也称为即时编译)。通过扫描运行上下文,移除不太可能有共享资源竞争的锁。通过这种方式消除不必要的锁,可以节省请求锁的无意义时间。StringBuffer的append是同步方法,但是在add方法中的StringBuffer是局部变量,不会被其他线程使用。因此,StringBuffer不存在共享资源竞争的可能,JVM会自动解除对它的锁。为了保证数据的完整性,我们需要对这部分操作进行同步控制,但是在某些情况下,JVM检测到没有共享数据竞争,JVM就会消除这些同步锁。锁消除的基础是逃逸分析的数据支持。如果没有竞争,为什么需要锁定呢?所以锁消除可以节省请求锁的无意义时间。变量转义是否需要对虚拟机进行数据流分析来判断,但是我们程序员不是很清楚吗?我们是否将同步放在我们知道没有数据竞争的代码块之前?但是有时候程序并不是我们想的那样?虽然我们没有展示锁的使用,但是当我们使用一些JDK内置的API时,比如StringBuffer、Vector、HashTable等,这时候就会有不可见的加锁操作。比如StringBuffer的append()方法和Vector的add()方法:COPYpublicvoidvectorTest(){Vector
