一直想把这个特别重要的关键词的底层实现搞明白。(当然现在也没有完全明白,如果有错误以后修改这篇文章)

首先,这个关键词synchronize可以说是个语法糖,它的具体用法网上很多博客都讲的比较明了了。

简而言之就是对一个对象“加锁”。首先,找个地方的对象不一定是堆里面的类的实例对象,也有可能是方法区的类对象。其次,这个关键词修饰的代码块的加锁过程有两个,进入的时候尝试获得锁(java字节码 monitorenter),退出时释放锁(java字节码monitorexit)。这两个操作的再下一层是基于mutex lock的lock()和unluck()。

这两个函数的具体实现由操作系统提供。lock()操作是“获得锁”“上锁”“进入临界区”,等等,不同的地方描述不一致。它的具体过程是:查看一个信号量(由这个锁持有),看当前能否获得锁,如果能直接获得,并且修改这个信号量的值(比如把1改成0)。如果不能,就把这个索取锁的线程自己加入一个队列,这队列专门放“困”在这个对象(这个锁)上的线程,接着阻塞这个线程自己。unlock()操作是“释放锁”“解锁”“离开临界区”。他可以直接修改信号量的值。同时他看是否有进程“困”在这个对象(锁)上。如果有,唤醒它放入就绪队列。(信号量的具体实现各不相同,记录型信号量可以更方便理解这个过程)。当然这些基础操作也是原子性的。

这个地方还有一个很重要的点,锁,线程,对象这3个东西到底怎么联系到一起的。而要讲明白这个,又不得不讲一下锁不一定是重量级的由操作系统提供的“互斥锁”。还有一种锁:轻量级锁。这种锁是一种运行时优化,如果用synchronize修饰的代码块没有发生并发行为就可以直接用这种“锁”。

一开始,要明白Mark Word。这是每一个对象的对象头中有的一个32bit或者64bit(由JVM确定)的一个区域(叫Mark Word,对象头还有一个和它一样大的区域保存了一个指针指向方法区的类对象)。保存了对象在运行过程中的一些数据,比如哈希码,GC分代年龄,上锁标志位等等。它的存在是必要的,因为确实有些运行时信息要通过这种形式保留。一个线程根据java字节码找到上锁的对象,查看上锁标志位,看看是否已经被别的线程获得锁,如果没有,就在这个线程的栈帧中建立一个锁记录(Lock Record,有的地方叫Monitor Record),保存这个Mark Word的一份拷贝,接着用一个Owner指针指向这个对象。对象则直接把Mark Word改成一个指针指向这个锁记录。(记得前面提到Mark Word的大小是刚好和当前操作系统指针的大小一样,所以可以直接改而不需要补位等操作)

但是这个地方一开始我很不理解,为什么要这么绕呢?既然锁在对象上面,为什么不直接在对象头保留一个空间,记录或者这个“锁”的线程呢,比如用线程的ID或者内部标识符。每次线程进入临界区,访问这个对象的时候直接去对象头看这个值是不是自己这个线程,如果不是就阻塞自己。

原因是:对象头是很珍贵的,因为每一个对象都有,它虽然有必要但是它的内容又确实不是对象本身真正的内容。也就是说要想尽一切办法缩小它的大小否则效率很低(试想你开辟的空间有一大半都储存了一些杂七杂八的信息)。相比这个对象头,运行时的程序栈可谓是非常广阔的空间。一个珍惜这点空间一个无所谓这点空间。这样就刚好通过“复用”来实现储存空间的优化。把本来需要额外增加的空间直接用Mark Word储存而它本来的值丢给线程的栈。这样就一举两得,首先储存了持有这个对象的锁的线程,同时也没有弄丢Mark Word(反正我用了指针指向这个线程也不怕找不到)。而线程则再用一个指针Owner指向这个对象。很完美。

这个是轻量级锁的做法,如果不是轻量级呢?其实JVM的优化策略保证了一开始都把他当做轻量级来处理(JVM的优化策略有自旋锁,锁消除,锁粗化,轻量级锁,偏向锁等等)。这个地方也要解释2点,第一,好处都有啥,第二,为什么能这么做。第三,为什么要这么做。

首先,如果直接用操作系统提供的Mutex Lock互斥锁的话,会使用操作系统调用,从用户态转为核心态,开销很大。用这种方式(轻量级锁)则只是一个CAS操作(要保证其原子性)的花费。第二,马上下面讲到如果轻量级锁没用了(也就是发生了竞争别的线程试图拿这个锁),它可以直接“膨胀”成一个重量级锁(Mutex Lock)。第三,现实情况是很多加了synchronize修饰的代码其实在实际运行过程中并没有发生竞争的情况,这么做在运行时直接减少了很多开销。

然后,什么情况下会从一个轻量级膨胀成一个重量级的Mutex Lock呢?其实jvm这部分的优化是这样的,一开始先“认为”这次加锁和大多数情况一样并没有发生竞争,于是先“机智”地用轻量级,这个时候如果发生竞争,也就是有别的线程尝试获得锁,就“膨胀”为一个重量级。再具体一点就是别的线程调用Lock(),发现当前这个对象的对象头的标志位是“加了轻量级锁的”。它再去看Owner,如果是自己就是一个“重入”。如果不是就说明发生了竞争,接着进行“膨胀”。这是第一种可能,也就是一个线程先在跑,后一个加入发生膨胀。其实还有第二种可能,发生在前一个线程刚放锁的时候,这个时候所有线程都认为没锁,同时通过CAS竞争,有一个会成功,其他的会失败,于是也进行膨胀。

