原创声明:本文转载自公众号【胖滚猪学编程】,转载务必注明出处!

并发编程BUG源头文章中,我们初识了并发编程的三个bug源头:可见性、原子性、有序性。在如何解决可见性和原子性文章中我们大致了解了可见性和有序性的解决思路,今天轮到最后一个大bug,那就是原子性。

知识回顾

锁模型



JAVA中的锁模型

锁是一种通用的技术方案,Java 语言提供的 synchronized 关键字,就是锁的一种实现。

  • synchronized 是独占锁/排他锁(就是有你没我的意思),但是注意!synchronized并不能改变CPU时间片切换的特点,只是当其他线程要访问这个资源时,发现锁还未释放,所以只能在外面等待。
  • synchronized一定能保证原子性,因为被 synchronized 修饰某段代码后,无论是单核 CPU 还是多核 CPU,只有一个线程能够执行该代码,所以一定能保证原子操作
  • synchronized也能够保证可见性和有序性。根据前第二篇文章:Happens-Before 规则之管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。即前一个线程的解锁操作对后一个线程的加锁操作可见。综合 Happens-Before 的传递性原则,我们就能得出前一个线程在临界区修改的共享变量(该操作在解锁之前),对后续进入临界区(该操作在加锁之后)的线程是可见的。- synchronized 关键字可以用来修饰静态方法,非静态方法,也可以用来修饰代码块

理论说完了,来点实际的吧!首先我们用synchronized 修饰非静态方法来改写第一章中原子性问题的那段代码:

    private long count = 0;

    // 修饰非静态方法 当修饰非静态方法的时候,锁定的是当前实例对象 this。
// 当该类中有多个普通方法被Synchronized修饰(同步),那么这些方法的锁都是这个类的一个对象this。多个线程访问这些方法时,如果这些线程调用方法时使用的是同一个该类的对象,虽然他们访问不同方法,但是他们使用同一个对象来调用,那么这些方法的锁就是一样的,就是这个对象,那么会造成阻塞。如果多个线程通过不同的对象来调用方法,那么他们的锁就是不一样的,不会造成阻塞。
private synchronized void add10K(){
int start = 0;
while (start ++ < 10000){
this.count ++;
}
} public static void main(String[] args) throws InterruptedException {
TestSynchronized2 test = new TestSynchronized2();
// 创建两个线程,执行 add() 操作
Thread th1 = new Thread(()->{
test.add10K();
});
Thread th2 = new Thread(()->{
test.add10K();
});
// 启动两个线程
th1.start();th2.start();
// 等待两个线程执行结束
th1.join();th2.join();
System.out.println(test.count);
}

运行一下吧!你会发现永远都可以达到我们想要的效果了~

除了上面代码中修饰非静态方法,还可以修饰静态方法和代码块

    // 修饰静态方法 当修饰静态方法的时候,锁定的是当前类的 Class 对象,即TestSynchronized2.class 。这个范围就比对象锁大。这里就算是不同对象,但是只要是该类的对象,就使用的是同一把锁。
synchronized static void bar() {
// 临界区
}
// 修饰代码块 java中经典的双重锁检查机制
private volatile static TestSynchronized2 instance;
public static TestSynchronized2 getInstance() {
if (instance == null) {
synchronized (TestSynchronized2.class) {
if (instance == null) {
instance = new TestSynchronized2();
}
}
}
return instance;
}

明确锁和资源的关系

深入分析锁定的对象和受保护资源的关系,综合考虑受保护资源的访问路径,多方面考量才能用好互斥锁。受保护资源和锁之间的关联关系是 N:1 的关系。如果一个资源用N个锁,那肯定出问题的,就好像一个厕所坑位,你有10把钥匙,那不是可以10个人同时进了?

现在给出两段错误代码,想一想到底为啥错了吧?

    static long value1 = 0L;

    synchronized long get1() {
return value1;
} synchronized static void addOne1() {
value1 += 1;
}
    long value = 0L;

    long get() {
synchronized (new Object()) {
return value;
}
}

第一段错误原因:

因为我们说过synchronized修饰普通方法 锁定的是当前实例对象 this 而修饰静态方法 锁定的是当前类的 Class 对象

