锁的本质

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

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

我们在这里打个比方,假设有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. 更改CentOS7登录画面的分辨率

    设置成用VNC Viewer去连接虚拟机的CentOS7 , 可是分辨率都得等登录了才能生效. 登录画面显示时,分辨率老大了. 找到了下面的文章 , 把 home/<user>/.conf ...

  2. 用Python操作Named pipe命名管道,实用做法——os.read 或 os.write

    https://blog.csdn.net/mayao11/article/details/50618598

  3. angular 自定义指令参数详解

    restrict:指令在dom中的声明形式 E(元素)A(属性)C(类名)M(注释) priority优先级:一个元素上存在两个指令,来决定那个指令被优先执行 terminal:true或false, ...

  4. Integer与int的种种比较你知道多少

    如果面试官问Integer与int的区别:估计大多数人只会说道两点,Ingeter是int的包装类,int的初值为0,Ingeter的初值为null. 但是如果面试官再问一下Integer i = 1 ...

  5. facade外观模式

    通过买股票与通过基金买股票引出外观模式: package com.disign.facade; /** * Created by zhen on 2017-05-18. */ public class ...

  6. mysql远程连接的设置

    有时数据库所在机器与项目运行的机器不是同一个,那么就涉及到远程链接数据库了,配置远程连接数据库的步骤如下: 1.查看mysql数据库中,user表中的信息,如下图,先使用use mysql切换到mys ...

  7. 各开源 bbs 程序比较

    主要是集中在 php 开源轻巧的程序. 搜索到一个逼乎的一个帖子:https://www.zhihu.com/question/20655704 ,顺藤摸瓜 下. carbon forum 第一个测试 ...

  8. iOS-----线程同步与线程通信

    线程同步与线程通信 多线程是有趣的事情,它很容易突然出现”错误情况”,这是由于系统的线程调度具有一定的随机性造成的.不过,即使程序偶然出现问题,那么是由于编程不当所引起的.当使用多个线程来访问同一个数 ...

  9. /usr/include/c++/4.8/functional:1697:61: error: no type named ‘type’ in ‘class std::result_of<std::_Mem_fn<void

    /usr/include/c++/4.8/functional:1697:61: error: no type named ‘type’ in ‘class std::result_of<std ...

  10. Java反射机制的使用

    一:反射是什么 JAVA反射机制是在运行状态中,对于任意一个类,都能够获取这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取类信息以及动态调用对象内容就称为jav ...