线程银行转账模拟和同步

时间:2011-05-08 10:27:25

标签: java multithreading

我有以下情况。我有一个帐户课程,每个帐户都有余额,钱可以转移到它。

public class Account {
    private int balance;

    public Account() {
        this.balance = 0;
    }

    public void transfer(int amount) {
        this.balance += amount;
    }

    @Override
    public String toString() {
        return "Account (balance: " + balance + ")";
    }
}

我有一名转职经理。转账只需要两个账户和一笔钱转账。传输管理器可以通过将传输添加到阵列列表(传输队列)来发出传输。将所有传输添加到队列后,可以调用performTransfers方法,该方法在每次传输时调用performTransfer方法。

import java.util.ArrayList;

public class TransferManager {
    private ArrayList<Transfer> openTransfers;
    private int issuedTransfers;
    private int performedTransfers;

    public TransferManager() {
        openTransfers = new ArrayList<Transfer>();
        issuedTransfers = 0;
        performedTransfers = 0;
    }

    public void issueTransfer(Account from, Account to, int amount) {
        openTransfers.add(new Transfer(from, to, amount));
        issuedTransfers++;
    }

    public void performTransfers() {
        for(Transfer transaction : openTransfers) {
            transaction.performTransfer();
            performedTransfers++;
        }       
        openTransfers.clear();
    }

    @Override
    public String toString() {
        return "TransferManager (openTransfers: " + openTransfers.size() + "; issuedTransfers: " + issuedTransfers + "; performedTransfers: " + performedTransfers + ")";
    }

    private static class Transfer {
        private Account from, to;
        private int amount;

        public Transfer(Account from, Account to, int amount) {
            this.from = from;
            this.to = to;
            this.amount = amount;
        }

        public void performTransfer() {
            from.transfer(-amount);
            to.transfer(amount);
        }
    }
}

现在我添加多线程:

import java.util.Random;

public class BankingTest extends Thread {
    private Account[] accounts;
    private static Random random = new Random();

    public BankingTest(Account[] accounts) {
        this.accounts = accounts;
    }

    public void run() {
        final TransferManager manager = new TransferManager();

        //simulate some transfers
        for(int i = 0; i < accounts.length; i++) {
            final int index = i;
            Thread thread = new Thread() {
                public void run() {
                    try {
                        for(int j = 0; j < 10; j++) {
                            manager.issueTransfer(accounts[index], accounts[(index+1)%accounts.length], 100);
                            Thread.sleep(random.nextInt(10));
                        }
                    } catch (InterruptedException e) {                      
                        e.printStackTrace();
                    }
                }
            };
            thread.start();
        }

        //wait a bit
        try {
            Thread.sleep(60);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        manager.performTransfers();

        System.out.println(manager);
    }
}

BankingTest会收集10个空白帐户。现在它没有同步,我试着理解为什么我会得到这些错误:

Exception in thread "Thread-2" java.lang.NullPointerException
at
gp2.ha5.exercise2.TransferManager.performTransfers(TransferManager.java:23)
at gp2.ha5.exercise2.BankingTest.run(BankingTest.java:41)

and

TransferManager (openTransfers: 0; issuedTransfers: 99; performedTransfers:
99)

我知道为什么会出现这些错误以及同步有何帮助?

enter image description here

(您可以放大以查看详细信息;))

TransferManager:http://pastebin.com/Je4ExhUz

BankTest:http://pastebin.com/cdpWhHPb

开始:http://pastebin.com/v7pwJ5T1


我将synchronized添加到issueTransfer和performTransfers方法后,我不断收到错误:

enter image description here

2 个答案:

答案 0 :(得分:2)

好的,简单地说,所有线程都尝试执行此方法:

public void issueTransfer(Account from, Account to, int amount) {
    openTransfers.add(new Transfer(from, to, amount));
    issuedTransfers++;
}

但是添加到ArrayList时没有同步。你必须明白,因为列表操作不是原子的,并且因为几个线程同时访问它,所以几乎任何东西都可以附加并且你列出的内容会被破坏。

然后,当您尝试读取列表时,您会在其中找到Null元素,即使您没有请求在第一个位置插入一个元素。这是一个如何在没有正确处理的情况下访问相同数据可能会破坏您的数据的例子。

编辑:

因此,只要您拥有共享状态并且想要使用多个线程访问它,就必须进行同步。这不仅适用于issueTransfer方法。

另一个问题是你如何产生线程。 这与您的初始问题无关。

    //simulate some transfers
    for(int i = 0; i < accounts.length; i++) {
        final int index = i;
        Thread thread = new Thread() {
            public void run() {
                try {
                    for(int j = 0; j < 10; j++) {
                        manager.issueTransfer(accounts[index],
     

帐户[(索引+ 1)%accounts.length]   100);                               了Thread.sleep(random.nextInt(10));                           }                       } catch(InterruptedException e){

                    e.printStackTrace();
                }
            }
        };
        thread.start();

从这里的代码中,所有线程都访问一个全局状态:用于访问数组中帐户的索引。但是主线程用for循环递增,而线程可以随时执行。线程启动时存在很高的风险,索引值已经改变。

正如你所看到的,当你没有足够的注意力时,并发总是会咬你;)

答案 1 :(得分:1)

除了Nicolas所说的 - 仅仅issueTransfer同步是不够的,因为主线程可能已经在performTransfers中,并且某些线程仍然可以卡在issueTransfer中。这意味着您的ArrayList仍然可以被多个线程访问。

您可以创建一个锁定对象并使用

synchronized (lock) {
   // vulnerable code goes here
}

保护那些可能被多个线程触及的代码片段。 或者,您可以使相关方法(issueTransferperformTransfers)同步。