Java锁---偏向锁、轻量级锁、自旋锁、重量级锁

以前作过一个测试,反复执行过屡次,发现结果是同样的: 
1. 单线程下synchronized效率最高(当时感受它的效率应该是最差才对); 
2. AtomicInteger效率最不稳定,不一样并发状况下表现不同:短期低并发下,效率比synchronized高,有时甚至比LongAdder还高出一点,可是高并发下,性能还不如synchronized,不一样状况下性能表现很不稳定; 
3. LongAdder性能稳定,在各类并发状况下表现都不错,总体表现最好,短期的低并发下比AtomicInteger性能差一点,长时间高并发下性能最高(可让AtomicInteger下台了);java

理解锁的基础知识

若是想要透彻的理解java锁的前因后果,须要先了解如下基础知识。数组

基础知识之一:锁的类型

锁从宏观上分类,分为悲观锁与乐观锁。缓存

乐观锁

乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采起在写时先读出当前版本号,而后加锁操做(比较跟上一次的版本号,若是同样则更新),若是失败则要重复读-比较-写的操做。安全

java中的乐观锁基本都是经过CAS操做实现的,CAS是一种更新的原子操做,比较当前值跟传入值是否同样,同样则更新,不然失败。数据结构

悲观锁

悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,因此每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。多线程

基础知识之二:java线程阻塞的代价

java的线程是映射到操做系统原生线程之上的,若是要阻塞或唤醒一个线程就须要操做系统介入,须要在户态与核心态之间切换,这种切换会消耗大量的系统资源,由于用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态须要传递给许多变量、参数给内核,内核也须要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工做。并发

  1. 若是线程状态切换是一个高频操做时,这将会消耗不少CPU处理时间;
  2. 若是对于那些须要同步的简单的代码块,获取锁挂起操做消耗的时间比用户代码执行的时间还要长,这种同步策略显然很是糟糕的。

synchronized会致使争用不到锁的线程进入阻塞状态,因此说它是java语言中一个重量级的同步操纵,被称为重量级锁,为了缓解上述性能问题,JVM从1.5开始,引入了轻量锁与偏向锁,默认启用了自旋锁,他们都属于乐观锁。框架

明确java线程切换的代价,是理解java中各类锁的优缺点的基础之一。jvm

基础知识之三:markword

在介绍java锁以前,先说下什么是markword,markword是java对象数据结构中的一部分,要详细了解java对象的结构能够点击这里,这里只作markword的详细介绍,由于对象的markword和java各类类型的锁密切相关;函数

markword数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记当前对象的状态,对象的所处的状态,决定了markword存储的内容,以下表所示:

状态 标志位 存储内容
未锁定 01 对象哈希码、对象分代年龄
轻量级锁定 00 指向锁记录的指针
膨胀(重量级锁定) 10 执行重量级锁定的指针
GC标记 11 空(不须要记录信息)
可偏向 01 偏向线程ID、偏向时间戳、对象分代年龄

32位虚拟机在不一样状态下markword结构以下图所示:

这里写图片描述

了解了markword结构,有助于后面了解java锁的加锁解锁过程;

小结

前面提到了java的4种锁,他们分别是重量级锁、自旋锁、轻量级锁和偏向锁, 
不一样的锁有不一样特色,每种锁只有在其特定的场景下,才会有出色的表现,java中没有哪一种锁可以在全部状况下都能有出色的效率,引入这么多锁的缘由就是为了应对不一样的状况;

前面讲到了重量级锁是悲观锁的一种,自旋锁、轻量级锁与偏向锁属于乐观锁,因此如今你就可以大体理解了他们的适用范围,可是具体如何使用这几种锁呢,就要看后面的具体分析他们的特性;

java中的锁

自旋锁

自旋锁原理很是简单,若是持有锁的线程能在很短期内释放锁资源,那么那些等待竞争锁的线程就不须要作内核态和用户态之间的切换进入阻塞挂起状态,它们只须要等一等(自旋),等持有锁的线程释放锁后便可当即获取锁,这样就避免用户线程和内核的切换的消耗。

