synchronized底层原理详解#
一、特性##
-
原子性:操作整体要么全部完成,要么全部未完成。就是为了保证数据一致,线程安全。
-
有序性:程序的执行顺序按照代码的顺序执行。一般情况下,虚拟机为了提高执行效率,会对代码进行指令重排序,运行的顺序可能和代码的顺序不一致,结果不变。单线程不会出现问题,多线程有可能出现问题。
深入理解Java虚拟机中有这么一句话:
Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”( Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象
理解:
怎么在一个线程中?
用户的指令都是在线程中执行的。指令执行在哪个线程里,就是“在”哪一个线程。
什么叫观察?
线程的运行通常没有办法直接观察到,一般只能观察到线程执行的后果,比如内存的改变。于是观察,多数情况下是指去读取被修改了内存的值。
在一个线程中观察另一个
在一个线程中执行的指令,去读取另一个线程修改的变量的值。
所谓无序,就是说读取线程中读取到的变量值发生改变的顺序,和修改线程中修改变量的顺序,不一定一致。 -
可见性:变量的修改对所有的线程都是可见的。
理解:
synchronized对一个对象加锁,这时其他线程都无法操作。当前线程释放锁之后,其他线程才能获取到synchronized里的内容。
二、synchronized和ReentrantLock的区别##
- 从使用来说,synchronized是Java的关键字,对象只有在同步块或者同步方法中才能调用wait/notify方法。ReentrantLock是JDK1.5之后提供的API层面的锁,需要主动创建,配合condition的await/signal使用
- ReentrantLock比较灵活,可以尝试获取锁、可以锁多个条件、可以中断等
- ReentrantLock必须手动使用调用获取锁和释放锁的方法,synchronized由系统调用
- ReentrantLock只能用于锁代码块,而synchronized可以修饰静态方法、实例方法和代码块
- 从性能上说,ReentrantLock略高于synchronized.JDK6及之后,synchronized被优化为无锁、偏向锁、轻量级锁、重量级锁和GC标记等状态,在升级为重量级锁之前,性能还是很好地。
- synchronized是悲观锁、可重入锁、非公平锁,ReentrantLock是乐观锁、可重入锁,可设置为公平锁或非公平锁。
- 从锁的对象来说,synchronized锁的是对象,ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。
- 从锁的实现来说,synchronized是在软件层面依赖JVM实现,而j.u.c.Lock是在硬件层面依赖特殊的CPU指令实现。
三、底层原理
-
synchronized不论是修饰静态方法、实例方法或者是代码块,最后锁住的要么是实例化后的对象,要么是一个类。对于修饰一个(静态/实例)方法时,JVM会在字节码层面给该方法打上一个ACC_SYNCHRONIZE标识,当有线程访问这个方法时,都会尝试去获取对象的objectMonitor对象锁,得到锁的线程才能继续访问该方法。修饰代码块时,JVM会在字节码层面给方法块入口处加monitorenter,出口处添加monitorexit标识,一般出口有两个,正常出口和异常出口,所以一般1个monitorenter对应2个monitorexit。线程执行到monitorenter处就需要尝试获取objectMonitor对象锁,获取不到就会一直阻塞,获取到了才能继续运行。
ObjectMonitor() { _header = NULL; _count = 0; // 记录个数 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
-
在JDK6之后,锁被优化为无锁、偏向锁、轻量级锁和重量级锁。在编译过程中有锁粗化,锁消除,在运行时有锁升级。
- 锁粗化:如果虚拟机探测到有一系列的连续操作都对同一个对象加锁,甚至加锁操作出现在循环中,那么将会把加锁同步范围扩展到整个操作的外部,这就是锁粗化。
- 锁消除:经过逃逸分析后,发现同步代码块不可能存在共享数据竞争的情况,那么就会将锁消除。逃逸分析,主要是分析对象的动态作用范围,比如在一个方法里一个对象创建后,在调用外部方法时,该对象作为参数传递到其他方法中,成为方法逃逸;当被其他线程访问,如赋值给其他线程中的实例变量,则成为线程逃逸。
- 锁升级:JD6之后分为无锁,偏向锁,轻量级锁,重量级锁。其中偏向锁->轻量级锁->重量级锁的升级过程不可逆。
一句话概括偏向锁、轻量级锁、重量级锁
偏向锁:当一个线程第一次获取到锁之后,再次申请就可以直接取到锁
轻量级锁:没有多线程竞争,但有多个线程交替执行
重量级锁:有多线程竞争,线程获取不到锁进入阻塞状态
Java对象的内存结构在64位操作系统中,占16个字节:分为对象头、实例数据、对齐填充
对象头占12个字节,实例数据+对齐填充占4个字节,实例数据如果不足4个字节,才会有对齐填充
对象头分markword和classAddressMethod,其中markword占8个字节
锁升级过程:
无锁升级为偏向锁
- 线程访问同步代码块,判断锁标识位(01)
- 判断是否偏向锁
- 否,CAS操作替换线程ID
- 成功,获得偏向锁
偏向锁升级为轻量级锁
- 线程访问同步代码块,判断锁标识位(01)
- 判断是否偏向锁
- 是,检查对象头的markword中记录的是否是当前线程ID
- 是,获得偏向锁
- 不是,CAS操作替换线程ID
- 成功,获取偏向锁
- 失败,线程进入阻塞状态,等待原持有线程到达安全点
- 原持有线程到达安全点,检查线程状态
- 已退出同步代码块,释放偏向锁
- 未退出代码块,升级为偏向锁,在原持有线程的栈中分配lock record(锁记录),拷贝对象头中的markword到lock record中,对象头中的markword修改为指向线程中锁记录的指针,升级成功
- 唤醒线程继续执行
轻量级锁升级为重量级锁
- 线程访问同步代码块,判断锁标识位(00)
- 判断是否轻量级锁
- 是,当前线程的栈中分配lock record
- 拷贝对象头中的markword到lock record中
- CAS操作尝试获取将对象头中的锁记录指针指向当前线程的锁记录
- 成功,当前线程得到轻量级锁
- 执行代码块
- 开始轻量级锁解锁
- CAS操作,判断对象头的锁记录指针是否仍指向当前线程锁记录,拷贝在当前线程锁记录的mark word信息与当前线程的锁记录指针是否一致
- 两个条件都一致,释放锁
- 不一致,释放锁(锁已经升级为重量级锁了),唤醒其他线程
- 5失败,自旋尝试5、
- 自旋过程中成功了,执行6,7,8,9,10,11
- 自旋一定次数仍然失败,升级为重量级锁