JVM-锁消除+锁粗化 自旋锁、偏向锁、轻量级锁 逃逸分析-30

自旋锁

自旋锁其实就是一个线程自转,空转,什么都不操作,但也不挂起,在那里空循环。空循环的作用就是等待一把锁。自旋锁是明确的会产生竞争的情况下使用的。

当竞争存在时,如果线程可以很快获得锁,那么就没有必要在(操作系统)OS层面挂起线程(因为在操作系统层面去挂起,他的性能消耗是非常严重的,因此如果我们能假定他能很快获取锁,就不需要让线程挂起),而是让线程做几个空操作(称为自旋)

互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。而在很多应用上,共享数据的锁定状态只会持续很短的一段时间。若实体机上有多个处理器,能让两个以上的线程同时并行执行,我们就可以让后面请求锁的那个线程原地自旋(不放弃CPU时间),看看持有锁的线程是否很快就会释放锁。

为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是自旋锁。

如果锁长时间被占用,则浪费处理器资源,因此自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了(默认10次)。

使用方式

JDK1.6中 -XX:+UseSpinning 开启

JDK1.7中,去掉此参数,改为内置实现

ps: 换言之,JDK1.7 以后无法通过参数指定。

适用场景

如果同步块很长,会导致后面线程自旋的成功率,因此自旋失败,会降低系统性能。

因为在自旋花去的时间后还没有获取到锁,这么长时间都是浪费的,所以会造成性能降低。

如果同步块很短,自旋成功成功率很高,因此自旋成功,会节省线程挂起切换时间,提升系统性能。

自旋适应锁

JDK1.6引入自适应的自旋锁:自旋时间不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。

偏向锁

大部分情况是没有竞争的,所以可以通过偏向来提高性能

所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程(也就是说,这个线程已经占有这个锁,当他在次试图去获取这个锁的时候,他会已最快的方式去拿到这个锁,而不需要在进行一些monitor操作,因此这方面他是会对性能有所提升的,因为在大部分情况下是没有竞争的,所以锁此时是没用的,所以使用偏向锁是可以提高性能的)

在使用偏向锁的时候会将对象头Mark的标记设置为偏向,并将拿到锁的线程的ID写入对象头Mark,这样就可以很快识别出这个线程是否拿到的锁。

只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步(这段时间就省下来了)

当其他线程请求相同的锁时,偏向模式结束

在竞争激烈的场合,偏向锁会增加系统负担。

案例

vector 内部是有同步锁的操作的,所以在jdk内部是有锁的。

使用它也就是说明你此时的代码拥有锁了。

public static List<Integer> numberList =new Vector<Integer>();
public static void main(String[] args) throws InterruptedException {
	long begin=System.currentTimeMillis();
	int count=0;
	int startnum=0;
	while(count<10000000){
		numberList.add(startnum);
		startnum+=2;
		count++;
	}
	long end=System.currentTimeMillis();
	System.out.println(end-begin);
}

偏向锁的开启

  • 使用偏向锁。

方式一执行:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

BiasedLockingStartupDelay=0设置偏向锁的启动时间,为零0,则是在系统启动时就启用偏向锁,但一般在系统启动时竞争是非常大的,使用它是非常耗时的,这里案例因为代码执行会很短,为了测试效果,在运行程度时,就启动偏向锁,所以置为0.

  • 不使用偏向锁

方式二:-XX:-UseBiasedLocking

运行结果:本例中,使用偏向锁,可以获得5%以上的性能提升

提升性能的建议

  • 取消偏向锁 -XX:-UseBiasedLocking

JDK1.6 开始默认打开的偏向锁,会尝试把锁赋给第一个访问它的线程,取消同步块上的synchronized原语。

如果始终只有一条线程在访问它,就成功略过同步操作以获得性能提升。

但一旦有第二条线程访问这把锁,JVM就要撤销偏向锁恢复到未锁定线程的状态,详见 JVM的Stop The World,安全点,黑暗的地底世界,可以看到不少RevokeBiasd的纪录,像GC一样,会Stop The World的干活,虽然只是很短很短的停顿,但对于多线程并发的应用,取消掉它反而有性能的提升和延时的极微的缩短,所以Cassandra就取消了它。

ps: 对于高并发的情况,锁竞争比较严重,建议取消偏向锁。

轻量级锁

