Java中的不变性和同步性 [英] Immutability and synchronization in Java
问题描述
由于我已经阅读了 Java Concurrency in Practice 这本书,我想知道我是怎么做的可以使用 immutability 来简化线程之间的同步问题。
Since I've read the book Java Concurrency in Practice I was wondering how I could use immutability to simplify synchronization problems between threads.
我完全理解不可变对象线程安全 。初始化后它的状态不能改变,因此根本不可能存在共享可变状态。但是必须正确使用不可变对象才能在同步问题中使用。
I perfectly understand that an immutable object is thread-safe. Its state cannot change after initialization, so there cannot be "shared mutable states" at all. But immutable object have to be use properly to be considered useful in synchronization problems.
以这段代码为例,它描述了拥有许多帐户并暴露的银行我们可以通过这种方式在账户之间转账。
Take for example this piece of code, that describes a bank wich owns many accounts and that exposes a method through which we can transfer money among accounts.
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 < NUMBER_OF_ACCOUNT; i++) {
accounts[i] = singleAmount;
}
lock = new ReentrantLock();
sufficientFunds = lock.newCondition();
}
private double getAdditionalAmount(double amount) throws InterruptedException {
Thread.sleep(1000);
return amount * 0.04D;
}
public void transfer(int from, int to, double amount) {
try {
// Not synchronized operation
double additionalAmount = getAdditionalAmount(amount);
// Acquiring lock
lock.lock();
// Verifying condition
while (amount + additionalAmount > 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 的书,它通过显式锁使用同步。代码显然很难阅读并且容易出错。
In the above example, that comes from the book Core Java Volume I, it is used synchronization through explicit locks. The code is clearly difficult to read and error prone.
我们如何使用不变性来简化上述代码?我试图创建一个不可变的 Accounts
类来保存帐户值,给 Bank
类a volatile
帐户
的实例。但是我没有达到目标。
How can we use immutability to simplify the above code? I've tried to create an immutable Accounts
class to hold the accounts value, giving to the Bank
class a volatile
instance of Accounts
. However I've not reach my goal.
有人可以解释我是否可以使用不变性简化同步?
Can anybody explain me if it is possible to simplify synchronization using immutability?
---编辑---
可能我没有很好地解释自己。我知道一个不可变对象一旦创建就无法改变它的状态。我知道中实施的规则Java内存模型(JSR-133),保证在初始化后完全构造不可变对象(使用一些 distingua )。
Probably I did not explain myself well. I know that an immutable object cannot change its state once it was created. And I know that for the rules implemented in the Java Memory Model (JSR-133), immutable objects are guaranteed to be seen fully constructed after their initialization (with some distingua).
然后我尝试使用这些概念从 Bank
类中删除显式同步。我开发了这个不可变的账户
类:
Then I've try to use these concepts to delete explicit synchronization from the Bank
class. I developed this immutable Accounts
class:
class Accounts {
private final List<Double> accounts;
public Accounts(List<Double> 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<Double> getList() {
return this.accounts;
}
}
的帐户属性银行
类必须使用 volatile
变量发布:
The accounts attribute of the Bank
class have to be published using a volatile
variable:
private volatile Accounts accounts;
显然,银行的转账方式
class将相应更改:
Clearly, the transfer method of the Bank
class will be changed accordingly:
public void transfer(int from, int to, double amount) {
this.accounts = new Accounts(this.accounts, from, to, amount);
}
使用不可变对象( Accounts
)存储类的状态( Bank
)应该是一个发布模式,在JCIP一书的第3.4.2节中描述。
Using an immutable object (Accounts
) to store the state of a class (Bank
) should be a publishing pattern, that is described at paragraph 3.4.2 of the book JCIP.
然而,在某处仍然存在竞争条件,我无法弄清楚在哪里(以及为什么!!!)。
However, there is still a race condition somewhere and I can't figure out where (and why!!!).
推荐答案
您的帐户
值本质上是可变的(具有不可变余额的银行帐户不是这非常有用,但你可以通过使用像演员模型<一>。您的帐户
类实现 Runnable ,每个帐户
对象负责更新其值
。
Your Account
values are inherently mutable (a bank account with an immutable balance isn't very useful), however you can reduce the complexity by encapsulating the mutable state using something like the Actor Model. Your Account
class implements Runnable, and each Account
object is responsible for updating its value
.
public class Bank {
// use a ConcurrentMap so that all threads will see updates to it
private final ConcurrentMap<Integer, Account> 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<Message> 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;
}
}
}
帐户#transfer
和帐户#credit
方法可以从任何线程安全地调用,因为 BlockingQueue
是线程安全的。 值
字段仅在帐户的 run
方法中进行修改,因此不存在并发修改的风险; 值
需要 volatile
,这样所有线程都可以看到更新(你使用的是 ThreadPoolExecutor
执行所有帐户
,因此无法保证帐户的
run
方法将每次都在相同的 Thread
上执行。
The Account#transfer
and Account#credit
methods can be safely called from any thread because the BlockingQueue
is thread-safe. The value
field is only modified from within the account's run
method and so there's no risk of concurrent modification; the value
needs to be volatile
so that updates are visible to all threads (you're using a ThreadPoolExecutor
to execute all of the Accounts
so there's no guarantee that an Account's
run
method will execute on the same Thread
every time).
您还应该在执行它之前在 Bank
类中记录传输,以便您可以从系统故障中恢复 - 如果服务器在从帐户被扣除之后但在之前崩溃对帐户进行贷记,然后您需要一种方法在服务器恢复后重新建立一致性。
You should also log the transfer in the Bank
class before executing it so that you can recover from a system failure - if the server crashes after the from account is debited but before the to account is credited then you'll need a way to reestablish consistency after the server comes back up.
这篇关于Java中的不变性和同步性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!