# 《并发设计模式》第09章-保护性暂挂模式-解决交易过程加锁的安全性问题

作者:冰河
星球:http://m6z.cn/6aeFbs (opens new window)
博客:https://binghe.gitcode.host (opens new window)
文章汇总:https://binghe.gitcode.host/md/all/all.html (opens new window)
源码获取地址:https://t.zsxq.com/0dhvFs5oR (opens new window)

沉淀,成长,突破,帮助他人,成就自我。

  • 本章难度:★★☆☆☆
  • 本章重点:掌握多线程下对共享资源的正确加锁方式,掌握根据业务关系分析加锁的安全性,重点掌握加锁在哪些场景是不安全的,在哪些场景是安全的,并能够将正确的加锁方式灵活应用到自身实际项目中。

大家好,我是冰河~~

在编写多线程并发程序时,我明明对共享资源加锁了啊?为什么还是出问题呢?问题到底出在哪里呢?其实,我想说的是:你的加锁姿势正确吗?你真的会使用锁吗?错误的加锁方式不但不能解决并发问题,而且还会带来各种诡异的Bug问题,有时难以复现!

# 一、故事背景

小菜在开发公司分配的交易系统转账功能时,在开发功能之前,将需求梳理清楚了,也很顺利的完成了功能开发,但交付测试后,实际的测试效果性能太差。

然而,老王看完小菜写的代码后,事情远远没有那么简单,随后老王为小菜讲解了线程的生命周期和状态流转过程。第2天,热心肠的老王又给小菜讲起了加锁的安全性问题。

# 二、一探究竟

这天,小菜来到公司后,还是在想着老王昨天给他讲的线程生命周期和状态流转过程,但是思来想去,小菜觉得只学这点知识貌似还是不能解决自己遇到的问题。

于是,等老王过来后,他主动来到老王的身边说:“老大,昨天你给我讲的线程生命周期和状态流转过程,我听懂了,但是我还是不能解决遇到的问题,可以再给我讲讲吗?”。

老王说到:“其实你写的代码不只是性能问题,在加锁安全性方面也存在问题,我想给你讲讲加锁的安全性问题吧”。

“好的”。

“讲完加锁的安全性问题,你可以尝试解决锁安全性的问题”。

“好的”。

“我们还是去会议室讲”。

于是,老王和小菜又一起走到了会议室。

# 三、分析场景

“其实,加锁到底是不是安全的,我们不能一概而论,要分不同的场景进行分析,接下来,我们就通过不同的场景来分析下加锁的安全性问题”。随后,老王便开始给小菜讲起了锁安全性的问题。

在分析多线程中如何使用同一把锁保护多个资源时,可以将其结合具体的业务场景来看,比如:需要保护的多个资源之间有没有直接的业务关系。如果需要保护的资源之间没有直接的业务关系,那么如何对其加锁;如果有直接的业务关系,那么如何对其加锁?接下来,我们就顺着这两个方向进行深入说明。

# 四、没有直接业务关系的场景

“我们先来看没有直接业务关系的场景”,老王说道。

“好的”。

于是老王便开始吧啦吧啦的讲起来。

假设我们的交易系统,有针对余额的付款操作,也有针对账户密码的修改操作。本质上,这两种操作之间没有直接的业务关系,此时,我们可以为账户的余额和账户密码分配不同的锁来解决并发问题。

例如,在支付宝账户AlipayAccount类中,有两个成员变量,分别是账户的余额balance和账户的密码password。付款操作的pay()方法和查看余额操作的getBalance()方法会访问账户中的成员变量balance,对此,我们可以创建一个balanceLock锁对象来保护balance资源;另外,更改密码操作的updatePassword()方法和查看密码的getPassowrd()方法会访问账户中的成员变量password,对此,我们可以创建一个passwordLock锁对象来保护password资源。

于是老王快速写了一个模拟交易的账户类AlipayAccount出来,AlipayAccount类的源码详见:concurrent-design-patterns-guarded-suspension工程下的io.binghe.concurrent.design.guarded.suspension.lock.right.AlipayAccount。

public class AlipayAccount {
    //保护balance资源的锁对象
    private final Object balanceLock = new Object();
    //保护password资源的锁对象
    private final Object passwordLock = new Object();
    //账户余额
    private Integer balance;
    //账户的密码
    private String password;

    //支付方法
    public void pay(Integer money){
        synchronized(balanceLock){
            if(this.balance >= money){
                this.balance -= money;
            }
        }
    }
    //查看账户中的余额
    public Integer getBalance(){
        synchronized(balanceLock){
            return this.balance;
        }
    }

    //修改账户的密码
    public void updatePassword(String password){
        synchronized(passwordLock){
            this.password = password;
        }
    }

    //查看账户的密码
    public String getPassword(){
        synchronized(passwordLock){
            return this.password;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

这里,我们也可以使用一把互斥锁来保护balance资源和password资源,例如都使用balanceLock锁对象,也可以都使用passwordLock锁对象,甚至也都可以使用this对象或者干脆每个方法前加一个synchronized关键字。

但是,如果都使用同一个锁对象的话,那么,程序的性能就太差了。会导致没有直接业务关系的各种操作都串行执行,这就违背了我们并发编程的初衷。实际上,我们使用两个锁对象分别保护balance资源和password资源,付款和修改账户密码是可以并行的。

# 查看全文

加入冰河技术 (opens new window)知识星球,解锁完整技术文章与完整代码