【概述】
- 在Java中,synchronized关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized代码段不被多个线程同时执行。
- synchronized可以修饰类、方法、变量。
- 在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,Java的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized效率低的原因。
- 在java 6之后Java官方对从JVM的层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
【synchronized实现原理】
- synchronized的语义底层是通过监视器锁(monitor)的对象来完成,每个对象有一个monitor。每个synchronized修饰过的代码当它的monitor 被占用时就会处于锁定状态并且尝试获取monitor的所有权,过程如下所示:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为 1,该线程即为 monitor的所有者。
- 如果线程己经占有该monitor,只是重新进入,则进入monitor 的进入数加1。
- 如果其他线程己经占用了monitor,则该线程进入阻塞状态,直到 monitor的进入数为 0,再重新尝试获取 monitor 的所有权。
【为什么采用自旋而不是等待】
- 很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做“忙循环”,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
- 什么是忙循环? 就是用循环让一个线程等待,不像传统方法wait()、sleep()或yield(),它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU 缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。
【synchronize锁升级原理】
- 在锁对象的对象头里面有一个threadId,在第一次访问的时候,threadId为空,jvm让其持有偏向锁,并将threadId设置为其线程 id,再次进入的时候会先判断threadId是否与其线程id一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了synchronized 锁的升级。锁的升级的目的是为了减低了锁带来的性能消耗。在Java 6之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
- 偏向锁:顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程竞争的情况,则线程是不需要触发同步的,减少加锁/ 解锁的一些CAS操作(比如等待队列的一些 CAS操作),这种情况下,就会给线程加一个偏向锁。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
- 轻量级锁:是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁竞争的时候,轻量级锁就会升级为重量级锁。
- 重量级锁:是JVM中最为基础的锁实现。在这种状态下,Java虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。