菜单 学习猿地 - LMONKEY

VIP

开通学习猿地VIP

尊享10项VIP特权 持续新增

知识通关挑战

打卡带练!告别无效练习

接私单赚外块

VIP优先接,累计金额超百万

学习猿地私房课免费学

大厂实战课仅对VIP开放

你的一对一导师

每月可免费咨询大牛30次

领取更多软件工程师实用特权

入驻
436
0

synchronized

原创
05/13 14:22
阅读数 37132

java中主要有两种加锁机制

(1)sychronized
(2)lock

两者加锁的原理不同

synchronized使用了一对字节码指令monitorenter和monitorexit来实现
lock使用了java代码和底层调用实现

synchronized锁的状态:

无锁状态
偏向锁状态
轻量级锁状态
重量级锁状态
四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,轻量级锁升级到重量级锁时还用到了自旋锁

为什么会有上述4种状态?

一开始只有重量级锁,但是使用重量级锁,每次都需要线程的阻塞,线程的阻塞又会消耗很大操作系统资源,为了节省系统资源所以设计了其他几种锁

阻塞代价

java线程是映射到操作系统线程的,每次线程的阻塞和挂起都需要操作系统的配合,
都需要在用户态和内核态之间切换,会消耗大量的系统资源

自旋锁

线程在获取锁时发现此锁已被其他线程占用,
此时线程不直接进入阻塞状态,而是执行一定次数的空循环(等一等),等持有锁的线程释放锁,然后尝试获取锁
这样就避免用户线程和内核的切换的消耗

自旋锁的优点

自旋锁尽可能的减少线程的阻塞,
这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!

自旋锁的缺点

如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的CPU时间。
这通常发生在锁持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁。

偏向锁

适用于大多数情况都是同一线程进入同步代码块的情况
偏向锁会偏向上一次使用该锁的线程,如果还是上一次的线程进入该同步代码块,
则会省去加锁过程,也会省去cas过程,
如果该线程在持有此锁的过程中遇到了其他线程的争用,
则该线程会挂起,并释放偏向锁,然后升级为轻量级锁

偏向锁优点

加锁和解锁不需要额外的消耗

偏向锁缺点

如果线程间存在锁竞争,会带来额外的锁撤销的消耗

轻量级锁

轻量级锁是由偏向所升级来的
一个线程在持有偏向锁运行在同步代码块的情况下,当其他线程也要取得该对象锁时,偏向锁就会升级为轻量级锁;

轻量级锁优点

竞争的线程不会阻塞,提高了程序的响应速度

轻量级锁缺点

如果始终得不到锁竞争的线程使用自旋会消耗CPU

重量级锁

重量级锁由轻量级锁升级而来
一个线程在持有轻量级锁运行在同步代码块的情况下,当其他线程通过自旋也未获得锁时,轻量级锁就会升级为重量级锁
重量级锁会造成线程阻塞,内核态与用户态切换

重量级锁优点

线程竞争不使用自旋,节省了自旋消耗的CPU

重量级锁缺点

线程阻塞,消耗系统资源,响应时间缓慢

偏向锁、轻量级锁、重量级锁适用于不同的并发场景:

偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
重量级锁:有实际竞争,且锁竞争时间长。

java对象头

在32位系统中,一个word占4bytes
在64位系统钟,一个word占8bytes	
每一个java对象都至少占两个word(数组占3个word)
其中第一个word被称为markword,包含了多种不同的信息,其中就包含对象锁相关的信息
第二个word是一个指针,指向该对象类信息的指针

cas指令

cas指令是cpu层级的原子性指令
cas有三个参数(目标地址/值1/值2)
该指令会比较目标地址的内容和值1是否相等,如果相等,就是用值2替换目标地址的内容

程序进入同步代码块时

	判断当前锁状态
	if(无锁){
		使用CAS加锁
		如果 CAS 操作成功, 则认为已经获取到该对象的偏向锁, 执行同步块代码
		如果 CAS 操作失败, 则说明, 有另外一个线程 Thread B 抢先获取了偏向锁。 这种状态说明该对象的竞争比较激烈, 此时需要撤销 Thread B 获                得的偏向锁,将 Thread B 持有的锁升级为轻量级锁。 该操作需要等待全局安全点 JVM safepoint ( 此时间点, 没有线程在执行字节码) 。
	}else if(偏向锁){
		则检测 MarkWord 中存储的 thread ID 是否等于当前 thread ID 。
		如果相等, 则证明本线程已经获取到偏向锁, 可以直接继续执行同步代码块
		如果不等, 则证明该对象目前偏向于其他线程, 若偏向的线程已退出同步代码块,则CAS加锁,否则升级为轻量级锁
	}else if(轻量级锁){
		使用CAS加锁
		如果 CAS 操作成功,则认为已经获取到该对象的轻量级锁, 执行同步块代码
		如果 CAS 操作失败,自旋获取轻量级锁,通过自旋一定次数,如果获取到了锁,执行同步代码块,否则,升级为重量级锁                 
	}else if(重量级锁){
		则检测 MarkWord 中存储的 thread ID 是否等于当前 thread ID 。
		如果相等, 则证明本线程已经获取到偏向锁, 可以直接继续执行同步代码块
		如果不等, 则证明该对象目前偏向于其他线程, 需要撤销偏向锁
	}

偏向锁的获取过程

线程进入同步代码块之前,发现当前锁为偏向锁,
查看偏向锁中的信息,看是否是偏向本线程,如果是,则直接执行同步代码块中的程序,如果不是,则继续向下走
使用cas尝试修改mark word中的信息,让偏向锁偏向本线程
若修改成功,则直接执行程序,若修改失败,则升级为轻量级锁

偏向锁的撤销

通过 MarkWord 中已经存在的 Thread Id 找到成功获取了偏向锁的那个线程, 
然后在该线程的栈帧中补充上轻量级加锁, 会保存的锁记录(Lock Record),
然后将被获取了偏向锁对象的 MarkWord 更新为指向这条锁记录的指针

偏向锁的撤销结果有可能是两个

第一/无状态锁	如果之前持有偏向锁的线程已经退出同步代码块
第二/轻量级锁	如果之前持有偏向锁的线程还未退出同步代码块

锁优化——减少锁的时间

不需要同步执行的代码,能不放在同步块里面执行就不要放在同步块内,可以让锁尽快释放;

锁优化——减少锁的粒度

它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁竞争。它的思想也是用空间来换时间;
ConcurrentHashMap
LongAdder
LinkedBlockingQueue

锁的使用情况

在几乎无竞争的条件下, 会使用偏向锁, 在轻度竞争的条件下, 会由偏向锁升级为轻量级锁, 在重度竞争的情况下, 会升级到重量级锁。

锁的升级过程

一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是无状态的。
当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁,偏向第一个线程。
这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的线程ID,
之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。

一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象是偏向状态,这时表明在这个对象上已经存在竞争了,
检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,
如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,
(偏向锁就是这个时候升级为轻量级锁的)。如果不需要持有偏向锁,则可以将对象回复成无锁状态,然后重新偏向。
轻量级锁认为竞争存在,但是竞争的程度很轻,可以稍微等待一下(自旋),另一个线程就会释放锁。 
但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,
重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

发表评论

0/200
436 点赞
0 评论
收藏
为你推荐 换一批