乐趣区

关于java:Java并发编程死锁上追求性能的代价

后面几篇文章,咱们始终在关注如何解决并发问题,也就是程序的原子性、可见性、有序性。这些问题一旦呈现,程序的后果就没法保障。

好在 Java 是一门弱小的语言, 锁 -synchronized 是一味万能药。你只有用好锁,简直能解决所有并发问题。

不过,并发编程有一个特点:解决完一个问题,总会冒出另一个新问题。

锁带来的性能问题

理论开发中,锁尽管是一副万能药,但应用起来要十分小心。因为你岂但要思考锁和资源的关系,还得思考性能问题。

咱们之所以写并发程序,不就是想进步性能吗?

然而, 锁的实质是串行化,程序要排队轮流执行。这样一来,多线程的劣势就没法施展了,性能天然会降落。

比方,银行的转账操作,如果想保障后果的正确,就得用到锁。

class Account {
    // 余额
    private Integer balance;

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

在这里,咱们对 Account.class 进行加锁,解决了银行转账的并发问题,代码也特地简略,看似很完满。然而, 这里有一个致命缺点:性能太差,所有账户的转账操作都是串行的。

在事实世界中,账户 A 转账户 B,账户 C 转账户 D,这些都是能够并行处理的。但在这个计划中,却没思考这些,转账只能一笔一笔的解决。

试想一下,中国网民即便每天只交易一次,就有 10 亿笔转账,均匀每秒转账超过 1 万次。如果交易只能一笔笔解决,那后果是对了,性能却基本没法看。

因而,在理论工作中,咱们不光要思考程序的后果对不对,还得思考程序的性能好不好。

如何进步锁的性能

咱们曾提到过,如果想用好锁,那么锁要笼罩所有受爱护的资源。不然的话,就没法施展锁的互斥作用。PS. 能够温习这篇文章:用锁的正确姿态

然而, 如果锁笼罩的范畴太大,程序的性能也会大幅降落。

比方,后面的转账操作切实是株连微小,你再看一下代码:

class Account {
    // 余额
    private Integer balance;

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

每笔转账只波及了两个账号,但咱们却把整个 Account.class 锁住了。这个计划尽管简略,但 Account.class 这个资源笼罩的范畴切实太大了。

而且,账户不止转账一个性能,还有查余额、提现等等操作,可这些操作也得串行解决的,那性能天然好不了。

那这样行不行?既然问题是锁笼罩的范畴太大,那我放大覆盖范围,问题不就解决了吗?

十分正确,这样的锁叫: 细粒度锁

所谓细粒度锁,就是放大资源的覆盖范围,而后用不同的锁对资源做精细化治理,从而进步程序的并行度,以此来晋升性能。

那依照这个思路,咱们来剖析一下代码,转账只波及到两个账户,别离是:thistarget。既然如此,咱们就不必锁定整个 Account.class,只须要锁定两个账户,做两次加锁操作就好了。

首先,咱们尝试锁定转出账户 this;而后,再尝试锁定转入账户 target。只有两个账户都锁定胜利时,能力执行转账操作。你能够看上面这副图:

思路有了,接下来,就得转换成代码了。

class Account {
    // 余额
    private Integer balance;

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

相比原来的代码,当初只是多用了一个 synchronized,如同没有什么变动,但转账的并行度却大大提高。

你想一下,当初同时呈现两笔交易,账户 A 转账户 B,账户 C 转账户 D。

本来的转账操作是锁定了整个 Account.class,转账只能一笔一笔地解决。

但通过革新后,第一笔转账只锁定了 A、B 两个账户,第二笔转账只锁定了 C、D 两个账户,这两笔转账齐全能够并行处理。

这样一来,程序的性能晋升了好几个品位,而这都是细粒度锁的功绩。

细粒度锁的代价——死锁

在转账这个例子中,咱们一开始用是 Account.class 来做锁,但为了优化性能,咱们用了细粒度锁,只锁定和转账相干的两个账号。这样一来,性能有了很大的晋升。

然而,天下没有收费的午餐。细粒度锁看上去这么简略,是不是也有代价呢?

没错, 细粒度锁可能造成死锁。所谓死锁,就是两个以上的线程在执行的时候,因为竞争资源造成相互期待,从而进入“永恒”阻塞的状态。

听起来有点简单,咱们还是持续看转账的例子吧。

class Account {
    // 余额
    private Integer balance;

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

当初同时有两笔交易,账户 A 转 账户 B,账户 B 转 账户 A。这个就有了两个线程,别离是:线程一、线程二。

其中,线程一先锁定了账户 A,线程二先锁定了账户 B。

那么,问题来了。线程一想持续对账户 B 加锁,但发现账户 B 被锁定,转账没法执行上来,只能进入期待。

同样的情理,线程二想持续对账户 A 加锁,但发现账户 A 被锁定,转账也没法执行,也进入了期待。

这样一来,线程一、线程二都在死死地等着,这就是经典的死锁问题了,你能够看下这副图。

在这副图中,两个线程造成一个完满的闭环,基本没法进来。

而且,转账随时都会产生,但这两个线程始终占用着资源,新的订单没法解决,只能堆在一起,越来越多。这不仅节约大量的计算机资源,还影响其它性能的运行。

此外,程序一旦产生死锁,那除了重启利用外,没有别的路能走。但银行分分钟进出几十亿,重启利用也是死路一条。

能够说,如何彻底解决死锁问题,就是程序员价值所在。

写在最初

锁的实质是串行化,如果锁笼罩的范畴太大,会导致程序的性能低下。

为了晋升性能,咱们用了细粒度锁,但这又带来了死锁问题。

既然如此,死锁该怎么解决?咱们下次再聊。

退出移动版