线程中并发安全问题
Sychronized关键字的底层原理
sychronized对象锁采用互斥方式让同一时刻至多只有一个线程能持有对象锁,其他线程想获取这个对象锁只能被阻塞。
Monitor
Sychronized的底层实现Monitor。
- WaitSet:关联调用了wait方法的线程,用于存储处于等待状态的线程。
- EntryList:关联了没有获得的线程,用于存储处于Blocked状态的线程。
- Owner:存储当前获取锁的线程,只有一个线程可以获取到(如果Owner为NULL则表示线程可以获取否则就在EntryList中等待,等到当前线程执行完毕,EntryList中的其他线程对锁进行争抢。
Monitor是一个重量级锁(牵扯到一个新的概念:锁升级)
monitor实现锁里面涉及到了内核态和用户态的切换,进程的上下文切换成本较高,性能较低。
所以再JDK1.6之后引入了两种新型的锁机制:轻量级锁和偏向锁,他们的引入分别解决了同步块中代码不存在竞争,在不同线程交替执行同步代码块中代码和不存在竞争这两种因使用传统锁机制产生的性能消耗问题的情况。
所以对象锁是如何关联Monitor的?
对象的内存结构
在HotSpot虚拟机中,对象在内存中存储的分布可分为3个区域:对象头(Header),实例数据(Instance Data),对其填充。
MarkWord
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oIHjZVN0-1689425757766)(D:\线程中并发安全问题\QQ截图20230715201033.png)]
- hashcode:25位的对象标识Hash码
- age:对象分代年龄占4位
- biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁
- thread:持有偏向锁的线程ID,占23位
- epoch:偏向时间戳,占2位
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位
Monitor重量级锁
每个对象都可以关联一个Monitor,如果使用sychronized给对象上锁(重量级)后,该对象头中MarkWord中就被修改为指向Monitor对象的指针。
轻量级锁
在很多情况下,Java中的同步代码块并不会被竞争使用,可能会有多个线程交替执行同步块中的代码,这样的情况没必要使用重量级锁,因此Jvm引入了轻量级锁概念。
当代码执行完成,如果锁记录(Lock Record)为NULL就直接将该锁记录清除,若是不为NULL(也就是原本锁对象CAS过来的MarkWord)此时再进行一次CAS操作将原本的MarkWord交换回来,就代表着解锁成功。
总结:
轻量级锁加锁流程:
- 在线程栈中创建出一条LockRecord锁记录,将其指向锁对象。
- 通过CAS指令,将LockRecord地址存储到锁对象对象头MarkWord中,若对象处于无锁状态,则修改成功,表示当前线程已经获取到了轻量级锁。
- 若是当前线程已经持有该锁,代表这是一次锁重入,设置一个新的LockRecard到线程栈中,设置其第一部分为null,起到一个锁重入计数器的功能。
- 如果CAS修改失败,说明发生了锁竞争,要将其膨胀为重量级锁。
轻量级锁解锁过程:
- 遍历线程栈找到所有的obj字段(也就是Lock Record地址)为当前锁对象的锁记录。
- 若是lock record的Markword为null代表着是一次重入,直接清除当前锁记录。
- 若是不为null则利用cas指令将对象头中的MarkWord交换回来,恢复为无锁状态,若是失败则膨胀为重量级锁。
偏向锁
轻量级锁在没有竞争时,每次重入锁都要进行CAS操作,java6中引入了偏向锁来进一步的优化:只有第一次使用CAS将线程ID设置到对象的MarkWord中,之后每次重入只要发现这个线程ID是自己的就表示没有竞争,不用CAS,以后只要不发生竞争,这个锁对象就归该线程所有。
总结
Java中的Sychronized分为偏向锁,轻量级锁和重量级锁三种形式,分别对应着锁只被一个线程持有,不同线程交替持有,多线程竞争锁。
- 重量级锁:底层使用Monitor实现,里面设计了用户态和内核态的切换,上下文切换,成本高性能低。
- 轻量级锁:线程的加锁时间是分开的,也就是没有竞争,可以使用轻量级锁来优化,轻量级锁修改对象头的锁标志,相对重量级锁性能提升,每次修改都是CAS保证原子性。
- 偏向锁:一段时间内都是同一个线程持有锁,可以使用偏向锁,在第一次获取锁后,只会一次CAS操作,之后该线程获取锁,只需要判断线程ID是否正确,而不是开销较大的CAS操作。