可是线程自旋是须要消耗cup的,说白了就是让cup在作无用功,若是一直获取不到锁,那线程也不能一直占用cup自旋作无用功,因此须要设定一个自旋等待的最大时间。

若是持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会致使其它争用锁的线程在最大等待时间内仍是获取不到锁,这时争用线程会中止自旋进入阻塞状态。

自旋锁的优缺点

自旋锁尽量的减小线程的阻塞,这对于锁的竞争不激烈,且占用锁时间很是短的代码块来讲性能能大幅度的提高,由于自旋的消耗会小于线程阻塞挂起再唤醒的操做的消耗,这些操做会致使线程发生两次上下文切换!

可是若是锁的竞争激烈,或者持有锁的线程须要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,由于自旋锁在获取锁前一直都是占用cpu作无用功,占着XX不XX,同时有大量线程在竞争一个锁,会致使获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操做的消耗,其它须要cup的线程又不能获取到cpu,形成cpu的浪费。因此这种状况下咱们要关闭自旋锁;

自旋锁时间阈值

自旋锁的目的是为了占着CPU的资源不释放,等到获取到锁当即进行处理。可是如何去选择自旋的执行时间呢?若是自旋执行时间太长,会有大量的线程处于自旋状态占用CPU资源,进而会影响总体系统的性能。所以自旋的周期选的额外重要!

JVM对于自旋周期的选择,jdk1.5这个限度是必定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷状况作了较多的优化

  1. 若是平均负载小于CPUs则一直自旋

  2. 若是有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞

  3. 若是正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞

  4. 若是CPU处于节电模式则中止自旋

  5. 自旋时间的最坏状况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)

  6. 自旋时会适当放弃线程优先级之间的差别

自旋锁的开启

JDK1.6中-XX:+UseSpinning开启; 
-XX:PreBlockSpin=10 为自旋次数; 
JDK1.7后,去掉此参数,由jvm控制;

重量级锁Synchronized

Synchronized的做用

在JDK1.5以前都是使用synchronized关键字保证同步的,Synchronized的做用相信你们都已经很是熟悉了;

它能够把任意一个非NULL的对象看成锁。

  1. 做用于方法时,锁住的是对象的实例(this);
  2. 看成用于静态方法时,锁住的是Class实例,又由于Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,所以静态方法锁至关于类的一个全局锁,会锁全部调用该方法的线程;
  3. synchronized做用于一个对象实例时,锁住的是全部以该对象为锁的代码块。

Synchronized的实现

实现以下图所示;

这里写图片描述

它有多个队列,当多个线程一块儿访问某个对象监视器的时候,对象监视器会将这些线程存储在不一样的容器中。

  1. Contention List:竞争队列,全部请求锁的线程首先被放在这个竞争队列中;

  2. Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;

  3. Wait Set:哪些调用wait方法被阻塞的线程被放置在这里;

  4. OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;

  5. Owner:当前已经获取到所资源的线程被称为Owner;

  6. !Owner:当前释放锁的线程。

JVM每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),可是并发状况下,ContentionList会被大量的并发线程进行CAS访问,为了下降对尾部元素的竞争,JVM会将一部分线程移动到EntryList中做为候选竞争线程。Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为OnDeck线程(通常是最早进去的那个线程)。Owner线程并不直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,OnDeck须要从新竞争锁。这样虽然牺牲了一些公平性,可是能极大的提高系统的吞吐量,在JVM中,也把这种选择行为称之为“竞争切换”。

OnDeck线程获取到锁资源后会变为Owner线程,而没有获得锁资源的仍然停留在EntryList中。若是Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻经过notify或者notifyAll唤醒,会从新进去EntryList中。

处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操做系统来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。

Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先尝试自旋获取锁,若是获取不到就进入ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占OnDeck线程的锁资源。

偏向锁

Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。 
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,若是在运行过程当中,同步锁只有一个线程访问,不存在多线程争用的状况,则线程是不须要触发同步的,这种状况下,就会给线程加一个偏向锁。 
若是在运行过程当中,遇到了其余线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