所以这里有两把锁 分别是 this 和 TestSynchronized3.class

由于临界区 get() 和 addOne() 是用两个锁保护的,因此这两个临界区没有互斥关系,临界区 addOne() 对 value 的修改对临界区 get() 也没有可见性保证,这就导致并发问题了。

第二段错误原因:

加锁本质就是在锁对象的对象头中写入当前线程id,但是synchronized (new Object())每次在内存中都是新对象,所以加锁无效。

问:刚刚的例子都是多个锁保护一个资源,这样百分百是不行的。那么一个锁保护多个资源,就一定可以了吗?

答:如果多个资源彼此之间是没有关联的,那可以用一个锁来保护。如果有关联的话,那是不行的。比如说银行转账操作,你给我转账,我账户多100,你账户少100,我不能用我的锁来保护你,就像现实生活中我的锁是不能保护你的财产的。

划重点!要区分多个资源是否有关联!但是一个锁保护多个没关联的资源,未免性能太差了哦,比如我听歌和玩游戏可以同时进行,你非得让我做完一个再做另一个,岂不是要双倍时间。所以即使一个锁可以保护多个没关联的资源,但是一般而已,会各自用不同的锁,能够提升性能。这种锁还有个名字,叫细粒度锁。

问:刚刚说到银行转账的案例,那么假如某天在某银行同时发生这样一个事,柜员小王需要完成A账户给B账户转账100元,柜员小李需要完成B账户给A账户转账100元,请问如何实现呢?

