4-3 解决原子性问题

2021-08-07  本文已影响0人  nieniemin

一个或多个操作在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不同加锁方法,加锁的对象。在通过转账例子使用细粒度锁的时候又碰到了死锁问题。下一节来整理下如何避免死锁。

上一篇下一篇

猜你喜欢

热点阅读