它经过消除资源无竞争状况下的同步原语,进一步提升了程序的运行性能。

偏向锁的实现

偏向锁获取过程:

  1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。

  2. 若是为可偏向状态,则测试线程ID是否指向当前线程,若是是,进入步骤5,不然进入步骤3。

  3. 若是线程ID并未指向当前线程,则经过CAS操做竞争锁。若是竞争成功,则将Mark Word中线程ID设置为当前线程ID,而后执行5;若是竞争失败,执行4。

  4. 若是CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时得到偏向锁的线程被挂起,偏向锁升级为轻量级锁,而后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会致使stop the word)

  5. 执行同步代码。

注意:第四步中到达安全点safepoint会致使stop the word,时间很短。

偏向锁的释放:

偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,须要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

偏向锁的适用场景

始终只有一个线程在执行同步块,在它没有执行完释放锁以前,没有其它线程去执行同步块,在锁无竞争的状况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候须要撤销偏向锁,撤销偏向锁的时候会致使stop the word操做; 
在有锁的竞争时,偏向锁会多作不少额外操做,尤为是撤销偏向所的时候会致使进入安全点,安全点会致使stw,致使性能降低,这种状况下应当禁用;

查看停顿–安全点停顿日志

要查看安全点停顿,能够打开安全点日志,经过设置JVM参数 -XX:+PrintGCApplicationStoppedTime 会打出系统中止的时间,添加-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 这两个参数会打印出详细信息,能够查看到使用偏向锁致使的停顿,时间很是短暂,可是争用严重的状况下,停顿次数也会很是多;

注意:安全点日志不能一直打开: 
1. 安全点日志默认输出到stdout,一是stdout日志的整洁性,二是stdout所重定向的文件若是不在/dev/shm,可能被锁。 
2. 对于一些很短的停顿,好比取消偏向锁,打印的消耗比停顿自己还大。 
3. 安全点日志是在安全点内打印的,自己加大了安全点的停顿时间。

因此安全日志应该只在问题排查时打开。 
若是在生产系统上要打开,再再增长下面四个参数: 
-XX:+UnlockDiagnosticVMOptions -XX: -DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log 
打开Diagnostic(只是开放了更多的flag可选,不会主动激活某个flag),关掉输出VM日志到stdout,输出到独立文件,/dev/shm目录(内存文件系统)。

这里写图片描述

此日志分三部分: 
第一部分是时间戳,VM Operation的类型 
第二部分是线程概况,被中括号括起来 
total: 安全点里的总线程数 
initially_running: 安全点开始时正在运行状态的线程数 
wait_to_block: 在VM Operation开始前须要等待其暂停的线程数

第三部分是到达安全点时的各个阶段以及执行操做所花的时间,其中最重要的是vmop

  • spin: 等待线程响应safepoint号召的时间;
  • block: 暂停全部线程所用的时间;
  • sync: 等于 spin+block,这是从开始到进入安全点所耗的时间,可用于判断进入安全点耗时;
  • cleanup: 清理所用时间;
  • vmop: 真正执行VM Operation的时间。

可见,那些不少但又很短的安全点,全都是RevokeBias, 高并发的应用会禁用掉偏向锁。

jvm开启/关闭偏向锁

  • 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 关闭偏向锁:-XX:-UseBiasedLocking

轻量级锁

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的状况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁; 
轻量级锁的加锁过程:

  1. 在代码进入同步块的时候,若是同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。这时候线程堆栈与对象头的状态如图: 
      这里写图片描述所示。

  2. 拷贝对象头中的Mark Word复制到锁记录中;

  3. 拷贝成功后,虚拟机将使用CAS操做尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。若是更新成功,则执行步骤4,不然执行步骤5。

  4. 若是这个更新动做成功了,那么这个线程就拥有了该对象的锁,而且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。 
      这里写图片描述

  5. 若是这个更新操做失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,若是是就说明当前线程已经拥有了这个对象的锁,那就能够直接进入同步块继续执行。不然说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了避免让线程阻塞,而采用循环去获取锁的过程。

