并发编程之synchronized(二)------jvm对synchronized的优化

一、锁的粗化

看如下代码

public class Test {
    StringBuffer stb = new StringBuffer();


    public void test1(){
        //jvm的优化,锁的粗化
        stb.append("1");

        stb.append("2");

        stb.append("3");

        stb.append("4");
    }

首先我们要清除StringBuffer是线程安全的,因为它在每一个方法上都加了synchronized锁,下图是StringBuffer的源码

 

 按照正常的理解synchronized是对当前对象加锁,那么我们调用了四次append方法,那么jvm是将这把对象锁加了四次吗?如下图:

 

 那这样的化,jvm就需要加四次锁,当然也要释放四次锁,频繁加解锁引起线程上下文的切换,非常消耗性能,所以jvm做了优化,只加一次锁,叫做锁的粗化,可以理解为将锁的颗粒度放大

 二、锁的消除

如图看下面代码

 public void test2(){
        //jvm的优化,JVM不会对同步块进行加锁
        synchronized (new Object()) {
            //伪代码:很多逻辑
            //jvm是否会加锁?
            //jvm会进行逃逸分析
        }
    }

这个地方加锁等于没有加锁,因为每个线程都会new object,大家都不会用同一把锁,jvm分析优化后不会对这种代码加锁(逃逸分析),所以,我们平时加锁一定要注意,加锁要加同一把锁。

三、锁的膨胀升级

1、锁的升级

synchronized的锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,锁状态的升级不可逆。

 

 

 

JDK1.6版本之后对synchronized的实现进行了各种优化,如自旋锁、偏向锁和轻量级锁 并默认开启偏向锁 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 关闭偏向锁:-XX:-UseBiasedLocking。如果直接上来就是重量级锁,那么实在是太消耗资源了。

2、锁的状态记录在哪里

HotSpot虚拟机的对象头包括两部分信息,第一部分是“Mark Word”,用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂 不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。

 

 3、对锁的理解:

(1)、偏向锁,偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。
(2)轻量级锁 、倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
(3)自旋锁轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

4、锁的膨胀升级过程

 

 

 

注意一下几点:

线程1获取轻量级锁后会将Object Mark Word 复制自己的一份到自己的栈空间,然后在自己的栈空间开辟一个指针lockerecord 指向Object Mark Word,同时Object Mark Word也会指向lockerecord,当线程1执行完代码块释放轻量级锁之后,发现Object Mark Word不在指向自己,说明当前锁已经改为重量级锁,那么它会唤醒阻塞队列中所有线程重新竞争锁。

总结:偏向锁,轻量级锁都是基于Object Mark Word的标记实现,java尽可能避免使用重量级锁。

 

posted @ 2020-07-15 00:00  菜鸟的java世界  阅读(471)  评论(0编辑  收藏  举报