Synchronized在JDK1.6中的优化

1.适应性自旋锁

首先什么是自旋锁?有时候线程去竞争锁失败,进入了阻塞状态,但刚刚进入阻塞状态后持有锁的线程就释放了锁资源,这个时候线程又会被唤醒继续执行,考虑到这种情况,jdk1.4中引入了自旋锁的概念,就是在sychronized同步代码块中,如果线程没有竞争到锁,则让它先进行一段无意义的自旋,避免线程直接进入阻塞状态,自旋结束后如果持有锁的线程已释放锁,则当前线程可直接开始执行。显然,这样处理会有个坏处,如果每一次线程未竞争到锁都进行一段时间的自旋,且每一次自旋结束后都依然没有获取到锁,那么自旋的操作就白白浪费了cpu的性能。在jdk1.4引入自旋锁的时候我们可以通过jvm启动参数-XX:+UseSpinning开启自旋锁,使用-XX:PreBlockSpin设置自旋锁的等待次数,而jdk1.6引入适应性自旋锁后,自旋锁机制变得更加"聪明",它可以动态的调整每一次自旋的时间,调整的依据就是上一个获取到锁的线程获取锁用了多少时间,如果用时较短,它就认为这个锁比较容易获取,那就适当的延长自旋的时间以等待这个锁的释放,如果用时较长,则缩短当前线程自旋的时间或者不自旋直接进入阻塞状态,以避免长时间自旋后依然无法获取锁的情况。

2.锁消除

锁消除简单来说,就是在不可能出现锁资源的竞争,不需要加锁的情况下,如果我们使用了synchronized锁,则jvm会在编译的时候直接将这个锁消除,以减少锁的操作,提升代码的执行效率。jvm判断这个方法是否线程安全,是否不可能出现锁资源竞争的时候,会进行逃逸分析,所以锁消除有两个前提,1.该程序必须以server模式运行 2.必须开启逃逸分析和锁消除(通过jvm启动参数设置,-XX:+DoEscapeAnalysis表示开启逃逸分析,-XX:+EliminateLocks表示锁消除)

3.锁粗化

一般在加锁的时候,都会让锁的粒度足够小,尽量在synchronized同步代码块中只包含真正需要加锁的代码,以减少锁占用的时间,提高代码执行性能。但有时候,一味的减小锁的粒度,会增加竞争锁的次数,同一个线程会频繁获取同一个锁,出现这种情况时,sychronized会自动帮我们提升锁的粒度,减少竞争锁的次数,如下,以极端情况举例:

锁粗化前:

public void test(){
    for (int i=0;i<10;i++){
        synchronized (this){
            count++;
        }
    }
}

锁粗化后:

 

public void test() {
    synchronized (this) {
        for (int i = 0; i < 10; i++) {
            count++;
        }
    }
}

4.偏向锁和轻量级锁

锁的分类

jdk1.6之后,synchronized有偏向锁、轻量级锁、重量级锁三种锁机制。synchronized有一个特点,就是使用synchronized同步代码块或synchronized方法时,所有的对象都可以作为锁对象,用synchronized修饰的方法,锁对象为当前类的实例对象;synchronized修饰的静态方法,锁对象为当前类的class对象。那既然所有对象都能作为synchronized的锁,那储存对象的锁信息(当前对象的锁状态,被哪个线程持有锁,有哪些线程在等待锁等)最好的方式就是将其存储在对象的对象头中。

对象头

在HotSpot虚拟机中,对象在内存中的分布可分为三个部分:1.对象头 2.实例信息 3.数据填充。其中对象头又分为Mark Word和类型指针两部分,Mark Word用于存储对象自身的运行时数据,如:GC分代年龄、hashcode、数组长度(如果该对象为数组)、锁状态标志、是否偏向、偏向线程id等等。类型指针部分则存储该对象指向其类的元数据的指针。

Mark Word的存储特点

在32位和64位的系统中,对象头的Mark Word分别占用32个和64个bitmap,为了尽可能多的存储对象运行时的数据,Mark Word被设计成了可根据锁状态动态分配各部分存储空间大小的数据结构,见下图

 

几种锁的状态   

    1.无锁状态

    当对象为无锁状态时,Mark Word主要存储对象的hashcode、分代年龄等信息,如果对象为数组类型,则还会存储该数组的长度。这就是为什么通过类的元数据可以直接获取对象的大小,但通过类的元数据不能获取数组的长度。

    2.偏向锁

    当线程进入synchronized同步代码块或同步方法时,如果对象锁的对象头中保存的偏向锁线程id为0,则该对象会在Mark Word中以CAS的方式保存当前线程的线程id。如果有另一个线程id来竞争这个对象锁,则升级为轻量级锁

适用场景:很多函数(特别是第三方包提供的)虽然在设计时考虑了并发的情况,做了线程安全的控制,但在实际使用场景中,或许根本就只有一条线程在调用,这种情况就可以使用偏向锁,将使用这个锁的线程id记录,避免同一条线程频繁的竞争和释放同一个锁而带来性能开销。而一旦出现另一条线程来获取锁,说明这个锁对象是存在锁资源竞争的,则将该对象的锁状态升级为轻量锁。

    3.轻量级锁

    当对象的锁状态为轻量级锁时,MarkWord区记录的是“指向栈中锁记录的指针”,这个指针指向的是当前持有锁的线程的栈空间中的一个栈帧,这个栈帧叫做lock record,如下图所示,lock record分为两部分,一部分复制保存锁对象的mark word,一部分保存锁对象的地址。

 

当线程第一次进入同步代码块时,会在当前线程的栈空间中生成一个lock record,将锁对象的mark word区保存的对象头信息保存下来,并且记录锁对象的地址,然后通过CAS的方式将lock record的地址记录到锁对象的markword,解锁时也是通过CAS的方式将lock record中记录的锁对象的hashcode、分代年龄等信息记录到锁对象的markword区,并将锁标志改为无锁状态。无论加锁还是解锁的CAS操作如果操作失败,则将锁升级为重量级锁。

适用场景:很多情况下,我们会尽量让锁的粒度足够小,以较少锁占用的时间,所以在并发量不大时,同步块中的代码其实是各个线程交替执行而并不会发生锁竞争的,这种时候就应该使用轻量级锁,加锁和解锁都使用CAS操作,不会涉及线程上下文的切换、用户态和内核态的切换。而出现锁竞争的情况时,就升级为重量级锁。

总结:

    偏向锁适用于只有一条线程访问同步块,轻量级锁适用于多条线程交替访问同步块,重量级锁适用于多条线程同时访问同步块。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值