Java中的不变性和同步性 [英] Immutability and synchronization in Java

查看:159
本文介绍了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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