轻量级锁的释放

释放锁线程视角:由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,以前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候若是它发如今它持有锁的期间有其余线程来尝试获取锁了,而且该线程对markword作了修改,二者比对发现不一致,则切换到重量锁。

由于重量级锁被修改了,全部display mark word和原来的markword不同了。

怎么补救,就是进入mutex前,compare一下obj的markword状态。确认该markword是否被其余线程持有。

此时若是线程已经释放了markword,那么经过CAS后就能够直接进入线程,无需进入mutex,就这个做用。

尝试获取锁线程视角:若是线程尝试获取锁的时候,轻量锁正被其余线程占有,那么它就会修改markword,修改重量级锁,表示该进入重量锁了。

还有一个注意点:等待轻量锁的线程不会阻塞,它会一直自旋等待锁,并如上所说修改markword。

这就是自旋锁,尝试获取锁的线程,在没有得到锁的时候,不被挂起,而转而去执行一个空循环,即自旋。在若干个自旋后,若是尚未得到锁,则才被挂起,得到锁,则执行代码。

总结

这里写图片描述

synchronized的执行过程: 
1. 检测Mark Word里面是否是当前线程的ID,若是是,表示当前线程处于偏向锁 
2. 若是不是,则使用CAS将当前线程的ID替换Mard Word,若是成功则表示当前线程得到偏向锁,置偏向标志位1 
3. 若是失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。 
4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,若是成功,当前线程得到锁 
5. 若是失败,表示其余线程竞争锁,当前线程便尝试使用自旋来获取锁。 
6. 若是自旋成功则依然处于轻量级状态。 
7. 若是自旋失败,则升级为重量级锁。

上面几种锁都是JVM本身内部实现,当咱们执行synchronized同步块的时候jvm会根据启用的锁和当前线程的争用状况,决定如何执行同步操做;

在全部的锁都启用的状况下线程进入临界区时会先去获取偏向锁,若是已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁,若是自旋也没有获取到锁,则使用重量级锁,没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;

偏向锁是在无锁争用的状况下使用的,也就是同步开在当前线程没有执行完以前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,若是轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁;

若是线程争用激烈,那么应该禁用偏向锁。

锁优化

以上介绍的锁不是咱们代码中可以控制的,可是借鉴上面的思想,咱们能够优化咱们本身线程的加锁操做;

减小锁的时间

不须要同步执行的代码,能不放在同步快里面执行就不要放在同步快内,可让锁尽快释放;

减小锁的粒度

它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增长并行度,从而下降锁竞争。它的思想也是用空间来换时间;

java中不少数据结构都是采用这种方法提升并发操做的效率:

ConcurrentHashMap

java中的ConcurrentHashMap在jdk1.8以前的版本,使用一个Segment 数组

Segment< K,V >[] segments
  • 1

Segment继承自ReenTrantLock,因此每一个Segment就是个可重入锁,每一个Segment 有一个HashEntry< K,V >数组用来存放数据,put操做时,先肯定往哪一个Segment放数据,只须要锁定这个Segment,执行put,其它的Segment不会被锁定;因此数组中有多少个Segment就容许同一时刻多少个线程存放数据,这样增长了并发能力。

LongAdder

LongAdder 实现思路也相似ConcurrentHashMap,LongAdder有一个根据当前并发情况动态改变的Cell数组,Cell对象里面有一个long类型的value用来存储值; 
开始没有并发争用的时候或者是cells数组正在初始化的时候,会使用cas来将值累加到成员变量的base上,在并发争用的状况下,LongAdder会初始化cells数组,在Cell数组中选定一个Cell加锁,数组有多少个cell,就容许同时有多少线程进行修改,最后将数组中每一个Cell中的value相加,在加上base的值,就是最终的值;cell数组还能根据当前线程争用状况进行扩容,初始长度为2,每次扩容会增加一倍,直到扩容到大于等于cpu数量就再也不扩容,这也就是为何LongAdder比cas和AtomicInteger效率要高的缘由,后面二者都是volatile+cas实现的,他们的竞争维度是1,LongAdder的竞争维度为“Cell个数+1”为何要+1?由于它还有一个base,若是竞争不到锁还会尝试将数值加到base上;

