请简单说说 synchronized 关键字的底层原理

java 说到多线程绝对绕不开 synchronized,很多 java 工程师对 synchronized 是又爱又恨。为什么呢?主要原因包括以下几点:

  1. 在网上找到的各种学习资料,内容杂乱很多都是基于老版本写的,自己实践起来发现和网上说的不一样,不是那么回儿事儿。烦躁……

  2. 每次出去面试都会问这个问题,又没法直接看源码。烦躁

  3. 在小公司的开发同事们一定会发现,如果是做 javaWeb 项目的,在实际工作中很少会遇到多线程的问题。因为数据量小,请求数量小等各种原因。

所以经过这段时间的学习总结(瞎看,瞎扒拉),我想在这里简单输出一下我对 synchronized 关键字的底层原理的理解。

monitor 计数器

这里先声明一个前提,synchronized 是可重入锁,也就是说已加锁的对象可以再次被获取到锁的线程再次加锁。是不是有点绕嘴,看看下面这段代码:

public class SynchronizedDemo {

    public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
synchronizedDemo.test();
} public synchronized void test() {
System.out.println("来一把锁");
test1();
} public synchronized void test1() {
System.out.println("再次加锁");
}
} /* output 来一把锁 再次加锁*/

简单来解释一下这段代码。我们创建了一个对象 synchronizedDemo,然后调用方法 test,由于 synchronized 修饰了该方法,所以我们将对象 synchronizedDemo 进行了加锁。然后,test 方法内部又调用了 test1 方法,这个时候我们发现 test1 也是 synchronized 修饰的,所以我们再次对 synchronizedDemo 进行了加锁,这是对该对象的第二次加锁。

这里其实体现了 synchronized 是可重入锁的特性。广义上说可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁。

好了言归正传,synchronized 是如何做到的呢?

简单来说其底层有一个 monitor 计数器,当当一个线程第一次获取到对象的时候,会将对象头中的计数器改为 1,在加锁周期内再次加锁的话,那么就在原有的基础上再+1,以此类推。这是怎么回事儿呢?

可以这么理解 test 方法是这么执行的

  1. 现场会首先判断synchronizedDemo 对象是否已经被加密了,也就是计数器是否为 0

  2. 如果已经是 1 了,那说明这个对象已经被其他线程占有了,当前线程无法获取这个对象,这个时候只能等待

  3. 如果计数器为 0,说明这个对象当前没有别的线程在使用,当前线程就可以对其进行加锁。monitor 计数器+1(从 0 变成 1)

  4. 如果加锁方法中还调用了其他加锁方法,每次执行一个加锁方法嵌套都会使monitor 计数器+1,方法执行完成之后再-1.

  5. 最终synchronized 修饰的方法执行完毕之后,对象的 monitor 计数器为 0,等待其他线程使用。

这样说好像还是有点模糊,我在这里简单抽象的模拟一下这个过程大致是这样的:

monitorenter  +1
test();
monitorenter +1
test1();
monitorexit; -1
monitorexit; -1

当执行test 方法之前,monitorenter 将计数器+1(这个时候计数器的值是 1,获取到这个对象之前,对象的计数器一定是 0,否则获取不到),然后 test 方法中又调用了 test1 方法,而这个方法也是被 synchronized 修饰的,那么会再次执行monitorenter将计数器加 1(这个时候计数器的值为 2)。当 test1 方法执行完之后,monitorexit 会将计数器的值-1(这个时候就是 1 了,2 - 1 = 1),然后 test 方法执行完了,monitorexit 将计数器的值再-1,当这个时候计数器的值就是 0了。也就是锁已经被释放,这个对象的锁可以继续被其他线程获取了。

synchronized 锁方法是锁的什么?

相信大家都知道 synchronized 可以对 对象和方法进行加锁。

	Map<String, Object> map = new HashMap<>();
// 修饰方法
public synchronized void test() {
System.out.println("来一把锁");
// 锁对象
synchronized (map) {
System.out.println("对 map 对象进行加锁");
}
test1();
}

