Почему поток Java ведет себя по-другому, когда счетчик больше в моем классе?

#java #multithreading

#java #многопоточность

Вопрос:

Вот мой код:

 public class MyRunnableClass implements Runnable {

    static int x = 25;
    int y = 0;
    private static final Object sharedLock = new Object();


    @Override
    public void run() {
        while(x>0){
            someMethod();
        }
    }

    public synchronized void someMethod(){
        synchronized (sharedLock){
            x--;
            y  ;
        }
    }
}
  

и тестовый класс:

 public class MyRunnableClassTest {

    public static void main(String[] args) throws InterruptedException {

        MyRunnableClass aa = new MyRunnableClass();
        MyRunnableClass bb = new MyRunnableClass();

        Thread a = new Thread(aa);
        Thread b = new Thread(bb);

        a.start();
        b.start();

        a.join();
        b.join();

        System.out.println(aa.y   bb.y);

    }
}
  

Когда я запускаю этот код как есть, я вижу вывод 25, что нормально, но когда x равно 250, я вижу 251.. Почему? Почему не 250?

Комментарии:

1. Если вы запускаете снова и снова с x = 25, вы также увидите тот же эффект, потому что у вас состояние гонки.

2. Создание x volatile не имеет никакого значения @user3580294

3. Ну, тогда не бери в голову… Но вопрос о другом значении x остается в силе.

Ответ №1:

Вы должны расширить synchronized область видимости, чтобы это также охватывало операцию чтения на x :

 @Override
public void run() {
    for (;;) {
        synchronized (sharedObject) {
            if (x <= 0) break;
            someMethod();
        }
    }
}
  

Ответ №2:

Совпадение. То же самое может произойти с 25 , как и с любым другим числом.

Например, во время выполнения

 while(x>0){
    someMethod();
}
  

который не синхронизирован, после множества циклов давайте примем x равным 1. Первый поток начинает выполнение итерации (входит в тело), затем потоки переключаются, второй поток видит, что x равно 1, поэтому также входит в тело цикла. Оба будут увеличивать свое количество, и их сумма будет на единицу больше исходного x значения.

Это условие гонки, и вам просто легче увидеть последствия с большими числами.

Комментарии:

1. Комментарии удалены? Почему?

2. @KorayTugay Я отредактировал свой ответ, чтобы отразить то, что было сказано.

Ответ №3:

Когда вы делаете:

     while(x>0){
        someMethod();
    }
  

Допустим, x = 1 и:

Поток A оценивает x > 0 как true и входит в цикл. Допустим, поток A прерывается перед выполнением следующей строки. Поток B также оценит x > 0 как true и войдет в цикл.

Оба будут уменьшать x один за другим и увеличивать их y.

Чтобы решить эту проблему, проверка на x > 0 также должна быть заблокирована.

Пример:

 public class MyRunnableClass implements Runnable {

static int x = 25;
int y = 0;
private static final Object sharedLock = new Object();


@Override
public void run() {
    while(x>0){
        someMethod();
    }
}

public synchronized void someMethod(){
    synchronized (sharedLock){
        if(x > 0){
            x--;
            y  ;
       }
    }
}
  

}

Ответ №4:

Иногда и поток a, и поток b могут вызывать someMethod(), потому что x равно 1. Один поток блокирует sharedLock, делает x равным 0, y равным 250, а затем освобождает sharedLock, после чего другой поток вызывает someMethod() и делает y равным 251, а x равным -1.

Ответ №5:

Вы также могли бы решить эту проблему с помощью AtomicInteger :

 import java.util.concurrent.atomic.AtomicInteger;

public class MyRunnableClass implements Runnable {

    private static final AtomicInteger xHolder = new AtomicInteger(25);
    int y = 0;

    @Override
    public void run() {
        while (xHolder.decrementAndGet() >= 0) {
            y  ;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnableClass aa = new MyRunnableClass();
        MyRunnableClass bb = new MyRunnableClass();

        Thread a = new Thread(aa);
        Thread b = new Thread(bb);

        a.start();
        b.start();

        a.join();
        b.join();

        System.out.println(aa.y   bb.y);
    }
}
  

Или с некоторым расширенным параллелизмом для тестирования:

 import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;

public class MyRunnableClass implements Callable<Integer> {

    private static final AtomicInteger xHolder = new AtomicInteger(250000);
    int y = 0;

    @Override
    public Integer call() throws Exception {                
        while (xHolder.decrementAndGet() >= 0) {
            y  ;
        }
        System.out.println(Thread.currentThread().getName()   " returns "   y);
        return y;
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        ExecutorCompletionService<Integer> completionService = new ExecutorCompletionService<Integer>(
                executorService);

        int parallelism = 5;

        for (int i = 0; i < parallelism;   i) {
            completionService.submit(new MyRunnableClass());
        } // for

        int ySum = 0;       
        for (int j = 0; j < parallelism;   j) {
            Future<Integer> future = completionService.take();          
            ySum  = future.get();
        } // for    

        System.out.println(ySum);
        executorService.shutdown();
    }

}
  

Вывод:

 pool-1-thread-3 returns 26619
pool-1-thread-5 returns 0
pool-1-thread-1 returns 104302
pool-1-thread-2 returns 95981
pool-1-thread-4 returns 23098
250000