synchronized
一、synchronized用法
- volatile关键字可以保证共享变量的可见性和有序性,但并不能保证原子性。
- 既保证共享变量的可见性和有序性,又保证原子性,synchronized关键字。
- 作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。
- 修饰 实例方法、静态方法、代码块
public synchronized void add(){...}
public static synchronized void add(){...}
public void add() { synchronized (this) {...} }
二、Java对象头与Monitor对象
对象在JVM内存中的存储分为三个区域:对象头、实例数据以及填充数据。
对象头: 是实现synchronized的锁对象的基础。
实例数据: 存放类的属性数据信息,包括父类的属性信息,这部分内存按4字节对齐。
填充数据: 由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
2.1 对象头
- 以Hotspot虚拟机为例,对象头主要包括三部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)和数组长度(只有数组对象有)。
- JVM中采用2个字来存储对象头,如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度。
- Mark Word 用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
- Klass Point 是对象指向它的类元数据的指针,来确定这个对象是哪个类的实例。
- 数组长度是数组对象特有的部分,该数据在32位和64位JVM中长度都是32bit。
2.2 Mark Word(标记字段)
- Mark Word 用于存储对象自身的运行时数据,同时记录了对象和锁有关的信息。
- Mark Word在不同的锁状态下存储的内容不同:

(1)当没有被当成锁时,就是普通的对象,锁标志位是01,是否偏向锁那一位是0。
(2)当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1。
(3)当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,线程id相同,表示线程A已经获得了这个偏向锁。
(4)当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,线程B会先用CAS操作试图获得锁,成功改为线程B的id,抢锁失败,则继续执行步骤5。
(5)偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。抢到了同步锁,就把锁标志位改成00。
(6)轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。
(7)自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
2.3 CAS 操作
CAS(Compare And Swap)操作,也叫做比较与交换,是一种无锁原子算法。
- 使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。
- CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。
- 出现冲突就重试当前操作直到没有冲突为止。
三、monitor对象
- 重量级锁synchronized的标识位是10,Mark Word存储了指向堆中的Monitor对象的指针。
- monitor对象,也称为管程或监视器锁。
- 每一个对象实例都会关联一个Monitor对象。
- 这个Monitor对象既可以与对象一起创建销毁,也可以在线程试图获取对象锁时自动生成。
- 当这个Monitor对象被线程持有后,它便处于锁定状态。
- 过程:
(1) 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
(2) 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
(3) 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
四、等待/唤醒 可重入
- 等待唤醒机制主要指的是notify/notifyAll和wait方法。
- 调用这几个方法前必须拿到当前对象的监视器monitor对象。
- 在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常。
- synchronized的可重入性:
(1) 从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态。
(2) 但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。
(3) synchronized是基于原子性的内部锁机制,是可重入的。
(4) 在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。
五、JVM对synchronized的优化
- 在Java早期版本中,synchronized属于重量级锁,效率低下。因为在实现上,JVM会阻塞未获取到锁的线程,直到锁被释放的时候才唤醒这些线程。
- 阻塞和唤醒操作是依赖操作系统来完成的,需要从用户态切换到内核态,开销很大。
- Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁。
- 锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
- 随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级到重量级锁,但是锁的升级是单向的,只能从低到高升级,不会出现锁的降级。
5.1偏向锁
- 在大多数情况下锁不仅不存在多线程竞争关系,而且大多数情况都是被同一线程多次获得。为了减少同一线程获取锁的代价而引入了偏向锁的概念。
- 如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构。
- 当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提升了程序的性能。
- 对于锁竞争比较激烈的情况,会升级为轻量级锁。
5.2轻量级锁
- 对于大部分的锁,在整个同步生命周期内都不存在竞争。 适应线程交替执行同步块的的场景.
- 如果存在同一时间访问同一锁的场合,就会导致轻量级锁就会失效,进而膨胀为重量级锁。
5.3自旋锁
- 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
- 基于在大多数情况下,线程持有锁的时间都不会太长。
- 自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),不断的尝试获取锁。
参考
https://blog.csdn.net/u012723673/article/details/102681942
https://zhangpan.site/2021/06/14/39-synchronized/