美文网首页
4-3 解决原子性问题

4-3 解决原子性问题

作者: nieniemin | 来源:发表于2021-08-07 20:50 被阅读0次

    一个或多个操作在CPU执行的过程中不被中断的特性,称为原子性。
    线程出现原子性的问题是因为线程切换导致,同一时刻只能有一个线程操作共享对象才能解决原子性问题。自然而然我们想到加锁方式来搞定原子性问题。线程A持有锁之后才能访问加锁后的资源,其他线程只能等待,直到线程A释放锁后,才有机会抢占持有锁。实现互斥的条件。

    王宝令老师举了一个很生动的例子来说明锁模型。一般办公室早高峰是蹲坑的黄金时间,大家都抢着上厕所,无奈坑位有限,运气好的话你去的时候有坑位,这时候你蹲坑锁门,享受一泻千里的快感。其他同事只能在门外苦苦等候,直到你打开门出来后,其他人才有机会进入这个坑位。这个例子中,我们锁的是不是就是坑位,保护的是我们正常拉屎的隐私和权力。换到代码中,是不是就是锁的是共享变量的访问,保护的是共享变量,这是我们理解锁的关键。

    在上一节中已经提到synchronized,Java 语言提供的 synchronized 关键字,就是锁的一种实现。synchronized 关键字可以用来修饰方法,也可以用来修饰代码块。他的使用方法如下:

    public class Synchronized {
        private final Object LOCK = new Object();
    
        //  修饰非静态方法
        public synchronized void lockMethod() {
            // 受锁保护的资源,临界区
        }
        //  修饰静态方法
        public synchronized static void lockStaticMethod() {
            // 受锁保护的资源,临界区
        }
    
        public void  method() {
            // 修饰代码块
            synchronized (LOCK) {
                // 受锁保护的资源,临界区
            }
        }
    }
    

    synchronized 的加锁lock() 和解锁 unlock()是由java编译器在修饰方法或代码块前后自动添加,无需我们手动进行加锁和解锁步骤。在上面代码中synchronized 修饰代码块时可以看到锁的是LOCK对象,而synchronized 修饰静态方法锁的是当前类的Class对象,即我们的类Synchronized;当修饰非静态方法时锁的是当前的实例对象this。

    也就是说下面代码中lockMethod方法和lockStaticMethod方法是由两个不同的锁this,Synchronized.class保护资源i,不会存在互斥关系,会导致并发问题。一个资源只能由同一把锁保护,同一把锁可以保护N个资源。

    public class Synchronized {
    
        static int i = 0;
    
        //  修饰非静态方法
        public synchronized void lockMethod() {
            // 受锁保护的资源,临界区
            i++;
    
        }
        //  修饰静态方法
        public synchronized static int lockStaticMethod() {
            // 受锁保护的资源,临界区
           return i;
        }
    }
    

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

    1. 资源间没有关联关系

      比如我们银行卡账户余额balance和银行卡密码pwd两个资源没有直接关联关系。那么我们可以通过balanceLock和pwdLock两个锁来分别管理,不同的资源用不同的锁保护,各自管各自的。当然你也可以用同一把锁来管理这两个资源,只不过会造成不必要的性能浪费,因为这会导致操作串行化。用不同的锁对受保护资源进行精细化管理,能够提升性能,尽量使用细粒度锁是保证性能的关键所在。

    public class Account {
        private Double balance;
        private String pwd;
        private final Object balanceLOCK = new Object();
        private final Object pwdLOCK = new Object();
    
        // 取款
        public void withdraw(Double amt) {
            synchronized (balanceLOCK) {
                if (this.balance > amt) {
                    this.balance -= amt;
                }
            }
        }
    
        // 查看余额
        public Double getBalance() {
            synchronized (balanceLOCK) {
                return balance;
            }
        }
        // 更新密码
        public void updatePassword(String pwd) {
            synchronized (pwdLOCK) {
                this.pwd = pwd;
            }
        }
    
        // 查看余额
        public String getPwd() {
            synchronized (pwdLOCK) {
                return pwd;
            }
        }
    }
    
    1. 资源间存在关联关系

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

    public class Account {
    
        private Double balance;
        private String name;
        public Account(String name, Double balance) {
            this.name = name;
            this.balance = balance;
        }
    
    
        public void transfer(Account target, Double money) {
            synchronized (Account.class) {
                if (this.balance > money) {
                    this.balance -= money;
                    target.balance += money;
                }
            }
    
        }
    
    

    我们通过Account.class作为共享锁来实现,Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。但是如果使用class对象作为锁的话,会导致所有的操作都变成串行,降低了执行效率。因此,并不是最合适的选择。

    前面我们提到了锁尽可能小的范围细粒度锁,对于上面转账操作A账户转账到B账户,分别加锁。效率肯定就提升了。我们用代码来实现一下A,B两个账户分别加锁:

    转账操作
    public class Account {
        //账号
        private String accountName;
        // 余额
        private int balance;
        public Account(String accountName,int balance){
            this.accountName = accountName;
            this.balance = balance;
        }
      // 省略get/set方法
    }
    
    public class AccountMain implements Runnable {
        //转出账户
        public Account fromAccount;
        //转入账户
        public Account toAccount;
        //转出金额
        public int amount;
    
        public AccountMain(Account fromAccount,Account toAccount,int amount){
            this.fromAccount = fromAccount;
            this.toAccount = toAccount;
            this.amount = amount;
        }
        @Override
        public void run(){
            while(true){
                //获取fromAccount对象的锁
                synchronized(fromAccount){
                    //获取toAccount对象的锁
                    synchronized(toAccount){
                        //转账进行的条件:判断转出账户的余额是否大于0
                        if(fromAccount.getBalance() <= 0){
                            System.out.println(fromAccount.getAccountName() + "账户余额不足!");
                            return;
                        }else{
                            //更新转出账户的余额:
                            fromAccount.setBalance(fromAccount.getBalance() - amount);
                            //更新转入账户的余额:
                            toAccount.setBalance(toAccount.getBalance() + amount);
                        }
                    }
                }
           System.out.println("转出用户:" + fromAccount.getAccountName() + "余额:" + fromAccount.getBalance());
                System.out.println("转入用户:" +toAccount.getAccountName() + "余额:" + toAccount.getBalance());
            }
        }
    
        public static void main(String[] args) {
          
            Account fromAccount = new Account("张三",200000);
            Account toAccount = new Account("李四",200000);
    
            //  每次转出2元.
            Thread a = new Thread(new AccountMain(fromAccount,toAccount,2));
            Thread b = new Thread(new AccountMain(toAccount,fromAccount,2));
    
            a.start();
            b.start();
        }
    }
    

    当我们按照思路写完执行发现,等了好久都没有等到程序结束。尴尬的发现死锁了。



    6. 使用synchronized实现死锁

    我们来分析下这个例子死锁是怎么造成的,线程a在执行转账操作张三->李四的同一时刻,线程b也执行账户 李四 转账户 张三 的操作。两个线程同时执行到了(1)处,此时线程a的fromAccount是不是就是张三,而对于b线程来说fromAccount就是李四了。此时执行到(2),a 试图获取账户 李四 的锁时,发现账户 李四 已经被锁定(被 b线程 锁定),所以 a 开始等待;b 则试图获取账户 张三 的锁时,发现账户 张三 已经被锁定(被 a线程 锁定),所以 b 也开始等待。于是 a和 b 会无期限地等待下去,最终造成了死锁。

      synchronized(fromAccount){(1)
                    //获取toAccount对象的锁
                    synchronized(toAccount){(2)
        }
    }
    

    总结

    这节我们了解到通过加锁互斥的方式可以解决原子性问题,以及synchronized不同加锁方法,加锁的对象。在通过转账例子使用细粒度锁的时候又碰到了死锁问题。下一节来整理下如何避免死锁。

    相关文章

      网友评论

          本文标题:4-3 解决原子性问题

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