对象头锁机制原则
Synchronized 的原理是什么
Synchronized 是由JVM实现的一种实现互斥同步的实现方式。如果查看synchronized关键字修饰的字节码,会发现在编译器生成了monitorenter和monitorexit两个字节码指令。
这两个指令的意思就是在虚拟机执行到monitorenter指令的时候,首先尝试获取到对象锁,对象头这部分在对象的最前端,包含两部分或者三部分:Mark Words、Klass Words,如果对象是一个数组,那么还可能包含第三部分:数组的长度。Mark Word需要重点说一下,这里面主要包含对象的哈希值、年龄分代、锁标志位等,大小为32位或64位
如果这个对象没有锁定,或者当前线程已经拥有了这个对象锁,那么就把锁的计数器进行+1操作,当执行monitorexit指令的时候将锁计数器进行-1操作,当计数器为0的时候,锁就被释放了。
如果获取对象失败了,当前线程就要阻塞等待,直到对象锁被另外一个线程释放。
刚刚提到的对象所,这个锁到底是什么?如何确定对象的锁?
锁 的本质是monitorenter和monitorexit 字节码指令的一个Reference类型的参数,也就是是要锁定和解锁的对象。
Synchronized可以修饰不同的对象,所以对应的对象锁可以通过如下的方式确定
- 1、如果Synchronized明确指定了锁定对象,说明加锁对象为该对象。
- 2、如果没有明确指定:
如果Synchronized修饰的方法为非静态方法,表示此方法对应的对象为锁对象;
如果Synchronized修饰的方法为静态方法,则表示这个方法对应的类对象为锁对象。
需要注意的是,当一个对象被锁住的时候,对象里面所有用synchronized修饰的方法都将会被阻塞,而对象里非synchronized修饰方法可以正常被调用,不受到影响。
什么是可重入性,为什么synchronized是可重入锁?
可重入性是锁的基本要求,是为了解决死锁的情况发生。
如图所示,在一个类中同步调用了另一个同步方法,假如synchronized不支持重入,进入method2的时候当前线程获取锁,method2中又执行了method1的时候当前线程又要尝试获取锁,这个时候如果不支持重入,那就要等待释放,所以自己获取锁,自己等释放,就会导致死锁。
对于synchronized来说,重要性是显而易见的。在执行monitorenter指令的时候 ,如果这个对象没有锁定,或者当前线程已经拥有了这对象锁,就把锁的计数器加一,实际上就是通过这种机制来实现锁重入。
JVM对于Java的原生锁做了那些优化
在Java6之前Monitor实现完全依赖底层操作系统的互斥锁来实现,也就是在上面提到的获取/释放锁的逻辑。
由于Java层面的线程与操作系统的原生线程有对应的映射关系,如果要将一个线程进行阻塞或者唤醒都需要操作系统线程的协助,也就是从用户态到内核态的切换,这种相互之间的切换其实是一件非常消耗内存的事情。
所以JDK对于这种操作进行了很多的优化。
使用自旋锁,也就是说把线程进行阻塞操作之前让线程自旋等待一段时间,可能在等待的时候其他线程已经解锁了,这个时候就不需要线程在等待了,避免了用户态到内核态的切换。
在JDK中提供了三种不同的Monitor的实现,也就是三种不同的锁机制。
- 偏向锁
- 轻量级锁
- 重量级锁
这三种锁机制使得JDK对于synchronized的优化进行进一步的提升,当JVM检测到不同的资源竞争状况的时候,会自动切换到合适的锁实现机制,也就是所谓的锁的升级与降级。
当没有出现竞争的时候,默认使用的是偏向锁。
JVM会利用CAS操作,在对象头的MarkWord部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏向锁可以降低无竞争开销。
如果有一个线程试图锁定偏向锁对象的时候,JVM就撤销偏向锁,切换到轻量级锁。
轻量级锁通过CAS操作Mark Word来试图获取锁,如果重试成功,就使用普通的轻量级锁,否则就升级为重量级锁。
为什么说synchronized是非公平锁?
非公平主要表现在获取锁的行为上,并非按照申请锁的时间前后给等待线程分配锁,每当锁被释放之后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是会产生线饥饿现象。
什么是锁消除和锁粗化?
- 锁消除:指虚拟机及时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。开发者怎么会在明知道不会存在数据竞争的情况下使用同步操作呢?很多的操作其实不是开发者自己加入的。
- 锁粗化:原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。锁粗化就是增加锁的作用域。
为什么说 synchronized是一个悲观锁?乐观锁的实现原理又是什么?什么是CAS,它有什么特性?
synchronized是一个悲观锁,因为他的并发策略是悲观的,不管是否会产生竞争,任何的数据都必须要加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。
随着硬件指令集的发展,可以使用基于冲突检测的乐观锁并发策略。先进性操作,如果没有其他线程征用数据,那么就操作成功。
如果共享数据有被用到,产生了冲突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。
乐观锁的核心算法是CAS(Compareand Swap,比较交换),涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等的时候才将内存值修改新值。
这样处理的逻辑是,首先检查某块内存的值是否跟之前读取过的值一样,如果不一样则表示期间此内存值已经被别的线程修改过了,舍弃本次操作,否则则说明在此期间没有其他线程对此内存值进行修改,就可以直接使用该值。
CAS具有原子性,他的原子性有CPU硬件指令实现保证,即使用JNI调用Native方法调用C++编写的硬件指令,JDK中提供了Unsafe类执行这些操作。
乐观锁一定就是好的?
乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它的缺点:
- 1、乐观锁只能保证一个共享变量的原子性操作,如果多一个或者几个变量,乐观锁就有点力不从心了,但互斥锁能轻易地解决该问题,不管对象数量多少以及对象大小。
- 2、长时间自旋操作可能导致开销较大,加入CAS长时间不成功而一直自旋,就会导致CPU消耗过大。
- 3、ABA问题,CAS的核心思想是比较交换,如果在过程中判断逻辑不够严谨,就会导致在一个线程将数据从A改成B,在另一个线程使用的时候,该线程又将数据改回了A,这样的时候对于另一个线程其实并没有感知到B值的存在。其实在整个过程中这个值是被修改过的。这种情况对于运算依赖比较大的场景影响比较大。解决的思路就是引入版本机制。