锁优化文章已同步至GitHub开源项目:JVM底层原理解析高效并发是JDK5升级到JDK6后的重要提升。HotSpot虚拟机开发团队在这个版本上花费了巨大的资源,进行了各种Lock优化。例如自旋锁、自适应自旋锁、锁消除、锁扩展、轻量级锁、偏向锁等,这些技术都是为了更高效地在线程间共享数据,解决竞争问题。从而提高程序的运行效率。自旋锁和自适应自旋锁自旋锁在互斥同步过程中,对性能影响最大的是阻塞的实现。挂起线程、恢复线程等操作都需要将用户态转换为内核态才能完成。这些操作给性能带来了巨大的压力。虚拟机的开发团队也注意到,共享数据的锁定状态只会持续很短的时间。对于这短短的一段时间,线程被挂起,然后转入内核态的时间可能比处于锁定状态的时间要长。因此,我们可以让等待同步锁的进程不进入块,而是原地等待一段时间,不要放弃处理器的执行时间,看持有锁的线程会不会很快释放锁。为了让线程等待,我们可以让线程执行一个忙循环(原地自旋),这就是自旋锁。自旋锁是在JDK1.4.2之后引入的,但是默认是禁用的。我们可以使用-XX:+UseSpinning参数来启用它。JDK1.6以后默认开启。自旋锁不是阻塞的,所以避免了从用户态到内核态的频繁转换,但是会占用处理器的执行时间。如果持有对象锁的线程在短时间内执行,那么释放了锁,那么自旋锁的效果会很好。如果持有对象锁的线程执行时间很长,自旋锁会白白消耗处理器的执行时间,从而导致浪费性能。在这种情况下,最好阻塞等待线程。默认的自旋次数是10,也就是说一个线程自旋10次还没有获得对象锁,就会被阻塞。我们也可以通过参数-XX:PreBlockSpin来改变。Adaptivespinlock无论你使用默认的10次还是用户定义的次数,所有线程对于整个虚拟机都是一样的。但是,同一个虚拟机中线程的状态是不一样的。有些锁对象较长,有些较短。因此在JDK1.6中,引入了自适应自旋锁。自适应自旋锁是指自旋时间不再固定,而是根据当前情况动态设置。主要看同一个锁的自旋时间和锁拥有者的状态。如果在同一个对象锁上,最后一个获得对象锁的线程成功自旋等待,但没有进入阻塞状态,说明表示这个对象锁的线程执行时间会很短,虚拟机认为这次可能又成功了,进而让这个自旋时间更长。如果对于某个锁,很少获取到锁在自旋状态下,意味着这个对象锁的线程执行时间比较长,以后虚拟机可能会直接省略自旋过程。避免浪费处理器资源。随着自适应自旋锁的加入,随着程序运行时间的增加和性能监控系统信息的不断完善,虚拟机对程序自旋时间的预测越来越准确,也就是虚拟机越来越智能。LockElimination锁消除是指JIT编译器运行时,代码需要某个代码块进行互斥同步,但虚拟机检测到由于没有共享数据,不需要互斥同步。此时对虚拟机会进行优化,消除互斥锁同步。判断锁消除的主要依据来自逃逸分析的数据支撑。具体来说,如果虚拟机判断在一段代码中,创建的对象不会逃逸到其他线程,那么就可以把它当做栈上的数据,不需要同步。不过,大家肯定有疑问,是否变量逃逸,写代码的程序员应该比虚拟机清楚,程序员对是否加同步互斥量很有把握。你还想让虚拟机通过复杂的进程间分析吗?这个问题的答案是:互斥同步有很多要求不是程序员自己加的。互斥同步代码在Java中频繁出现。我们举个例子。publicStringconcat(Strings1,Strings2){returns1+s2;}上面的代码很简单。它连接两个字符串然后返回,没有任何互斥同步要求。但是,让我们编译它0new#23dup4invokespecial#3:()V>7aload_18invokevirtual#411aload_212invokevirtual#415invokevirtual#518areturn你会发现字节码中出现了stringBuilder的拼接操作。因为字符串是不可变的,所以在编译时会自动优化字符串的连接。也就是使用StringBuilder来连接。我们都知道这个类是线程安全的,也就是说StringBuilder的拼接操作需要互斥和同步。此时,代码流程可能如下publicStringconcat(Strings1,Strings2){StringBuildersb=newStringBuilder();sb.append(s1);sb.append(s2);返回sb.toString();}此时,代码会产生互斥同步,锁定对象sb。在这种情况下,会出现程序员没有添加互斥同步条件,但字节码中有。这时候,锁消除就派上用场了。通过虚拟机的逃逸分析,发现对象sb不会逃逸,其他线程永远访问不到。sb的动态范围在此方法中。这时,锁消除会消除这里的互斥锁同步。运行时会忽略同步措施,直接执行。LockCoarseness原则上,我们在写代码的时候,总是建议尽量缩小同步代码块的范围,只同步共享数据的地方。这是为了减少同步操作的次数,等待锁的线程可以尽快拿到锁。但是,如果一段代码从头到尾都对同一个对象加锁,那么这个对象就会反复加锁、释放、加锁、释放。频繁切换用户态和内核态会降低效率。上面的代码就是这种情况。每个追加操作加锁和释放sb,加锁和释放。如果虚拟机检测到有一系列重复锁定??和释放对象的零碎操作,此时,虚拟机会将锁同步的范围粗化到整个操作的最外层。以上面代码为例,虚拟机从第一个append延伸到最后一个append。在这种情况下,您只需要锁定和释放一次。轻量级锁轻量级锁是JDK1.6之后新增的一种锁机制。Lightweight相当于操作系统互斥体实现的传统锁。因此,传统锁被称为重量级锁。但是需要注意的是,轻量级并不是用来代替重量级的。它的设计初衷是为了在没有多线程竞争的情况下减少传统重量级锁带来的性能消耗。首先,要了解轻量级锁以及后面的偏向锁,首先要知道HotSpot中对象的内存布局。对象的内存布局分为三部分,一部分是对象头(MarkWord),一部分是实例数据,一部分是填充,这样对象的大小是8个字节。对象头中有两部分数据,包括对象的哈希码、GC分代年龄、锁状态等。如果对象是数组,则多出一段存储长度这些内容在第2章运行时数据区的实例化内存布局和访问位置+对象的直接内存中。我们已经说过了。不再赘述,这里我们只是进一步细化锁的角度。由于对象头中存储的信息是独立于对象自身定义数据的额外存储成本,为了节省效率,设计成动态数据结构.它会根据对象的状态重用自己的存储空间。具体来说,根据当前的锁状态,对各部分的值赋予不同的含义。对象头在32位操作系统下的HotSpot虚拟机中占用32字节,在64位操作系统下占用64字节。我们以32位操作系统进行演示。以下是不同锁状态下各部分数据的含义。接下来,我们可以介绍一下轻量级锁的工作过程。在加锁过程中,当代码即将进入同步块时,虚拟机会在当前栈帧中创建一个名为锁记录(LockRecord)的空间。然后将堆中对象的对象头复制到锁记录(官方前缀是Displaced)中,用来存放之前修改对象头引用时的信息。此时线程栈和对象头的情况如下:然后,虚拟机会使用CAS(atomic)操作尝试更新堆中对象的对象头中的前30个字节为对锁定记录的引用。如果成功,说明当前线程已经拥有该对象的对象锁。然后将堆中对象头的锁标志位改为00,此时代表对象处于轻量锁状态。状态如下。如果失败,即堆中对象头的锁状态已经为0,说明该对象的对象锁没有被带走。虚拟机判断对象的前30个字节是否指向当前线程。如果是,说明当前线程已经获得了对象锁,可以直接执行同步代码块。如果没有,说明对象锁已经被其他线程拿走了,必须等待。即进入自旋模式。如果自旋一定次数后仍未获得锁,则轻量级锁扩展为重量级锁。如果发现有两个以上的线程在竞争同一个对象锁,那么轻量级锁就不再有效,必须扩展为重度锁,对象的锁状态变为10。此时,堆中对象的对象头的前30字节的引用指向重量级锁。解锁过程如果堆中对象头的前30字节指向当前线程,则表示当前线程拥有对象锁,则在加锁时使用CAS操作替换复制到栈帧锁记录中的对象头与堆对象头中的对象头。并将堆中对象头的锁状态更改为01,如果替换成功,则解锁完成。如果发现有其他线程试图获取堆中对象的对象锁,则需要在释放锁的同时唤醒被阻塞的线程。后记轻量级锁提升性能的基础是大部分锁在整个同步过程中没有竞争。这样通过CAS操作就避免了操作系统中使用mutex的开销。如果确实存在多线程的锁竞争,除了mutex本身的开销之外,还会产生CAS操作的额外开销。因此,在存在竞争的情况下,轻量级锁会比传统的重量级锁慢。偏向锁偏向锁也是JDK1.6之后引入的一个特性。其目的是消除非竞争状态下数据的同步原语,进一步提高程序的运行速度。轻量级锁利用CAS原子操作消除操作系统非竞争互斥量,偏向锁消除整个同步没有竞争。偏向锁的bias是bias的bias,也就是说锁会偏向第一个获得它的线程。如果在后续执行过程中,锁还没有被其他线程获取到,那么持有偏向锁的线程就不需要同步,直接执行。假设当前虚拟机开启了偏向锁(默认开启)1.6之后),当锁对象第一次被线程获取时,虚拟机不会设置对象头中锁标志最后2个字节的值,依然是01,设置第三次-to-lastbytebiasmode为01,即开启偏置模式。同时使用CAS原子操作在对象头中记录获取对象锁的线程。如果操作成功,那么持有偏向锁的线程每次进入同步代码块时,虚拟机都不会执行同步操作。一旦有另一个线程获取到锁,偏向模式立即结束。是否取消偏向是根据锁对象当前是否被锁来决定的。注销后,锁标志返回解锁状态(01)或轻量级锁(00)。后续操作按照轻量级锁执行。偏向锁和轻量级锁的状态转换如下:.但是我们发现在偏向锁的过程中没有复制。这个时候,我们要使用原来对象头的数据怎么办呢?虚拟机的实现也考虑到了这个问题。对象的hashcode不是在创建对象的时候计算的,而是在第一次使用的时候计算的。比如下面String的hash方法的源码/**演示了hash的计算时间作者:杜少雄*/publicinthashCode(){inth=hash;//如果之前没有计算过,调用的时候会计算。否则直接返回if(h==0&&value.length>0){charval[]=value;for(inti=0;i