看到这里集合上面说的计数器,就会有同学提出疑问了。

不是说计数器在对象头里面存储的吗?那方法加锁是针对哪里加的锁啊?先说结论:对方法加锁,锁是还是加载对象上的,哪个对象调用的这个方法,就是在哪个对象上加锁。

举个例子:

public class SynchronizedDemo {

    HashMap<String, Object> map = new HashMap<>();

    public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
synchronizedDemo.test1();
} public synchronized void test1() {
System.out.println("再次加锁");
} }

这里可以看到 test1 方法被 synchronized 修饰了,我们加锁是记载 synchronizedDemo 对象上的,是这个对象调用 test1 方法。所以是对他进行加锁的。

简单说说 CAS的理解

像 synchronized 这种独占锁属于悲观锁,它是在悲观的任务加锁的这个地方一定会发生冲突。除了悲观锁之后,还有乐观锁,乐观锁的含义就是我乐观的认为这个的地方不会发生冲突,如果没有发生冲突我就正常执行,如果发生了冲突,我就重试。

CAS 就属于乐观锁。

为了方便理解 CAS,我们说个典型的例子。假设多个线程执行这个方法increment,势必会发生线程安全问题。因为 i++不是原子性操作,而且 increment方法没有加锁。

public class CASDemo {

    int i = 0;
public void increment() {
i++;
} }

解决方法有两种,第一个肯定是刚才我们说的通过 synchronized 来加锁。

public class CASDemo {

    int i = 0;
public synchronized void increment() {
i++;
} public static void main(String[] args) {
CASDemo casDemo = new CASDemo();
casDemo.increment();
}
}

这里就是对 casDemo进行加锁,只有一个线程可以成功的对casDemo进行加锁,可以对他关联的monitor 计数器+1,加锁。其他线程就会等待这个对象被释放。这样的画出就是多个线程在这变成了串行化,效率会有损耗。多个线程在这排队。

第二个办法就是将 i++变成原子性操作,如何做到呢 java.util.concurrent.atomic包中带有大量原子性的对象,比如 AtomicInteger。

public class CASDemo {

    AtomicInteger i = new AtomicInteger(0);

    public void increment() {
i.incrementAndGet();
} }

由于 increment 方法只有一行命令,而且这个方法还是原子性的,那么这个方法自然不存在线程安全问题。

看到这里很多哥们就会问了,你不是说 CAS 吗,怎么扯到这个了?别着急啊,前面都是铺垫,我这不是正要说了嘛。

其实 incrementAndGet 就是一个 CAS 操作。CAS 的全称是 compare and set ,比较并替换。CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。其业务逻辑原理如图所示

CAS 存在的问题

  1. ABA问题

CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是CAS的ABA问题。 常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 目前在JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  1. 循环时间长开销大

上面我们说过如果CAS不成功,则会原地自旋,如果长时间自旋会给CPU带来非常大的执行开销。

参考资料《Java CAS 原理解析》

互联网Java工程师面试突击(第三季)

《Java 并发变成实战》


未完待遇

