生活中随处可见并行的例子,并行 顾名思义就是一起进行的意思,同样的程序在某些时候也需要并行来提高效率,在上一篇文章中我们了解了 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. 保护有关系的多个资源

对于 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 你真的用对了吗的更多相关文章

  1. Java基础知识强化101:Java 中的 String对象真的不可变吗 ?

    1. 什么是不可变对象?       众所周知, 在Java中, String类是不可变的.那么到底什么是不可变的对象呢? 可以这样认为:如果一个对象,在它创建完成之后,不能再改变它的状态,那么这个对 ...

  2. Java中volatile关键字你真的理解了吗?

    面:你怎样理解volatile关键字时? 我:不加思索的说出,volatile修饰的成员变量,可保证线程可见性.不保证原子性和禁止指令重排. 面:你能谈谈什么是线程可见性吗? 我:各个线程对主内存中共 ...

  3. Java 中的 String 真的是不可变吗?

    我们都知道 Java 中的 String 类的设计是不可变的,来看下 String 类的源码. public final class String implements java.io.Seriali ...

  4. Java中真的只有值传递么?

    Java中真的只有值传递么? (本文非引战或diss,只是说出自己的理解,欢迎摆正心态观看或探讨) 回顾值传递和引用传递 关于Java是值传递还是引用传递,网上有不一样的说法. 1.基本类型或基本类型 ...

  5. 用好Java中的枚举真的没有那么简单

    1.概览 在本文中,我们将看到什么是 Java 枚举,它们解决了哪些问题以及如何在实践中使用 Java 枚举实现一些设计模式. enum关键字在 java5 中引入,表示一种特殊类型的类,其总是继承j ...

  6. Java中LinkedList的remove方法真的耗时O(1)吗?

    这个问题其实来源于Leetcode的一道题目,也就是上一篇日志 LRU Cache.在使用LinkedList超时后,换成ArrayList居然AC了,而问题居然是在于List.remove(Obje ...

  7. 你真的了解JAVA中与Webservice相关的规范和实现吗?

    非常多人在项目中使用Webservice,仅仅是知道怎样公布Webservice,怎样调用Webservice,但真要论其来龙去脉,还真不一定清楚. 一切一切还要从我们伟大的sun公司规范说起. JA ...

  8. Java内存管理-你真的理解Java中的数据类型吗(十)

    勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 作为Java程序员,Java 的数据类型这个是一定要知道的! 但是不管是那种数据类型最 ...

  9. 在Java中String类为什么要设计成final?String真的不可变吗?其他基本类型的包装类也是不可变的吗?

    最近突然被问到String为什么被设计为不可变,当时有点懵,这个问题一直像bug一样存在,竟然没有发现,没有思考到,在此总结一下. 1.String的不可变String类被final修饰,是不可继承和 ...

随机推荐

  1. cs231n--详解卷积神经网络

    原版地址:http://cs231n.github.io/convolutional-networks/ 知乎翻译地址:https://zhuanlan.zhihu.com/p/22038289?re ...

  2. [Spring cloud 一步步实现广告系统] 2. 配置&Eureka服务

    父项目管理 首先,我们在创建投放系统之前,先看一下我们的工程结构: mscx-ad-sponsor就是我们的广告投放系统.如上结构,我们需要首先创建一个Parent Project mscx-ad 来 ...

  3. Springboot源码分析之项目结构

    Springboot源码分析之项目结构 摘要: 无论是从IDEA还是其他的SDS开发工具亦或是https://start.spring.io/ 进行解压,我们都会得到同样的一个pom.xml文件 4. ...

  4. app登录接口请求报:“签名验证失败”???已解决

    根据抓包数据获得url.param.header,在charles中compose请求结果为成功,在pycharm中运行则报:“签名验证失败”. 运行结果:

  5. 使用Mybatis-Generator 自动生成代码

    前提:已经有SpringBoot的工程,且在上面实现了MyBatis的应用,只不过全是以手动方式创建mapper.xml映射文件.pojo类等. 在POM中添加MyBatis.generator依赖 ...

  6. React Native的APP打包教程

    1.改软件的名称 2.改软件的图标 3.给做好的项目打成APP包 改软件的名称 找到项目的改名的位置 然后用记事本打开strings.xml,然后改自己想要的名字 改软件的图标 找到如下5个文件,然后 ...

  7. Jmeter发送post请求报错Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported

    常识普及: Content-type,在Request Headers里面,告诉服务器,我们发送的请求信息格式,在JMeter中,信息头存储在信息头管理器中,所以在做接口测试的时候,我们维护Conte ...

  8. 初识JAVA语言

    推荐阅读:  我的CSDN  我的博客园  QQ群:704621321 前言        很多游戏开发者可能会有疑问,你会C#,JS,TS,为什么还要初识JAVA呢?有人可能会说,多学点对自己有好处 ...

  9. 设计模式(C#)——02抽象工厂模式

    推荐阅读:  我的CSDN  我的博客园  QQ群:704621321       在工厂模式中,一个工厂只能创建一种产品,但我们往往希望,一个工厂能创建一系列产品.很明显工厂模式已经不能满足我们的需 ...

  10. MSIL实用指南-类相关生成

    一.创建class用MethodBuilder的DefineType方法,可以指定父类,得到一个TypeBuilder对象. 二.实现继承接口用TypeBuilder的AddInterfaceImpl ...