Java多线程 synchronized关键字

Synchronized原理分析

加锁和释放锁的原理

.class文件中的Monitorenter和Monitorexit指令,让锁对象的计数器加1或者减1。

monitorenter指令:线程尝试获得锁时,会发生如下3中情况之一:

  • 锁对象的计数器为0,说明该锁还没有被获得,那这个线程就会立刻获得然后把锁计数器+1;
  • 若当前线程已获取了该锁,又重入了这把锁,那锁计数器就会累加,而不会重复获取锁,不会执行monitorenter指令
  • 若锁对象的计数器不为0,说明已有线程获取该锁对象,需要等待

monitorexit指令:释放锁,即将锁对象的计数器减1,如果减完以后,计数器不是0,则当前线程重入了这把锁,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。

线程对Object的访问,首先要先获得Object的监视器。如果获取失败了,线程进入同步队列阻塞队列),线程状态变为BLOCKED。当访问Object的线程(获得了所的线程)释放了锁,则该释放操作唤醒在同步队列中的线程,使其重新尝试对监视器的获取。

 

Synchronied同步锁,一共有四种状态:无锁偏向锁轻量级所重量级锁,它会随着竞争情况逐渐升级。锁可以升级但是不可以降级

 

 

JVM中锁的优化

简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用Mutex Lock那么将严重的影响程序的性能。不过在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销

锁粗化(Lock Coarsening):也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。

锁消除(Lock Elimination):通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。

轻量级锁(Lightweight Locking):这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒(具体处理步骤下面详细讨论)。

偏向锁(Biased Locking):是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟。

适应性自旋(Adaptive Spinning):当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态。

 

偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。

轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。

重量级锁:有实际竞争,且锁竞争时间长。

 

重量级锁

监视器锁是通过操作系统中的互斥量(mutex)来实现同步。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换(包括挂起线程和恢复线程)等。


自旋锁

自旋锁的目的是锁的持有时间比较短的情况下,减少线程阻塞造成的线程切换

如果锁的持有时间比较短,可能比线程切换的时间还短,因此不阻塞而是空循环。

步骤:当线程竞争锁失败时,不直接阻塞自己,而是自旋(空等待,比如一个空的有限for循环)一会,在自旋的同时重新竞争锁。如果在自旋过程中获得了锁,那么锁获取成功;如果自旋结束仍未获得锁,再阻塞自己。

缺点

  • 如果锁持有时间长,竞争激烈,那么自旋通常不能获得锁,白白浪费了自旋占用的CPU时间。

使用-XX:-UseSpinning参数关闭自旋锁优化;-XX:PreBlockSpin参数修改默认的自旋次数。

 

自适应自旋锁

自适应自旋解决的是“锁竞争时间不确定”的问题

自旋的时间不固定,根据上一次自旋是否成功来调整下一次自旋的时间。

  • 如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。
  • 相反的,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。

缺点

然而,自适应自旋也没能彻底解决该问题,如果默认的自旋次数设置不合理(过高或过低),那么自适应的过程将很难收敛到合适的值

 

轻量级锁

自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。

轻量级锁的目标是,无实际竞争情况下,减少重量级锁的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。

轻量级锁过程:不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新,指向当前线程栈帧中的Lock Record(锁记录),如果更新成功,则说明轻量级锁获取成功,将锁对象的状态改为轻量级锁;如果更新失败,说明已经有线程获得了轻量级锁,发生了锁竞争,因此升级为重量级锁(可以用自旋锁优化,自旋失败后再膨胀为重量级锁)。

缺点

同自旋锁相似:

  • 如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁,那么维持轻量级锁的过程就成了浪费。

 

偏向锁

偏向锁的目标是,无实际竞争且只有一个线程访问同步块的情况下,防止同一个线程反复获取所释放锁

轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。

“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁。以后有线程重复获取该对象锁时,若当前线程等于owner,说明就是同一线程,可以直接获得锁;否则,说明是其他线程在竞争这个对象锁,因此要膨胀为轻量级锁。

偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。



思考

synchronized在jvm的实现原理

synchronized几种加锁方式,加在对象上和加在类上在内存里来看有什么区别

Synchroized 4种锁状态,锁升级的过程

偏向锁、轻量锁、重量锁怎么加锁解锁、怎么转化

synchronized的作用,用法,可以用在哪?
synchronized里用wait会怎样

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值