JDK1.6开始引入了偏向锁和轻量级锁,在之前的版本里面,只有无锁和重量级锁两种状态,线程去执行一个同步块的时候,不管现在同步块是社么状态,都直接通过CAS来获取锁,CAS操作维护一个包含竞争锁的队列和一个信号阻塞队列,一个做互斥,一个用于线程同步;但是在实际的场景中,存在很多同步块在同一时间不被很多线程竞争的情况,比如说同一个线程会多次进入一个同步块,还有线程进入同步块的时候该同步块并不存在锁竞争;
先来看下利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
·对于普通同步方法,锁是当前实例对象。
·对于静态同步方法,锁是当前类的Class对象。
·对于同步方法块,锁是Synchonized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁到底存在哪里呢?锁里面会存储什么信息呢?
从JVM规范中可以看到Synchonized在JVM里的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
介绍锁之前,先说一下锁的位置,在Java中,一切都是对象,在每个对象里面都有一个对象头,对象头的长度如图:
synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit,如表2-2所示。锁标志位就在Mark Word里面,占2bit,无锁和偏向锁都是01,轻量级锁为00,重量级锁为10。
偏向锁:
对于同一个线程会多次进入同一个同步块的情况,JDK1.6引入了偏向锁的机制,在一个线程进入同步块的时候,会在对象的锁记录里面存放自己的线程ID,之后该线程再进入这同步块的时候,就不需要通过CAS来获取锁,直接判断对象的锁记录里面是否存放了自己的线程ID,如果是自己线程的ID,就直接获得锁,如果不是,则撤销偏向锁,(偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程)。偏向锁是可以在JVM里面设置启动延迟和关闭的,偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
轻量级锁:
如果决定使用轻量级锁,线程进入同步块的时候,有两种情况
(1)同步对象的锁标志位是01,无锁或者偏向锁状态,虚拟机在当前线程的栈帧中建立一个锁记录的空间,用于存放该对象的Mark Word的拷贝,然后通过CAS将该对象的的Mark Word更新为指向当前线程的指针。并且将同步对象的锁标志位置为00,表示处于轻量级锁定状态
更新成功,当前线程获得锁;如果更新失败,那么虚拟机会检测同步对象所指向的线程ID是不是当前线程,如果是,就直接获得锁开始执行同步块,如果不是,则存在线程竞争,轻量级锁升级为重量级锁。
最后记录一下各种锁的优缺点:
这个大概给自己做个学习记录,最近在看《Java并发编程的艺术》,如果想详细了解的话,建议去阅读一下第二章,里面说的很清楚,这个书的电子文档网上也很好找。