锁优化(synchronized的原理及升级原理)

锁优化(synchronized的原理及升级原理)

高效并发是jdk5升级到jdk6的重要改进项,里面包括各种锁优化技术,如适应性自旋,锁消除,锁膨胀,轻量级锁,偏向锁

自旋锁和适应性自旋

讨论锁的性能的时候,总会提到阻塞,对锁性能最大的影响就是阻塞的实现。java中挂起线程和回复线程对要切换到内核态中完成,这些操作极大的影响了java的并发性能。这是java虚拟机的开发团队也注意大部分情况下共享数据的锁定持续的时间很短,为了这段时间就去挂起和回复线程不值得,加上现在电脑都是多核处理器,我们可以让后面请求锁的线程稍等一会而不放弃处理器的执行时间,看看持有锁的线程会不会很快的释放锁。为了线程的等待,可以让线程执行一个忙循环,这就是所谓的自旋锁。

自旋锁在jdk1.4.2已经引入,不过默认是关闭的。到了jdk6改为默认开启,还对自旋次数做了优化。自旋锁等待时会占用处理器时间,所以如果等待的时间比较短,效果就很好,相反如果需要等待的时间比较长,就白白浪费了性能,所以自选的次数需要有所限制,jdk6优化的就是将以前固定的自旋次数改为自适应的自旋次数。简单来说就是同一个对象的自旋后成功的越多,就认为你更应该自旋,你的自旋次数就比较多,反之如果自旋时间经常拿不到锁就减少你自旋的次数甚至不让你自旋了,这样就让一个共享资源存活的时间越长,对该资源锁的预判就越准确。

锁消除

就是java虚拟机如果检测到不必要的锁,就会被编译器优化掉。

比如下面的代码:

public String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}

如果在JDK5之前,实际执行效果会变成这样

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();
}

现在大家还认为这段代码没有涉及同步吗?每个StringBuffer.append()方法中都有一个同步块,锁 就是sb对象。虚拟机观察变量sb,经过逃逸分析后会发现它的动态作用域被限制在concatString()方法内 部。也就是sb的所有引用都永远不会逃逸到concatString()方法之外,其他线程无法访问到它,所以这里虽然有锁,但是可以被安全地消除掉。在解释执行时这里仍然会加锁,但在经过服务端编译器的即时编译之后,这段代码就会忽略所有的同步措施而直接执行。

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

锁粗化

原则上写代码的后都是尽量让锁的粒度尽量的细,这样就可以就算存在锁竞争,等待的线程也能尽快的拿到锁。

大多数情况都是对的,但是如果你是在一系列的操作中对同一个对象反复加锁和解锁,甚至在循环体中加锁,那其实就降低了没有线程竞争时的性能。所以当虚拟机在执行上面这段代码的时候,会直接将锁粗化到整个操作的外部,只加锁一次。

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();
}

偏向锁和轻量锁

这就是synchronized的锁升级了,轻量锁的概念是相对于jdk6之前就有的锁机制而言的,传统锁基于操作系统互斥量实现,被称为重量级锁。轻量锁的目的是为了在竞争不激烈的时候,减少重量锁的使用以提高并发性能。

要理解轻量锁和偏向锁,就需要对虚拟机的对象结构有了解,主要是对象头部份。以下为对象头的结构,为了空间使用效率被设计成了动态的结构。

接下来可以对照着mark word来看看锁升级的过程了。如果线程进入时发现对象未被锁定,即对象头后三位为001,就会将后三位置为101并将自己的线程id使用CAS操作记录到对象头中,之后持有偏向锁的线程每次进入的时候不i会有任何的同步操作。

一旦有另一个线程尝试获取该对象作为锁,偏向模式立即结束。此时会根据锁对象目前是否被使用决定是否撤销偏向(偏向模式设为 0),即如果当前锁对象正在被人使用,直接升级为轻量锁,如果当前锁对象只是进入了偏向模式但当前没被使用就可以考虑将你置为该对象的偏向线程(考虑的依据和过程先不管)。但是偏向模式存在一个问题,偏向模式下对象头原来放哈希码的位置被放入了线程id,那哈希码怎么办?在java中一个对象如果被计算过哈希码就应该保持该值不变,否则很多依赖哈希码的api可能会有问题。

以下引用自《深入理解Java虚拟机》:

而作为绝大多数对象哈希码来源的Object::hashCode()方法,返回的是对象的一致性哈希 码(Identity Hash Code),这个值是能强制保证不变的,它通过在对象头中存储计算结果来保证第一次计算之后,再次调用该方法取到的哈希码值永远不会再发生改变。因此,当一个对象已经计算过一 致性哈希码后,它就再也无法进入偏向锁状态了;而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求[1]时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。在重量级锁 的实现中,对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类里有字段可以记录非加锁 状态(标志位为“01”)下的Mark Word,其中自然可以存储原来的哈希码。

对象有偏向锁升级到轻量锁时,会在当前线程创建一个叫锁记录的空间用于存储锁对象的对象头的拷贝(官方叫 Displaced Mark Word )。然后当前线程会使用CAS操作尝试将当前对象的对象头(Mark Word)的前30bit指向当前线程的栈地址,如果这个动作成功,即代表该线程拥有这个对象的锁,并将对象头的后2bit改为00。

如果更新操作失败了,说明至少存在一个线程跟它竞争该锁,这是会按照上面所说的自旋锁等待。如果出现两个以上线程争用同一个锁,轻量锁会升级为重量锁,及后面来的线程会直接阻塞。

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

欢迎斧正。

相关资源:

深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明

偏向锁的重偏向机制可以参考:

java 偏向锁 重偏向_偏向锁的【批量重偏向与批量撤销】机制_Lucas HC的博客-CSDN博客

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值