锁优化

在JDK1.6以后,为了减少消耗,锁进行了很多的升级。并且有了四种状态,从低到高

无锁状态
偏向锁状态
轻量级锁状态
重量级锁状态
下面就介绍一下这四种不同等级的锁

不同等级间的锁可以进行升级,但是不能进行降级

对象内存布局
在讲锁之前我们应该了解对象的内存布局,因为后面锁的判定时需要用到,

该内容可以看我的这篇文章,对象的内存布局,重点看对象头中的运行时数据(Mark Word)

看了这个你应该知道,Mark Word(以后简称MW) 会随着 标志位的变化而变化

MW变化
先把他放出来,免得等会儿你自己翻

无锁状态


可以看到,这种情况下,MW被分为了四个部分,他锁的标志位为 01,是否是偏向锁标志位为 0.

偏向锁(biasedLock)
先看MW变化(在对象头变化的那一张图片中)

可以看到,他的锁标志位和无锁的标志位是一样的 都是 01,但是是否是偏向锁的标志位就变了 变成了 1.并且整个MW部分变成了5部分

目的
经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引人了偏向锁

偏向锁,就如同他的名字一样,“偏向”,“偏心”,英文"biased"也是偏爱的意思。

他为啥是叫这个呢,因为这个锁偏向于第一个获取到他的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步

步骤


上面一共被分为三大部分,

无锁状态
偏向锁状态
撤销偏向锁
上面的图中,线程一演示了偏向锁的初始化流程,线程2演示了偏向锁撤销的过程

初始化过程
当锁对象第一次被线程获取的时候,虚拟机会将对象头中的锁标志位置为 “01”(偏向模式)
同时,使用CAS(如果不了解CAS,可以看这篇文章,悲观锁和乐观锁)操作,把获取到这个锁的线程的ID记录在对象的MW中,
如果 CAS成功,持有偏向锁的线程每次进入这个锁相关的同步块时,虚拟机可以不进行任何同步操作
撤销过程
首先暂停拥有偏向锁的线程
然后检查持有偏向锁的线程是否活着
不活跃,将对象头设置成无锁状态 (标志位"01",但不可偏向)
活,
CAS成功,重新偏向,更改线程ID
失败,恢复成无锁状态,或者变成轻量级锁定状态。
轻量级锁
标志位为"00",可以看最开始的图

为啥叫轻量级锁,因为这是相比于传统的重量级锁而言,原来传统的重量级锁,使用的是系统互斥量实现的

他的出现并不是代替重量级锁,而是在没有多线程竞争的前提下,减少系统互斥量操作产生的性能消耗

步骤
先看图

加锁
线程在执行同步块之前,JVM会现在当前线程的栈帧中创建用于存储锁记录(下图的LockRecord)的空间,并将对象头中的MW复制到锁记录中,官方称为 Displaced Mark Word
然后,虚拟机将使用CAS操作,将对象的MW更新为指向锁记录的指针
如果这个操作成功,那么该线程就有了该对象的锁,并且对象的MW的锁标志位置为 “00”,表示该对象处于轻量级锁定状态
如果更新失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁


解锁
使用CAS操作将 Displaced Mark Word 替换回到对象头
如果成功,则说明没有发生竞争
失败,则表示当前锁存在竞争,锁就会膨胀成重量级锁
释放锁,并且唤醒等待的线程
缺点
轻量级能提升程序同步性能的依据是"对于绝大部分的锁,在整个同步周期内都是不存在竞争的",这是一个经验数据。

如果没有竞争,轻量级锁使用CAS操作,避免使用互斥量
如果存在竞争,除了互斥量的开销,还有 CAS的操作,不仅没有提升,反而性能会下降
各个锁之间的转换


对比
锁    优点    缺点    适用场景
偏向锁    加锁和解锁都不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距    如果线程间存在锁竞争,会带来额外的锁撤销的消耗    只有一个线程访问同步块
轻量级锁    竞争的线程不会阻塞,提高了程序的响应速度    如果始终得不到锁竞争的线程,使用自旋会消耗CPU    追求响应时间
同步块执行速度非常块
重量级锁    线程竞争不使用自旋,不会消耗CPU    线程阻塞,响应时间慢    追求吞吐量
同步块执行时间较长

 

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。锁削除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

 