轻量级锁并不是用来代替重量级锁(传统锁机制,如互斥等)的,目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

加锁过程

在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),这时候线程堆栈与对象头的状态如下图所示。

然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个Bits)将转变为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如下图所示。

加锁失败了怎么办

###《深入理解JVM》的说法

失败了,去查看MarkWord的值。有2种可能:1,指向当前线程的指针,2,别的值。

如果是1,那么说明发生了“重入”的情况,直接当做成功获得锁处理。

其实这个有个疑问,为什么获得锁成功了而CAS失败了,这里其实要牵扯到CAS的具体过程:先比较某个值是不是预测的值,是的话就动用原子操作交换(或赋值),否则不操作直接返回失败。在用CAS的时候期待的值是其原本的MarkWord。发生“重入”的时候会发现其值不是期待的原本的MarkWord,而是一个指针,所以当然就返回失败,但是如果这个指针指向这个线程,那么说明其实已经获得了锁,不过是再进入一次。如果不是这个线程,那么情况2:

如果是2,那么发生了竞争,锁会膨胀为一个重量级锁(MutexLock)

###《并发编程的艺术》的说法

失败了直接自旋。期望在自旋的时间内获得锁,如果还是不能获得,那么开始膨胀,修改锁的MarkWord改为重量级锁的指针,并且阻塞自己。

解锁过程:(那个拿到锁的线程)用CAS把MarkWord换回到原来的对象头,如果成功,那么没有竞争发生,解锁完成。如果失败,表示存在竞争(之前有线程试图通过CAS修改MarkWord),这时要释放锁并且唤醒阻塞的线程。

解锁过程

解锁过程也是通过CAS操作来进行的,如果对象的Mark Word仍然指向着线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。

轻量级锁小结

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。

底层原理

在了解轻量锁之前,先了解下面jvm中一个锁:

1、BasicObjectLock

这个锁他是嵌入在线程栈中的对象,也就是说这个锁是放在线程栈中的。

什么是线程栈,在之前博客也有介绍,这里简单说明:在系统调用过程中,它里面会存放一些线程执行的情况;比如说:局部变量,参数,操作数栈等等;还有一个就是这个BasicObjectLock,他是一个锁,他包含如下两部分:

他的主要部分则是后面的 markOop_displaced_header, 他其实就是一个对象头。

下面一部分就是一个指向锁的一个指针。这两部分组成。

普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。

过程

过程:如果对象没有被锁定; 将对象头的Mark指针保存到锁对象中;并且将对象头设置为指向锁的指针(在线程栈空间中)。

这样一来实际上就是对象的头是在指针当中,并且是放在的线程栈中的,而线程栈中有个指针是指向这个指针的,因此形成了相互间的引用关系。

实现代码

实现代码如下:首先指定锁中专门存放对象头的这个header,备份到锁中,把对象的mark放在锁中,里面进行交换,lock本身放入对象头当中去,形成相互交换;

lock->set_displaced_header(mark);
 if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return;
}

lock位于线程栈中,因此如何判断线程持有这个锁,我们只需要判断这个对象头的指针所指向的方向,是不是在这个线程栈当中。如果是,则说明这个线程持有这把锁。

竞争失败的情况

如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁也就是常说的monitor)

使用建议

在没有锁竞争的前提下,减少传统锁(也就是重量级锁)使用OS互斥量产生的性能损耗(也就是说重量级锁他是会在操作系统上做一些操作,所以他的性能是非常糟糕的)

在竞争激烈时,轻量级锁多半会失败,因此轻量级锁会多做很多额外操作,导致性能下降

竞争激烈时,不建议使用。

偏向锁,轻量级锁,自旋锁总结

这三个锁不是Java语言层面的锁优化方法。

他是jvm中锁的优化,其实是jvm获取锁的步骤。

步骤

内置于JVM中的获取锁的优化方法和获取锁的步骤

  1. 偏向锁可用会先尝试偏向锁

  2. 轻量级锁可用会先尝试轻量级锁

  3. 以上都失败,尝试自旋锁

  4. 再失败,尝试普通锁,使用OS互斥量在操作系统层挂起

锁优化

主要分为 jvm 层面和代码编程两个方面。

  • jvm

锁消除

锁粗化

  • 编程

减少锁持有的时间

锁分离

减小锁的力度

锁消除

锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。

锁削除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

也许读者会有疑问,变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?

答案是有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。

例子

比如,StringBuffer类的append操作:

@Override
public synchronized StringBuffer append(String str) {
    toStringCache = null;
    super.append(str);
    return this;
}

从源码中可以看出,append方法用了synchronized关键词,它是线程安全的。

但我们可能仅在线程内部把StringBuffer当作局部变量使用:

public class Demo {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int size = 10000;
        for (int i = 0; i < size; i++) {
            createStringBuffer("Hyes", "为分享技术而生");
        }
        long timeCost = System.currentTimeMillis() - start;
        System.out.println("createStringBuffer:" + timeCost + " ms");
    }
    public static String createStringBuffer(String str1, String str2) {
        StringBuffer sBuf = new StringBuffer();
        sBuf.append(str1);// append方法是同步操作
        sBuf.append(str2);
        return sBuf.toString();
    }
}

代码中createStringBuffer方法中的局部对象sBuf,就只在该方法内的作用域有效,不同线程同时调用createStringBuffer()方法时,都会创建不同的sBuf对象,因此此时的append操作若是使用同步操作,就是白白浪费的系统资源。

开启锁消除

这时我们可以通过编译器将其优化,将锁消除,前提是java必须运行在server模式(server模式会比client模式作更多的优化),同时必须开启逃逸分析:

-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks

其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。

逃逸分析:比如上面的代码,它要看sBuf是否可能逃出它的作用域?

如果将sBuf作为方法的返回值进行返回,那么它在方法外部可能被当作一个全局对象使用,就有可能发生线程安全问题,这时就可以说sBuf这个对象发生逃逸了,因而不应将append操作的锁消除,但我们上面的代码没有发生锁逃逸,锁消除就可以带来一定的性能提升。

我们来看看下面代码清单13-6中的例子,这段非常简单的代码仅仅是输出三个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。

案例 2

  • 代码清单 13-6 一段看起来没有同步的代码
public String concatString(String s1, String s2, String s3) {  
    return s1 + s2 + s3;  
}  

我们也知道,由于String是一个不可变的类,对字符串的连接操作总是通过生成新的String对象来进行的,因此Javac编译器会对String连接做自动优化。

在JDK 1.5之前,会转化为StringBuffer对象的连续append()操作,在JDK 1.5及以后的版本中,会转化为StringBuilder对象的连续append()操作。

即代码清单13-6中的代码可能会变成代码清单13-7的样子 。

  • 代码清单 13-7 Javac转化后的字符串连接操作
public String concatString(String s1, String s2, String s3) {  
    StringBuffer sb = new StringBuffer();  
    sb.append(s1);  
    sb.append(s2);  
    sb.append(s3);  
    return sb.toString();  
}  

(注1:实事求是地说,既然谈到锁削除与逃逸分析,那虚拟机就不可能是JDK 1.5之前的版本,所以实际上会转化为非线程安全的StringBuilder来完成字符串拼接,并不会加锁。但是这也不影响笔者用这个例子证明Java对象中同步的普遍性。)

现在大家还认为这段代码没有涉及同步吗?

每个StringBuffer.append()方法中都有一个同步块,锁就是sb对象。

虚拟机观察变量sb,很快就会发现它的动态作用域被限制在concatString()方法内部。也就是sb的所有引用永远不会“逃逸”到concatString()方法之外,其他线程无法访问到它,所以这里虽然有锁,但是可以被安全地削除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。

锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部(由多次加锁编程只加锁一次)。

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。

锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

例子

  • 场景1
public void doSomethingMethod(){
    synchronized(lock){
        //do some thing
    }
    //这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
    synchronized(lock){
        //do other thing
    }
}

上面的代码是有两块需要同步操作的,但在这两块需要同步操作的代码之间,需要做一些其它的工作,而这些工作只会花费很少的时间,那么我们就可以把这些工作代码放入锁内,将两个同步代码块合并成一个,以降低多次锁请求、同步、释放带来的系统性能消耗,

合并后的代码如下:

public void doSomethingMethod(){
    //进行锁粗化:整合成一次锁请求、同步、释放
    synchronized(lock){
        //do some thing
        //做其它不需要同步但能很快执行完的工作
        //do other thing
    }
}
  • 场景2

另一种需要锁粗化的极端的情况是:

