锁的本质

我们先来讨论锁的出现是为了解决什么问题,锁要保证的事情其实很好理解,同一件事(一个代码块)在同一时刻只能由一个人(线程)操作。

这里所说的锁为排他锁,暂不考虑读写锁的情况

我们在这里打个比方,假设有10个人要过独木桥(独木桥只能承载一个人的重量),他们可以排好队一个一个的过,后面一个人看到前面过去了之后他便跟着过去,直到所有的人都过去。

那如果我们用计算机模拟这个过程呢,没错,我们的程序不会排好队,更不会有看到前面的人已经通过这种主观能动性。所以这有点类似于所有的人都是蒙着眼睛的,但他们的听力是良好的,如果有人过去了之后在桥的另一头大喊一声“我已经通过了”,其他人便开始争着喊“下一个我过”。如果两个人几乎同时喊,在现实中我们很难搞清楚谁先谁后,甚至两个暴躁的人会打起来。但在计算机中他们不会,他们都如此听话如此可靠,而且在时间上总会分清谁先谁后,不会出现同时喊的状况。

我们先来总结一下这个过程正常工作的两个先决条件

  • 同一时刻,只能有一个人抢到锁(过桥的权利)
  • 当操作完成之后,必须释放锁(过去桥之后,要告诉其他人现在可以过桥了)

很简单,对吧,但锁的真正意义就在于此,只是不同的场景对这两点有不同的实现方式罢了。

Java中的锁

可见性

提到Java中的锁,就不得不提Java的内存模型,如下图(假设在多核CPU上),这里可以使CPU的一个核心类比一个线程(这是一个简化的模型,事实上比这个模型复杂的多):

注意:这里只是类比,CPU的Cache与JMM中的工作内存并不严格一致,但两者有一定交集,在这里做这样的类比并不会误导我们想要的出来的结论

读到这里或许有的读者会有问题,为什么CPU要把数据抓到工作内存去,而不是直接从主内存里面拿呢。这要从计算机的组成原理上讲起,CPU和内存在物理上是分离的,CPU从主存抓取数据如同远房探亲。从主内存中抓取数据比对数据的操作上要快数十倍甚至上百倍。这有点像你想和小明打一个小时游戏,但是要花几天甚至数十天的时间把小明请过来。这样为了省时间,我们可以把小明请过来,多打几天游戏在让他回去。事实上也的确如此,我们的CPU之所以要在Cache 中操作之前Fetch过来的数据,就是为了节省这一段时间。但这就在两个Thread中产生两个副本,而他们互相不知道对方有没有更改过cache到的数据。但充满智慧的CPU架构师给出了这种通知的保证(MESI协议,这大概是相当早的分布式缓存一致性解决方案了),这个协议的原理比较复杂,在此不在赘述,但这并不影响我们对锁的理解。我们只要知道,操作系统提供了这样的支持,并留给了system callnative api就足够了。这个方案解决了CPU对变量的可见性。在java中通过使用volatile实现变量的可见性保证,而其保证的原理正是借助与CPU的缓存一致性协议实现的,操作系统将其抽象为lock操作,

原子性

似乎有了可见性对元素的操作就完全可靠了,但事实并非如此,这取决于对变量进行操作的过程,我们i++为例说明这一点,但在此之前,我们先来看一下i++在Java中的执行过程

代码如下:

public class Test {
static int i=1;
public static void main(String[] args) {
i++;
}
}

我们使用javap -verbose Test.class查看Java中main方法的虚指令

0: getstatic     #2                  // Field i:I
3: iconst_2
4: iadd
5: putstatic #2 // Field i:I
8: return

这个过程的语义与下图相同

考虑以下情况,在第三步回写的过程中算出的结果已经保留了,假设线程A在此卡顿了一会儿,其他线程已经更改了i的值,然后线程A才回过神来,但结果还是刚才算出的结果3,这时它进行回写操作的时候,就会覆盖其他线程对i的赋值,就会导致值的不一致现象。

