《深入理解Java虚拟机》:锁优化

《深入理解Java虚拟机》:线程安全 , 两种同步锁实现
自旋锁与自适应自旋1、上一节讨论同步锁提到 , 等待锁的线程进入阻塞状态是需要从用户态切换到内核态 , 这种操作会给系统的并发带来非常大的压力 。 于是 , 在有多个处理器的情况下 , 我们乐观的认为 “等一下” , 不放弃处理器时间 , 或许持有锁的线程很快就会释放锁 。 为了让线程等待 , 需要让线程执行一个忙循环(while(true)) , 这项技术就叫做自旋 。
自旋锁解决的问题就是:避免用户线程和内核的切换的消耗 。
但线程自旋是需要消耗cup的 , 如果一直获取不到锁 , 那线程也不能一直占用cup自旋做无用功 , 所以需要设定一个自旋等待的最大时间 。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁 , 就会导致其它争用锁的线程在最大等待时间内还是获取不到锁 , 这时争用线程会停止自旋进入阻塞状态 。
自旋锁的优缺点:自旋锁尽可能的减少线程的阻塞 , 这对于锁的竞争不激烈 , 且占用锁时间非常短的代码块来说性能能大幅度的提升 , 因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗 , 阻塞挂起再唤醒这些操作会导致线程发生两次上下文切换!
但是如果锁的竞争激烈 , 或者持有锁的线程需要长时间占用锁执行同步块 , 线程占用cpu自旋的消耗大于线程阻塞挂起操作的消耗 , 其它需要cup的线程又不能获取到cpu , 造成cpu的浪费 , 所以这种情况下我们要关闭自旋锁 。
2、JVM对于自旋周期的选择 , jdk1.5这个限度是一定的写死的 , 在1.6引入了适应性自旋锁 , 适应性自旋锁意味着自旋的时间不再是固定的了 , 而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定 。 如果在同一个锁对象上 , 自旋等待刚刚成功获得过锁 , 并且持有锁的线程正在运行当中 , 那么虚拟机认为这次也有可能再次成功 , 进而允许它自旋时间更久一点 , 比如100个循环 。 相反 , 如果对于某个锁 , 自旋很少成功过, 那么会忽略自旋过程直接进入阻塞 , 以免造成cpu浪费 。
自旋锁的开启 JDK1.6中-XX:+UseSpinning开启;
-XX:PreBlockSpin=10 为自旋次数;
JDK1.7后 , 去掉此参数 , 由jvm控制;
锁消除锁消除是在编译器级别的事情 。 虚拟机即时编译器在运行时 , 如果发现不可能存在共享数据竞争的锁 , 则可以消除这些对象的锁操作 。
程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?有些是程序员编码不规范引起 , 但很多不是程序员自己加入的 。
锁粗化原则上 , 同步块的作用范围要尽量小 。 但是如果一系列的连续操作都对同一个对象反复加锁和解锁 , 甚至加锁操作出现在循环体内 , 频繁地进行互斥同步操作也会导致不必要的性能损耗 。
锁粗化就是增大锁的作用域 。
如果虚拟机检测到对同一个对象锁不停的进行请求、同步和释放 , 将会把加锁同步的范围整个操作序列的外部 , 如下代码 , 就会扩展到第一个append()操作之前直至最后一个append()操作之后 , 这样只需要加一次锁就可以了 。
public static void main(String[] args) {StringBuffer sb = new StringBuffer();sb.append(args[0]);sb.append(args[1]);sb.append(args[2]);System.out.println(sb.toString()); }偏向锁
《深入理解Java虚拟机》:锁优化文章插图
当没有竞争出现时 , 默认会使用偏向锁 。
JVM 会利用 CAS 操作 , 在对象头上的 Mark Word 部分设置当前线程指针 ID , 在对象头部用101标记 。 以表示这个对象偏向于当前线程 , 所以并不涉及真正的互斥锁 , 因为在很多应用场景中 , 大部分对象生命周期中最多会被一个线程锁定 , 使用偏斜锁可以降低无竞争开销 。