请简单说说 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. 测试工程师有福啦!一键生成api文档及测试功能

    最近发现一个比较好用的插件,可根据api的功能注释说明一键生成文档以及功能demo: swaggerUI 是一个简单的Restful API 测试和文档工具.简单.漂亮.易用.通过读取JSON 配置显 ...

  2. cesium 实现 3d-tiles 平移旋转贴地(附源码下载)

    前言 cesium 官网的api文档介绍地址cesium官网api,里面详细的介绍 cesium 各个类的介绍,还有就是在线例子:cesium 官网在线例子,这个也是学习 cesium 的好素材. 内 ...

  3. hdu1710 Binary Tree Traversals

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1710 题意:给前序.中序求后序,多组 前序:根左右 中序:左右根 分析:因为前序(根左右)最先出现的总 ...

  4. Python学习心得体会总结,不要采坑

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:樱桃小丸子0093   大家要持续关注哦,不定时更新Python知识 ...

  5. OV7670 RAW输出 bayer 解码

    今天终于搞定OV7670 raw输出啦,兴奋!! 参考链接: https://pikacode.com/liplianin/s2-liplianin/commit/dab97f5d6e3b http: ...

  6. 【CSS】343- CSS Grid 网格布局入门

    CSS Grid(网格) 布局使我们能够比以往任何时候都可以更灵活构建和控制自定义网格.Grid(网格) 布局使我们能够将网页分成具有简单属性的行和列. 它还能使我们在不改变任何HTML的情况下,使用 ...

  7. 【Webpack】320- Webpack4 入门手册(共 18 章)(下)

    介绍 1. 背景 最近和部门老大,一起在研究团队[EFT - 前端新手村]的建设,目的在于:帮助新人快速了解和融入公司团队,帮助零基础新人学习和入门前端开发并且达到公司业务开发水平. 本文也是属于[E ...

  8. java设计模式(二)单例模式,一生只爱一人,只争一朝一夕

    单例模式:保证一个类在内存中的对象唯一,有且仅能实例化一次.(如多个代码块需要读取配置文件,or开启事务,orjdbc读取数据源就是个经典例子)参考:吟啸且徐行 实现步骤: 私有构造方法.保证唯一的 ...

  9. 【AHOI 2013】差异

    Problem Description 给定一个长度为 \(n\) 的字符串 \(S\),令 \(T_i\) 表示它从第 \(i\) 个字符开始的后缀.求 \(\sum_{1\leqslant i&l ...

  10. 分布式事务之解决方案(XA和2PC)

    3. 分布式事务解决方案之2PC(两阶段提交) 针对不同的分布式场景业界常见的解决方案有2PC.TCC.可靠消息最终一致性.最大努力通知这几种. 3.1. 什么是2PC 2PC即两阶段提交协议,是将整 ...