总述:先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
- ·对于普通同步方法,锁是当前实例对象。
- ·对于静态同步方法,锁是当前类的Class对象。
- ·对于同步方法块,锁是Synchonized括号里配置的对象
一、从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对
象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter
和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有
详细说明。但是,方法的同步同样可以使用这两个指令来实现
二、monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结
束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有
一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter
指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
三、Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在
Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状
态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏
向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高
获得锁和释放锁的效率
四、原子性
原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意
为“不可被中断的一个或一系列操作”。在多处理器上实现原子操作就变得有点复杂。让我们
一起来聊一聊在Intel处理器和Java里是如何实现原子操作的。
4.1处理器如何实现原子操作
(1)使用总线锁保证原子性
(2)使用缓存锁保证原子性
缓存对处理器的影响更小,效率更高,但是有两种情况下处理器不会使用缓存锁定。
第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行
(cache line)时,则处理器会调用总线锁定。
第二种情况是:有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的
内存区域在处理器的缓存行中也会调用总线锁定。
4.2Java如何实现原子操作
(1)使用循环CAS实现原子操作
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本
思路就是循环进行CAS操作直到成功为止,
(2)CAS实现原子操作的三大问题
1)ABA问题:因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化
则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它
的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面
追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从
Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个
类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是
否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值
2)循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
3)只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循
环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子
性,这个时候就可以用锁。
(3)使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁
机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环
CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时
候使用循环CAS释放锁。
参考资料 并发编程的艺术 写的非常好 推荐