线程安全与锁优化(二)

时间记录:2020-3-4
我们之前了解到了线程的安全方面的内容,知道了关于保持线程安全的手段,最常用的就是同步,而同步会对性能有一定的损耗,而jdk在内部的开发中也对此进行了一定的优化,我们不免思考,线程同步指令是否一定必须要执行的,也就是说在不需要进行线程同步操作的地方执行了同步的指令是不合理的,那么如何去避免这样的问题,虚拟机就提出了一些的优化方案,比如适应性自旋锁消除锁粗化轻量级锁偏向锁等,这些都是为了线程之间更搞笑地共享数据,以及解决竞争问题

一:自旋转锁与自适应锁
互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核中完成,这些操作会对系统的并发性能带来很大的压力。我们知道共享数据的锁定状态一般持续的时间是非常短的,如果此时为了让其他线程进行挂起和恢复操作是十分划不来的,假设让其他线程继续尝试获取这个锁,继续等待一会,等这个共享数据的锁定状态结束后,看这个锁是不是很快就释放了,也就是让获取锁的线程做一个循环操作,也就是自旋转操作,这样就避免了进行挂起和恢复带来的消耗,此相对于占用的很短时间来说是很划算的,这就是自旋锁技术。
注意: 如果等待没有和期望的一样那么就会白白的消耗了,所以这个自旋的次数是要进行严格的控制的。在jdk中引入了此项技术,可以通过 -XX:+UseSpinning 参数来开启,自选次数可以通过 -XX:PreBlockSpin 来进行更改。自选的默认次数是10次。而后面对自选的时间进行了智能的调整,在jdk1.6中引入了自适应自选锁,也就是说自选的次数不在固定,在一个对象的自旋操作成功过则认为此成功率极大,则下次的自选操作会更多,反之未成功过就会直接放弃自旋转操作。

二:锁消除
锁消除是指虚拟机及时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,主要分析手段为逃逸分析[后续进行了解]。那么我们不免思考如果自己知道这个情况还会加锁吗,那肯定是不会的,但是在jdk中存在大量的同步,但是我们不是那么清楚,所以编译器就帮着优化了。
我们知道String是一个不可变的类,在进行+操作的时候会自动转化,形成一个新的String对象,这样就造成了new操作的频繁,在1.5之前的版本中就使用StringBuffer 来代替这个+操作,而append这个方法里面就加锁了,但是在一个方法内的append加锁是没有必要的,所以会将其优化了。在后面的版本里面使用的是StringBuilder来进行优化。

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

注意:我们在使用大量的+的时候自觉的将其替换成StringBuilder或者StringBuffer来实现,可以优化编译。但是StringBuilder里面的append却没有添加同步操作,也就是说不是线程安全的,这里需要注意下平时使用的选择。

三:锁粗化
原则上在编写代码的时候,都推荐奖同步块作用范围限制的尽量小,只在恭共享数据的作用域内进行同步操作,但是如果一系列的操作都是作用在一个对象上,而且反复的进行互斥同步操作,就会造成性能的损耗,所以就会对这一些列的同步操作进行扩大锁的作用范围,而没有用的锁就会被消除,从而保证了数据安全。
StringBufferappend就是一个例子。

{
StringBuffer sb = new StringBuffer();
sb.append(x);
sb.append(y);
sb.append(z);
}

我么注意到了在同一个方法内连续进行append操作,而其实加锁的方法,所以就造成了不必要的浪费,这个时候就会进行粗化和消除操作,将锁扩大到这个操作序列外,也就是在第一次append开始到最后一次append结束。

四:轻量级锁
轻量级锁是相对于使用操作系统互斥量来视线传统锁而言,其本意是在没有多线程竞争的前提下,减少传统的重量级锁带来的性能消耗,与所有的锁优化技术目标相同。其和偏向锁的实现主要依据于对象头内的数据。

对象头: 对象头主要分两个部分,一个是用于存储对象自身的运行数据[哈希码,GC等信息],另一部分是存储指向方法去对象类型数据的指针,如果对象是数组的话,还会有一部分额外的部分用于存储数组的长度。当然这个存储内容是十分小的且存储尽量多的信息。

