Java中的不变性和同步性

自从我阅读了Java Concurrency in Practice一书后,我想知道如何使用不变性来简化线程之间的同步问题。

我完全理解不可变对象是线程安全的 。 初始化后它的状态不能改变,因此根本不可能存在“共享可变状态”。 但必须正确使用不可变对象才能在同步问题中被认为有用。

以这段代码为例,它描述了一家银行,它拥有许多账户,并且公开了一种方法,通过这种方法我们可以在账户之间转账。

public class Bank { public static final int NUMBER_OF_ACCOUNT = 100; private double[] accounts = new double[NUMBER_OF_ACCOUNT]; private Lock lock; private Condition sufficientFunds; public Bank(double total) { double singleAmount = total / 100D; for (int i = 0; i  accounts[from]) { sufficientFunds.await(); } // Transferring funds accounts[from] -= amount + additionalAmount; accounts[to] += amount + additionalAmount; // Signaling that something has changed sufficientFunds.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public double getTotal() { double total = 0.0D; lock.lock(); try { for (int i = 0; i < NUMBER_OF_ACCOUNT; i++) { total += accounts[i]; } } finally { lock.unlock(); } return total; } public static void main(String[] args) { Bank bank = new Bank(100000D); for (int i = 0; i < 1000; i++) { new Thread(new TransferRunnable(bank)).start(); } } } 

在上面的示例中,它来自Core Java Volume I一书,它通过显式锁使用同步。 代码显然难以阅读且容易出错。

我们如何使用不变性来简化上述代码? 我试图创建一个不可变的Accounts类来保存帐户值,为Bank类提供一个volatileAccounts实例。 但是我没达到目标。

有人可以解释我是否可以使用不变性来简化同步?

– -编辑 – –

可能我没有很好地解释自己。 我知道一个不可变对象一旦创建就无法改变它的状态。 我知道,对于Java内存模型 (JSR-133)中实现的规则,保证在初始化之后(使用某些distingua )可以看到不可变对象。

然后我尝试使用这些概念从Bank类中删除显式同步。 我开发了这个不可变的Accounts类:

 class Accounts { private final List accounts; public Accounts(List accounts) { this.accounts = new CopyOnWriteArrayList(accounts); } public Accounts(Accounts accounts, int from, int to, double amount) { this(accounts.getList()); this.accounts.set(from, -amount); this.accounts.set(to, amount); } public double get(int account) { return this.accounts.get(account); } private List getList() { return this.accounts; } } 

必须使用volatile变量发布Bank类的accounts属性:

 private volatile Accounts accounts; 

显然, Bank类的转账方式会相应改变:

 public void transfer(int from, int to, double amount) { this.accounts = new Accounts(this.accounts, from, to, amount); } 

使用不可变对象( Accounts )来存储类( Bank )的状态应该是一个发布模式,在JCIP一书的第3.4.2节中有描述。

但是,某处仍然存在竞争状况 ,我无法弄清楚在哪里(以及为什么!!!)。

您的Account值本质上是可变的(具有不可变余额的银行帐户不是很有用),但是您可以通过使用诸如Actor模型之类的东西封装可变状态来降低复杂性。 您的Account类实现Runnable ,每个Account对象负责更新其value

 public class Bank { // use a ConcurrentMap so that all threads will see updates to it private final ConcurrentMap accounts; private final ExecutorService executor = Executors.newCachedThreadPool(); public void newAccount(int acctNumber) { Account newAcct = new Account(); executor.execute(newAcct); accounts.put(acctNumber, newAcct); } public void transfer(int from, int to, double amount) { Account fromAcct = accounts.get(from); Account toAcct = accounts.get(to); if(fromAcct == null || toAcct == null) throw new IllegalArgumentException(); fromAcct.transfer(amount, toAcct); } } public interface Message { public double getAmount(); } public class Transfer implements Message { // initialize in constructor, implement getters private final double amount; private final Account toAcct; } public class Credit implements Message { // initialize in constructor, implement getters private final double amount; } public class Account implements Runnable { private volatile double value; private final BlockingQueue queue = new ArrayBlockingQueue<>(8); public void transfer(double amount, Account toAcct) { queue.put(new Transfer(amount, toAcct)); } public void credit(double amount) { queue.put(new Credit(amount)); } public void run() { try { while(true) { Message message = queue.take(); if(message instanceof Transfer) { Transfer transfer = (Transfer)message; if(value >= transfer.getAmount()) { value -= transfer.getAmount(); transfer.getToAcct().credit(transfer.getAmount()); } else { /* log failure */ } } else if(message instanceof Credit) { value += message.getAmount(); } else { /* log unrecognized message */ } } } catch(InterruptedException e) { return; } } } 

可以从任何线程安全地调用Account#transferAccount#credit方法,因为BlockingQueue是线程安全的。 value字段仅在帐户的run方法中进行修改,因此不存在并发修改的风险; value必须是volatile以便所有线程都可以看到更新(您使用ThreadPoolExecutor来执行所有Accounts因此无法保证Account's run方法每次都会在同一个Thread上执行)。

您还应该在执行之前在Bank类中记录传输,以便您可以从系统故障中恢复 – 如果服务器在从帐户被扣款后但在记入到帐户之前崩溃,那么您将需要一种方法来重新建立一致性服务器恢复后。