LinkedBlockingQueue

LinkedBlockingQueue也体现了这样的思想,在队列头入队,在队列尾出队,入队和出队使用不一样的锁,相对于LinkedBlockingArray只有一个锁效率要高;

拆锁的粒度不能无限拆,最多能够将一个锁拆为当前cup数量个锁便可;

锁粗化

大部分状况下咱们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度; 
在如下场景下须要粗化锁的粒度: 
假若有一个循环,循环内的操做须要加锁,咱们应该把锁放到循环外面,不然每次进出循环,都进出一次临界区,效率是很是差的;

使用读写锁

ReentrantReadWriteLock 是一个读写锁,读操做加读锁,能够并发读,写操做使用写锁,只能单线程写;

读写分离

CopyOnWriteArrayList 、CopyOnWriteArraySet 
CopyOnWrite容器即写时复制的容器。通俗的理解是当咱们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,而后新的容器里添加元素,添加完元素以后,再将原容器的引用指向新的容器。这样作的好处是咱们能够对CopyOnWrite容器进行并发的读,而不须要加锁,由于当前容器不会添加任何元素。因此CopyOnWrite容器也是一种读写分离的思想,读和写不一样的容器。 
 CopyOnWrite并发容器用于读多写少的并发场景,由于,读的时候没有锁,可是对其进行更改的时候是会加锁的,不然会致使多个线程同时复制出多个副本,各自修改各自的;

使用cas

若是须要同步的操做执行速度很是快,而且线程竞争并不激烈,这时候使用cas效率会更高,由于加锁会致使线程的上下文切换,若是上下文切换的耗时比同步操做自己更耗时,且线程对资源的竞争不激烈,使用volatiled+cas操做会是很是高效的选择;

消除缓存行的伪共享

除了咱们在代码中使用的同步锁和jvm本身内置的同步锁外,还有一种隐藏的锁就是缓存行,它也被称为性能杀手。 
在多核cup的处理器中,每一个cup都有本身独占的一级缓存、二级缓存,甚至还有一个共享的三级缓存,为了提升性能,cpu读写数据是以缓存行为最小单元读写的;32位的cpu缓存行为32字节,64位cup的缓存行为64字节,这就致使了一些问题。 
例如,多个不须要同步的变量由于存储在连续的32字节或64字节里面,当须要其中的一个变量时,就将它们做为一个缓存行一块儿加载到某个cup-1私有的缓存中(虽然只须要一个变量,可是cpu读取会以缓存行为最小单位,将其相邻的变量一块儿读入),被读入cpu缓存的变量至关因而对主内存变量的一个拷贝,也至关于变相的将在同一个缓存行中的几个变量加了一把锁,这个缓存行中任何一个变量发生了变化,当cup-2须要读取这个缓存行时,就须要先将cup-1中被改变了的整个缓存行更新回主存(即便其它变量没有更改),而后cup-2才可以读取,而cup-2可能须要更改这个缓存行的变量与cpu-1已经更改的缓存行中的变量是不同的,因此这至关于给几个绝不相关的变量加了一把同步锁; 
为了防止伪共享,不一样jdk版本实现方式是不同的: 
1. 在jdk1.7以前会 将须要独占缓存行的变量先后添加一组long类型的变量,依靠这些无心义的数组的填充作到一个变量本身独占一个缓存行; 
2. 在jdk1.7由于jvm会将这些没有用到的变量优化掉,因此采用继承一个声明了好多long变量的类的方式来实现; 
3. 在jdk1.8中经过添加sun.misc.Contended注解来解决这个问题,若要使该注解有效必须在jvm中添加如下参数: 
-XX:-RestrictContended

sun.misc.Contended注解会在变量前面添加128字节的padding将当前变量与其余变量进行隔离;