接着来讲膨胀的过程。第一,改变对象锁标志的状态值,把Mark Word中保存的指针指向Mutex Lock(当然Mark Word的内容还是不能丢)我看的是深入理解Java虚拟机这本书,再加上一些网络上的博客。都没把这个部分讲明白,这个地方我“猜”一下:调用操作系统互斥锁,生成一个互斥锁并且把Mark Word的值指向它。同时让这个互斥锁或者别的什么数据结构和方式保存Mark Word原本的运行时信息。这个地方的Mutex Lock可能就是真真正正的“重量级”锁了,它的具体实现我估计和记录型信号量的PV原语操作差不多,同时还要保留一些标志位储存Owner,重入个数,等待队列的元素个数,等待队列指针,如果当前释放锁是否有线程需要唤醒等信息。

以上内容能够保证正确的是Mark Word指向一个Mutex Lock。这之后的过程就是再有线程调用Lock()尝试访问临界区,发先对象头指向一个锁,再进入锁发现已经上了锁并且自己不是Owner,于是就阻塞自己。出临界区的时候去唤醒别的阻塞线程。这都没啥说的了。

java多线程synchronized底层实现的更多相关文章

  1. java多线程02-----------------synchronized底层实现及JVM对synchronized的优化

    java多线程02-----------------synchronized底层实现及JVM对synchronized的优化 提到java多线程,我们首先想到的就是synchronized关键字,它在 ...

  2. Java 多线程 —— synchronized关键字

    java 多线程 目录: Java 多线程——基础知识 Java 多线程 —— synchronized关键字 java 多线程——一个定时调度的例子 java 多线程——quartz 定时调度的例子 ...

  3. java 多线程 synchronized与lock的通信机制等问题,结合相应实例说明

    1. 利用多线程实现如下需求: 写两个线程,一个线程打印1~52,另一个线程打印A~Z,打印顺序是12A34B...5152Z: 2. 使用synchronized 实现 public class T ...

  4. Java多线程synchronized同步

    非线程安全问题 “非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程问题”.也即是说,方法中的变量永远是线程安全的. 如果多个线程共同访问1个对象中的实例变量,则可能线程 ...

  5. JAVA多线程synchronized详解

    Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 当两个并发线程访问同一个对象object中的这个synchronized(this)同 ...

  6. java多线程-synchronized

    一.线程安全问题 多线程操作各自线程创建的资源的时候,不存在线程安全问题.但多线程操作同一个资源的时候就会出现线程安全问题.下例为两个线程操作同一个name资源时发生的问题. class TestSy ...

  7. java 多线程 Synchronized方法和方法块 synchronized(this)和synchronized(object)的理解

    synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块. 1. synchronized 方法:通过在方法声明中加入 synchronized ...

  8. Java 多线程 - Synchronized关键字

    目录 1-Synchronized 关键字概述 2- Synchronized关键字作用域 3- Synchronized 原理(反编译指令解释) 正文 1-Synchronized 关键字概述 由于 ...

  9. java多线程:synchronized和lock比较浅析

    转载:http://www.toutiao.com/a6392135944652587266/?tt_from=weixin&utm_campaign=client_share&app ...

随机推荐

  1. noj[1581] 筷子

    题目描述 A先生有很多双筷子.确切的说应该是很多根,因为筷子的长度不一,很难判断出哪两根是一双的.这天,A先生家里来了K个客人,A先生留下他们吃晚饭.加上A先生,A夫人和他们的孩子小A,共K+3个人. ...

  2. bzoj1067 降雨量&&vijos1265 暴风雨

    描述 话说这日,李逍遥与阿奴正欲前往桃花源拿寿葫芦,突然电闪雷鸣,天降暴雨,弄得两人措手不及,只得到附近的树洞避雨. "哎,大理不是本应旱灾的吗?怎么会突降暴雨呢?"李逍遥嘀咕道. ...

  3. java 继承(下)

    1.抽象方法一定要写在抽象类中. 2.抽象类只在描述该事事务应该具备的东西. 3.抽象只能是抽象类和抽象方法. 4,抽象类没有任何抽象方法,这种类是不让创建对象. private  static

  4. 改变TableView中的分割线位置

    加上以下代码,可以让系统的分割线位置置于起始位置 #pragma mark --- 设置分割线位置为起始位置-(void)viewDidLayoutSubviews{    if ([self.tab ...

  5. Dubbo架构设计详解

    from:http://shiyanjun.cn/archives/325.html Dubbo是Alibaba开源的分布式服务框架,它最大的特点是按照分层的方式来架构,使用这种方式可以使各个层之间解 ...

  6. 【点滴积累,厚积薄发】windows schedule task中.exe程序的路径问题等问题总结

    1.在发布ReportMgmt的Job时遇到一个路径问题,代码如下: doc.Load(@"Configuration\Business\business.config");   ...

  7. 投入Html5的怀抱,最近在研究的Egret

    html5没有办法不关注,实在太火热了,几年前还不行,如今确是环境较好,typescript语言很好学习,可能基于之前的基础,不到一个星期就基本上差不多了,虽然还有一些小问题,但那都是经验积累下来可以 ...

  8. junit

    junit测试代码也视为开发内容的一部分,强烈建议在开发过程中编写junit代码作为开发调试工具,用junit调试代码不需要启动应用服务器,实际上会加快开发速度.

  9. Oracle 常用操作【02】数据库特性

    1. 导出 oracle 注释 -- 表明細+表注释+字段明细+字段注释 a.一个用户下的表明細+表注释+字段明细+字段注释 select ATC.OWNER, atC.TABLE_NAME, utc ...

  10. Visual Studio 2015初体验——前端开发工作的问题

    前言 因为后台项目开发适用的VS2015,为了跟后台开发配合,前端部门也统一从VS2013升级到了VS2015. 因为C盘空间不足要先卸载,这里就不说卸载2013时花了多长时间,只说安装2015时用了 ...