永远都要把wait()放到循环语句里面!

今天在网上看到一段代码,是模拟银行转账的,如何保证多次转账并发执行的时候,转出账户和转入账户的金额一致。

代码可谓巧妙绝伦!先看代码:

public class MyLock {
    public static void main(String[] args) {
        // 模拟转出和转入账户
        Account src = new Account(100000);
        Account target = new Account(100000);

        // 设置倒计时
        CountDownLatch countDownLatch = new CountDownLatch(99999);
        for (int i = 0; i < 99999; i++) {
            Thread t1 = new Thread(() -> {
                // 每次转1元钱,共转9999次
                src.transactionToTarget(1, target);
                countDownLatch.countDown();
            });

            t1.setName("Thread: " + i);
            t1.start();
        }

        try {
            // 等待所有线程执行完毕
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 打印账户余额
        System.out.println("src: " + src.getBanalce());
        System.out.println("target: " + target.getBanalce());

    }

    /**
     * 账户类
     */
    static class Account {
        // 余额
        private Integer banalce;

        public Account(Integer banalce) {
            this.banalce = banalce;
        }

        /**
         * 转账
         */
        public void transactionToTarget(Integer money, Account target) {
            Allocator.getInstance().apply(this, target);

            this.banalce = this.banalce - money;
            target.setBanalce(target.getBanalce() + money);

            Allocator.getInstance().release(this, target);
        }

        public Integer getBanalce() {
            return banalce;
        }

        public void setBanalce(Integer banalce) {
            this.banalce = banalce;
        }
    }

    /**
     * 账户管理器
     */
    static class Allocator {
        private Allocator() {
        }

        // 账户锁
        private List<Account> locks = new ArrayList<>();

        /**
         * 为转出账户和转入账户申请锁。
         *
         * @param src
         * @param target
         */
        public synchronized void apply(Account src, Account target) {
            while (locks.contains(src) || locks.contains(target)) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            locks.add(src);
            locks.add(target);
        }

        /**
         * 释放锁。
         *
         * @param src
         * @param target
         */
        public synchronized void release(Account src, Account target) {
            locks.remove(src);
            locks.remove(target);
            this.notifyAll();
        }

        public static Allocator getInstance() {
            return AllocatorSingle.install;
        }

        static class AllocatorSingle {
            public static Allocator install = new Allocator();
        }
    }
}

有几个关键点:

1、为了在转账前,同时获得转出账户和转入账户的锁,设计了Allocator这个账户的管理器。

它就像一个账户管理员,拿着所有账户的账本。当有申请者来申请账户锁的时候,他会控制转入账户和转出账户同时存在;如果有其他线程当前正在操作这两个账户(正在执行账户的转入或转出),则当前线程就开始wait;否则,就让当前线程获得这两个账户的锁。

2、上面的代码里,wait放在了while里,而不能改成if!

原因:

模拟执行:假设有A,B,C三个线程,A线程先执行apply()方法,while检查为false,成功获得锁后开始执行。

                  然后B线程进入apply()方法,这时因为A线程在持有锁,所以while检查为true,开始进入wait等待。

                  此时C线程也进入apply()方法,跟B线程一样,也进入wait等待。

                  过了一会,A线程的转账执行完成,在成功release锁以后,通过notifyAll(),B和C线程同时被唤醒。

                  在同时被唤醒的两个线程B和C里,只会有一个线程成功抢夺资源(Allocator的单实例对象)成功,

             (此处有一个疑问,如果D线程在apply()外等待,线程B、C、D会同时争夺资源呢,还是B和C先决胜出一个,再和D争夺资源?)

                 假设此处C成功抢夺了资源,while检查为false,便成功获得了锁,执行完apply()方法,开始执行转账操作。

              (如果此处的while改成if:因为C执行完apply()方法后,释放了apply()方法的操作权,

                  如果此时B趁虚而入,被notify()的线程B不会再进行if判断,而是直接开始执行加锁执行(导致locks对象里有四个对象),

                  这样就会导致锁的混乱和失败,造成转账操作的非原子性。这便是while不能改成if的根本原因了!

                 线程C执行完毕后,释放锁,B便开始执行了。

 

所以,几乎所有的java书籍都会建议开发者永远都要把wait()放到循环语句里面

这是前车之鉴,不是没有道理的。

3、作者刻意把Account#transactionToTarget()方法做成非syncronized的方法,以防止同一时刻只会发生一次转账操作,可谓用心良苦!

4、wait()和notify()、notifyAll()方法,一般在synchcronized的方法内或被synchcronized锁定的代码块里调用执行;并且wait()方法的执行要放置在while(条件)里。

 

相关推荐
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页