synchronized
1:锁作用在不同的位置,锁的对象不同
a) 对于同步方法,锁是当前实例对象。
b) 对于静态同步方法,锁是当前对象的Class对象。
c) 对于同步方法块,锁是synchonized括号里配置的对象。
如下图:
解析成字节码指令:
结论:同步方法和静态同步方法:依靠的是方法修饰符上的ACC_SYNCHRONIZED实现
a)方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置;
b)如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor;
c)在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
同步代码块:使用monitorenter和monitorexit指令实现的,
a) 会在同步块的区域通过监听器对象去获取锁和释放锁,从而在字节码层面来控制同步scope.
b) monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处(类似
try...finally...), JVM要保证每个 monitorenter必须有对应的monitorexit与之配对。
c ) 任何对象都有一个 monitor 与之关联,当且一个monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指 令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。
2:同步原理
2.1:锁的信息存储在对象头中,对象头主要分为两个部分:
1:"Klass Pointer":对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
(数组,对象头中还须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。)
2:"Mark Word":
a)用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏 向线程ID、偏向时间戳等等。
b)64位JVM下Mark Word大小的64位的,32位JVM下Mark Word大小的32位的。
c)在运行期间随着锁标志位的变化存储的数据也会变化,具体有如下几种运行期间(32位为例),锁标志位 和 是否偏向锁 确定唯一的锁状态:
2.2:Monitor
Monitor是 synchronized 重量级锁的实现关键。锁的标识位为 10 。
Monitor是线程私有的数据结构,每一个对象都有一个monitor与之关联。每一个线程都有一个可用monitor record列表(当前线 程对象monitor),同时还有一个全局可用列表(全局对象monitor)。每一个被锁住的对象,都会和一个monitor关联。
当一个monitor被某个线程持有后,它便处于锁定状态。此时,对象头中 MarkWord的指向互斥量的指针,就是指向锁对象的monitor起始地址。
monitor是由 ObjectMonitor 实现的,其主要数据结构如下:
》》EntryList 和 _WaitSet :用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象)
》》_owner :指向持有 objectMonitor的线程。
根据虚拟机规范的要求,在执行monitorenter指令时,会尝试获取对象的锁。如果对象没有被锁定(获取锁),获取对象已经被该线程锁定(锁重入)。则把计数器加1(_count 加1)。相应的,在执行monitorexit指令时,会讲计数器减1。当计数器为0时,_owner指向Null,锁就被释放。(摘自《深入理解JAVA虚拟机》)
当多个线程同时访问一个同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor后,会进入_owner 区域,然后把monitor中的 _owner 变量修改为当前线程,同时monitor中的计数器_count 会加1。
如果线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null,_count变量减1,同时该线程进入_WaitSet等待被唤醒。(wait()和sleep()的区别,wait是Object的,并且wait会释放锁。)
由此看来 monitor对象存在于每个Java对象的对象头中,synchronized锁便是通过这种方式获取锁的。
这就解释了为什么Java所有对象都可以作为锁,同时也解释了 wait() notify() notifyAll() 为什么存在于顶级对象Object中。
3: JVM中锁的优化
3.1:锁机制升级流程
偏向锁--》轻量级锁--》重量级锁
3.2 偏向锁
原因:
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
过程:
1)当一个线程访问同步块并获取锁时,简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁
2)如果成功,表示线程已经获得了锁。
3)如果测试失败,判断目前锁的状态是否是偏向锁(Mark Word中偏向锁的标识是否设置成1)
4)若当前是偏向锁,则尝试使用CAS将对象头的偏向锁指向当前线程
5)若不是偏向锁,则使用CAS竞争锁。
注意:当锁有竞争关系的时候,需要解除偏向锁,进入轻量级锁。
涉及参数:
偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,
如有必要可以使用JVM参数来关闭延迟:XX:BiasedLockingStartupDelay=0。
如果确定应用的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
3.3 轻量级锁
1>加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。
然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
2>解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
下图展示两个线程同时争夺锁,导致锁膨胀的流程图:
自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于轻量级锁的状态,如果自旋失败进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争
3.4 锁的优缺点对比
4:几个锁概念
4.1 锁粗化
就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:
publicclassLockCoarsening{privateStringBuffer stringBuffer =newStringBuffer(20);publicvoidappend(){ stringBuffer.append("w"); stringBuffer.append("h"); stringBuffer.append("y"); }}
这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。
4.2 锁消除
锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。逃逸分析和锁消除分别可以使用参数-XX:+DoEscapeAnalysis和-XX:+EliminateLocks(锁消除必须在-server模式下)开启
4.3 适应性自旋
当前锁处于膨胀,会进行自旋。自旋是需要消耗CPU的,如果一直获取不到锁的话,那线程一直处在自旋状态,消耗CPU资源。为了解决这个问题JDK采用—适应性自旋,线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。另外自旋虽然会占用CPU资源,但不会一直占用CPU资源,每隔一段时间会通过os::NakedYield方法放弃CPU资源,或通过park方法挂起;如果其他线程完成锁的膨胀操作,则退出自旋并返回.
参考文献:
https://www.jianshu.com/p/1ea87c152413
http://www.cnblogs.com/xdyixia/p/9364247.html
https://www.jianshu.com/p/73b9a8466b9c