生活中随处可见并行的例子,并行 顾名思义就是一起进行的意思,同样的程序在某些时候也需要并行来提高效率,在上一篇文章中我们了解了 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. centos7单机安装kafka,进行生产者消费者测试

    [转载请注明]: 原文出处:https://www.cnblogs.com/jstarseven/p/11364852.html   作者:jstarseven    码字挺辛苦的.....  一.k ...

  2. 依赖注入在 dotnet core 中实现与使用:1 基本概念

    关于 Microsoft Extension: DependencyInjection 的介绍已经很多,但是多数偏重于实现原理和一些特定的实现场景.作为 dotnet core 的核心基石,这里准备全 ...

  3. 如何彻底禁用 werfalut.exe

    在程序中调用 控制台程序 的时候,一旦出现控制台出现 crash 往往会弹出 werfault 窗口, 这样往往会锁死线程,导致程序无法继续运行. 那如何禁止 werfault 窗口的弹出呢? 在 s ...

  4. windows下用GCC编译DLL

    此程序有3个文件,分别为 export.h .export.c .main.c export.h 文件内容 /*此头很有必要,别人在调用的时候知道有哪些方法*/ #ifdef BUILD_DLL #d ...

  5. Jenkins使用aqua-microscanner-plugin进行容器漏洞扫描

    官方地址:https://github.com/jenkinsci/aqua-microscanner-plugin Step1 在jenkins安装"Aqua MicroScanner&q ...

  6. docker安装到基本使用

    记录docker概念,安装及入门日常使用 Docker安装(Linux / Debian) 查看官方文档,在Debian上安装Docker,其他平台在这里查阅,以下均在root用户下操作,省去sudo ...

  7. 剑指Offer(十八):二叉树的镜像

    剑指Offer(十八):二叉树的镜像 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.net/baidu ...

  8. Jupter NotebooK学习

    1.参考资料 B站上学习视频 Jupyter 安装与使用 2.安装 在cmd窗口中输入(创建的文件会再当前的目录下):pip install jupyter 然后输入:jupyter notebook ...

  9. Maven项目添加ojdbc8

    1.找到Oracle中的ojdbc8,它的位置在Oracle客户端 2.找到它的位置后,把你放ojdbc8的位置复制,改如下代码"D:\ojdbc8.jar"为你的ojdbc8位置 ...

  10. 利用ShardingSphere-JDBC实现分库分表--配置中心的实现

    在之前的文章中我详细描述了如何利用ShardingSphere-JDBC进行分库分表,同时也实现了简单的精确分库算法接口,详情见下面的链接: 利用ShardingSphere-JDBC实现分库分表 但 ...