先谈谈面试题
1. 谈谈你对Synchronized的理解;
2. Synchronized的锁升级你聊聊;
3. Synchronized的底层是如何是如何实现的,实现同步的时候用到CAS了吗?具体哪里用到了?
4. Synchronized实现原理?monitor对象是什么时候生成的?
5. 偏向锁和轻量锁有什么区别,详细说一下?
synchronized
在早期的版本中 synchronized 的实现我们采用的重量级锁(互斥锁 / 悲观锁),性能较低 因为它涉及到用户态和内核态的转换。
Java的线程是基于原生的操作系统上的,因此阻塞和唤醒都需要操作系统介入,需要用户态到内核态的一个转换,因为用户态和内核态都有各自的一个内存空间,所以在转换时 需要将其变量等一些列参数传递给内核以便于切换回用户态时正常的运行。
在早期版本中monitor(监视器锁)是依赖于操作系统的Mutex Lock实现的,线程的挂起和恢复都需要在内核态中完成,这种切换时间成本较高,效率低下!
内核态与用户态的转换
用户态:应用程度运行时所处的模****式;
内核态:操作系统内核运行时的模式;
用户态到内核态的切换是指从应用程序运行在用户态切换到操作系统内核运行在内核态的过程。在用户态下,应用程序只能访问有限的资源和执行受限的操作,而在内核态下,操作系统内核可以访问系统的所有资源和执行特权操作,如访问硬件设备、进行内存管理等。
重量级锁的实现依赖于操作系统底层的同步机制,会涉及到用户态到内核态的转换;
例如:用户态到内核态的切换:
线程获取锁时,发现锁被占用从而进入阻塞状态;在这个过程中发现锁被占用时需要将这个线程暂停 并加入阻塞队列, 然后切换到其他线程; 这个操作就需要切换到内核态去执行;
当锁被释放时,需要将其切换到内核态从队列中取出线程唤醒后并进行调度;
内核态到用户态的切换:
锁竞争:锁被其他线程释放时 会先切换到内核态将其唤醒,然后切换到用户态执行;
注意:状态模式的转换是以线程为基础的;
Monitor(监视器锁)
- monitor是一种同步机制(或同步工具),在Java的设计中每个对象都有成为monitor的潜质;
- jdk6之前monitor的生命周期与Java对象一样 随着Java对象的创建而创建 销毁同理,在jdk6之后由于加入了偏向锁和轻量锁则monitor对象的创建会延迟;
- jdk6之前在Java对象创建时(synchronized修饰的对象)会创建一个对应的监视器对象(monitor);
- jdk6之后只有在为重量级锁时才会创建并与锁对象关联 将其记录到Mark Word对象头中;
- 对于普通的Java对象在创建时并不会创建一个与之对应的监视器对象;
- 它的本质是基于操作系统的Mutex Lock实现的,而基于这种方式需要内核态的切换 导致时间成本提高!
注意:
- 在jdk中由于监视器对象随着Java对象的创建而创建,这里并不是对象创建后立即创建;
monitor如何与Java对象关联?
- 如果一个对象被线程锁住,这个对象的Mack Word的Lock word字段指向monitor的起始地址;
- monitor的Owner存放拥有相关联对象锁的线程id;
锁的升级过程
synchronized的升级步骤
从Java6开始为了减轻锁和所释放带来的开销,引入了轻量锁和偏向锁;
不会刚开始直接就重量级锁!
synchronized的锁升级主要依赖与Java对象中的Make word中的锁标志位和偏向锁标志位
无锁状态
偏向锁
在实际应用运行过程中发现,锁总是同一个线程持有,很少发生竞争,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。
那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁)。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
实现原理
一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还会有占用前54位来存储线程指针作为标识(通过CAS方式修改markword中的线程ID和偏向锁状态位)。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的Mark Word 中去判断一下是否有偏向锁指向本身的ID,无需再进入 Monitor 去竞争对象了。
偏向锁无需通过操作系统,所以不会去切换到内核态,从而Jvm无需与操作系统协商,且线程不会主动去释放锁,在jdk6以后偏向锁是默认开启的;
理论落地注意事项:
- 在jdk6以后偏向锁是默认开启的,但是会有4秒的延迟(在睡眠后 new 对象,否则无效!)
- -XX:BiasedLockingStartupDelay=0 关闭延迟;
- -XX:-UseBiasedLocking 关闭偏向锁;
- 普通的对象(没有使用锁的对象)默认4秒后锁的位置也会是偏向锁;
- 作为锁的对象会同时去记录锁标识和线程ID;
偏向锁的撤销
偏向锁是只有等到其他线程竞争的时候才会释放锁的一种机制,当有其他线程竞争时会将撤销持有偏向锁的线程;
偏向锁的撤销时会判断是否处于全局安全点(当前时间没有字节码正在执行),当处于全局安全点的时候才会去撤销偏向锁;
撤销步骤
- 当前线程正在执行synchronized中的同步代码,另一个线程竞争,此时当前线程并没有执行完当前的代码块,这时会撤销偏向锁并出现锁升级;升级为轻量级锁,并由原持有偏向锁的线程持有,竞争的线程自旋等待获取锁;
- 当前线程已执行玩synchronized中的同步代码,这是会将Make word中的锁标识置为无锁状态,并重新偏向;
偏向锁相较于重量级锁减少的开销:
- 线程阻塞和上下文切换开销
- 竞争CAS更新对象头失败后达到安全点如果当前偏向线程已退出同步代码,那么这时会重新偏向,但如果使用重量级锁这是就需要上下文的切换阻塞,涉及到切换到内核态去执行;
- 锁状态的更新和检查开销
- 重量级锁,在没有竞争的情况下每次执行代码块都需要去检查和更新monitor(锁);
- 锁的内存占用开销
- 重量级锁需要维护对象头中储存的额外信息;
轻量锁(自旋锁)
轻量级锁就是在线程近乎交替执行的情况下提高性能,通过CAS来减少重量级锁使用操作系统而产生的性能开销
升级轻量锁的时机
在当前线程获取偏向锁时,竞争线程发现Make word中的线程ID不是自己会尝试通过CAS获取锁,如果成功则会重新偏向竞争线程(这是还是偏向锁)如果失败(在全局安全点时当前线程还在执行同步代码)则升级为轻量锁,并将Make word中的Lock Record执行当前线程栈;
扩展(Displaced Mark Word(DMW))
JVM会在锁膨胀(轻量级锁升级重量级锁的时候)和锁撤销(偏向锁)的时候创建Displaced Mark Word
当锁对象为轻量级锁或重量级锁的时Mark word中储存的为指针信息,这是需要将原始的mark word信息保存到每个线程的栈帧或者重量级锁的objectMonitor类中
加锁
当一个线程获取锁时发现时轻量级锁时,会将线程的mark word信息复制到自己的线程栈中(Displaced Mark Word)然后_通过CAS操作将mark word中的信息替换为指向锁记录的指针(获取锁)_,如果成功 则成功获取了锁,如果失败,则表示锁被其他线程获取,会自旋尝试获取锁;
释放锁
当前线程将Displaced Mark Word中的值通过CAS复制到mark word里,如果这期间因为其他线程自旋多次导致锁升级为重量级锁,必然会复制失败,此时会释放锁并唤醒被阻塞的线程;
自旋的次数
- 在jdk6之前默认启用,默认情况下自旋的次数是 10 次 -XX:PreBlockSpin=10来修改或者自旋线程数超过cpu核数一半
- 在jdk6之后 自适应意味着自旋的次数不是固定不变的而是根据:同一个锁上一次自旋的时间和拥有锁线程的状态来决定。
重锁
有大量线程参与锁的竞争,冲突高!
总结
锁 | 优点 | 缺点 | 场景 |
---|---|---|---|
偏向锁 | 加锁和不加锁几乎不需要额外的开销; | 如果线程有竞争,会增加锁撤销的开销 | 适合单线程的情况; |
轻量锁 | 线程不会阻塞,提高响应速度 | 线程始终得不到锁,cpu会一直空转 | 适合竞争不激烈的情况,线程近乎交替执行提高响应速度; |
重量锁 | 线程不会自旋,不会消耗cpu | 响应慢,基于操作系统,性能开销大 | 高竞争的情况 |
- 实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式
- synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MarkWord来实现的。
- JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不是无论什么情况都使用重量级锁。
- 偏向锁: 适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
- 轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似), 存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
- 重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。
注意事项:
如果锁对象(无锁)在进入同步代码前调用hashcode方法 则直接升级为轻量级锁(如果对象计算过hashcode则无法在作为偏向锁,因为Make word中偏向锁记录线程ID和hashcode的标志位冲突,这样就会导致两次的计算结果不一致的情况)如果锁对象已经是偏向锁 则直接升级为重量级锁,在重量锁的实现中,ObjectMonitor类里有字段可以记录非加锁状态下的mark word,其中可以存储identity hash code的值。简单说就是重量锁可以存下identity hash code。