java多线程(2):synchronized关键字
synchronized的用法
指定加锁对象:对给定对象加锁,进入同步代码前需要活的给定对象的锁。
直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类(当前类内部的class对象)的锁。
synchronized是可重入的。
内部原理
Synchronized在古老的年代被成为重量级锁。但是java1.6对其进行了优化。为了减少获得锁和锁的释放带来的开销,java1.6为synchronized关键字实现了偏向锁,轻量级锁和重量级锁几种状态。
基础知识
首先,必须了解JVM中的对象头。对象头包含一个指向类型元数据的指针(klass point),和运行时数据(Mark Word)。Mark Word包含哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。
java中每个对象都有唯一的一个monitor对应。Mark Word中的LockWord指向自己monitor的起始地址。每个线程都有一个列表(monitor record)保存自己持有的monitor。
monitor的数据结构如下:
- Owner 拥有该monitor的线程的唯一标志
- EntryQ 一个互斥锁(semaphore),是一个计数信号量。用于阻塞线程。由操作系统提供。
- RcThis 所有被该monitor阻塞的线程个数
- Nest 可重入锁的计数(持有该monitor的线程可以再次获取此monitor,所以称为可重入,这时候需要一个计数器来表示是否该线程完全释放该锁)
- HashCode 对应对象的HashCode
- Candidate 标记是否需要唤醒下一个线程
JVM用monitorenter和monitorexit指令对同步提供显式支持。(sychronized“方法”通常不是用monitorenter和monitorexit指令实现的。往往是由“方法调用指令”检查常数池里的ACC_SYCHRONIZED标志)。JVM会把要加锁的对象放在栈顶,然后执行monitorenter指令,检查那个对象的计数器是否为0或者那个对象是否被当前线程加锁。上述两个条件只要有一个成立,JVM就会对计数器加1并确保该对象的monitor的中的Owner是自己。
偏向锁
实现原理为,采用cas替换对象的加锁标志位,将对象头markword中的拥有偏向锁线程id指向自己。然后就可以开心的取执行同步块的内容了。
偏向锁的特点是只要没有人竞争,则该线程一直持有该锁。如果线程2也要竞争该锁,则需要等到没有字节码正在执行的全局安全点。
偏向锁只要发生两个线程的竞争(因为一个线程一旦持有锁就不会释放所以其实就是两个线程申请锁),就会升级为轻量级锁。偏向锁其实是JVM对那些根本没必要加锁的代码的优化。
轻量级锁
轻量级锁使用了自旋锁,线程会首先尝试自旋获取锁,但是自旋次数会进行限制,超过这个限制后会转而使用阻塞锁,此时轻量级锁就会膨胀为阻塞锁。
重量级锁
使用队列维护线程列表,采用阻塞和唤醒机制。
几种状态的优缺点
偏向锁
加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。
如果线程间存在锁竞争,会带来额外的锁撤销的消耗。
轻量级锁
竞争的线程不会阻塞,提高了程序的响应速度。
如果始终得不到锁竞争的线程使用自旋会消耗CPU。
重量级锁
线程竞争不使用自旋,不会消耗CPU。
线程阻塞,响应时间缓慢。
这是因为线程的阻塞和唤醒都是cpu核心态的代码。执行阻塞和唤醒都需要cpu进行状态切换,频繁的切换状态对cpu负担很重。
锁的升级过程
参考
http://blog.****.net/xiaomin1991222/article/details/50981423