synchronized锁的优化

在多线程并发中synchronized一直被成为重量级锁,但是随着JDK1.6后对其进行各种优化,包括适应性自旋,锁消除,锁粗化,轻量级锁。偏向锁,有些情况下它就并不难笨重了。

Java中每一个对象都可以作为锁,具体表现为

(1) 对于普通同步方法,锁就是当前实例对象
(2) 对于静态同步方法, 锁就是当前类的Class对象
(3) 对于同步方法块,锁就是synchronized括号里配置的对象

在JVM规范中可以看到synchronized的实现原理。JVM基于进入和退出Monitor对象来实现方法的同步和代码块的同步。但两者是实现细节不同。同步代码块使用monitorenter和monitorexit指令实现的,方法同步的细节JVM规范没有详细说明,但是同样可以使用这两个指令实现。

monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit是插入到方法结束处和异常处。JVM保证每个monitorenter都有monitorexit与之相匹配。任何对象都有一个Monitor对象与之关联,当且一个Monitor被持有后,它将处于锁定状态。例如线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

1.自旋锁和自适应锁

我们在讨论互斥同步的时候,提到了互斥同步对性能最大的影响就是阻塞的实现。挂起线程和恢复线程的操作都是需要在内核态中完成,这些操作给系统带来了很大的压力。虚拟机的开发团队注意的很多共享的数据锁定的时间都是很短的一段时间,为了这段时间就挂起线程并不值得。如果物理机有一个以上的处理器让两个或以上的线程并行执行,那么我们可以后面请求锁的那个线程“稍微等一下”,不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁,那么让后面线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就是自旋锁。

自旋锁在JDK1.4.2就引入,JDK1.6默认开启。自旋锁不能代替阻塞。自旋锁虽然可以避免线程切换的开销,但是它是要占用处理器时间的。如果锁被占用的时间较短,自旋的时间固然很好。如果锁占用的时间较长,那么就会白白浪费处理器资源。因为自旋次数一定要有限制,如果超过指定次数,就按照传统的方式将线程挂起,默认是10次。

在JDK1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不是固定的了。而是由前一次在同一个锁的自旋时间和锁拥有者的状态来决定。例如在同一个锁对象,上次的自旋等待获得锁成功,并且持有锁的线程也正在运行,那么虚拟机认为这次自旋等待也极有可能成功,从而允许延迟自旋等待的时间,例如100个循环。另一方面,如果对于某一个锁,自旋很少成功,那么以后获取这个锁可能就会自动略掉自旋的操作。

2.锁消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。为什么会在明知道不存在数据竞争用的情况下加同步呢,答案是有许多同步措施并不是程序员自己加的。

例如如下代码,在字面上和语义上都没有同步

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

由于String是不可变类,在JDK1.5之前String类的相加操作底层是new 一个线程安全的StringBuffer类,执行append()方法来操作。JDK1.5及其以后是new一个非线程安全的Stringbuild类,执行append()方法操作。也就是说以上操作,在JDK1.5之前等同与下面的操作。

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

现在大家都已经认为代码已经是同步的了吧。每一个sb.append()都是一个同步块,锁就是sb这个对象,而且sb对象是方法体里面的,也就是每一个线程都会有一份,不存在锁竞争的关系。所以这里有锁,但是可以被安全地消除掉。在即时编译后,这段代码就会忽略掉所有的同步而直接执行了。

3.锁粗化

原则上我们在编写代码的时候,总推荐编写同步代码块的作用于越小越好——只在共享数据的实际作用域中才进行,这样是为了同步的操作数量尽可能小,等待锁的线程也能尽快得到锁。

在绝大数情况,这个原则是适用的,但是如果一系列操作都是对同一个对象进行加锁和解锁,甚至加锁操作是在循环体里,那么即使没有线程竞争,频繁的加锁也会导致不必要的性能消耗。例如上述的代码,在一个方法中多次调用了同步方法append(),如果虚拟机探测到这一串零碎的操作都是对同一个对象加锁,那么将会把加锁的范围扩大,就是将这个同步块扩展到从第一个append()操作开始到最后一个append()结束。 这个就是锁的粗化。

4.轻量级锁

在说轻量级锁之前,得先有点知识作为铺垫。synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2个子宽存储。在32位虚拟机,1子宽等于4字节。
这里写图片描述

Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄,锁标记位。32位的JVM的Mark Word的默认存储结构如下图:
这里写图片描述

在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化成为存储以下4中数据。
这里写图片描述

在64位虚拟机下,Mark Word是64bit的,其存储结构如下:

这里写图片描述

我们再来说轻量级锁,轻量级锁是JDK1.6之中加入的锁机制,轻量级是相同传统的互斥锁而言的,所以这里吧传统的互斥锁成为“重量级锁”。轻量级锁不是为了替代重量级锁的,两个锁的应用场景不同。轻量级的本意是在没有多线程竞争的前提前,减少传统的重量级锁使用操作系统互斥量产生的性能

线程在执行同步块之前,JVM会先在当前线程额栈中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中。官方称作Displaced Mark Word 然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,如果成功,当前线程获得锁,并且对象Mark Word的锁标志位转变成“00”,表示此对象出于轻量级锁定状态,如果失败,JVM首先检查对象的Mark Word是否指向当前线程的栈,如果是,说明当前线程拥有了这个对象的锁,则直接进入同步代码块。否则说明这个锁对象已经被其他线程先占。这时候第二个线程做自旋操作等待获取,如果有两条以上的线程来竞争同一把锁或者自旋获取锁操作失败,那么轻量级锁膨胀为重量级锁。
这里写图片描述

5.偏向锁

偏向锁也是JDK1.6引入的一项锁优化,它的目的是消除数据在无竞争状态下的同步原语,如果说轻量级锁是在无竞争状态下使用CAS操作消除同步使用的互斥量,那偏向锁就是在无竞争情况下把整个同步都消掉,连CAS都不做了。

偏向锁的偏就是指这个锁会偏向第一个获得它的线程,如果接下来的执行过程中,该锁没有被其他线程获取,那么持有偏向锁的线程永远不需要再进行同步。

如果当前虚拟机启用了偏向锁,JDK1.6是自动开启,那么当锁对象第一次被线程获取的时候,JVM将会把对象头的标志位设置为“01”,即偏向模式。同时使用CAS将当前线程的ID记录到Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁的相关同步块时,虚拟机都可以不用做任何同步操作。

这是当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,如果锁对象未被锁定目前没有线程执行,则撤销偏向后恢复到未锁定(标志位“01”),如果被锁定正在有线程执行说明存在了竞争关系,则撤销偏向后升级为轻量级锁定的状态(标志位“00”)。

这里写图片描述

6.偏向锁、轻量级锁、重量级锁的优缺点

这里写图片描述

总结:我们来捋一遍偏向锁升级到轻量级锁再升级到重量级锁的整个过程,这个过程是不可逆的,只有升级,没有降级。

只有一个线程时,启动偏向锁后,这个线程进出同步块不用在加额外的同步操作,当来了第二个线程发生了竞争关系,则偏向锁升级为轻量级锁。一个轻量级锁被一个线程拿到,第二个线程来了后会首先进行CAS自旋尝试获取,这时当第三个线程过来竞争同一把锁或第二个线程自旋失败,则轻量级锁会膨胀成为重量级锁,进行传统的互斥和阻塞操作。

参考:《深入理解java虚拟机》、《Java并发编程的艺术》

(与其说是参考,不如说是这两本书结合的读书笔记)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值