关于java:Java并发编程用锁的正确姿势为什么加了锁但余额还是出错

37次阅读

共计 3565 个字符,预计需要花费 9 分钟才能阅读完成。

在 Java 中,锁如同是颗万能药,没什么问题是加锁解决不了的。确实,锁能解决绝大部分的并发问题。

然而,最简略的货色也往往最容易呈现问题。你只有稍有不慎,岂但 Bug 没有解决,还得破费大量的工夫做各种排查。

既然如此,咱们就来好好看看:为什么用了锁,程序还是出错了。

什么才是正确的锁模型

一说到锁,你的大脑中可能立马想起这个模型。

两头蓝色的一段代码,叫做:临界区 。线程进入临界区前,先尝试 加锁 ,如果胜利,就进入临界区。此时,这个线程持有锁,等执行完临界区的代码后,持有锁的线程就会执行 解锁

同样的情理,如果加锁失败,线程就会始终期待,直到持有锁的线程解锁后,再从新尝试加锁。

这是锁的繁难模型,看起来简单明了,但这个模型是错的,它疏忽了最最重要的一点:锁与资源的关系

你还记得吗?锁是为了实现互斥,即:在同一时刻,一个资源只能由一个线程操作。

换句话说,之所以要用锁,就是要爱护某些资源。因而,锁与资源之间的关系,是重点中的重点。很多并发 Bug 就是疏忽了这一点导致的。比方,你看上面这段代码:

class Account {
    private int balance;

    // 转账
    synchronized void transfer(Account target, int amt){if (this.balance > amt) {
            this.balance -= amt;
            target.balance += amt;
        }
    }
}

你可能会感觉,这段代码没问题呀?transfer() 办法不是加锁了吗?但在并发环境下,转账后,余额很可能对不上。起因也很简略,没思考锁和资源的关系。

既然如此,那正确的锁模型是怎么的呢?

首先,咱们要标注出受爱护的资源 R;而后,为资源 R 创立一把锁 LR;最初,在进出临界区的时候,再加上加锁、解锁操作。

其中,锁 LR 受爱护资源 R 之间有一条关联线,这十分重要,代表的是:锁与资源之间,是有对应关系的。

打个比方,你家的锁只能爱护你家的货色,我家的锁只能爱护我家的货色。这个关系要搞清楚,不然,就是锁自家的门来爱护他家的货色。

能够说,咱们平时对锁的了解都是错的。尽管这个别不会有什么问题,但只有呈现问题,就是公司破产之类的小事。

比方,我前东家的领取零碎,就是因为锁的问题,巨亏了几十万,负责人引咎辞职。在濒临开张之际,我接手了,这个我的项目才起死回生。

所以,如果你不想吃一堑; 长一智,而是想升职加薪。那必须要把握正确的锁模型,千万不能疏忽 锁和资源的关系

锁与资源的关系

你曾经晓得了,想要用好用锁,要害是搞清楚锁和资源的关系。那么,这两者的关系是怎么的呢?

资源和锁之间的关系是 N:1。

换句话说,你能够用一把锁,来爱护多个资源。然而,你不能用多把锁,来爱护一个资源。起因在于,如果你针对一个资源创立了多把锁,那么就达不到互斥的成果了。

打个比方,事实世界中,你能够用好几把锁,来爱护你家的货色。但在编程世界中,你不能这样做。你看这个例子:

class SafeCalc {
    private long value = 0L;

    void addOne() {synchronized (this) {value += 1;}
    }

    void addTwo() {synchronized (SafeCalc.class) {value += 2;}
    }
}

你看下 addOne()addTwo() 两个办法,它们别离用了两把锁 thisSafeCalc.class。尽管都是爱护同一个资源,但临界区被拆成了 2 个,而这 2 个临界区是没有互斥关系的,这导致了并发问题。

那问题该怎么解决呢?

很简略,不要用多把锁来爱护一个资源,你能够只用 this,又或者只用 SafeCalc.class

当然,这个例子比较简单。在事实工作中,状况必定更加简单,咱们要爱护的资源不止一个,而是多个,这又该怎么解决呢?

如何爱护多个资源

无论是单个资源,还是多个资源,要害都在于:锁要笼罩所有受爱护的资源。

