Java 中的 syncronized 你真的用对了吗
生活中随处可见并行的例子,并行 顾名思义就是一起进行的意思,同样的程序在某些时候也需要并行来提高效率,在上一篇文章中我们了解了 Java 语言对缓存导致的可见性问题、编译优化导致的顺序性问题的解决方法,下面我们就来看看 Java 中解决因线程切换导致的原子性问题的解决方案 -- 锁 。
说到锁我们并不陌生,日常工作中也可能经常会用到,但是我们不能只停留在用的层面上,为什么要加锁,不加锁行不行,不行的话会导致哪些问题,这些都是在使用加锁语句时我们需要考虑的。
来看一个使用 32 位的 CPU 写 long 型变量需不需要加锁的问题:
我们知道 long 型变量长度为 64 位,在 32 位 CPU 上写 long 型变量至少需要拆分成 2 个步骤:一次写 高 32 位,一次写低 32 位。
对于单核 CPU 来说,同一时刻只有一个线程在执行,禁止 CPU 中断就意味着禁止线程切换,获得 CPU 使用权的这个线程就会一直运行,所以 2 次写操作要么同时都被执行,要么都不被执行,单核 CPU 是保证原子性的。
对于多核 CPU,同一时刻,一个线程在 CPU-1 上运行,另一个线程在 CPU-2 上运行,此时禁止 CPU 切换,只能保证 CPU 上有线程运行,并不能保证同一时刻只有一个线程运行,如果两个线程同时都在写高位,那么得出的结果可就不正确了。
所以,互斥修改共享变量这个条件非常重要,也就是说同一时刻只有一个线程在修改共享变量,只要保证这个条件,不论单核还是多核,操作就都是原子性的了。
一说到互斥、原子性,我们马上就想到了代码加锁,没错加锁是正确的选择,但是怎么加呢? 要想知道怎么加锁,首先我们要知道加锁锁的是什么以及我们想要保护的资源是什么,看下图说说锁的是什么,要保护的是什么呢?
图中锁的 M 资源,保护的也是 M 资源。
程序中的锁与现实中的锁也是类似的,每一把锁都有自己要保护的资源,这是至关重要的,如图保护资源 M 的锁为 LM,就像我家大门的锁保护我家,你家大门的锁保护你家一样,如果程序出现类似我家大门锁保护你家的情况,那么就会导致诡异的并发问题了。
了解了锁的是什么与保护的是什么之后,我们看看怎么加锁的问题,还是用 count += 1 的例子,看代码:
class Test{
long value = 0L;
long get() {
return value;
}
synchronized void addOne() {
value += 1;
}
}
分析一下,这段代码中锁的是当前对象,要保护的资源是对象中的成员属性 value,这样的加锁方式开启10 个线程分别调用 10000次 addOne()方法,我们预期的结果是 value 最终会达到 100000,结果如何呢 ?
经过测试,addOne() 不加 synchronized 结果会出现小于 100000 的情况,加上 synchronized 结果符合我们的预期,针对测试结果,简要分析如下:
加锁之后,线程之间是互斥的,也就是说同一时刻只有一个线程执行,这样就原子性可以保证了。
那么可见性呢?一个线程操作结束后另一个线程能获取到上一个线程的操作结果吗?答案是肯定的,这就跟我们上一章说的 happen before 原则联系到一起了,“一个锁的解锁操作对另一个锁的加锁操作是可见的”,再结合传递性规则,一个锁在解锁前,对共享变量的修改,即解锁前对共享变量修改 happen before 于 这个锁的解锁,这个锁的解锁操作 happen before于另一个锁的加锁。
所以,解锁前对共享变量修改happen before于另一个锁的加锁,也就是说解锁前对共享变量修改对于另一个锁的加锁是可见的。
到这一切看似还挺完美,其实我们忽略了 get() 方法,多线程操作 get() 方法会是安全的吗?在没有任何前提操作的情况下,直接调用 get() 方法当然没问题,就是取值又不涉及修改。但是如果在执行 addOne() 方法后调用呢?显然,这时候 value 值的修改对 get() 方法是不可见的,happen before 中只说了锁的规则,这里要想保证可见性,对 get()方法也需要加上一把锁。代码如下:
class Test{
long value = 0L;
synchronized long get() {
return value;
}
synchronized void addOne() {
value += 1;
}
}
这里我们用同一把锁,保护了共享资源 value。说到这,我们根据资源关系来将使用锁的情况分为两种:
保护没有关系的多个资源
保护有关系的多个资源
对于 1 的情况,由于属性之间没有关系,每个资源都用一把锁来控制,例如修改账户的密码、修改余额操作,密码与余额是没有关系的资源,分别用两把锁来控制即可,这种锁叫做细粒度锁,使用不同的锁对受保护的资源进行精细化管理,可以提升性能。
对于 2 的情况 ,则需要粒度更大的锁去保护多个资源,看下面这段代码:
class Account {
private int balance;
// 转账
synchronized void transfer(
Account target, int amt){
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
乍一看,没问题,转账操作加了锁,妥妥的。其实则不然,看图就明白了:
现在这就是"用我家锁锁了你家"的典型例子,这时候临界区有多个资源,我们应该使用更大粒度的锁,看看这样改怎么样:
class Account {
private int balance;
// 转账
void transfer(Account target, int amt){
synchronized(Account.class) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
}
这里我们用 Account.class 作为更大粒度的锁是可行的, class 就是我们常说的 “类模板”,在 JVM 中只会加载一次,所以所有 Account 对象的类模板都是相同的,这样就能够保证用一把大锁锁住了有关系的共享资源。
问题是解决了,仔细一想,如果用 Account.class 作为锁,那岂不是所有的转账操作都是串行了,这样肯定是不行的,生活中转账肯定也不是串行的,如果串行那效率真的是很太差了。
正确的方式应该是这样的:
class Account {
//静态属性 替代 Account.class 作为一把大锁
private static Object lock = new Object();
private int balance;
// 转账
void transfer(Account target, int amt){
synchronized(lock) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}
}
这样一改,效率就上来了,问题也解决了,实际在开发中我们这也是我们最常用的加锁的方式,使用静态成员属性作为锁去保护有关系的多个资源。
总结:
我们从导致并发 bug 的原子性问题解决办法---加锁入手,了解了常规加锁方式背后的逻辑---锁的是什么与保护的是什么,与加锁后变量的传递性规则,到最后不同资源关系对应着不同的加锁方式---细粒度锁,粗粒度锁。
如果想了解更多关于锁知识,请看我的这篇文章: 聊聊锁机制
Java 中的 syncronized 你真的用对了吗的更多相关文章
- Java基础知识强化101:Java 中的 String对象真的不可变吗 ?
1. 什么是不可变对象? 众所周知, 在Java中, String类是不可变的.那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对 ...
- Java中volatile关键字你真的理解了吗?
面:你怎样理解volatile关键字时? 我:不加思索的说出,volatile修饰的成员变量,可保证线程可见性.不保证原子性和禁止指令重排. 面:你能谈谈什么是线程可见性吗? 我:各个线程对主内存中共 ...
- Java 中的 String 真的是不可变吗?
我们都知道 Java 中的 String 类的设计是不可变的,来看下 String 类的源码. public final class String implements java.io.Seriali ...
- Java中真的只有值传递么?
Java中真的只有值传递么? (本文非引战或diss,只是说出自己的理解,欢迎摆正心态观看或探讨) 回顾值传递和引用传递 关于Java是值传递还是引用传递,网上有不一样的说法. 1.基本类型或基本类型 ...
- 用好Java中的枚举真的没有那么简单
1.概览 在本文中,我们将看到什么是 Java 枚举,它们解决了哪些问题以及如何在实践中使用 Java 枚举实现一些设计模式. enum关键字在 java5 中引入,表示一种特殊类型的类,其总是继承j ...
- Java中LinkedList的remove方法真的耗时O(1)吗?
这个问题其实来源于Leetcode的一道题目,也就是上一篇日志 LRU Cache.在使用LinkedList超时后,换成ArrayList居然AC了,而问题居然是在于List.remove(Obje ...
- 你真的了解JAVA中与Webservice相关的规范和实现吗?
非常多人在项目中使用Webservice,仅仅是知道怎样公布Webservice,怎样调用Webservice,但真要论其来龙去脉,还真不一定清楚. 一切一切还要从我们伟大的sun公司规范说起. JA ...
- Java内存管理-你真的理解Java中的数据类型吗(十)
勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 作为Java程序员,Java 的数据类型这个是一定要知道的! 但是不管是那种数据类型最 ...
- 在Java中String类为什么要设计成final?String真的不可变吗?其他基本类型的包装类也是不可变的吗?
最近突然被问到String为什么被设计为不可变,当时有点懵,这个问题一直像bug一样存在,竟然没有发现,没有思考到,在此总结一下. 1.String的不可变String类被final修饰,是不可继承和 ...
随机推荐
- centos7单机安装kafka,进行生产者消费者测试
[转载请注明]: 原文出处:https://www.cnblogs.com/jstarseven/p/11364852.html 作者:jstarseven 码字挺辛苦的..... 一.k ...
- 依赖注入在 dotnet core 中实现与使用:1 基本概念
关于 Microsoft Extension: DependencyInjection 的介绍已经很多,但是多数偏重于实现原理和一些特定的实现场景.作为 dotnet core 的核心基石,这里准备全 ...
- 如何彻底禁用 werfalut.exe
在程序中调用 控制台程序 的时候,一旦出现控制台出现 crash 往往会弹出 werfault 窗口, 这样往往会锁死线程,导致程序无法继续运行. 那如何禁止 werfault 窗口的弹出呢? 在 s ...
- windows下用GCC编译DLL
此程序有3个文件,分别为 export.h .export.c .main.c export.h 文件内容 /*此头很有必要,别人在调用的时候知道有哪些方法*/ #ifdef BUILD_DLL #d ...
- Jenkins使用aqua-microscanner-plugin进行容器漏洞扫描
官方地址:https://github.com/jenkinsci/aqua-microscanner-plugin Step1 在jenkins安装"Aqua MicroScanner&q ...
- docker安装到基本使用
记录docker概念,安装及入门日常使用 Docker安装(Linux / Debian) 查看官方文档,在Debian上安装Docker,其他平台在这里查阅,以下均在root用户下操作,省去sudo ...
- 剑指Offer(十八):二叉树的镜像
剑指Offer(十八):二叉树的镜像 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/baidu ...
- Jupter NotebooK学习
1.参考资料 B站上学习视频 Jupyter 安装与使用 2.安装 在cmd窗口中输入(创建的文件会再当前的目录下):pip install jupyter 然后输入:jupyter notebook ...
- Maven项目添加ojdbc8
1.找到Oracle中的ojdbc8,它的位置在Oracle客户端 2.找到它的位置后,把你放ojdbc8的位置复制,改如下代码"D:\ojdbc8.jar"为你的ojdbc8位置 ...
- 利用ShardingSphere-JDBC实现分库分表--配置中心的实现
在之前的文章中我详细描述了如何利用ShardingSphere-JDBC进行分库分表,同时也实现了简单的精确分库算法接口,详情见下面的链接: 利用ShardingSphere-JDBC实现分库分表 但 ...