簡體   English   中英

這個java類線程安全嗎?

[英]Is this java class thread safe?

這不是我的功課這是給大學學生的一項任務。 出於個人興趣,我對解決方案感興趣。

任務是創建一個包含整數的類(Calc)。 這兩個方法add和mul應該添加或乘以這個整數。

同時設置兩個線程。 一個線程應該調用c.add(3)十次,另一個應該調用c.mul(3)十次(當然在相同的Calc對象上)。

Calc類應確保操作交替完成(add,mul,add,mul,add,mul,..)。

我沒有經常處理與並發相關的問題 - 更不用說Java了。 我已經為Calc提出了以下實現:

class Calc{

    private int sum = 0;
    //Is volatile actually needed? Or is bool atomic by default? Or it's read operation, at least.
    private volatile bool b = true;

    public void add(int i){
        while(!b){}
        synchronized(this){
                sum += i;
            b = true;   
        }
    }

    public void mul(int i){
        while(b){}
        synchronized(this){
            sum *= i;
            b = false;  
        }
    }

}

我想知道我是否走在正確的軌道上。 對於while(b)部分來說,肯定有一種更優雅的方式。 我想聽聽你們這些人的想法。

PS:不得更改方法的簽名。 除此之外,我不受限制。

嘗試使用Lock界面:

class Calc {

    private int sum = 0;
    final Lock lock = new ReentrantLock();
    final Condition addition  = lock.newCondition(); 
    final Condition multiplication  = lock.newCondition(); 

    public void add(int i){

        lock.lock();
        try {
            if(sum != 0) {
                multiplication.await();
            }
            sum += i;
            addition.signal(); 

        } 
        finally {
           lock.unlock();
        }
    }

    public void mul(int i){
        lock.lock();
        try {
            addition.await();
            sum *= i;
            multiplication.signal(); 

        } finally {
           lock.unlock();
        }
    }
}

鎖定就像你的同步塊一樣。 但是如果另一個線程持有lock那么方法將在.await()處等待,直到.signal()

你所做的是一個繁忙的循環:你正在運行一個只在變量發生變化時停止的循環。 這是一個糟糕的技術,因為它使CPU非常繁忙,而不是簡單地使線程等待,直到更改標志。

我會使用兩個信號量 :一個用於multiply ,一個用於add add必須在添加之前獲取addSemaphore ,並在multiplySemaphore完成時釋放許可,反之亦然。

private Semaphore addSemaphore = new Semaphore(1);
private Semaphore multiplySemaphore = new Semaphore(0);

public void add(int i) {
    try {
        addSemaphore.acquire();
        sum += i;
        multiplySemaphore.release();
    }
    catch (InterrupedException e) {
        Thread.currentThread().interrupt();
    }
}

public void mul(int i) {
    try {
        multiplySemaphore.acquire();
        sum *= i;
        addSemaphore.release();
    }
    catch (InterrupedException e) {
        Thread.currentThread().interrupt();
    }
}

正如其他人所說的volatile在您的解決方案是必需的。 此外,您的解決方案旋轉等待,這可能會浪費相當多的CPU周期。 也就是說,就有關的正確性而言,我看不出任何問題。

我個人會用一對信號量來實現它:

private final Semaphore semAdd = new Semaphore(1);
private final Semaphore semMul = new Semaphore(0);
private int sum = 0;

public void add(int i) throws InterruptedException {
    semAdd.acquire();
    sum += i;
    semMul.release();
}

public void mul(int i) throws InterruptedException {
    semMul.acquire();
    sum *= i;
    semAdd.release();
}

是的,需要volatile ,不是因為從boolean到另一個boolean的賦值不是原子的,而是為了防止變量的緩存使得其更新的值對於正在讀取它的其他線程是不可見的。 如果您關心最終結果, sum應該是volatile

話雖如此,使用waitnotify來創建這種交錯效果可能會更優雅。

class Calc{

    private int sum = 0;
    private Object event1 = new Object();
    private Object event2 = new Object();

    public void initiate() {
        synchronized(event1){
           event1.notify();
        }
    }

    public void add(int i){
        synchronized(event1) {
           event1.wait();
        }
        sum += i;
        synchronized(event2){
           event2.notify();
        }
    }

    public void mul(int i){
        synchronized(event2) {
           event2.wait();
        }
        sum *= i;
        synchronized(event1){
           event1.notify();
        }
    }
}

然后在啟動兩個線程后,調用initiate以釋放第一個線程。

需要volatile,否則優化器可能會優化循環, if(b)while(true){}

但你可以通過waitnotify來做到這一點

public void add(int i){

    synchronized(this){
        while(!b){try{wait();}catch(InterruptedException e){}}//swallowing is not recommended log or reset the flag
            sum += i;
        b = true;   
        notify();
    }
}

public void mul(int i){
    synchronized(this){
        while(b){try{wait();}catch(InterruptedException e){}}
        sum *= i;
        b = false;  
        notify();
    }
}

但是在這種情況下(b在同步塊內檢查)不需要volatile

嗯。 您的解決方案存在許多問題。 首先,原子性不需要揮發性,而是可見性。 我不會在這里討論,但您可以閱讀有關Java內存模型的更多信息。 (是的,布爾是原子的,但這里無關緊要)。 此外,如果僅在同步塊訪問變量,則它們不必是易失性的。

現在,我認為這是偶然的,但是你的b變量只是在同步塊中才被訪問,而且它恰好是易變的,所以實際上你的解決方案會起作用,但它既不是慣用的也不是推薦的,因為你在等待b到在繁忙的循環中改變。 你沒有任何東西燃燒CPU周期(這就是我們所謂的自旋鎖,有時它可能很有用)。

慣用解決方案如下所示:

class Code {
    private int sum = 0;
    private boolean nextAdd = true;

    public synchronized void add(int i) throws InterruptedException {
        while(!nextAdd )
            wait();
        sum += i;
        nextAdd = false;
        notify();
    }

    public synchronized void mul(int i) throws InterruptedException {
        while(nextAdd)
            wait();
        sum *= i;
        nextAdd = true;
        notify();
    }
}

該程序是完全線程安全的:

  1. 布爾標志設置為volatile,因此JVM知道不緩存值並且一次保持對一個線程的寫訪問。

  2. 兩個關鍵部分鎖定當前對象,這意味着一次只有一個線程可以訪問。 請注意,如果一個線程在同步塊內,則任何其他關鍵部分都不能有線程。

以上將適用於該類的每個實例。 例如,如果創建了兩個實例,則線程將能夠一次輸入多個關鍵部分,但每個關鍵部分將限制為每個實例一個線程。 那有意義嗎?

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM