文章系列

摘要

在上一篇文章02Java如何解决可见性和有序性问题当中,我们解决了可见性和有序性的问题,那么还有一个原子性问题咱们还没解决。在第一篇文章01并发编程的Bug源头当中,讲到了把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性,那么原子性的问题该如何解决。

同一时刻只有一个线程执行这个条件非常重要,我们称为互斥,如果能保护对共享变量的修改时互斥的,那么就能保住原子性。

简易锁

我们把一段需要互斥执行的代码称为临界区,线程进入临界区之前,首先尝试获取加锁,若加锁成功则可以进入临界区执行代码,否则就等待,直到持有锁的线程执行了解锁unlock()操作。如下图:

但是有两个点要我们理解清楚:我们的锁是什么?要保护的又是什么?

改进后的锁模型

在并发编程世界中,锁和锁要保护的资源是有对应关系的。

首先我们需要把临界区要保护的资源R标记出来,然后需要创建一把该资源的锁LR,最后针对这把锁,我们需要在进出临界区时添加加锁lock(LR)操作和解锁unlock(LR)操作。如下:

Java语言提供的锁技术:synchronized

synchronized可修饰方法和代码块。加锁lock()和解锁unlock()都会在synchronized修饰的方法或代码块前后自动加上加锁lock()和解锁unlock()操作。这样做的好处就是加锁和解锁操作会成对出现,毕竟忘了执行解锁unlock()操作可是会让其他线程死等下去。

那我们怎么去锁住需要保护的资源呢?在下面的代码中,add1()非静态方法锁定的是this对象(当前实例对象),add2()静态方法锁定的是X.class(当前类的Class对象)

public class X {

public synchronized void add1() {

// 临界区

}

public synchronized static void add2() {

// 临界区

}

}

上面的代码可以理解为这样:

public class X {

public synchronized(this) void add() {

// 临界区

}

public synchronized(X.class) static void add2() {

// 临界区

}

}

使用synchronized 解决 count += 1 问题

在01 并发编程的Bug源头文章当中,我们提到过count += 1 存在的并发问题,现在我们尝试使用synchronized解决该问题。

public class Calc {

private int value = 0;

public synchronized int get() {

return value;

}

public synchronized void addOne() {

value += 1;

}

}

addOne()方法被synchronized修饰后,只有一个线程能执行,所以一定能保证原子性,那么可见性问题呢?在上一篇文章02 Java如何解决可见性和有序性问题当中,提到了管程中的锁规则,一个锁的解锁 Happens-Before 于后续对这个锁的加锁。管程,在这里就是synchronized(管程的在后续的文章中介绍)。根据这个规则,前一个线程执行了value += 1操作是对后续线程可见的。而查看get()方法也必须加上synchronized修饰,否则也没法保证其可见性。

上面这个例子如下图:

那么可以使用多个锁保护一个资源吗,修改一下上面的例子后,get()方法使用this对象锁来保护资源value,addOne()方法使用Calc.class类对象来保护资源value,代码如下:

public class Calc {

private static int value = 0;

public synchronized int get() {

return value;

}

public static synchronized void addOne() {

value += 1;

}

}

上面的例子用图来表示:

在这个例子当中,get()方法使用的是this锁,addOne()方法使用的是Calc.class锁,因此这两个临界区(方法)并没有互斥性,addOne()方法的修改对get()方法是不可见的,所以就会导致并发问题。

结论:不可使用多把锁保护一个资源,但能使用一把锁保护多个资源(这里没写例子,只写了一把锁保护一个资源)

保护没有关联关系的多个资源

在银行的业务当中,修改密码和取款是两个再经常不过的操作了,修改密码操作和取款操作是没有关联关系的,没有关联关系的资源我们可以使用不同的互斥锁来解决并发问题。代码如下:

public class Account {

// 保护密码的锁

private final Object pwLock = new Object();

// 密码

private String password;

// 保护余额的锁

private final Object moneyLock = new Object();

// 余额

private Long money;

public void updatePassword(String password) {

synchronized (pwLock) {

// 修改密码

}

}

public void withdrawals(Long money) {

synchronized (moneyLock) {

// 取款

}

}

}

分别使用pwLock和moneyLock来保护密码和余额,这样修改密码和修改余额就可以并行了。使用不同的锁对受保护的资源进行进行更细化管理,能够提升性能,这种锁叫做细粒度锁。

在这个例子当中,你可能发现我使用了final Object来当成一把锁,这里解释一下:使用锁必须是不可变对象,若把可变对象作为锁,当可变对象被修改时相当于换锁,而且使用Long或Integer作为锁时,在-128到127之间时,会使用缓存,详情可查看他们的valueOf()方法。

保护有关联关系的多个资源

在银行业务当中,除了修改密码和取款的操作比较多之外,还有一个操作比较多的功能就是转账。账户 A 转账给 账户B 100元,账户A的余额减少100元,账户B的余额增加100元,那么这两个账户就是有关联关系的。在没有理解互斥锁之前,写出的代码可能如下:

public class Account {

// 余额

private Long money;

public synchronized void transfer(Account target, Long money) {

this.money -= money;

if (this.money < 0) {

// throw exception

}

target.money += money;

}

}

在转账transfer方法当中,锁定的是this对象(用户A),那么这里的目标用户target(用户B)的能被锁定吗?当然不能。这两个对象是没有关联关系的。正确的操作应该是获取this锁和target锁才能去进行转账操作,正确的代码如下:

public class Account {

// 余额

private Long money;

public synchronized void transfer(Account target, Long money) {

synchronized(this) {

synchronized (target) {

this.money -= money;

if (this.money < 0) {

// throw exception

}

target.money += money;

}

}

}

}

在这个例子当中,我们需要清晰的明白要保护的资源是什么,只要我们的锁能覆盖所有受保护的资源就可以了。

但是你以为这个例子很完美?那就错了,这里面很有可能会发生死锁。你看出来了吗?下一篇文章我就用这个例子来聊聊死锁。

总结

使用互斥锁最最重要的是:我们的锁是什么?锁要保护的资源是什么?,要理清楚这两点就好下手了。而且锁必须为不可变对象。使用不同的锁保护不同的资源,可以细化管理,提升性能,称为细粒度锁。

如果我的文章帮助到您,可以关注我的微信公众号,第一时间分享文章给您

java的尝试性问题_Java并发编程实战 03互斥锁 解决原子性问题相关推荐

  1. Java并发编程实战之互斥锁

    文章目录 Java并发编程实战之互斥锁 如何解决原子性问题? 锁模型 Java synchronized 关键字 Java synchronized 关键字 只能解决原子性问题? 如何正确使用Java ...

  2. c++并发编程实战_Java 并发编程实战:JAVA中断线程几种基本方法

    一个多线程Java程序,只有当其全部线程执行结束时(更具体地说,是所有非守护线程结束或者某个线程调用system.exit()方法的时候) ,才会结束运行.有时,为了终止程序或者取消一个线程对象所执行 ...

  3. java volatile 原子性_Java并发编程之验证volatile不能保证原子性

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

  4. java计算时间差_JAVA并发编程三大Bug源头(可见性、原子性、有序性),彻底弄懂...

    原创声明:本文转载自公众号[胖滚猪学编程]​ 某日,胖滚猪写的代码导致了一个生产bug,奋战到凌晨三点依旧没有解决问题.胖滚熊一看,只用了一个volatile就解决了.并告知胖滚猪,这是并发编程导致的 ...

  5. java判断一个月连续打卡时间_java并发编程实战《五》死锁 挑战打卡60天

    一不小心就死锁了,怎么办? 在上一篇文章中,我们用 Account.class 作为互斥锁,来解决银行业务里面的转账问题,虽然这个方案不存在并发问题,但是所有账户的转账操作都是串行的,性能太差. 向现 ...

  6. java并发编程 目录_Java并发编程实战的作品目录

    展开全部 对本书的赞誉 译者序 前 言 第1章 简介 1.1 并发简史 1.2 线程的优势 1.2.1 发挥多处理器的强大能力e5a48de588b662616964757a686964616f313 ...

  7. 轻量级锁_并发编程实战05:锁的状态

    无锁.偏向锁 .轻量级锁和重量级锁这四种锁是指锁的状态,专门针对synchronized的.在介绍这四种锁状态之前还需要介绍一些额外的知识. 首先为什么Synchronized能实现线程同步?在回答这 ...

  8. java线程同步的实现_Java并发编程(三) - 实战:线程同步的实现

    synchronized关键字 首先,来看一个多线程竞争临界资源导致的同步不安全问题. package com.example.weishj.mytester.concurrency.sync; /* ...

  9. java 线程安全的原因_Java并发编程——线程安全性深层原因

    线程安全性深层原因 这里我们将会从计算机硬件和编辑器等方面来详细了解线程安全产生的深层原因. 缓存一致性问题 CPU内存架构 随着CPU的发展,而因为CPU的速度和内存速度不匹配的问题(CPU寄存器的 ...

最新文章

  1. 如何禁止浏览器自动填充
  2. kettle分批处理大表数据_kettle 分批次拿数据库
  3. 吵架后一个老公的检讨书(超经典)
  4. python中数据分析的流程为-python数据分析011_数据分析流程
  5. 【vue-number-scroll】数字逐渐增加或者减少的滚动解决方案
  6. 信息学奥赛C++语言: 第n小的质数
  7. 一次项目测评反思:数据准备、测评要求和各种问题记录
  8. 矩阵分析 第三章 内积空间 正规矩阵 Hermite矩阵
  9. HDU1370 Biorhythms【中国剩余定理】
  10. 案例分享:Qt的PPT播放器
  11. python 用余弦值反算出角度
  12. 线程系列2---线程同步
  13. 字符输出流 (Write)
  14. 【转】伽马校正(Gamma Correction)
  15. 计算机网络(谢希仁-第八版)第五章习题全解
  16. 蓝桥杯--封印之门( Floyd算法)
  17. [Kaggle]图片去噪题解阅读笔记
  18. js获取URL后面参数
  19. docker容器添加微软雅黑字体
  20. 干式电抗器gim模型要求

热门文章

  1. JVM性能优化, Part 2 ―― 编译器
  2. 祭奠IT男孩大学的生活
  3. Mocha BSM产品亮点——关联事件分析
  4. 加密货币支付卡公司与BCH达成合作
  5. 使用Typescript重写axios
  6. 教师课堂教学必备的100个妙招,总有一个适合你!
  7. 官宣!微软宣布桌面版 Edge将基于Chromium进行开发\n
  8. Hibernate5.x Eclipse搭建
  9. Android--向SD卡读写数据
  10. hdu1395 数论 欧拉函数