锁粗化

通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。

 

 

自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

  由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。

  信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用),而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。

  如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共巷资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。

  自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。

  跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。

  无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。

  自旋锁的API有:

spin_lock_init(x)

  该宏用于初始化自旋锁x。自旋锁在真正使用前必须先初始化。该宏用于动态初始化。

DEFINE_SPINLOCK(x)
  该宏声明一个自旋锁x并初始化它。该宏在2.6.11中第一次被定义,在先前的内核中并没有该宏。

SPIN_LOCK_UNLOCKED
  该宏用于静态初始化一个自旋锁。

DEFINE_SPINLOCK(x)等同于spinlock_t x = SPIN_LOCK_UNLOCKED spin_is_locked(x)
  该宏用于判断自旋锁x是否已经被某执行单元保持(即被锁),如果是,返回真,否则返回假。

spin_unlock_wait(x)
  该宏用于等待自旋锁x变得没有被任何执行单元保持,如果没有任何执行单元保持该自旋锁,该宏立即返回,否则将循环在那里,直到该自旋锁被保持者释放。

spin_trylock(lock)
  该宏尽力获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,否则不能立即获得锁,立即返回假。它不会自旋等待lock被释放。

spin_lock(lock)
  该宏用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放,这时,它获得锁并返回。总之,只有它获得锁才返回。

spin_lock_irqsave(lock, flags)
  该宏获得自旋锁的同时把标志寄存器的值保存到变量flags中并失效本地中断。

spin_lock_irq(lock)
  该宏类似于spin_lock_irqsave,只是该宏不保存标志寄存器的值。

spin_lock_bh(lock)
  该宏在得到自旋锁的同时失效本地软中断。

spin_unlock(lock)
  该宏释放自旋锁lock,它与spin_trylock或spin_lock配对使用。如果spin_trylock返回假,表明没有获得自旋锁,因此不必使用spin_unlock释放。

spin_unlock_irqrestore(lock, flags)
  该宏释放自旋锁lock的同时,也恢复标志寄存器的值为变量flags保存的值。它与spin_lock_irqsave配对使用。

spin_unlock_irq(lock)
  该宏释放自旋锁lock的同时,也使能本地中断。它与spin_lock_irq配对应用。

spin_unlock_bh(lock)
  该宏释放自旋锁lock的同时,也使能本地的软中断。它与spin_lock_bh配对使用。

spin_trylock_irqsave(lock, flags)

  该宏如果获得自旋锁lock,它也将保存标志寄存器的值到变量flags中,并且失效本地中断,如果没有获得锁,它什么也不做。
  因此如果能够立即获得锁,它等同于spin_lock_irqsave,如果不能获得锁,它等同于spin_trylock。如果该宏获得自旋锁lock,那需要使用spin_unlock_irqrestore来释放。

spin_trylock_irq(lock)

  该宏类似于spin_trylock_irqsave,只是该宏不保存标志寄存器。如果该宏获得自旋锁lock,需要使用spin_unlock_irq来释放。

spin_trylock_bh(lock)
  该宏如果获得了自旋锁,它也将失效本地软中断。如果得不到锁,它什么也不做。因此,如果得到了锁,它等同于spin_lock_bh,如果得不到锁,它等同于spin_trylock。如果该宏得到了自旋锁,需要使用spin_unlock_bh来释放。

spin_can_lock(lock)
  该宏用于判断自旋锁lock是否能够被锁,它实际是spin_is_locked取反。如果lock没有被锁,它返回真,否则,返回假。该宏在2.6.11中第一次被定义,在先前的内核中并没有该宏。

  获得自旋锁和释放自旋锁有好几个版本,因此让读者知道在什么样的情况下使用什么版本的获得和释放锁的宏是非常必要的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值