synchronized2
synchronized2
1. Java的对象头
在JVM中,对象是分成三部分存在的:对象头、实例数据、对齐填充
对象头是我们需要关注的重点,它是synchronized
实现锁的基础,因为synchronized
申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word
和 Class Metadata Address
组成:
Mark Word
:存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address
:是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。
锁也分不同状态,JDK6之前只有两个状态:无锁、有锁(重量级锁),而在JDK6之后对synchronized
进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁。锁的类型和状态在对象头Mark Word
中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word
数据。
2. 锁升级过程
2.1 为什么需要锁升级?
在JDK1.6之前,synchronized
属于重量级锁,重量级锁在用户态发起系统调用,向内核申请mutex
互斥量,这个过程需要发生上下文切换 以及 用户态内核态的切换,基于此实现线程的阻塞与唤醒。而大量的用户态内核态切换是很浪费时间和资源的。
既然线程的阻塞唤醒比较慢,那么在低并发、锁竞争比较少的情况下,就不需要阻塞,那么就不需要用户态内核态的切换,就能减少开销,提高性能;并发量高、锁竞争激烈的情况下,再去阻塞,于是在JDK1.6之后引进了锁升级。所以说,锁升级是为了提高低并发时的性能,毕竟低并发才是常态,高并发只有那么几个时间点会出现。
上面讲到锁有四种状态,并且会因实际情况进行升级,其升级方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且升级方向不可逆
2.2 无锁
无锁状态是对象初始化后的默认锁状态,表示对象当前未被任何线程锁定。在这种状态下,对象头的锁标志位通常为空或特定的无锁标识,表明对象不受任何同步控制,任何线程都能够无障碍地访问该对象。
2.3 偏向锁
何谓“偏向”?就是锁对象会偏向于第一个获得它的线程。
当一个线程访问同步代码块并获取锁时,该锁会进入偏向模式,锁的拥有者被设置为当前线程。当该线程执行完同步代码块后,线程并不会主动释放偏向锁。当线程再次进入同步代码块时,会首先判断此时持有锁的线程与它是否为同一线程,如果是则正常往下执行,由于此前是没有释放锁的,所以这次就不会有任何的获取锁操作。
偏向锁的锁释放是一个被动过程,线程不会主动释放偏向锁。如果有别的线程来竞争偏向锁时,通过CAS操作竞争,竞争成功则更改锁拥有者;否则说明有多线程竞争锁的情况,当到达全局安全点(所有线程会暂停),获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块
所以,引入偏向锁的目的是认为当前环境下是不存在多线程竞争的场景,可以认为是单线程环境,同一个线程多次持有锁,减少单线程环境下获取锁带来的不必要。
2.4 轻量级锁
当一个线程持有偏向锁时且仍处于活动状态时,另外一个线程来竞争锁,这时偏向锁就会升级为轻量级锁。
轻量级锁的竞争方式是一种比较轻量级的竞争方式,当某个线程没有获取到锁,它并不是立刻被阻塞,而是采取CAS+自旋的方式来竞争锁资源。在竞争较少的情况下,轻量级锁通过减少线程阻塞和唤醒操作,可以提高性能。
轻量级锁的目的在于它认为系统当前的竞争环境不是很激烈,如果采取阻塞和唤醒线程的方式,则会过多地消耗系统资源。如果某个线程没有获取到轻量级锁,则采取自旋的方式来判断锁资源是否已被释放。这种方式减少了上下文的切换。
2.5 重量级锁
短时间的自旋性能是不错的,但轻量级锁自旋是要有限度的,不能一直在那里空转,这样也是很消耗CPU资源的,所以如果锁竞争环境比较严重,当自旋次数达到某个阈值(默认 10 次,可自动调整)后 或者 等待轻量级锁的线程很多时,就停止自旋,此时锁升级为重量级锁。当其膨胀为重量级锁后,其他线程就不再是等待了,而是阻塞等待。重量级锁依赖对象内部的监视器(monitor
)实现,而 monitor
依赖的是操作系统的 mutex
原语。
2.6 为什么JDK18中废止了偏向锁
- 性能收益不明显
- 受益于偏向锁的应用程序通常使用了早期的 Java 集合 API,例如 HashTable、Vector,在这些集合类中通过
synchronized
来控制同步,这样在单线程频繁访问时,通过偏向锁会减少同步开销 - 随着 JDK 的发展,出现了 ConcurrentHashMap 高性能的集合类,在集合类内部进行了许多性能优化,此时偏向锁带来的性能收益就不明显了
- 受益于偏向锁的应用程序通常使用了早期的 Java 集合 API,例如 HashTable、Vector,在这些集合类中通过
- JVM内部维护代码成本太高
3. Monitor
重量级锁的实现
每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现,主要数据结构如下)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。
1 |
|
ObjectMonitor中有两个队列,WaitSet 和 EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),owner指向持有ObjectMonitor对象的线程。
当多个线程同时访问一段同步代码时,首先会进入 EntryList 集合,当线程获取到对象的monitor 后把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1,竞争锁失败的线程则留在EntryList中。若线程调用 wait()
方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor并复位变量的值,以便其他线程获取monitor。
后进入EntryList的线程被允许直接尝试获得锁,这说明synchronized
是非公平锁
3.1 为什么需要EntryList、WaitSet两个集合呢?集合里的元素不都是在等待锁吗?
EntryList里的元素是竞争锁失败的线程,这是锁的互斥问题;而WaitSet里的元素是调用wait()
方法主动释放锁并等待唤醒的线程,这是线程通信的问题