本篇内容包括:Synchronized 关键字简介、synchronized 的修饰对象、对象的内存布局(64位)、Synchronized 锁升级过程等内容。
Synchronize 翻译成中文:同步,使同步。synchronized:已同步。synchronized 能够保证同一时刻最多只有一个线程执行该段代码,以达到并发安全的效果。也就是说 Synchronized 在某个线程将资源锁住了之后,其他线程只有在当前线程使用完成后,才可以接着使用。
synchronized 是 Java 中解决并发问题的一种最常用也最简单的一种方法。synchronized 的作用主要有三个:
synchronized 可以修饰普通方法,静态方法和代码块。 当synchronized修饰一个方法或者一个代码块的时候,它能够保证在同一时刻最多只有一个线程执行该段代码。
我们知道,静态方法实际不属于类的任何一个对象实例,它是直属于“类”的。如果在静态方法上加上 Synchronized 关键字,那么它锁住的就是这个类
普通方法并不是这个类独有的:创建多少个对象实例,这个方法就会有多少个。那么当 Synchronized 关键字修饰类的普通方法时,它锁住的就是这个类的对象实例
同步代码块锁主要是对代码块进行加锁,此时同一时刻只能有一个线程获取到该资源,要注意每一把锁只负责当前的代码块,其他的代码块不管
因为 Synchronized 都是对对象进行加锁,那我们在了解它的底层实现原理之前,应该了解一下Java对象在内存中的布局,这样比较有利于我们理解,对于一个普通对象来说,它分为四个部分:
synchronized锁有四种状态,无锁,偏向锁,轻量级锁,重量级锁。锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态
jdk1.6之前都是重量级锁,大多数时候是不存在锁竞争的,如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入锁升级。
(线程1)获取锁对象时,会在 Java 对象头和栈帧中记录偏向的锁的 ThreadID,下一次,线程获取该锁时会比较 ThreadID 是否一致:
如果线程1和线程2的执行时间刚好错开,那么锁只会在偏向锁之间切换而不会升级为轻量级锁,在使用Synchronized的情况下避开了获取锁的成本,所以效率和无锁状态非常接近
对象被多个线程竞争(或关闭偏向锁功能)时,锁由偏向锁升级为轻量级锁,其他线程会通过 CAS + 自旋 的形式尝试获取锁(JDK 1.7 之前是普通自旋,会设定一个最大的自旋次数,默认是 10 次,超过这个阈值就停止自旋。JDK 1.7 之后,引入了适应性自旋。简单来说就是:这次自旋获取到锁了,自旋的次数就会增加;这次自旋没拿到锁,自旋的次数就会减少。)
轻量级锁获取过程:
在代码进入同步块的时候,如果同步对象锁状态为无锁状态,轻量级锁会构造一个Lock Record锁记录,用于存储锁对象目前的 Mark-Word 的拷贝
Lock Record是线程私有的数据结构,每一个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每一个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识(或者
object mark word
),表示该锁被这个线程占用。
拷贝成功后,虚拟机将使用 CAS 尝试将对象头的 MarkWord 的 Lock-Word(锁记录指针) 指向当前线程 Lock Record 的起始地址,并将 Lock Record 的 owner 指向对象的 Mark-Word:
00
,表示此对象处于轻量级锁定状态。当线程的自旋后依然没获取到锁或者判定多个线程竞争锁时,为避免CPU无端耗费,锁由轻量级锁升级为重量级锁。
升级为重量级锁时,锁标志的状态值变为 10
,此时 MarkWord 的 Lock-Word 指向重量级锁的指针,获取锁的同时会阻塞其他正在竞争该锁的线程,依赖对象内部的监视器(monitor)实现,monitor 又依赖操作系统底层,需要从用户态切换到内核态,成本非常高。
Synchronized 中对 monitor 锁的实现有用到两个指令: monitorenter 和 monitorexit (可通过 javap -verbose XXX.class 反汇编查看)。
Synchronized 在 JVM 里的实现都是 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的 MonitorEnter 和 MonitorExit 指令来实现,可以把执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁,每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0。
monitorenter:执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:
- 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。
- 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。
- 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。
monitorexit:monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权
再底层的,monitor又依赖操作系统的 MutexLock(互斥锁)来实现的,所以重量级锁也被称为互斥锁。(Mutex 在 Windows 和 Linux中的实现有着显著的区别)