并发编程—如何使用一把锁保护多个资源?

上篇文章中,我们提到了受保护资源和锁之间合理的关系应该是N:1的关系,也就是说可以用一把锁来保护多个资源,而不能用多把锁来保护一个资源。那么如何使用一把锁保护多个资源呢?

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

比如如下所示的代码,在Account类中有余额 balance 和 密码 password 两个属性,而修改密码和取款两个是不相干的操作,在转账时,可以修改密码,在修改密码时也,也可以转账。当然可以使用同一把锁来同时保护 balance 和 password 那么就会导致这几个操作是串行的,就会影响性能。像这种不相干资源咱们可以使用不同的锁保护。用不同的锁对受保护资源进精细化管理,能够提升性能。这种锁就叫“细粒度锁

public class Account {

    private Object bLock = new Object();
    private Object pwLock = new Object();

    private double balance;

    private String password;

    //取款
    public  void withdraw(double atm) {
        synchronized ( bLock) {
            if(this.balance > atm) {
                this.balance -= atm;
            }

        }
    }

    //查询余额
    public double getBlance() {
        synchronized ( bLock) {
            return this.balance;
        }
    }

    //修改密码
    public void updatePassword(String newPwd) {
        synchronized (pwLock) {
            this.password = newPwd;
        }
    }

    //查询密码
    public String getPassWord() {
        synchronized (pwLock) {
            return this.password;   
        }
    }

}

image.gif

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

如果多个资源有关联关系的,那这个问题就会复杂很多。例如银行里面的转账业务,账户 A 转账给 B 100元,那么这两个账户就有关联关系了,两个操作需要A的账户 减少100元,B的账户 增加100元。那么这种存在关联关系的操作如何解决呢 ?如下代码所示:

public class Account {

    private double balance;

    public void transfer(Account target, double amount) {

        if(this.balance > amount) {
            this.balance = this.balance - amount;
            target.balance += amount;
        }

    }

}
image.gif

有人可能说给 transfer() 方法加一个 synchronized 关键字加锁就可以了啊!如下所示:

public class Account {

    private double balance;

    public synchronized void transfer(Account target, double amount) {

        if(this.balance > amount) {
            this.balance = this.balance - amount;
            target.balance += amount;
        }

    }
}
image.gif

这样加上synchronized后真的会如你所愿吗?答案是否定的。因为临界区有两个资源,this.balance 和 target.balance,并且使用的锁是 this,符合我们前面说到的一个锁保护多个资源,看似正确,实时上却并非如此,问题就出现在 this 这把锁上,this 这把锁只能保护 this.balance,却保护不了target.balance,就像你不能用自家的锁保护别人家的财产。

那我们分析一下,假设 A、B、C账户上都是 200元,假设线程A执行 A 转账给 B,线程B 执行 B 转账给 C。那么这两个线程分别再两颗CPU上执行,那么这两个是互斥的吗?答案是否定的,因为A线程锁的是 A实例 (A.this),而线程B锁定的是 B实例 (B.this),显然练个线程持有的不是同一把锁,那么执完后,B账户上有多少钱呢,可能是 300,也可能是100。如下图所示:

image

并发转账示意图

image.gif

如果两个线程进入临界区后,读到 账户 B的余额都是 200,如果先执行 线程A,那么线程B的结果就会覆盖,线程A的结果,最终账户B的余额为100。

如果线程B先执行完,线程A 的结果就会覆盖 线程B的结果,那么 最终账户 B的余额就会为300。

三、正确使用锁的姿势

在上篇文章中,我们提到可以用一把锁来保护多个资源,那么如何实现呢?其实,很简单,只要我们的锁能够覆盖所有受保护的资源就可以了。在上面的例子中,我们可以是所有的转账操作都使用同一把锁,如下所示:

public class Account {

    private Object lock;

    private double balance;

    public Account(Object lock) {
        this.lock = lock;
    }

    public  void transfer(Account target, double amount) {
        synchronized (lock) {
            if(this.balance > amount) {
                this.balance = this.balance - amount;
                target.balance += amount;
            }
        }

    }
}
image.gif

这样实现必须要求,在创建 Account实例是,传入的 lock 必须是同一个对象,这样实现起来相对比较麻烦,也很难控制,我们可以通过如下方式实现,使用Account.class作为锁。

public class Account {

    private double balance;

    public  void transfer(Account target, double amount) {
        synchronized (Account.class) {
            if(this.balance > amount) {
                this.balance = this.balance - amount;
                target.balance += amount;
            }
        }

    }
}
image.gif

四、总结

看完这篇文章后是否对如何保护多个资源有了心得呢,如何使用锁来保护资源,关键是要分析多个资源之间是否存在关系。

1、如果多个资源之间不存在关系,很好处理,就每个资源使用一把锁就行了,使用细粒度锁。

2、如果多个资源之间存在关系,那么就使用一个粒度更大的锁,这个锁应该能够覆盖所有的相关资源。

除此之外,还要舒立春有哪些访问路径,所有的访问路径都要设置合适的锁。

引申一下,关联关系如果用更具体、更专业的语言来描述的话,其实就是一种“原子性”特征。“原子性”的本质,其实就是不可分割,不可分割只是外在表现,其本质就是多个资源间有一致性的要求,操作的中间状态对外不可见。所以 解决原子性问题,是要保证中间状态对外不可见

参考资料:

[Java并发编程实战](https://time.geekbang.org/column/article/84601)

你可能感兴趣的:(并发编程—如何使用一把锁保护多个资源?)