虚拟机对象头Mark Word

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

在代码执行的时候如果进入到了同步块的时候,如果对象没有被锁定,虚拟机首先会将当前线程栈帧中简历一个名为锁记录(Lock Record) 的空间,用于存储当前对象的Mark Word的拷贝(官方把这个拷贝加了一个前缀称之为Displaced Mark Word)。
然后虚拟机会将对象的Mark Work更新指向Lock Record的指针,如果修改成功则表示当前这个线程拥有这个对象,并且将对象Mark Word的锁标志位变成00,也就是轻量级锁定状态。
如果更新失败了就会检查当前对象的Mark Work是否指向当前线程的栈帧,如果是的话就直接进入同步块继续执行,反之不是则表示这个对象已经被其他线程占用了,就会膨胀变成重量级锁
而更新的过程就是加锁的过程,如果对象的Mark Work仍然是指向线程的锁记录,那就用CAS操作将对象当前的Mark Work和线程复制中的Displaced Mark Word进行交换,如果替换失败就表示有其他的线程在尝试获取该锁,然后就要在释放锁的同时唤醒被挂起的线程。(这里的交换成功应该是指的比较两个值, 也就是说当前的Mark Word指向的是不是当前的这个线程,后续查找资料证实)

注意: 这里的依据是“对于绝大部分的锁,在整个同步周期内是不存在竞争的”,如果不是这样的就会造成CAS带来的开销

CAS操作时的堆栈对象状态

轻量级锁CAS操作之前堆栈与对象状态轻量级锁CAS操作之前堆栈与对象状态
轻量级锁CAS操作之后堆栈与对象状态
轻量级锁CAS操作之后堆栈与对象状态
从图上可以明显的看出来,当前线程的栈帧将对象的Mark Work拷贝到自己的一个锁记录的空间里,然后将对象的Mark Work跟新指向锁记录的空间,然后就代表这个对象被该线程所拥有的,后面就是检查膨胀。其实这里就是做了一个标记说你是我的我不会做同步,如果你一旦变了我就要进行其他操作,也就是进行膨胀变成重量级锁。

轻量级锁膨胀到重量级锁过程
大致流程就是当一个对象在遇到一个同步的对象块的时候,然后将对象Mark Word拷贝到我的线程栈帧中,然后进行CAS操作,将对象的Mark Work的指针更新指向我线程的栈帧中记录的内容,然后就是标记改为00标识这个处于轻量级锁的状态,然后再CAS操作的过程中有其他线程也要进行同步那么我就是进行改变变成重量级锁然后其他线程就进入到阻塞状态,然后等我释放锁后我再唤醒你,然后你就可以继续执行下一个操作了。

五:偏向锁
偏向锁也是一项优化,目的在于消除数据在无竞争情况下的同步操作,偏向锁就是在武竞争的情况下把整个同步操作都取消掉,相对于偏向锁省略了CAS操作。
偏向锁的偏是偏心的意思,意思就是这个锁回偏向于第一个获得到它的线程,如果接下来的操作改锁没有被其它线程获取到,则持有偏向锁的线程将不会进行同步操作,不同于轻量级锁的尝试。在jdk中通过 -XX:+UseBiasedLocking 参数来启用偏向锁。在锁对象第一次被线程获取到的时候,虚拟机会将这个对象的对象头的标志位设为“01”,也就是偏向模式。虚拟机会进行CAS操作把获取到这个锁的线程ID记录在对象头(MARK WORD)中,这样就是偏向于这个线程,当有其他的线程去获取这个锁的时候,偏向模式就结束了,就会恢复标志位为未锁定(01)或者是轻量级锁(00)
注意 偏向锁可以提高有同步但是武竞争的程序性能,如果程序中大多数锁总是被多个不同线程访问,那么偏向模式就是多余的,可以通过参数禁止偏向锁优化反而提高性能。

时间记录:2020-3-7

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值