1、Synchronized实现原理依赖于锁机制和内存屏障
从线程安全的三个特性分别对应做了如下的实现:
原子性:锁,其中涉及
锁优化(锁升级(偏向锁、轻量级锁、重量级锁(objectMonitor))、锁粗化、锁消除)
可见性:加了load屏障和store屏障,释放锁flush数据,加锁会refresh数据
有序性:acquire屏障和release屏障,同步代码块内的指令可能重排,但同步代码块内的指令和外面的指令是不会重排的
2、synchronized用的锁是存在Java对象头里的(图可不记)
MarkWord:(存储对象的锁信息和hashcode)(图可不记)
Cms free表示空闲的内存块,无意义
3、synchronized修饰方法和代码块的字节码层面实现区别
从JVM规范中可以了解到,无论是synchronized修饰方法(实例/静态方法)还是代码块都是基于进入(enter)和退出(exit)monitor对象来实现,但是两种修饰方式在字节码层面实现上有着很大区别,
synchronized修饰代码块会在同步代码块之前加monitorenter指令,同时在代码块正常退出和异常退出的地方插入monitorexit指令,从而保证monitorenter和monitorexit的成对执行(保证同步代码块执行结束的同时释放锁资源)
synchronized修饰方法并没有通过插入monitorenter和monitorexit指令来实现,而是在方法的访问标志(flags)设置ACC_SYNCHRONIZED标志来实现。线程在执行方法前先判断flags是否标记ACC_SYNCHRONIZED,如果标记则在执行方法前先去获取monitor对象,获取成功则执行方法代码且执行完毕后释放monitor对象,获取失败则表示monitor对象被其他线程获取从而阻塞当前线程
4、锁优化有三种(jdk1.6):
1、锁升级(锁膨胀):
锁升级(锁膨胀)的优化是针对于不同同步场景进行的优化,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁,存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效的,但是如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。偏向锁和轻量级锁都是在jdk1.6以后对锁机制进行优化时引进的;
偏向锁:
当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,引入偏向锁是为了不存在线程竞争的情况下尽量减少不必要的加解锁。
轻量级锁:
markword中保存指向栈中锁记录的指针;
当一个线程获取到该锁后,另一个线程也来获取该锁,这个线程并不会被直接阻塞,而是通过自旋来等待该锁被释放。自旋就是让线程执行一段无意义的循环。分为自适应自旋锁和固定次数自旋锁,前者是自旋次数位动态的,JVM通过之前这把锁的获得情况来自动的选择增加或者减少自旋次数直至阻塞。后者即固定次数自旋,超过则阻塞。
自旋锁设计的原因:Java的线程是映射到操作系统原生线程之上的,阻塞或唤醒线程要操作系统从用户态和核心态之间切换, 线程刚刚进入阻塞状态,这个锁就被其他线程释放了,则需要操作系统来唤醒,浪费资源。
轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换ThreadID 的时候依赖一次 CAS 原子指令。
重量级锁:由对象内置锁ObjectMonitor实现:
Mark Word中保存了指向重量级锁的指针;
objectMonitor流程:
1、所有期待获得锁的线程,在锁已经被其它线程拥有的时候,这些期待获得锁的线程就进入了对象锁的entry set区域(监控区)。
2、所有曾经获得过锁,但是由于其它必要条件不满足而需要wait的时候,线程就进入了对象锁的wait set区域(待授权区) 。
3、在wait set区域的线程获得Notify/notifyAll通知的时候,随机的一个Thread(Notify)或者是全部的Thread(NotifyALL)从对象锁的wait set区域进入了entry set中。
4、在当前拥有锁的线程释放掉锁的时候,处于该对象锁的entryset区域的线程都会抢占该锁,但是只能有任意的一个Thread能取得该锁,而其他线程依然在entry set中等待下次来抢占到锁之后再执行。
2、锁粗化:
因加锁解锁也需要消耗资源,锁粗化就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁操作。
3、锁消除:
Java虚拟机在JIT编译时(代码第一次编译时), 通过对运行上下文的扫描,经过逃逸分析,去除不可能存在共享资源竞争的锁.
逃逸分析:当一个对象在方法中被定义后,他被外部方法所引用,则认为发生了逃逸。
5、有序性和可见性的保证
我们按可见性分类,把屏障可以分为两类:加载屏障(Load Barrier)和存储屏障(Store Barrier)。
加载屏障:StoreLoad屏障可充当加载屏障,刷新处理器缓存,flush当读取数据时强制将无效队列中invalidate message刷新到高速缓存标记数据为无效重新从其他处理器高速缓存和主存中读取
存储屏障:StoreLoad屏障也可充当存储屏障,冲刷处理器缓存。reflush:当写数据时强制将写缓冲区的数据写入到高速缓存中
JVM会在MonitorEnter(申请锁)对应的机器码指令后面临界区代码开始之前插入Load Barrier,以达到临界区内部使用的共享变量都是新值。同样,会在MonitorExit(释放锁)对应的指令后插入Store Barrier,以达到临界区对共享变量的更改及时写回主存。
再按有序性划分:分为获取屏障(Acquire Barrier)和释放屏障(Release Barrier)。
获取屏障:相当于Load Load & Load Store,在一个读操作之后插入一个屏障,禁止与之后的任何读写操作重排。
释放屏障:相当于Load Store & Store Store,在一个写操作之前插入一个屏障,禁止与其前面任何读写操作重排。
JVM会在MonitorEnter(申请锁)对应的机器码指令后面临界区代码开始之前插入Acquire Barrier,并在临界区之后MonitorExit之前插入Release Barrier。