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会进入安 ...
随机推荐
- 华为云MySQL金融版正式商用,高可靠的金融级数据库来了
摘要:全新三节点架构,基于深度优化的MGR组复制技术,提供金融级的数据一致性. 日前,华为云数据库MySQL 金融版正式商业化发布,MySQL金融版采用MGR技术,基于Paxos协议,采用一主两备三节 ...
- Apache ServiceComb 开源两周年,聊聊其与微服务的前世今生
欢迎添加华为云小助手微信(微信号:HWCloud002 或 HWCloud003),输入关键字"加群",加入华为云线上技术讨论群:输入关键字"最新活动",获取华 ...
- Python中的Tcp协议应用之TCP服务端-协程版(推荐)
利用gevent第三方库,实现协程. 通过协程实现一个服务端服务多个客户端需求. 使用协程的好处是协程比线程更加节省内存资源. gevent安装命令: pip3 install gevent 注意:在 ...
- luogu P4462 [CQOI2018]异或序列 |莫队
题目描述 已知一个长度为n的整数数列a1,a2,...,an,给定查询参数l.r,问在al,al+1,...,ar区间内,有多少子序列满足异或和等于k.也就是说,对于所有的x,y (I ≤ x ≤ ...
- 使用 nginx 实现虚拟主机
当多个系统需要部署的时候,有系统访问很小,为了节省成本,就需要将多个系统部署到同一台服务器上,怎么在同一台服务器上,完成不同系统的部署和访问,就需要使用虚拟主机实现. 使用端口实现虚拟主机 配置 ng ...
- 胸部CT提取分割肺部
1. 肺部分割提取简介 在处理胸部CT时,我们常常需要获取肺部的一个mask,也就是将肺部结构从数据中提取出来.二维图像还好说,但是三维图像就会变得复杂复杂一点.肺部的分割常常做后续操作的预处理,所以 ...
- Web 前端学习大纲
什么是前端? 前端即网站前台部分,也叫前端开发,运行在PC端,移动端等浏览器上展现给用户浏览的网页.随着互联网的发展,HTML5,CSS3,前端框架的应用,跨平台响应式网页设计能够适应各种屏幕分辨率, ...
- 视频来了!Visual Studio Online 东半球首秀 @ .NET Conf 2019
2019 年 11 月 9 日,.NET Conf 2019 中国峰会于上海中谷小南国花园酒店举行,全国的 .NET 大咖相聚上海. 这次我演讲的主题是<Visual Studio Code — ...
- Mybatis需要注意的细节
mybatis第二篇 1.${}和#{}的区别 1.#在传参的时候,会自动拼接单引号:$不能拼接单引号; 2.$传参时,一般不支持jdbcType指定类型的写法;#则可以;如: #{name,jd ...
- webpack学习2.2webpack简介,初步了解
webpack V1功能进化 编译打包 HMR(模块热更新) 代码分割 文件处理(loader) webpack V2功能进化 tree shaking(并欸有在项目中使用的代码不会打包到里面,打包之 ...