锁的本质

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

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

我们在这里打个比方,假设有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. kue

    尝试着看英文的文档,你发现他其实并不难. https://www.npmjs.com/package/kue

  2. sed:轻量级流编辑器

    一. sed命令 sed是一种几乎包括在所有UNIX平台(包括Linux)的轻量级流编辑器.sed主要是用来将数据进行选取.替换.删除.新增的命令 注意:vi命令只能修改文件,但不能修改命令的结果,如 ...

  3. 2-10~2-11 配置iptables防火墙增强服务 selinux简单讲解

    学习一个服务的过程: 1.此服务器的概述:名字,功能,特点,端口号 2.安装 3.配置文件的位置 4.服务启动关闭脚本,查看端口 5.此服务的使用方法 6.修改配置文件,实战举例 7.排错(从下到上, ...

  4. Hive之基本操作

    1,CREATE table. CREATE [EXTERNAL] TABLE [IF NOT EXISTS] table_name [(col_name data_type [COMMENT col ...

  5. 一. Spring框架防XXS跨站攻击

    使用 Spring 框架进行 Java Web 开发,可以在 web.xml 文件中设置 HTML encode,在 JSP 文件页面元素 form 中确定实施. web.xml 加上: <co ...

  6. 理解Javascript的Event Loop

    一.单线程 js作为浏览器脚本语言,他的主要用途是与用户交互,以及操作DOM,这决定了它只能是单线程,为什么呢?因为假如js同时有两个线程,一个线程是在DOM上增加内容,另一个线程是删除这个节点,那么 ...

  7. python学习笔记(三)---python关键字及其用法

    转载出处:https://www.cnblogs.com/ECJTUACM-873284962/p/7576959.html 前言 最近在学习Java Sockst的时候遇到了一些麻烦事,我觉得我很有 ...

  8. IIS网页GZIP压缩

    1.开GZIP有什么好处? 答:Gzip开启以后会将输出到用户浏览器的数据进行压缩的处理,这样就会减小通过网络传输的数据量,提高浏览的速度. 2.如何启用IIS的Gzip压缩功能: 答:首先,如果你需 ...

  9. 用django发送异步邮件

    太阳底下没有新鲜事,github是一个神奇的地方,你有什么想法,需求,点子.其实别人早就想到,而且也已经做到. 所以不要高估自己,有什么想法还是GITHUB一下,免得成了井底之娃. 这几天一直在研究p ...

  10. 创建目录mkdir

    mkdir -p 在创建目录时,我们通常会先检查一下是否存在,如果不存在,就创建,这个时候通常用mkdir -p进行,但是-p是干什么用的呢. mkdir --help一下吧.也就说,如果上级目录不存 ...