可以看到,之所以会出现这种现象,就是因为i++这个操作没有像我们想象的那样,一下子就完成,而是分成了很多步。我们称这种操作为非原子性操作,就是i++操作的非原子性,导致在哪怕保证了变量的可见性的情况下仍然会导致数据操作相互覆盖(线程不安全)的情况。

隔离区(临界区)

终于讲到Java中的锁了,根据独木桥的例子,要想保证多个线程对变量的操作绝对安全,就要保证对变量操作的串行化。Java中使用synchronized关键字提供了前文提到过的两个先决条件。下面我们来详讲一下java中的synchronized关键字。我们先来看以下代码

public class Test {
public static void main(String[] args) {
synchronized (Test.class) {
}
}
}

同样使用javap -verbose Test.class

0: ldc           #2                  // class Test
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: return

我们重点看monitorenter和monitorexit两个指令,根据我们前面所讲的两个先决条件,我们至少可以推断monitorenter在背后所做的事情有

  • 告诉其他线程,我拿到了锁(下一个过独木桥的人)

而monitorexit在背后做的事情当有

  • 告诉其他线程,我释放了锁(你们可以过桥了)

这里其实还有个问题,它标示锁的方式是什么,这就要提到java对象在内存中的模型了,事实上Test.class对象在内存中有个头部,通过设置这个对象头获取该对象的锁,而对这个锁的设置操作是用指令cmpxchg 保证原子性的,由操作系统和硬件底层支持。

事实上Java对锁进行了优化,包括偏向锁和轻量级锁。所以通不通知其他线程并不是那么绝对的,而且monitor背后所做的事情也绝对不是这么简单,在这个模型中,其他线程确认自己有没有获得锁是主动过来看Test.class的对象头有没有被设置为已获取锁状态。如果没有,自己就上锁。如果已经被锁住了,这个线程就需要发出system call 来阻塞自己,但Java自己做不了这件事情,它必须借助操作系统完成,借助操作系统发出system call到自己被阻塞这个过程需要几万的个时钟周期。而这个代价是相当昂贵的,对于CPU的执行速度来说,几万个时钟周期可以做很多的事情,这时如果我们乐观的认为,这个锁马上就能释放,我就愿意花费几百个时钟周期不停的判断这个锁是否释放,总比调用system call的开销要低一些,这就是乐观锁的原理。

synchronized释放锁之前,任何线程都不能进入synchronized的方法体内,不管在中间有多少操作,其他线程都必须等待操作完成之后释放锁的通知,这就保证了数据在多线程的绝对安全。

同时,在上面的字节码可以看出,当程序顺序执行时,在第6步monitorexit之后,会直接跳转到底15步返回,但若中间发生了异常,会在第12步先monitorexit然后,在抛出异常,这其实是编译器替我们完成了加锁和释放锁的过程,而且编译器替我们做了在发生异常的情况下也释放锁的保证。