Java 多线程应知应会的更多相关文章

  1. 测试TwemProxy的应知应会

    一.背景 最近中间件开发组对twemproxy的发现注册机制做了改造,之前没有接触过twemproxy,借这次测试的机会,初步学习了一下twemproxy相关的知识:下面用"测试语言&quo ...

  2. SpringMVC 应知应会

    springMVC 是表现层技术,可以用来代替 struts2,下面是简略图:主要是处理器和视图,只有这两个部分需要编写代码. springMVC 三大组件:处理器映射器,处理器适配器,视图解析器. ...

  3. Markdown的应知应会

    Markdown介绍 什么是Markdown Markdown是一种纯文本.轻量级的标记语言,常用作文本编辑器使用.和记事本.notepad++相比,Markdown可以进行排版:和Word相比,Ma ...

  4. Hibernate 应知应会

    Hibernate 的关联关系的配置: 一对一外键约束: 举例子是一个丈夫和妻子:[一个丈夫只能有一位妻子] 表结构: CREATE TABLE `tbl_hus` ( `uuid` ) NOT NU ...

  5. Linux用户应知应会的7个‘ls’命令的独特技巧

    在前面我们系列报道的两篇文章中,我们已经涵盖了关于‘ls’命令的绝大多数内容.本文时‘ls命令’系列的最后一部分.如果你还没有读过该系列的其它两篇文章,你可以访问下面的链接. Linux中的15个基本 ...

  6. 【应知应会】15个常用的JavaScript字符串操作方法

    1 初始化 //常用初始化方法 var stringVal = "hello iFat3"; //构造函数创建方法 var stringObj = new String(" ...

  7. Struts2 应知应会

    struts.xml 文件的 action 的配置: Struts2 中结果类型的配置来自于下面: 其中: dispatcher:转发技术,转发到一个 jsp 视图 redirect:重定向到一个 j ...

  8. .NET架构开发应知应会

    .NET程序是基于.NET framework..NET Core.Mono.UWP[.NET实现]开发和运行的 ,定义以上[.NET实现]的标准规范称为.NET Standard L1:.NET S ...

  9. 关于HDFS应知应会的N个问题 | 技术点

    1. Namenode的安全模式 ? 安全模式是Namenode的一种状态(Namenode主要有active/standby/safemode三种模式). 2. 哪些情况下,Namenode会进入安 ...

随机推荐

  1. 华为云DevCloud为开发者提供高效智能的可信开发环境

    在HUAWEI CONNECT 2019期间,在华为云云服务开发者分论坛上,华为云布道师做了<CloudIDE:开发者的高效.智能的可信开发环境>专题演讲,主要介绍了华为云DevCloud ...

  2. 转:java 解析excel,带合并单元的excel

    收集了一些对博主有帮助的博文,如下 >>>>>>>>>>>第一部分: 首先,mavn导入jar包 <!-- 解析excel需要导 ...

  3. shell 脚本运行 hive sql

    #!/b START=$(date +%s); datebegin=`date -d "$1" "+%Y%m%d"` dateend=`date -d &quo ...

  4. 求局域网内所有在线主机的ip

    在一个局域网内,已知其中一台主机的ip为192.,子网掩码为255.,求所有其他在线主机的ip. shell 编码实现 #!/bin/bash netWorkIP=. ) do $netWorkIP$ ...

  5. imageView的使用

    转自:http://www.runoob.com/ios/att-ios-ui-imageview.html 图像视图用于显示单个图像或动画序列的图像. 重要的属性 image highlighted ...

  6. textView的用法及技巧

    转自:http://bbs.9ria.com/thread-244445-1-1.html 一.新建一个textView //初始化 UITextView *textView = [[[UITextV ...

  7. wow钓鱼方案

    最近怀旧服启动了 玩(排)得我萎靡不堪 突然想起多年前写过一个钓鱼的按键精灵 赶紧搜出来助我一臂之力 奈何往年不知其珍贵 早不见了 千思万想才在群空间的文件夹内翻出来一个exe版本 而源代码已不知去向 ...

  8. 可扩展的Java线程池执行器

    分享一下最近优锐课学习笔记. Java线程池执行程序偏向于排队而不是产生新线程.从好的方面来说,我们有两种解决方法. 理想情况下,对任何线程池执行程序而言,期望如下: 预先创建了一组初始线程(核心线程 ...

  9. Java设计模式的7种设计原则还有很多人不知道

    前言 其实没有设计模式我们也能完成开发工作.但是为什么需要设计模式呢?让你看起来很牛,没错这个算一个.让你的代码层次感分明,可读性强而且容易维护.让你像我一样有更多的摸鱼划水时间. 可能有人说我一个类 ...

  10. 1. 初探--prometheus调研

    一.基本概念 Prometheus 是由前 Google 工程师从 2012 年开始在 Soundcloud 以开源软件的形式进行研发的系统监控和告警工具包,自此以后,许多公司和组织都采用了 Prom ...