for(int i=0;i<size;i++){
    synchronized(lock){
    }
}

上面代码每次循环都会进行锁的请求、同步与释放,看起来貌似没什么问题,且在jdk内部会对这类代码锁的请求做一些优化,但是还不如把加锁代码写在循环体的外面,这样一次锁的请求就可以达到我们的要求,除非有特殊的需要:循环需要花很长时间,但其它线程等不起,要给它们执行的机会。

锁粗化后的代码如下:

synchronized(lock){
    for(int i=0;i<size;i++){
    }
}

锁粗化的前提

这样做是有前提的,就是中间不需要同步的代码能够很快速地完成,如果不需要同步的代码需要花很长时间,就会导致同步块的执行需要花费很长的时间,这样做也就不合理了。

减少锁持有时间

在方法没有必要做同步的时候,就不需要放在锁中,因此在高并发下,等待的时间就会减少,就会提高自旋锁的成功率。

减小锁粒度

将大对象,拆成小对象,大大增加并行度,降低锁竞争。

偏向锁,轻量级锁成功率提高

比如 ConcurrentHashMap 对于竞争的优化,相对于 HashMap。

  • 思考:

减少锁粒度后,可能会带来什么负面影响呢?

以 ConcurrentHashMap 为例,说明分割为多个 Segment后,在什么情况下,会有性能损耗?

锁分离(读写分离)

根据功能进行锁分离

  • ReadWriteLock

读多写少的情况,可以提高性能

读写分离思想可以延伸,只要操作互不影响,锁就可以分离

LinkedBlockingQueue(所分离的扩展案例)锁分离的思想在很多场合下都可以使用。

无锁编程(Lock-Free)

无招胜有招,武功的最高境界就是无招,所以锁也是,最好的锁也就是无锁。

无锁跟有锁对比,那么锁则是悲观的操作,为什么会使用锁,因为我们预计这个时候竞争是存在的,所以要加锁。

无锁是乐观的操作,因为认为是没有竞争存在的。比较乐观。

无锁的一种实现方式 CAS (对比和交换)

CAS(Compare And Swap) 他是非阻塞的同步,他不会去等待,上来就会不断尝试,尝试失败在尝试。

要么失败要么直接成功,成功则退出。

基础知识

逃逸分析

对象头Mark

1、Mark Word,他是32位对象头的标记,

2、描述对象的hash、锁信息,垃圾回收标记,年龄;指向锁记录的指针(对于来说可以记录指向锁的指针);指向monitor的指针(monitor可以锁定对象,也可以锁定函数);GC标记(在垃圾标记的时候我们可以做一些标记);偏向锁线程ID(在偏向锁中可以记录偏向锁的ID)

由上可看出,mark是个多功能的头,在很多场合都可以用到,除了在锁中用到,比如在垃圾回收中可以记录年龄,gc的标记等等。

HotSpot虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等,这部分数据的长度在32位和64位的虚拟机中分别为32个和64个Bits,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。

Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。例如在32位的HotSpot虚拟机中对象未被锁定的状态下,Mark Word的32个Bits空间中的25Bits用于存储对象哈希码(HashCode),4Bits用于存储对象分代年龄,2Bits用于存储锁标志位,1Bit固定为0,在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下Mark Word的存储内容如下表所示。

  • Mark Word
存储内容 标志位 状态
对象Hash值、对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁定)
空,不记录信息 11 GC标记
偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向

参考资料

java虚拟机对锁的优化之锁粗化和锁消除

深入JVM内核(八)——jvm锁与jvm锁优化

JVM内部细节之二:偏向锁(Biased Locking)

JVM中锁优化,偏向锁、自旋锁、锁消除、锁膨胀

JAVA锁的优化(自旋锁, 锁膨胀, 锁消除)

https://www.cnblogs.com/virgosnail/p/9681013.html

java中锁的优化 – JVM对synchronized的优化

  • 锁消除

Java锁消除和锁粗化

https://mp.weixin.qq.com/s/T3SMaJODAuB2McEZldRnOQ

https://shipilev.net/jvm/anatomy-quarks/10-string-intern/

  • jvm 参数配置建议

唯品会资深架构师给你的 JVM 调优建议

目录

java 内存模型入门系列教程-00

你可能感兴趣的:(jvm,java,虚拟机(jvm)学习笔记,jmm)