2016-08-02 36 views
3

我最近試圖圍繞一些Java多線程概念進行研究,並撰寫了一小段代碼,以幫助我理解內存可見性並儘可能地獲得同步。根據我讀過的內容,似乎我們鎖定的代碼量越小,程序的效率就越高(一般情況下)。我寫了一個小的類來幫助我瞭解一些我可能會遇到的同步問題:瞭解Java多線程中的內存可見性

public class BankAccount { 
    private int balance_; 

    public BankAccount(int initialBalance) { 
     if (initialBalance < 300) { 
      throw new IllegalArgumentException("Balance needs to be at least 300"); 
     } 
     balance_ = initialBalance; 
    } 

    public void deposit(int amount) { 
     if (amount <= 0) { 
      throw new IllegalArgumentException("Deposit has to be positive"); 
     } 
     // should be atomic assignment 
     // copy should also be non-shared as it's on each thread's stack 
     int copy = balance_; 

     // do the work on the thread-local copy of the balance. This work should 
     // not be visible to other threads till below synchronization 
     copy += amount; 

     synchronized(this) { 
      balance_ = copy; // make the new balance visible to other threads 
     } 
    } 

    public void withdraw(int amount) { 
     // should be atomic assignment 
     // copy should also be non-shared as it's on each thread's stack 
     int copy = balance_; 

     if (amount > copy) { 
      throw new IllegalArgumentException("Withdrawal has to be <= current balance"); 
     } 

     copy -= amount; 

     synchronized (this) { 
      balance_ = copy; // update the balance and make it visible to other threads. 
     } 
    } 

    public synchronized getBalance() { 
     return balance_; 
    } 
} 

請忽略的事實是balance_應該是雙重的,而不是一個整數。我知道原始類型讀取/賦值是原子性的,除了雙精度和長整數,所以我選擇了簡單的整數

我試圖在函數中寫下注釋來描述我的想法。這個類是爲了獲得正確的同步以及最小化被鎖定的代碼量而編寫的。這裏是我的問題:

  1. 此編程是否正確?它會遇到任何數據/競爭條件嗎?所有的更新都可以被其他線程看到嗎?
  2. 這個代碼與僅僅進行方法級同步一樣有效嗎?我可以想象,隨着工作量的增加(這裏,它只是一個加法/減法),它可能導致顯着的性能問題,方法級同步。
  3. 此代碼可以更有效嗎?
+2

「 balance_應該是一個double而不是一個整數「不,建議不要使用浮點類型來表示貨幣。 –

+1

始終使用long或BigInteger作爲貨幣,永遠不會翻倍。 1.20€= 120. – dit

+0

@dit我會說長或BigDecimal。如果你用很長時間來表示美分,2^63將會不夠。例如,以美分爲單位的世界GDP只有2^46美元左右。如果你的客戶擁有的資金比他們的賬戶更多,我會更少擔心BigInteger,更多的是關於肆意破壞全球經濟的通貨膨脹。 ;) – yshavit

回答

1

此代碼很容易出現競爭狀況。

考慮這一部分:

int copy = balance_; 
copy += amount; 
// here! 
synchronized(this) { 
    balance_ = copy; // make the new balance visible to other threads 
} 

如果有人所說的 「這裏」 節期間withdrawdeposit會發生什麼?第二種方法將更改_balance,但該更改不會反映在您當地的copy中。然後,當您將copy寫入共享變量時,它將簡單地覆蓋該值。

處理這個問題的方法是將整個操作—的讀取,修改和寫入—的排他鎖。或者,您可以使用提供原子incrementAndGet方法的AtomicInteger。這通常可以編譯爲稱爲"compare and swap"的硬件基元,因此非常高效。缺點是它只爲這一個操作提供原子性;如果你需要一些其他操作也是原子的(也許你還想增加一個depositCounts字段?),那麼AtomicInteger將不起作用。

+0

換句話說。將你的公共方法標記爲'synchronized'。 – dit

2

不在同步塊內的任何代碼都可以由多個線程共同執行,您的解決方案是在同步塊之外創建新的天平,因此無法正常工作。讓我們來看一個例子:當程序啓動時,我們有

int copy = balance_; // 1 

copy += amount; //2 

synchronized(this) { 
    balance_ = copy; // 3 
} 
  1. _balance = 10
  2. 然後我們開始2個線程試圖添加10和15的平衡
  3. 線程1分配10可變拷貝
  4. 線程2 10分配給變量複製
  5. 線程2增加了15複製並把結果賦給_balance - > 25
  6. 線程1加1 0複製和分配結果_balance - > 20

在的BankAccount具有20月底,但它應該是35

這是讓它正確的方法:

public class BankAccount { 
    private int balance_; 

    public BankAccount(int initialBalance) { 
     if (initialBalance < 300) { 
      throw new IllegalArgumentException("Balance needs to be at least 300"); 
     } 
     balance_ = initialBalance; 
    } 

    public void deposit(int amount) { 
     if (amount <= 0) { 
      throw new IllegalArgumentException("Deposit has to be positive"); 
     } 

     synchronized(this) { 
      balance_ += amount; 
     } 
    } 

    public void withdraw(int amount) { 
     synchronized (this) { 
      if (amount > balance_) { 
       throw new IllegalArgumentException("Withdrawal has to be <= current balance"); 
      } 

      balance_ -= amount; 
     } 
    } 

    public synchronized int getBalance() { 
     return balance_; 
    } 
}