Почему поток Java ведет себя так по-другому, если они не должны в этом сценарии? - PullRequest
0 голосов
/ 13 января 2019

У меня проблема с нитью. Внутри метода выполнения потока у меня есть синхронизированный блок и время ожидания. Каждый поток увеличивает или уменьшает общий класс «значение» на 5 единиц, а затем спит.

public class borr {

    public static void main(String[] args) {

        int times=5;
        int sleeptime=1000;
        int initial=50;
        Shared shared = new Shared(initial);

        ThreadClass tIncrement = new ThreadClass(shared,times,sleeptime,true);
        ThreadClass tDecrement = new ThreadClass(shared,times,sleeptime,false);
        tIncrement.start();
        tDecrement.start();
    }
}

class Shared{

    int  value=0;

    public Shared(int value) {
        super();
        this.value = value;
    }

    public int getValue() {
        return value;
    }

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

class ThreadClass extends Thread{

    Shared shared;
    int times=0;
    int sleeptime=0;
    boolean inc;

    public ThreadClass(Shared shared, int times, int sleeptime, boolean inc) {
        super();
        this.shared = shared;
        this.times = times;
        this.sleeptime = sleeptime;
        this.inc = inc;
    }

    public void run() {

        int aux;

        if(inc) {
            for(int i=0;i<times;i++) {
                synchronized(shared) {
                    aux=shared.getValue()+1;
                    shared.setValue(aux);
                    System.out.println("Increment, new value"+shared.getValue());

                    try {
                        Thread.sleep(sleeptime);
                    }catch(Exception e) {
                        e.printStackTrace();
                    }
                }
            }   
        }
        else {
            for(int i=0;i<times;i++) {
                synchronized(shared) {
                    aux=shared.getValue()-1;
                    shared.setValue(aux);
                    System.out.println("Decrement, new value"+shared.getValue());

                    try {
                        Thread.sleep(sleeptime);
                    }catch(Exception e) {
                        e.printStackTrace();
                    }
                }
            }   
        }
    }
}

Но если я переместу Thread.sleep из блока synchronized, например, это будет увеличение, уменьшение, увеличение, уменьшение. Когда он перестает спать и начинает новую итерацию цикла, не должен ли другой поток попытаться войти? вместо этого он продолжает цикл до тех пор, пока этот поток не закончится:

for(int i=0;i<times;i++) {
    synchronized(shared) {
        aux=shared.getValue()-1;
        shared.setValue(aux);
        System.out.println("Decrement, new value"+shared.getValue());
    }

    try {
        Thread.sleep(sleeptime);
    }catch(Exception e) {
        e.printStackTrace();
    }
}   

Ответы [ 3 ]

0 голосов
/ 13 января 2019

В варианте А вы используете две нити, которые ...

  • повторить 5 раз
    • введите блок синхронизации
      • прибавка
      • подождите 1 секунду
  • повторить 5 раз
    • введите блок синхронизации
      • декремент
      • подождите 1 секунду

В варианте B вы используете две нити, которые ...

  • повторить 5 раз
    • введите блок синхронизации
      • прирост
    • подождите 1 секунду
  • повторить 5 раз
    • введите блок синхронизации
      • декремент
    • подождите 1 секунду

В варианте A оба потока активны (= остаются в блоке синхронизации) все время.

В варианте B оба потока спят большую часть времени.

Поскольку нет абсолютно никакой гарантии, какие потоки будут выполняться следующим, неудивительно, что варианты A и B ведут себя так по-разному. Хотя в A оба потока - теоретически - могут быть активны параллельно, второй поток не имеет большого шанса быть активным, поскольку отсутствие контекста синхронизации не гарантирует, что переключение контекста выполняется в этот момент (и запускается другой поток). ). В варианте B это совершенно разное: поскольку оба потока спят большую часть времени, среда выполнения не имеет другого шанса запустить другой поток, пока он спит. Спящий режим инициирует переключение на другой поток, так как виртуальная машина пытается использовать все имеющиеся ресурсы ЦП.

Тем не менее: результат ПОСЛЕ того, как оба потока будут запущены, будет в точности одинаковым. Это единственный детерминизм, на который вы можете положиться. Все остальное зависит от конкретных деталей реализации того, как виртуальная машина будет обрабатывать потоки и блоки синхронизации, и даже может варьироваться от ОС к ОС или от одной реализации виртуальной машины к другой.

0 голосов
/ 13 января 2019

Это плохо:

for(...) {
    synchronized(some_lock_object) {
        ...
    }
}

Причина, по которой это плохо, заключается в том, что как только какой-то поток, A, попадает в этот цикл, затем каждый раз, когда он разблокирует блокировку, Следующее, что он делает , это снова блокирует блокировку.

Если телу цикла требуется какое-то значительное время для выполнения, то любой другой поток B, ожидающий блокировки, будет переведен в состояние ожидания операционной системой. Каждый раз, когда поток A снимает блокировку, поток B начинает просыпаться, но поток A сможет повторно захватить его, прежде чем поток B получит шанс.

Это классический пример голодания .

Одним из способов решения этой проблемы было бы использование ReentrantLock с политикой справедливого заказа вместо использования synchronized блока. Когда потоки конкурируют за справедливую блокировку, победителем всегда является тот, кто ждал дольше всех.

Но справедливые блокировки обходятся дорого. Намного лучшим решением будет всегда держать тело любого блока synchronized как можно более коротким и сладким. Обычно поток должен держать блокировку заблокированной не дольше, чем требуется для присвоения небольшого количества полей в каком-либо объекте.

0 голосов
/ 13 января 2019

Но если я вывожу Thread.sleep из синхронизированного блока, как это, вывод будет увеличение, уменьшение, увеличение, уменьшение. Спящий режим все еще находится внутри каждой итерации цикла, поэтому, не должен ли результат быть одинаковым в обоих случаях?:

когда он перестает спать и начинает новую итерацию цикла, не должен ли другой поток попытаться войти.

Они оба пытаются войти.

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

Это состояние гонки. Когда оба потока хотят блокировать одновременно, система может выбрать один из них. Кажется, он выбирает тот, который несколько инструкций назад только что выпустил. Может быть, вы можете изменить это, yield() ING. Возможно, нет. Но так или иначе, это не определено / детерминировано / справедливо. Если вы заботитесь о порядке исполнения, вам нужно четко составить расписание самостоятельно.

...