04 - 互斥锁(下):用一把锁保护多个资源

 

目录

 

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

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

3. 使用锁的正确姿势

4. 总结


在上文中,我们已经知道受保护资源和锁之间合理的关联关系应该是N:1 的关系,也就是说可以用一把锁保护多个资源,但是不能用多把锁保护一个资源,今天我们就来聊聊如何保护多个资源。

当我们要保护多个资源时,首先要区分这些资源是否存在关联

 

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

在编程领域,对于没有关联关系的资源,是很容易解决的。例如,银行业务中有针对账户余额的取款操作,也有针对账户密码的更改操作,我们可以为账户余额和账户密码分配不同的锁来解决并发问题。

相关的示例代码如下,账户类 Account 有两个成员变量,分别是账户余额 balance 和账户密码 password。取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance,我们创建一个 final 对象 balLock 作为锁(类比球赛门票);而更改密码 updatePassword() 和查看密码 getPassword() 操作会修改账户密码 password,我们创建一个 final 对象 pwLock 作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的,很简单。

 

class Account {
    // 锁:保护账户余额
    private final Object balLock = new Object();

    // 账户余额  
    private Integer balance;
  
    // 锁:保护账户密码
    private final Object pwLock = new Object();

    // 账户密码
    private String password;

    // 取款
    void withdraw(Integer amt) {
        synchronized (balLock) {
            if (this.balance > amt) {
                this.balance -= amt;
            }
        }
    }

    // 查看余额
    Integer getBalance() {
        synchronized (balLock) {
            return balance;
        }
    }

    // 更改密码
    void updatePassword(String pw) {
        synchronized (pwLock) {
            this.password = pw;
        }
    }

    // 查看密码
    String getPassword() {
        synchronized (pwLock) {
            return password;
        }
    }
}

 

当然,我们也可以用一把互斥锁来保护多个资源,如我们可以用 this 这一把锁来管理账户里面的所有资源,但是用一把锁有个问题,就是性能太差,会导致取款、查看余额、修改密码、查看密码都是串行的,而用两把锁,取款和修改密码可以是并行的。用不同的锁对受保护资源进行精细化管理,能够提升性能。这种锁就叫细粒度锁。

 

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

如果多个资源是有联系关系的,这个问题就有点复杂了。如银行转账业务,账户A 减少100元,账户B 增加100元。这两个账户是有关联关系的。那对于像转账这种有关联关系的操作,我们应该怎么去解决呢?先把这个问题代码化。我们声明了个账户类:Account,该类有一个成员变量余额:balance,还有一个用于转账的方法:transfer(),然后怎么保证转账操作 transfer() 没有并发问题呢?。如下代码:

 

class Account {
    private int balance;

    // 转账
    void transfer(
            Account target, int amt) {
        if (this.balance > amt) {
            this.balance -= amt;
            target.balance += amt;
        }
    }
}

 

直觉可能告诉我们,使用 Synchronized 关键字修饰一下 transfer() 方法就好了,在上面加了 synchronized 关键字修饰的代码中。

 

class Account {
    private int balance;

    // 转账
    synchronized void transfer(
            Account target, int amt) {
        if (this.balance > amt) {
            this.balance -= amt;
            target.balance += amt;
        }
    }
}

 

在这段代码中,临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance,并且用的是一把锁 this,符合我们前面提到的,多个资源可以用一把锁来保护,这看上去完全正确呀。真的是这样吗?可惜,这个方案仅仅是看似正确,为什么呢?

问题就出在 this 这把锁上,this 这把锁可以保护自己的余额 this.balance,却保护不了别人的余额 target.balance,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。

 

下面我们具体分析一下,假设有A、B、C 三个账户,余额都是200元,我们用两个线程分别执行两个转账操作:账户A 转给账户B 100元,账户B 给账户C 转账100元,我们期望的结果是账户A 余额100元,账户B 余额200元,账户C 的余额300元。

 

我们假设线程1执行账户A 转账给账户B 的操作,线程2执行账户B 转换给账户C 的操作。这两个线程分别在两颗CPU 上执行,那它们是互斥的么? 我们期望是,但实际上不是。因为线程1 锁定的是账户A 的实例(A.this),而线程2 锁定的是账户B 的实例(B.this),所以这两个线程都可以进入临界区,最终导致账户余额的不准确。

 

3. 使用锁的正确姿势

那么怎么来解决这个问题呢,上面我们提到用同一把锁来保护多个资源,也就是现实世界的“包场“,只要我们的锁能覆盖所有受保护资源就可以了。在上面的例子中 this 是对象级别的锁,所以 A 对象和 B 对象都有自己的锁,如何让 A 对象和 B 对象共享一把锁呢,那就是在Class 整个类上共用一把锁。

 

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 作为共享的锁,这个锁是所以Account 对象共享的,而且这个对象是Java 虚拟机在加载 Account 类的时候就创建的,所以我们不用担心它的唯一性。下图展示了共享锁是如何保护不同对象的临界区的。

但是,用 Account.class 作为共享的锁,真的就能很好的解决问题了么?这里暂时埋下一个伏笔,下文中我们在讨论这个问题。

 

4. 总结

相信你现在对于如何保护多个资源已经很有心得了,关键是要分析多个资源之间的关系,如果资源之间没有关系,很好处理,每个资源一把锁就可以了。如果资源之间有关联关系,就要选一个粒度更大的锁,这个锁应该能覆盖所有相关的资源。

原子性”的本质是什么?其实不是不可分割,不可分割只是外在表现,其本质是多个资源间有一致性的要求,操作的中间状态对外不可见。例如,在 32 位的机器上写 long 型变量有中间状态(只写了 64 位中的 32 位),在银行转账的操作中也有中间状态(账户 A 减少了 100,账户 B 还没来得及发生变化)。所以解决原子性问题,是要保证中间状态对外不可见

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值