[英]Java Thread safety - Understanding the need of synchronization
在運行多線程應用程序時,我很難理解同步方法,對象的概念並理解不這樣做的主要問題。
我了解到,使用synchronize
關鍵字可確保只有一個線程一次可與特定對象一起使用或一次輸入特定的塊或方法,基本上將其鎖定並在執行結束時將其解鎖,以便其他線程可以輸入它。
但是我真的不明白這個問題,我完全感到困惑,我創建了一個演示應用程序,其中有2個銀行帳戶,一個銀行類別有5000
資金,並且有一種方法可以將特定金額的資金轉入給定帳戶,並在其構造函數中創建2個銀行帳戶並啟動線程(每個帳戶都是一個線程)。
現在在銀行帳戶的類中,我有一個funds
字段,以及一個運行方法,該線程將在啟動時調用(該類繼承Thread),該運行方法將循環運行10次,並通過調用Bank#takeFunds(int amount)
從主銀行收取20美元Bank#takeFunds(int amount)
這樣,銀行類:
public class Bank {
private int bankmoney = 5000;
public Bank() {
Client a = new Client(this);
Client b = new Client(this);
a.start();
b.start();
}
public void takeMoney(Client c, int amount) {
if (bankmoney >= amount) {
bankmoney -= amount;
c.addFunds(amount);
}
}
public void print() {
System.out.println("left: " + bankmoney);
}
public static void main(String... args) {
new Bank();
}
}
和Client類:
public class Client extends Thread {
private Bank b;
private int funds;
Random r = new Random();
public Client(Bank b) {
this.b = b;
}
public void addFunds(int funds) {
this.funds += funds;
}
public void run() {
for (int i = 0; i < 10; i++) {
b.takeMoney(this, 20);
}
System.out.println(Thread.currentThread().getName() + " : " + funds);
b.print();
}
}
和輸出對我來說:
Thread-0 : 200
left: 4800
Thread-1 : 200
left: 4600
該程序在每個帳戶中以200 $結尾,在銀行中還有4600,所以我沒有真正看到這個問題,我沒有演示線程安全性問題,我認為這就是為什么我不明白它的原因。
我試圖獲得有關其確切工作方式的最簡單解釋,我的代碼如何使線程安全成為問題?
謝謝!
例:
static void transfer(Client c, Client c1, int amount) {
c.addFunds(-amount);
c1.addFunds(amount);
}
public static void main(String... args) {
final Client[] clients = new Client[]{new Client(), new Client()};
ExecutorService s = Executors.newFixedThreadPool(15);
for (int i = 0; i < 15; i++) {
s.submit(new Runnable() {
@Override
public void run() {
transfer(clients[0], clients[1], 200);
}
});
}
s.shutdown();
while(!s.isTerminated()) {
Thread.yield();
}
for (Client c : clients) {
c.printFunds();
}
}
印刷品:
My funds: 2000
My funds: 8000
首先,線程不是對象。 不要為每個客戶端分配單獨的線程。 線程確實起作用,並且對象包含指定必須完成的代碼。
當您在Client
對象上調用方法時,它們不會“在該客戶端的線程上”執行; 它們在調用它們的線程中執行。
為了使線程能夠完成某些工作,您需要將其移交給實現要在其上執行的代碼的對象。 這就是ExecutorService
允許您ExecutorService
完成的工作。
還要記住,鎖不會“鎖定對象”,並且synchronized(anObject)
不會阻止另一個線程同時調用anObject
的方法。 鎖只會阻止嘗試獲取同一鎖的其他線程繼續處理,直到第一個線程完成為止。
我測試了您的程序,實際上得到了以下輸出:
(結果與您的情況不同,為4600。)
關鍵是,僅僅因為它碰巧一次工作並不意味着它會一直工作。 多線程可以(在一個不同步的程序中)引入不確定性。
想象一下,如果您的操作花費了更長的時間才能執行。 讓我們用Thread.sleep
模擬它:
public void takeMoney(Client c, int amount) {
if (bankmoney >= amount) {
try { Thread.sleep(1000); } catch (InterruptedException e) { }
bankmoney -= amount;
c.addFunds(amount);
}
}
現在嘗試再次運行程序。
您的程序運行正常,因為您只扣除2000的總數。這遠小於初始值。 因此,此檢查沒有任何作用,即使您刪除了代碼,代碼仍然可以使用。
if (bankmoney >= amount) {
在這種情況下唯一可能發生的壞事是:如果client1檢查的金額超過了他需要提取的金額,但與此同時,其他客戶端也提取了該金額。
public void run() {
for (int i = 0; i < 100; i++) {
b.takeMoney(this, 200);
}
System.out.println(Thread.currentThread().getName() + " : " + funds);
b.print();
}
public void takeMoney(Client c, int amount) {
if (bankmoney >= amount) {
system.println("it is safer to withdraw as i have sufficient balance")
bankmoney -= amount;
c.addFunds(amount);
}
}
客戶有時會檢查銀行存款大於金額,但是當他提款時,金額會達到負數。 因為其他線程將占用該金額。 運行程序,您將認識到4-5次。
讓我們看一個更現實的例子,並為我們的銀行實現transfer
功能:
public boolean transfer(long amount, Client source, Client recipient) {
if(!source.mayTransferAmount(amount)) return false; // left as an exercise
source.balance -= amount;
recipient.balance += amount;
}
現在讓我們想象兩個線程。 線程A將單個單元從客戶端x轉移到客戶端y,而線程B將單個單元從客戶端y轉移到客戶端x。 現在您必須知道,如果沒有同步,您將無法確定CPU如何命令操作,因此可能是:
A: get x.balance (=100) to tmpXBalance
B: get x.balance (=100) to tmpXBalance
B: increment tmpXBalance (=101)
B: store tmpXBalance to x.balance (=101)
A: decrement tmpXBalance (=99)
A: store tmpXBalance to x.balance (=99)
(rest of exchange omitted for brevity)
哇! 我們只是賠錢! 客戶x不會很高興。 請注意,僅鎖定不會給您任何保證,還需要將balance
聲明為volatile
。
任何時候,如果要對多個線程共享的數據進行某種處理,如果需要執行多個步驟,則可能需要同步。
這需要三個步驟:
i++;
步驟是; (1)從存儲器中將i
的值存入寄存器,(2)將1加到寄存器,(3)將寄存器的值存回存儲器。
運行中的線程可以隨時被搶占 。 這意味着,操作系統可以暫停它,並使用CPU輪換其他線程。 因此,如果沒有同步,線程A可以執行使i
遞增的步驟(1)(可以將值存入寄存器),然后可以搶占它。 當線程A等待再次運行時,線程B,C和D可以分別遞增i
一千次。 然后,當線程A最終再次運行時,它將在其最初讀取的值上加1,然后將其存儲回內存中。 線程B,C和D的三千個增量將丟失。
每當一個線程將某些數據置於不希望其他線程看到或對其進行操作的臨時狀態時,就需要進行同步。 創建臨時狀態的代碼必須synchronized
,並且可以對同一數據進行操作的任何其他代碼必須同步,並且僅允許線程查看狀態的任何代碼都必須同步。
正如Marko Topolnik指出的那樣,同步不會對數據進行操作,也不會對方法進行操作。 您需要確保所有修改或查看特定數據集合的代碼都在同一對象上同步。 那是因為同步只做一件事,而只有一件事:
JVM不允許在同一對象上同時synchronized
兩個線程。 這就是全部。 使用方式取決於您。
如果您的數據在容器中,則可以方便地在容器對象上進行同步。
如果您的數據是同一個Foobar實例的所有實例變量,則可以方便地在該實例上進行同步。
如果您的數據都是靜態的,那么您可能應該在某個靜態對象上進行同步。
祝好運並玩得開心點。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.