Java 多线程应知应会
请简单说说 synchronized 关键字的底层原理
java 说到多线程绝对绕不开 synchronized,很多 java 工程师对 synchronized 是又爱又恨。为什么呢?主要原因包括以下几点:
在网上找到的各种学习资料,内容杂乱很多都是基于老版本写的,自己实践起来发现和网上说的不一样,不是那么回儿事儿。烦躁……
每次出去面试都会问这个问题,又没法直接看源码。烦躁
在小公司的开发同事们一定会发现,如果是做 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 方法是这么执行的
现场会首先判断synchronizedDemo 对象是否已经被加密了,也就是计数器是否为 0
如果已经是 1 了,那说明这个对象已经被其他线程占有了,当前线程无法获取这个对象,这个时候只能等待
如果计数器为 0,说明这个对象当前没有别的线程在使用,当前线程就可以对其进行加锁。monitor 计数器+1(从 0 变成 1)
如果加锁方法中还调用了其他加锁方法,每次执行一个加锁方法嵌套都会使monitor 计数器+1,方法执行完成之后再-1.
最终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 存在的问题
- ABA问题
CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是CAS的ABA问题。 常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 目前在JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
- 循环时间长开销大
上面我们说过如果CAS不成功,则会原地自旋,如果长时间自旋会给CPU带来非常大的执行开销。
《Java 并发变成实战》
未完待遇
Java 多线程应知应会的更多相关文章
- 测试TwemProxy的应知应会
一.背景 最近中间件开发组对twemproxy的发现注册机制做了改造,之前没有接触过twemproxy,借这次测试的机会,初步学习了一下twemproxy相关的知识:下面用"测试语言&quo ...
- SpringMVC 应知应会
springMVC 是表现层技术,可以用来代替 struts2,下面是简略图:主要是处理器和视图,只有这两个部分需要编写代码. springMVC 三大组件:处理器映射器,处理器适配器,视图解析器. ...
- Markdown的应知应会
Markdown介绍 什么是Markdown Markdown是一种纯文本.轻量级的标记语言,常用作文本编辑器使用.和记事本.notepad++相比,Markdown可以进行排版:和Word相比,Ma ...
- Hibernate 应知应会
Hibernate 的关联关系的配置: 一对一外键约束: 举例子是一个丈夫和妻子:[一个丈夫只能有一位妻子] 表结构: CREATE TABLE `tbl_hus` ( `uuid` ) NOT NU ...
- Linux用户应知应会的7个‘ls’命令的独特技巧
在前面我们系列报道的两篇文章中,我们已经涵盖了关于‘ls’命令的绝大多数内容.本文时‘ls命令’系列的最后一部分.如果你还没有读过该系列的其它两篇文章,你可以访问下面的链接. Linux中的15个基本 ...
- 【应知应会】15个常用的JavaScript字符串操作方法
1 初始化 //常用初始化方法 var stringVal = "hello iFat3"; //构造函数创建方法 var stringObj = new String(" ...
- Struts2 应知应会
struts.xml 文件的 action 的配置: Struts2 中结果类型的配置来自于下面: 其中: dispatcher:转发技术,转发到一个 jsp 视图 redirect:重定向到一个 j ...
- .NET架构开发应知应会
.NET程序是基于.NET framework..NET Core.Mono.UWP[.NET实现]开发和运行的 ,定义以上[.NET实现]的标准规范称为.NET Standard L1:.NET S ...
- 关于HDFS应知应会的N个问题 | 技术点
1. Namenode的安全模式 ? 安全模式是Namenode的一种状态(Namenode主要有active/standby/safemode三种模式). 2. 哪些情况下,Namenode会进入安 ...
随机推荐
- es6 proxy浅析
Proxy 使用proxy,你可以把老虎伪装成猫的外表,这有几个例子,希望能让你感受到proxy的威力. proxy 用来定义自定义的基本操作行为,比如查找.赋值.枚举性.函数调用等. proxy接受 ...
- 洛谷 P1920 成功密码 题解
这是蒟蒻的第一篇题解,(之前的都没过,估计这篇也过不了 回到正题 这题,本蒟蒻第一眼看到以后,就决定咦,这不是模拟吗? 看到世界范围,嗯,打扰了. 扯回正题 首先,暴力肯定是A不了的(至少我A不了 但 ...
- 获取iOS设备的型号
获取iOS设备的型号 需要#import "sys/utsname.h" structutsname systemInfo; uname(&systemIn ...
- 【译】Visual Studio 2019 中 WPF & UWP 的 XAML 开发工具新特性
原文 | Dmitry 翻译 | 郑子铭 自Visual Studio 2019推出以来,我们为使用WPF或UWP桌面应用程序的XAML开发人员发布了许多新功能.在本周的 Visual Studio ...
- 几个非常适合练手的python爬虫项目,总有一个能搞定!
前言本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理.作者:Python玩家 注意:如果你平时学Python有问题找不到人解答,可以 ...
- Vmware Workstation虚拟机
目录 一.虚拟机是什么? 二.虚拟机的作用: 三.虚拟机创建流程 四.新的虚拟机上安装系统 五.虚拟机里添加硬盘 六.磁盘分区 一.虚拟机是什么? 虚拟机指通过软件模拟的具有完整硬件系统功能的.运行在 ...
- Python生成器的用法
生成器,一定情况下可以节省很多空间 比如: >>> b = (x for x in range(10)) >>> b <generator object &l ...
- 如何上传项目到github
喜欢的同学可以看一下原文,讲得更加详细哦 原文地址:http://www.cnblogs.com/cxk1995/p/5800196.html 首先你需要一个github账号,所有还没有的话先去注册吧 ...
- django----中使用事务 数据库的三大范式
目录 orm中的事务操作 ⑴ 原子性(Atomicity) ⑵ 一致性(Consistency) ⑶ 隔离性(Isolation) ⑷ 持久性(Durability) django中使用事务 数据库三 ...
- NodeJS1-2 Node JS的优点
NodeJS并不适合所有server端的场景,它的使用场景是及其有限的,web是nodeJS最适合的场景 前端偏爱node的原因其内核就是JavaScript 前端职责范围变大,统一开发体验 在处理了 ...