并发系列(一)-----synchronized关键字

一 简介

    说到并发不得不提的synchronized,synchronized关键字是元老级别的角色。在Java SE 1.6以前synchronized被称为是重量,在1.6以后对同步进行了一系列的优化,使它的“重量”发生变化。这篇文章主要介绍同步的原理和它“重量”变化html

二 表现形式

    同步代码在表现的形式有三种同步在代码的表现形式有三种编程

    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规范中能够看出JVM基于进入和退出监测对象来实现方法同步和代码块同步,二者的实现细节不同。代码块同步是使用monitorenter和monitorexit指令实现的,用指令读取运行时常量池中方法的ACC_SYNCHRONIZED标志来隐式实现的.monitorenter 指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处理和异常处理,JVM要保证每一个monitorenter必须有对应monitorexit与之配对。任何对象都有一个监视与之关联,当且一个显示器被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的监控的全部权,即尝试得到对象的锁。ide

 四 锁的重量变化

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

https://www.artima.com/insidejvm/ed2/threadsynch3.html

http://www.iteye.com/topic/1018932

相关文章
相关标签/搜索