答:其实用两把锁就实现了,转出一把,转入另一把。只有当两者都成功时,才执行转账操作。

    public static void main(String[] args) throws InterruptedException {
Account a = new Account(200); //A的初始账户余额200
Account b = new Account(300); //B的初始账户余额200
Thread threadA = new Thread(()->{
try {
transfer(a,b,100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread threadB = new Thread(()->{
try {
transfer(b,a,100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threadA.start();
threadB.start();
} static void transfer(Account source,Account target, int amt) throws InterruptedException {
synchronized (source) {
log.info("持有锁{} 等待锁{}",source,target);
synchronized (target) {
if (source.getBalance() > amt) {
source.setBalance(source.getBalance() - amt);
target.setBalance(target.getBalance() + amt);
}
}
}
}

至此,恭喜你,一波问题解决了,可是遗憾的告诉你:又导致了另一个bug。这段代码是有可能发生死锁的!并发编程中要注意的东西可真是多哟。咱们先把死锁这个名词记住!持续关注【胖滚猪学编程】公众号!在我们后面的文章中找答案!

如何保证原子性

现在我们已经知道互斥锁可以保证原子性,也知道了如何使用synchronized来保证原子性。但synchronized 并不是JAVA中唯一能保证原子性的方案。

如果你粗略的看一下J.U.C(java.util.concurrent包),那么你可以很显眼的发现它俩:

一个是lock包,一个是atomic包,只要你英语过了四级。。我相信你都可以马上断定,它们可以解决原子性问题。

由于这两个包比较重要,所以会放在后面的模块单独说,持续关注【胖滚猪学编程】公众号吧!

本文转载自公众号【胖滚猪学编程】 用漫画让编程so easy and interesting!欢迎关注!形象来源于微信表情包【胖滚家族】喜欢可以下载哦~

【漫画】JAVA并发编程 如何解决原子性问题的更多相关文章

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

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

  2. Java并发编程实战 03互斥锁 解决原子性问题

    文章系列 Java并发编程实战 01并发编程的Bug源头 Java并发编程实战 02Java如何解决可见性和有序性问题 摘要 在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和 ...

  3. 【漫画】JAVA并发编程之并发模拟工具

    原创声明:本文来源于公众号[胖滚猪学编程],转载请注明出处. 上一节[漫画]JAVA并发编程三大Bug源头(可见性.原子性.有序性)我们聊了聊并发编程的三个bug源头,这还没开始进入并发世界,胖滚猪就 ...

  4. Java并发编程入门与高并发面试(三):线程安全性-原子性-CAS(CAS的ABA问题)

    摘要:本文介绍线程的安全性,原子性,java.lang.Number包下的类与CAS操作,synchronized锁,和原子性操作各方法间的对比. 线程安全性 线程安全? 线程安全性? 原子性 Ato ...

  5. Java并发编程之验证volatile不能保证原子性

    Java并发编程之验证volatile不能保证原子性 通过系列文章的学习,凯哥已经介绍了volatile的三大特性.1:保证可见性 2:不保证原子性 3:保证顺序.那么怎么来验证可见性呢?本文凯哥(凯 ...

  6. Java并发编程实战 02Java如何解决可见性和有序性问题

    摘要 在上一篇文章当中,讲到了CPU缓存导致可见性.线程切换导致了原子性.编译优化导致了有序性问题.那么这篇文章就先解决其中的可见性和有序性问题,引出了今天的主角:Java内存模型(面试并发的时候会经 ...

  7. Java 并发编程(二):如何保证共享变量的原子性?

    线程安全性是我们在进行 Java 并发编程的时候必须要先考虑清楚的一个问题.这个类在单线程环境下是没有问题的,那么我们就能确保它在多线程并发的情况下表现出正确的行为吗? 我这个人,在没有副业之前,一心 ...

  8. Java并发编程 | 从进程、线程到并发问题实例解决

    计划写几篇文章讲述下Java并发编程,帮助一些初学者成体系的理解并发编程并实际使用,而不只是碎片化的了解一些Synchronized.ReentrantLock等技术点.在讲述的过程中,也想融入一些相 ...

  9. Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...

随机推荐

  1. B. 复读机的力量

    我们规定一个人是复读机当且仅当他说的每一句话都是复读前一个人说的话. 我们规定一个人是复读机当且仅当他说的每一句话都是复读前一个人说的话. 我们规定一个人是复读机当且仅当他说的每一句话都是复读前一个人 ...

  2. Linux学习笔记(六)压缩和解压缩命令

    压缩和解压缩命令 zip unzip gzip gunzip bzip2 bunzip2 tar zip (.zip格式的压缩文件) 英文原意:package and compress (archiv ...

  3. webpack 中常用安装插件的一些命令

    1:npm install html-webpack-plugin --save-dev //自动快速的帮我们生成HTML.2:npm install css-loader style-loader  ...

  4. [linux] linux的top命令参数详解

    简介 top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器. top显示系统当前的进程和其他状况,是一个动态显示过程,即可以通过用户按 ...

  5. redis的多路复用是什么鬼

    有没有人和我一样, 自打知道了redis, 就一直听说什么redis单线程, 使用了多路复用等等. 天真的我以为多路复用是redis实现的技术. 今天才发现, 我被自己骗了, 多路复用是系统来实现的. ...

  6. GC日志分析详解

    点击返回上层目录 原创声明:作者:Arnold.zhao 博客园地址:https://www.cnblogs.com/zh94 GC日志分析详解 以ParallelGC为例,YoungGC日志解释如下 ...

  7. 尤雨溪在直播中讲到的Vue3.0 Beta的那些特性,快记笔记了

    前言 在那天风雨交加的夜晚,Vue的创作者尤雨溪尤大大在b站直播分享了Vue.js 3.0 Beta最新进展.我对直播的内容进行了一下整理.整整用了三天的空余时间赶上了 1. 全新文档RFCs Vue ...

  8. python入门学习之Python爬取最新笔趣阁小说

    Python爬取新笔趣阁小说,并保存到TXT文件中      我写的这篇文章,是利用Python爬取小说编写的程序,这是我学习Python爬虫当中自己独立写的第一个程序,中途也遇到了一些困难,但是最后 ...

  9. Python爬虫入门(基础实战)—— 模拟登录知乎

    模拟登录知乎 这几天在研究模拟登录, 以知乎 - 与世界分享你的知识.经验和见解为例.实现过程遇到不少疑问,借鉴了知乎xchaoinfo的代码,万分感激! 知乎登录分为邮箱登录和手机登录两种方式,通过 ...

  10. php正则匹配到字符串里面的a标签

    $cont = preg_replace('/<a href=\"(.*?)\".*?>(.*?)<\/a>/i','',$cont);