web-dev-qa-db-ja.com

Javaマルチスレッド-スレッドセーフカウンター

マルチスレッドの非常に簡単な例から始めます。スレッドセーフなカウンターを作ろうとしています。カウンターを断続的にインクリメントして1000に達する2つのスレッドを作成します。以下のコード:

public class ThreadsExample implements Runnable {
     static int counter = 1; // a global counter

     public ThreadsExample() {
     }

     static synchronized void incrementCounter() {
          System.out.println(Thread.currentThread().getName() + ": " + counter);
          counter++;
     }

     @Override
     public void run() {
          while(counter<1000){
               incrementCounter();
          }
     }

     public static void main(String[] args) {
          ThreadsExample te = new ThreadsExample();
          Thread thread1 = new Thread(te);
          Thread thread2 = new Thread(te);

          thread1.start();
          thread2.start();          
     }
}

私が知ることができることから、今のwhileループは、最初のスレッドだけが1000に達するまでカウンターにアクセスできることを意味します。出力:

Thread-0: 1
.
.
.
Thread-0: 999
Thread-1: 1000

どうすれば修正できますか?スレッドでカウンターを共有するにはどうすればよいですか?

10
BrotherBarnabas

両方のスレッドが変数にアクセスできます。

あなたが見ている現象はスレッド枯渇と呼ばれています。コードの保護された部分に入ると(申し訳ありませんが、これは見逃しました)、他のスレッドは、モニターを保持しているスレッドが完了するまで(つまり、モニターがリリースされたとき)、ブロックする必要があります。 。現在のスレッドがモニターを次のスレッドで待機して次のスレッドに渡すことを期待するかもしれませんが、同期ブロックの場合、Javaは、次にスレッドがモニターを受け取る公平性または順序付けポリシーを保証しません。- しばらく待っていた別のスレッドを介してモニターを解放するためにモニターを解放して再取得しようとするスレッドが完全に可能です(そして可能性さえあります)。

Oracleから:

飢餓とは、スレッドが共有リソースに定期的にアクセスできず、進行できない状況を指します。これは、「貪欲な」スレッドによって共有リソースが長期間使用できなくなると発生します。たとえば、オブジェクトが、戻るのに長い時間がかかる同期メソッドを提供するとします。 1つのスレッドがこのメソッドを頻繁に呼び出すと、同じオブジェクトへの頻繁な同期アクセスも必要とする他のスレッドがブロックされることがよくあります。

どちらのスレッドも「貪欲な」スレッドの例ですが(モニターを繰り返し解放して再取得するため)、スレッド0が技術的に最初に開始され、スレッド1が不足します。

解決策は、次に示すように、公平性をサポートする同時同期方法(ReentrantLockなど)を使用することです。

public class ThreadsExample implements Runnable {
    static int counter = 1; // a global counter

    static ReentrantLock counterLock = new ReentrantLock(true); // enable fairness policy

    static void incrementCounter(){
        counterLock.lock();

        // Always good practice to enclose locks in a try-finally block
        try{
            System.out.println(Thread.currentThread().getName() + ": " + counter);
            counter++;
        }finally{
             counterLock.unlock();
        }
     }

    @Override
    public void run() {
        while(counter<1000){
            incrementCounter();
        }
    }

    public static void main(String[] args) {
        ThreadsExample te = new ThreadsExample();
        Thread thread1 = new Thread(te);
        Thread thread2 = new Thread(te);

        thread1.start();
        thread2.start();          
    }
}

メソッド内のReentrantLockを優先して、synchronizedキーワードを削除したことに注意してください。公平性ポリシーを備えたこのようなシステムでは、長時間待機しているスレッドが実行される機会が与えられ、飢餓が解消されます。

13
initramfs

AtomicIntegerを使用できます。これは、アトミックにインクリメントできるクラスであるため、インクリメントメソッドを呼び出す2つの別々のスレッドがインターリーブしません。

public class ThreadsExample implements Runnable {
     static AtomicInteger counter = new AtomicInteger(1); // a global counter

     public ThreadsExample() {
     }

     static void incrementCounter() {
          System.out.println(Thread.currentThread().getName() + ": " + counter.getAndIncrement());
     }

     @Override
     public void run() {
          while(counter.get() < 1000){
               incrementCounter();
          }
     }

     public static void main(String[] args) {
          ThreadsExample te = new ThreadsExample();
          Thread thread1 = new Thread(te);
          Thread thread2 = new Thread(te);

          thread1.start();
          thread2.start();          
     }
}
11
Bert Peters

まあ、あなたのコードでは「正確に」断続的に取得する方法がわかりませんが、Thread.yield()を呼び出した後にincrementCounter()を使用すると、より良いディストリビューションが得られます。

public void run() {
         while(counter<1000){
              incrementCounter();
              Thread.yield();

         }
    }

それ以外の場合は、提案するものを取得するために、2つの異なるスレッドクラス(必要に応じてThreadsExample1とThreadsExample2)を作成し、別のクラスを共有変数にすることができます。

public class SharedVariable {
    private int value;
    private boolean turn; //false = ThreadsExample1 --> true = ThreadsExample2

    public SharedVariable (){
        this.value = 0;
        this.turn = false;
    }

    public void set (int v){
        this.value = v;
    }

    public int get (){
        return this.value;
    }

    public void inc (){
        this.value++;
    }

    public void shiftTurn(){
        if (this.turn){
            this.turn=false;
        }else{
            this.turn=true;
        }
    }

    public boolean getTurn(){
        return this.turn;
    }

}

ここで、メインは次のようになります。

public static void main(String[] args) {
        SharedVariable vCom = new SharedVariable();
        ThreadsExample1 hThread1 = new ThreadsExample1 (vCom);
        ThreadsExample2 hThread2 = new ThreadsExample2 (vCom);

        hThread1.start();
        hThread2.start();

        try {
            hThread1.join();
            hThread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

そして、あなたの行を変更する必要がありますstatic int counter = 1; // a global counter ために private SharedVariable counter;

そして、新しい実行は:

public void run() {
    for (int i = 0; i < 20; i++) {
        while (!counter.getTurno()){
            Thread.yield();
        }
        System.out.println(this.counter.get());
        this.counter.cambioTurno();
    }
}

}

はい、これは別のコードですが、少し役立つと思います。

1
Shondeslitch

スレッドをスリープさせて、他のスレッドが確実に成功するようにします。

     @Override
     public void run() {
          while(counter<1000){
               incrementCounter();
               Thread.sleep(1);
          }
     }
0
almeynman