锁的本质

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

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

我们在这里打个比方,假设有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. 递归--练习6--noi1755菲波那契数列

    递归--练习6--noi1755菲波那契数列 一.心得 二.题目 1755:菲波那契数列 总时间限制:  1000ms 内存限制:  65536kB 描述 菲波那契数列是指这样的数列: 数列的第一个和 ...

  2. mysql数据库的备份及免密码上传

    主要利用了mysqldump和sshpass进行备份和免密上传 以下是代码实现: #!/bin/bash #该脚本放在主服务器运行 #从服务器账号密码ipremotehost="xxxxxx ...

  3. 208. Implement Trie (Prefix Tree) -- 键树

    Implement a trie with insert, search, and startsWith methods. Note:You may assume that all inputs ar ...

  4. SPOJ VLATTICE Visible Lattice Points 莫比乌斯反演 难度:3

    http://www.spoj.com/problems/VLATTICE/ 明显,当gcd(x,y,z)=k,k!=1时,(x,y,z)被(x/k,y/k,z/k)遮挡,所以这道题要求的是gcd(x ...

  5. SGU 139. Help Needed! 逆序数,奇偶性,分析 难度:0

    139. Help Needed! time limit per test: 0.25 sec. memory limit per test: 4096 KB Little Johnny likes ...

  6. linux 命令-case

    case 命令作用: case语句使用于需要进行多重分支的应用情况 case 命令使用场景 在shell中的case结构与C/C++中的switch结构是相同的. 它允许通过判断来选择代码块中多条路径 ...

  7. BigDecimal 、BigInteger

    package com.BigDecimal; public class BigDecimalDemo { /* * 下面的运算的结果出乎我们的意料,有些准确,有些不准确 * 这是为什么呢? * 我们 ...

  8. L192 Virgin Galactic Completes Test of Spaceship to Carry Tourists

    Virgin Galactic says its spacecraft designed to launch tourists into space completed an important te ...

  9. 关于protel 99se 汉化后某些菜单消失的解决方法

    本人在使用protel 99se 画PCB时,遇到了好些问题,通过网上查资料基本都解决了. 下面给大家分享 关于protel 99se 汉化后某些菜单消失的解决方法. 其他的许多看不见的菜单也可以自己 ...

  10. IIS经典模式与集成模式

    在IIS7.0中Web应用程序有两种配置形式:经典和集成 经典模式 经典模式是为了与之前的版本兼容,使用ISAPI扩展来调用ASP.NET运行库,原先运行于IIS6.0下的Web应用程序迁移到IIS7 ...