Java多线程锁的优化

    Java多线程开发可以提高系统性能,但同时也会遇到并发冲突引起线程安全问题。在Java语言中解决线程安全问题通常的方式是加锁,因此对锁的优化,是开发高性能多线程系统的必然选择。

一.JVM对锁的优化
1.偏向锁
     所谓的偏向,就是偏心,即锁会偏向于当前已经占有锁的线程。只要没有竞争,获得偏向锁的线程,在将来进入同步块,不需要做同步。当其它线程请求相同的锁时,偏向模式结束。因为大部分情况是没有竞争的,所以可以通过偏向来提高性能。通过参数-XX:+UseBiasedLocking启动偏向锁,默认该选项是启用的。但要注意,在竞争激烈的场合,偏向锁会增加系统负担,这类场合应该关闭该选项。


2.轻量级锁

        如果偏向锁没有成功,可能是因为存在线程竞争,此时会尝试轻量级锁。轻量级锁就是对象头的Mark Word里面有一个轻量级锁指针,该指针指向持有锁的线程。线程加锁时判断一下该指针是不是指向自己的,如果是,就加锁成功。如果不是,加锁失败,说明有其它线程加了锁,此时升级为重量级锁。

        普通的锁处理性能不够理想,轻量级锁是一种快速的锁定方法。如果轻量级锁失败,表示存在竞争,升级为重量级锁(常规锁)。轻量级锁可以在没有锁竞争的前提下,减少传统锁使用OS互斥量产生的性能损耗。但在竞争激烈时,轻量级锁会多做很多额外操作,导致性能下降。

        偏向锁是在只有一个线程执行同步块时进一步提高性能,而轻量级锁则是为了在线程交替执行同步块时提高性能。


3.自旋锁

      如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程可以不在OS层挂起线程,也就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等,做几个空操作(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。JDK1.7中,该锁为内置实现。如果同步块很长,自旋失败,会降低系统性能。如果同步块很短,自旋成功,节省线程挂起切换时间,提升系统性能。

      线程自旋是需要消耗 cpu 的,说白了就是让 cpu 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cpu 自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

     自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说,性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁。


4.JVM中的获取锁的优化方法和获取锁的步骤
     a.偏向锁可用会先尝试偏向锁
     b.轻量级锁可用会先尝试轻量级锁
     c.以上都失败,会膨胀为普通锁,但会视情况优先尝试自旋锁
     d.再失败,使用OS互斥量在操作系统层挂起

二.程序开发对锁的优化
1.减少锁持有时间
      比如只锁定部分代码段,而不是锁定整个方法。

2.减小锁粒度
      将大对象,拆成小对象,大大增加并行度,降低锁竞争。这样能使偏向锁,轻量级锁成功率提高。最典型的的例子就是ConcurrentHashMap。HashMap内部本质是用数组实现的,ConcurrentHashMap则将一个大数组分成多个小数组。ConcurrentHashMap称之为段,即若干个Segment。在减小锁粒度后, ConcurrentHashMap允许若干个线程同时进入,只要这些线程操作的是不同的Segment。

3.锁分离
     根据功能进行锁分离,比较典型的就是按照读写进行分离。ReadWriteLock类就支持读和写两种锁,加读锁则只排斥写锁并不排斥其它读锁,加写锁则排斥任何读锁和写锁。在读多写少的情况,可以提高性能。

4.锁粗化
   通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,即在使用完公共资源后,应该立即释放锁。只有这样,等待在这个锁上的其他线程才能尽早的获得资源执行任务。
但是,凡事都有一个度,如果对同一个锁不停的进行请求、同步和释放,其本身也会消耗系统宝贵的资源,反而不利于性能的优化。
例如下面这段代码:
public void demoMethod(){
 synchronized(lock){
  //do sth.
 }
 //做其他不需要的同步的工作,但能很快执行完毕
 synchronized(lock){
  //do sth.
 }
}

在两次加锁之间只做了简单的操作,很快就完成,白白增加加锁和释放锁的资源消耗。因此,还不如整合成一次锁请求
public void demoMethod(){
  //整合成一次锁请求
 synchronized(lock){
  //do sth.
  //做其他不需要的同步的工作,但能很快执行完毕
 }
}

更极端的例子是循环:
for(int i=0;i<CIRCLE;i++){
 synchronized(lock){
  //do sth.
 }
}
改为以下代码,可大幅提高性能
synchronized(lock){
for(int i=0;i<CIRCLE;i++){
  //do sth.
 }
}

5.锁消除
    在即时编译器时,如果发现不可能被共享(发生并发冲突)的对象,则可以消除这些对象的锁操作。该功能也是JVM自动实现的。可以通过指定JVM参数-XX:+DoEscapeAnalysis -XX:+EliminateLocks来启动该功能。注意:第一个参数逃逸分析是必须要加上的。

6.无锁
     要提高性能最高效的方式是无锁的方式。这种方式不通过操作系统层面的资源控制,完全通过编程技巧来实现,因此可以大幅提高性能。锁是悲观的操作,无锁是乐观的操作。无锁的一种实现方式是CAS(Compare And Swap),它是非阻塞的同步。在应用层面判断多线程的干扰,如果有干扰,则通知线程重试或放弃。java.util.concurrent.atomic包使用CAS实现无锁操作,性能高于一般的有锁操作。
   CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即时没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。比较操作是原语操作,是一条CPU指令,所以可以保证不会在多线程下发生问题。

    这篇是虚拟机系列的第5篇了,不过主要内容和多线程有关,就算是一篇番外吧。原本想写一篇文章把虚拟机的内容总结一下,但内容太多了,居然写了5篇,其实还有很多内容,以后有机会再补充吧!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值