深度解析Java中的那把锁的更多相关文章

  1. 深度解析Java中的5个“黑魔法”

    现在的编程语言越来越复杂,尽管有大量的文档和书籍,这些学习资料仍然只能描述编程语言的冰山一角.而这些编程语言中的很多功能,可能被永远隐藏在黑暗角落.本文将为你解释其中5个Java中隐藏的秘密,可以称其 ...

  2. 深度剖析java中JDK动态代理机制

    https://www.jb51.net/article/110342.htm 本篇文章主要介绍了深度剖析java中JDK动态代理机制 ,动态代理避免了开发人员编写各个繁锁的静态代理类,只需简单地指定 ...

  3. 转:二十一、详细解析Java中抽象类和接口的区别

    转:二十一.详细解析Java中抽象类和接口的区别 http://blog.csdn.net/liujun13579/article/details/7737670 在Java语言中, abstract ...

  4. 深度解析javascript中的浅复制和深复制

    原文:深度解析javascript中的浅复制和深复制 在谈javascript的浅复制和深复制之前,我们有必要在来讨论下js的数据类型.我们都知道有Number,Boolean,String,Null ...

  5. Java中的双重检查锁(double checked locking)

    最初的代码 在最近的项目中,写出了这样的一段代码 private static SomeClass instance; public SomeClass getInstance() { if (nul ...

  6. 深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的实现分析

    深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的实现分析(上) 深度解析Java 8:AbstractQueuedSynchronizer的实现分析(下) A ...

  7. 深度解析VC中的消息(转发)

    http://blog.csdn.net/chenlycly/article/details/7586067 这篇转发的文章总结的比较好,但是没有告诉我为什么ON_MESSAGE的返回值必须是LRES ...

  8. 5000字 | 24张图带你彻底理解Java中的21种锁

    本篇主要内容如下: 本篇文章已收纳到我的Java在线文档. Github 我的SpringCloud实战项目持续更新中 帮你总结好的锁: 序号 锁名称 应用 1 乐观锁 CAS 2 悲观锁 synch ...

  9. Java中可重入锁ReentrantLock原理剖析

    本文由码农网 – 吴极心原创,转载请看清文末的转载要求,欢迎参与我们的付费投稿计划! 一. 概述 本文首先介绍Lock接口.ReentrantLock的类层次结构以及锁功能模板类AbstractQue ...

随机推荐

  1. 五句话搞定JavaScript作用域【转】

    JavaScript的作用域一直以来是前端开发中比较难以理解的知识点,对于JavaScript的作用域主要记住几句话,走遍天下都不怕... 一.“JavaScript中无块级作用域” 在Java或C# ...

  2. 流量监控iftop安装-CentOS7

    继之前撘的服务器后路由器一直崩溃,今天找到了原因.之前被下的木马并没有被删掉,而是一直在传输数据.占用了所有宽带. 官网(http://www.ex-parrot.com/pdw/iftop/down ...

  3. hdu 6386 Age of Moyu (重边判断)

    本来用一个map判重边结果T了, 实际上可以直接给边上打标记即可 int n, m; struct _ {int to,w,vis;}; vector<_> g[N]; int dis[N ...

  4. NYOJ 720 DP+二分

    项目安排 时间限制:3000 ms  |  内存限制:65535 KB 难度:4 描述 小明每天都在开源社区上做项目,假设每天他都有很多项目可以选,其中每个项目都有一个开始时间和截止时间,假设做完每个 ...

  5. 在请求的参数中设置可选值列表为当前职责可访问的所有OU

    方法一: 实现此需求的前提之一是为该请求开启多业务实体访问,开启方法 系统管理员->系统管理->并发->程序,进入OAF页面,查询你的并发,然后点更新,选择请求,在业务实体模式下选择 ...

  6. 安装MySQL后要做的事

    安装MySQL后要修改的配置 [mysql] default-character-set=utf8 [mysqld] # 关闭域名反解 skip_name_resolve # 每表一个独立的表空间文件 ...

  7. 联想A390T刷机ROOT教程

    一.联想A390T手动进入Recovery的方法: [步骤一]首先,将你的A390T手机关机,关机状态下,先按住电源键2秒,不要松开,再同时按下音量加.音量减两个键,此时,3个键一直按住不要放开,几秒 ...

  8. JS数组中级+高级技巧

    本文介绍JS数组一些比较进阶的方法: reverse:数组反转: join:(参数)以参数为连接符将数组拼接为字符串: 实例: var arr=[]; arr[3]="haha"; ...

  9. 解决sublime package control 出现There are no packages available for installation

    昨天在安装了一下Sublime Text 3,在安装插件的过程中出现了一些问题,现在记录一下,也给遇到同样问题的朋友一些帮助.在安装插件的时候,需要先安装一下Package Control. 安装Pa ...

  10. 结合File类浅析递归的使用

    递归算法就是方法自身直接或者间接地调用到了自身,它是一种写起来很简单,但理解起来不那么简单的算法. 一个功能在被重复地调用,并且运算的结果和上一次的调用有关, 这种时候,可以使用递归. * 注意: * ...