• Java并发编程实战 03互斥锁 解决原子性问题


    文章系列

    Java并发编程实战 01并发编程的Bug源头
    Java并发编程实战 02Java如何解决可见性和有序性问题

    摘要

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

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

    简易锁

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

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

    改进后的锁模型

    在并发编程世界中,锁和锁要保护的资源是有对应关系的。
    首先我们需要把临界区要保护的资源R标记出来,然后需要创建一把该资源的锁LR,最后针对这把锁,我们需要在进出临界区时添加加锁lock(LR)操作和解锁unlock(LR)操作。如下:
    互斥锁2.jpg

    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修饰,否则也没法保证其可见性。
    上面这个例子如下图:
    互斥锁3.jpg

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

    public class Calc {
        private static int value = 0;
        public synchronized int get() {
            return value;
        }
        public static synchronized void addOne() {
            value += 1;
        }
    }
    

    上面的例子用图来表示:
    互斥锁4.jpg

    在这个例子当中,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) {
                // 取款
            }
        }
    }
    

    分别使用pwLockmoneyLock来保护密码和余额,这样修改密码和修改余额就可以并行了。使用不同的锁对受保护的资源进行进行更细化管理,能够提升性能,这种锁叫做细粒度锁。
    在这个例子当中,你可能发现我使用了final Object来当成一把锁,这里解释一下:使用锁必须是不可变对象,若把可变对象作为锁,当可变对象被修改时相当于换锁,而且使用LongInteger作为锁时,在-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并发编程实战 03互斥锁(上)
    极客时间:Java并发编程实战 04互斥锁(下)

    个人博客网址: https://colablog.cn/

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

  • 相关阅读:
    高效编写微信小程序
    故事怎么讲才有逼格?
    基于RESTful API 怎么设计用户权限控制?
    【开源访谈】腾讯贺嘉:从小程序谈起,开发者该如何跟进新技术?
    图标字体设计
    微信小程序即将上线,创业者机会在哪里?
    微信小程序开发学习资料
    PC 微信扫码登陆
    一张二维码同时集成微信、支付宝支付
    支付宝Wap支付你了解多少?
  • 原文地址:https://www.cnblogs.com/Johnson-lin/p/12840672.html
Copyright © 2020-2023  润新知