一 简介
说到并发不得不提的synchronized,synchronized关键字是元老级别的角色。在Java SE 1.6之前synchronized被称为是重量,在1.6之后对同步进行了一系列的优化,使它的“重量”发生变化。这篇文章主要介绍同步的原理和它“重量”变化
二 表现形式
同步代码在表现的形式有三种同步在代码的表现形式有三种
1.对于同步方法,锁是当前实例对象(非静态方法)
2.对于静态同步方法,锁是当前类的类对象(静态方法)
3.对于同步方法块,锁是sysnchronized括号里配置的对象(代码块)
三 原理说明
在JVM(1.7)规范里面就说明了同步的原理
原文如下:Java虚拟机中的同步(同步)基于进入和退出管程(监视器)对象实现无论是显式同步(有明确的monitorenter和monitorexit指令)还是隐式同步(依赖方法调用和返回指令实现的)都是如此。在Java的语言中,可以被同步修饰的同步方法。标志来隐式实现的(参见§2.11.10“同步”)。
monitorenter和monitorexit指令用于实现同步语句块,譬如需要执行其对应的指令,而无论这个方法是正常结束(§2.6.4)还是异常结束(§2.6.5)。为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器(§2.10),这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit指令具体博客:JVM规范说明。
从上面JVM规范中可以看出JVM基于进入和退出监测对象来实现方法同步和代码块同步,两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,用指令读取运行时常量池中方法的ACC_SYNCHRONIZED标志来隐式实现的.monitorenter 指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处理和异常处理,JVM要保证每个monitorenter必须有对应monitorexit与之配对。任何对象都有一个监视与之关联,当且一个显示器被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的监控的所有权,即尝试获得对象的锁。
四 锁的重量变化
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁
4.1 对象头和栈帧的简单介绍
在介绍锁的重量变化的前读者可能要明白一些关于JVM的相关知识这里为了好理解只是简单的介绍一下对象头和栈帧。
栈帧:方法在执行的同时都会创建一个栈帧(方法运行时的基础数据结构)用于存储局部变量表,操做数栈,动态链接,方法出口等信息每一个方法从调用直至执行完成的过程,就对应这一个栈帧在虚拟机栈中入栈到出栈的过程。局部变量表存放了编译器可知的各种基本数据类型,对象引用(regerence,它不等同与对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他于此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
对象头:HotSpot虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode),GC分代年龄(Generational GC Age)等,这部分数据的长度在32位和64位的虚拟机中分别为32个和64个位,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。
对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间例如。在32位的热点虚拟机中对象未被锁定的状态下,标记字的32个位空间中的25位用于存储对象哈希码(HashCode),4位用于存储对象分代年龄,2位用于存储锁标志位,1位固定为0,在其他状态(轻量级锁定,重量级锁定,GC标记,可偏向)下对象的存储内容如表所示。
4.2 偏向锁
偏向锁的“偏”,就是偏心的“偏”,偏袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步这里。这点可以从偏向锁的的撤销体现出来偏向锁的撤销采用的是一种等到竞争出现才释放的机制,也就是说如果没有竞争那么偏向锁一直都不会释放。
4.2.1锁定获取
当一个线程访问同步块并获取锁时,会在对象头(包含运行时数据和类型指针)和栈帧(一种数据结构)中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下标记字中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
过程如下
(1)访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01--确认为可偏向状态。
(2)如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤(5),否则进入步骤(3)。
(3)如果线程ID并未指向当前线程,则通过CAS操作竞争锁如果竞争成功,则将标志字中线ID设置为当前线程ID,然后执行(5);如果竞争失败,执行(4)。
(4)如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(还原点详细信息参考JVM虚拟机)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
(5)执行同步代码。
4.2.2偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的标记字要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁(这时就需要锁升级了),最后唤醒暂停的线程。
4.3轻量级锁
轻量级锁是JDK 1.6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就被称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
4.3.1轻量级的加锁
线程在执行同步块之前,JVM会先在当前线程的第一个中创用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为DisplacedMark Word。然后线程尝试如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
4.3.2轻量级锁的解锁
轻量级解锁时,会使用原子的CAS操作将位移标记字替换回对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级
成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,
都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮
的夺锁之争。
五 锁的比较
六 说明
文章有差错地方希望大家指出,邮箱alemand@163.com
参考文档:
<< Java并发编程艺术>>方腾飞魏鹏程晓明
<<深入理解Java虚拟机:JVM高级特性与最佳实践>>周志明
<< Java虚拟机规范JavaSE1.7 >> Tim Lindholm,Frank Yellin, Gilad Bracha,Alex Buckley
https://www.tuicool.com/articles/2aeAZn