synchronized是Java提供的原子性内置锁,这种内置的并且使用者看不到的锁也被称为监视器锁。
synchronized通过在代码块前后加上monitorenter和monitorexit字节码指令用于实现进入和退出。
如果是同步方法,则是打上标记,隐式的使用monitorenter和monitorexit字节码指令。
在jdk1.5之前,加锁和释放锁是由操作系统来实现的,系统调用涉及到上下文切换,性能消耗极高,多以当时synchronized性能极差并且是公认的重量级锁。
在jdk1.6之后,引入了偏向锁和轻量级锁两个概念,通过锁升级策略来解决性能问题。
为了讲清楚Synchronized工作原理,需要理解以下几个问题:
-
程序 = 数据结构 + 算法,锁也是属于一种程序,所以它应该有对应的数据结构 + 算法。
-
Java对象内存布局,即synchronized使用了怎么样的数据结构?
每个Java对象在内存中的布局通常分为三部分:对象头、实例数据、对齐填充。对象头中包含Mark Word和Class Point
重点就是Mark Word,它包含一下信息:
-
哈希码
-
GC标志
-
锁标志位:判断当前对象处于什么锁状态(无锁、偏向锁、轻量级锁、重量级锁)
-
偏向锁的线程ID
-
轻量级锁的栈地址指针
-
重量级锁的Monitor指针
-
-
怎么理解每个对象都有成为锁的潜质?
由于每个对象都有Mark Word,都可以存储锁状态,都是可以支撑锁算法的数据结构,所以每个对象都有成为锁的潜质。
-
对象是怎么通过Mark Word成为一把锁的?即怎么使用这个数据结构标识不同的锁状态?
无锁:当一个对象新建时,没有被任何对象锁定,此时锁标志位01
偏向锁:第一个线程获取对象锁,锁标志位01,偏向标志位标记为1,并在对象头中记录锁的线程ID,如果没有竞争,下次直接根据ID直接获取,即可重入,避免了频繁CAS。
轻量级锁:发现锁竞争,即两个线程以上时,偏向锁取消,升级为轻量级锁,此时线程会在栈帧中创建一个锁记录,并尝试通过CAS将对象头的轻量级锁栈地址指针指向锁记录,成功则获取到了锁。锁记录中记录了重入次数,同时修改锁标志位,记录锁状态。
重量级锁:当竞争激烈,CAS次数超过重试阈值时,JVM决定升级为重量级锁,此时JVM创建Monitor对象或者使用已有的Monitor对象,并将Monitor指针指向Monitor对象,后续交由操作系统进行调度。
Monitor对象
Monitor对象不是原始Java对象,而是JVM内部使用的数据结构。它用于实现重量级锁的管理,通常包含以下字段:
- Owner:当前持有锁的线程。
- Recursion Count:重入计数器,记录当前线程的重入次数。
- Entry List:等待获取锁的线程队列。
- Wait Set:等待某个条件的线程队列(通常用于
wait
和notify
机制)。
线程进入同步代码块时,首先进入entryList,获取到锁后就更改Owner,并且计数器加1。
如果线程wait了,就会释放锁,释放锁之后就进入Wait Set,只有通过notify或者notifyAll才能唤醒,唤醒之后又进入entryList排队竞争锁。
获取到锁的线程执行完毕就释放锁,计数器减1,当前线程Owner置Null