单个资源比拟好了解,但如果要爱护的资源不止一个,那咱们首先要做的是,辨别这些资源是否有关联,这分为两种状况。

第一,如何爱护没有关联的多个资源,这和解决单个资源差不多。如果不思考性能,你能够用一把锁来爱护;如果想进步性能,你能够用不同的锁来爱护。

你看上面的代码:

class Account {
    // 余额
    private Integer balance;
    // 明码
    private String password;

    // 锁:爱护余额
    private final Object balLock = new Object();
    // 锁:爱护明码
    private final Object pwLock = new Object();

    // 取款
    void withdraw(Integer amt) {synchronized (balLock) {if (this.balance > amt) {this.balance -= amt;}
        }
    }

    // 更改明码
    void updatePassword(String pw) {synchronized (pwLock) {this.password = pw;}
    }
}

账户类 -Account有两个办法:取款 -withdraw()更改明码 -updatePassword()。从性能上看,这两个办法没什么关联。所以,为了晋升性能,我用了两把锁,让它们各管各的。

当然,你也能够用一把锁来治理所有资源,就像上面这样:

class Account {
    // 余额
    private Integer balance;
    // 明码
    private String password;

    // 取款
    synchronized void withdraw(Integer amt) {if (this.balance > amt) {this.balance -= amt;}
    }

    // 更改明码
    synchronized void updatePassword(String pw) {this.password = pw;}
}

在这里,我对性能没有要求,所以只有用 this 这一把锁,间接治理账户的所有资源。

第二,如何爱护有关联的多个资源,这个问题就有点简单了。

比方,银行的转账操作,不同的账户是有关联的,账户 A 如果缩小 100 元,账户 B 就得减少 100 元。这时候,该怎么防止转账的并发问题呢?

你可能想到加锁,用 synchronized 关键词润饰一下,就像上面这样:

class Account {
    // 余额
    private Integer balance;

    // 转账
    synchronized void transfer(Account target, Integer amt) {if (this.balance > amt) {
            this.balance -= amt;
            target.balance += amt;
        }
    }

}

一把锁能够爱护多个资源,这看上去没问题。然而,却是谬误的做法。

你设想一下这样的场景,A、B、C 三个账户的余额都是 100 元。这时候,有两笔转账操作:账户 A 转 100 元到账户 B,账户 B 转 100 元到账户 C。照理说,后果应该是:账户 A - 0 元,账户 B -100 元,账户 C -200 元。

然而,如果是并发转账,最终的后果还有两种可能。一种是:账户 A - 0 元,账户 B -200 元,账户 C -200 元;另一种是:账户 A - 0 元,账户 B - 0 元,账户 C -200 元。

简略来说,账户 B 的余额很可能是错的,问题就出在 this 这把锁上。

还记得吗?锁要笼罩所有受爱护的资源。

在这个例子中,却没能做到这一点。临界区内有两个资源:this.balancetarget.balance。但 this 这把锁只能爱护this.balance,不能爱护target.balance

打个比方,你家的锁就算再厉害,也没法爱护邻居家的货色吧?

因而,咱们要找到一把锁,同时笼罩所有账号。

计划还是挺多的,最简略的一个就是:Account.classAccount.class 共享给所有 Account 对象。而且,Account.class 由 Java 虚拟机创立,具备唯一性。

这样一来,转账的并发问题就解决了,代码也特地简略。

class Account {
    // 余额
    private Integer balance;

    // 转账
    void transfer(Account target, Integer amt) {synchronized (Account.class) {if (this.balance > amt) {
                this.balance -= amt;
                target.balance += amt;
            }
        }
    }
}

写在最初

在 Java 中,锁是解决并发的万能药,但咱们却往往用不好,这是因为咱们疏忽了 锁与资源的关系

个别状况下,这不会有什么大问题。

然而,一旦呈现多个资源,这些资源还互相关联,就很可能呈现公司破产之类的小事。因而,你要时刻记住 3 点:

  1. 锁与资源有对应关系;
  2. 资源和锁之间的关系是 N:1;
  3. 锁要笼罩所有受爱护的资源;

你只有查看好这三点,好事就轮不到你头上。

正文完
 0