美文网首页
并发编程—如何使用一把锁保护多个资源?

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

作者: 瞎胡扯1 | 来源:发表于2020-12-02 09:23 被阅读0次

    上篇文章中,我们提到了受保护资源和锁之间合理的关系应该是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

    <figcaption contenteditable="true" data-cke-widget-editable="caption" data-cke-enter-mode="2" data-cke-filter="63" class="cke_widget_editable" data-cke-display-name="标题">并发转账示意图</figcaption>

    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)
    

    相关文章

      网友评论

          本文标题:并发编程—如何使用一把锁保护多个资源?

          本文链接:https://www.haomeiwen.com/subject/icrywktx.html