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

classTest{long value = 0L;longget() {returnvalue;

}synchronized voidaddOne() {

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()方法也需要加上一把锁。代码如下:

classTest{long value = 0L;synchronized longget() {returnvalue;

}synchronized voidaddOne() {

value+= 1;

}

}

这里我们用同一把锁,保护了共享资源 value。说到这,我们根据资源关系来将使用锁的情况分为两种:

保护没有关系的多个资源

保护有关系的多个资源

对于 1 的情况,由于属性之间没有关系,每个资源都用一把锁来控制,例如修改账户的密码、修改余额操作,密码与余额是没有关系的资源,分别用两把锁来控制即可,这种锁叫做细粒度锁,使用不同的锁对受保护的资源进行精细化管理,可以提升性能。

对于 2 的情况 ,则需要粒度更大的锁去保护多个资源,看下面这段代码:

classAccount {private intbalance;//转账

synchronized voidtransfer(

Account target,intamt){if (this.balance >amt) {this.balance -=amt;

target.balance+=amt;

}

}

}

乍一看,没问题,转账操作加了锁,妥妥的。其实则不然,看图就明白了:

现在这就是"用我家锁锁了你家"的典型例子,这时候临界区有多个资源,我们应该使用更大粒度的锁,看看这样改怎么样:

classAccount {private intbalance;//转账

void transfer(Account target, intamt){synchronized(Account.class) {if (this.balance >amt) {this.balance -=amt;

target.balance+=amt;

}

}

}

}

这里我们用 Account.class 作为更大粒度的锁是可行的, class 就是我们常说的 “类模板”,在 JVM 中只会加载一次,所以所有 Account 对象的类模板都是相同的,这样就能够保证用一把大锁锁住了有关系的共享资源。

问题是解决了,仔细一想,如果用 Account.class 作为锁,那岂不是所有的转账操作都是串行了,这样肯定是不行的,生活中转账肯定也不是串行的,如果串行那效率真的是很太差了。

正确的方式应该是这样的:

classAccount {//静态属性 替代 Account.class 作为一把大锁

private static Object lock = newObject();private intbalance;//转账

void transfer(Account target, intamt){synchronized(lock) {if (this.balance >amt) {this.balance -=amt;

target.balance+=amt;

}

}

}

这样一改,效率就上来了,问题也解决了,实际在开发中我们这也是我们最常用的加锁的方式,使用静态成员属性作为锁去保护有关系的多个资源。

总结:

我们从导致并发 bug 的原子性问题解决办法---加锁入手,了解了常规加锁方式背后的逻辑---锁的是什么与保护的是什么,与加锁后变量的传递性规则,到最后不同资源关系对应着不同的加锁方式---细粒度锁,粗粒度锁。

如果想了解更多关于锁知识,请看我的这篇文章: 聊聊锁机制

java中 d_Java 中的 syncronized 你真的用对了吗相关推荐

  1. java中的console是干什么的_[Java教程]javascript中,你真的会用console吗?

    [Java教程]javascript中,你真的会用console吗? 0 2015-08-11 17:00:09 使用console进行性能测试和计算代码运行时间 对于前端开发人员,在开发过程中经常需 ...

  2. Java Math 类中的新功能--浮点数

    Java™语言规范第 5 版向 java.lang.Math和 java.lang.StrictMath添加了 10 种新方法,Java 6 又添加了 10 种.这个共两部分的系列文章的 第 1 部分 ...

  3. Java并发编程中的若干核心技术,向高手进阶

    来源:http://www.jianshu.com/p/5f499f8212e7 引言 本文试图从一个更高的视角来总结Java语言中的并发编程内容,希望阅读完本文之后,可以收获一些内容,至少应该知道在 ...

  4. [Google Guava] 2.3-强大的集合工具类:java.util.Collections中未包含的集合工具

    原文链接 译文链接 译者:沈义扬,校对:丁一 尚未完成: Queues, Tables工具类 任何对JDK集合框架有经验的程序员都熟悉和喜欢java.util.Collections包含的工具方法.G ...

  5. 在 Java EE 组件中使用 Camel Routes

    2019独角兽企业重金招聘Python工程师标准>>> 摘要:你可以通过集成 Camel 和 WildFly 应用服务器(使用 WildFly-Camel 子系统)在 Java EE ...

  6. java面试题32:Java网络程序设计中,下列正确的描述是()

    java面试题32:Java网络程序设计中,下列正确的描述是() A:Java网络编程API建立在Socket基础之上 B:Java网络接口只支持tcP以及其上层协议 C:Java网络接口只支持UDP ...

  7. Anders Hejlsberg谈C#、Java和C++中的泛型

    Anders Hejlsberg谈C#.Java和C++中的泛型 [翻译] lover_P 2004-03-25 ------------------------------------------- ...

  8. 相对于java,C++中的那些神奇语法

    空指针还可以调用成员函数 #include <cstdio>class Person {public:void sayHello() {printf("hello!\n" ...

  9. 你了解Java应用开发中的注入攻击吗?

    第31讲 | 你了解Java应用开发中的注入攻击吗? 安全是软件开发领域永远的主题之一,随着新技术浪潮的兴起,安全的重要性愈发凸显出来,对于金融等行业,甚至可以说安全是企业的生命线.不论是移动设备.普 ...

最新文章

  1. html component标签,(九)Component标签
  2. BlogEngine.net 学习笔记(一)
  3. 线下零售企业在数据驱动上的三个挑战和三条思路
  4. mos 多路模拟电子开关_【原创】单火线智能开关技术介绍及分析
  5. mysql用sql语句怎么做个脚本备份_mysql备份脚本
  6. linux创建定时任务命令,linux设置定时任务的方法步骤
  7. 从零开始学keras之卷积神经网络介绍
  8. 每天一道Java题[4]
  9. Swift基础一(代码)
  10. python不会英语不会数学怎么自学-学习Python数学英语基础重要吗?
  11. [导入]在asp.net中利用FileUplad控件从同一个页面上传多个文件
  12. 基于javaweb+SpringBoot+MyBatis网上书店管理系统在线购书系统(前台、后台)
  13. 最新Oreo支付系统平台完整源码+已全开源
  14. android集成建行龙支付,龙支付及建行信用卡分期支付SDK
  15. 信息科学技术与创新之“信息纽带”
  16. Linkerd 使用指南
  17. LaTeX 相对于 Word 有什么优势?
  18. JAVA对象布局之对象头(Object Header)
  19. 快速入门 | 篇十四:运动控制器基础轴参数与基础运动控制指令
  20. 雅虎的站长天下要关门了,哎,真是悲哀

热门文章

  1. 左神算法课笔记(二):链表、栈和队列、递归Master公式、哈希表、有序表
  2. 【Python】判断字符串中是否含有某个子串
  3. 【JavaScript】JS的变量、数组、计算器案例、函数、类、常用对象的方法
  4. 看动画学算法之:二叉堆Binary Heap
  5. c语言pta按等级统计学生成绩,第九周作业
  6. 中标麒麟linux系统安装打印机_中标软件+天津麒麟=中国国产操作系统新旗舰
  7. memcached和redis的区别
  8. eval?python顺序列表模拟栈实现计算器
  9. 使类和成员的可访问性最小化
  10. 保姆级解读!CSS属性transform变形+2D转换+3D转换实例+代码+解析——Web前端系列学习笔记