Как два потока могут быть "включены" в "синхронизированный" метод

Я действительно новый на Java concurrency, и я пытаюсь реализовать следующие спецификации:

  • У нас есть автостоянка, в которой есть парковые места.
  • Каждый автомобиль представлен как нить, которая бесконечно меняет состояние автомобиля от вождения - парковка. У каждого автомобиля есть свое место для парковки.
  • Когда автомобиль находится в состоянии парковки, он пытается припарковаться на месте (не обязательно его место). Если место бесплатное, тогда он припаркуется еще, он пропустит эту фазу парковки и вернется к приводу.
  • Автомобиль остается на месте, если владелец места не захочет припарковаться.

Это не точная спецификация, но единственная проблема, которая у меня есть, следующая:

Я не могу заставить машину пропустить ход. Если две машины выберут одни и те же пятна, то один припаркован, а другой ждет, пока парк не станет свободным. Который не тот, что я хочу. Моя первая идея состояла в том, чтобы просто синхронизировать чтение и запись с занятой переменной:

class Car implements Runnable {
    private CarState state = CarState.driving

    run {
        while(true) {
            switch(state) {
            case driving: 
                System.out.println(this + " driving.");
                state = parking;
                break;
            case parking: Spot s = CarPark.getRandomSpot();

                if(s.willingToPark(this)) {
                    System.out.println(s + " occupied. " + this 
                    + " skip park turn.");
                } else {
                    s.park(this);
                } 
                state = driving;
            }
        }

    }
}

class Spot {
    private boolean occupied = false;
    private Car owner = new Car(...);

    synchronized boolean willingToPark(Car c) {
        if(occupied) {
            return true;
        } else {
            occupied = true;
            return false;
    }

    synchronized void park(Car c) {
        System.out.println(c + " parking at " + this);
        //don't care how this is implemented, just keep in mind
        //that it will enter in a loop until the owner came back.
        occupied = false;
    }
}

Если я запустил это с тремя автомобилями, то в итоге я окажусь в автостоянке на месте1, car1 будет стоять на месте 0, car2 ждет на месте 0, потому что car1 выполняет синхронизированный блок-парк (Car c). Я не понимаю, как возможно, что две машины могут припарковаться в том же месте, если ReadyToPark синхронизируется.

Спасибо

Ответы

Ответ 1

Я не понимаю, как возможно, что две машины могут припарковаться в одном и том же месте если synchronToPark синхронизирован.

Это на самом деле просто. Car1 захватывает spot0 и начинает ждать владельца в цикле внутри метода park() (вы не предоставили код). Пока он ждет, он владеет монитором и не позволяет кому-либо вызывать синхронизированные методы на spot0.
Вот почему car2 зависает по методу willingToPark().

Ответ 2

Проблема в цикле park. Представьте себе следующее:

  • Два потока одновременно захватывают одно и то же пятно, s.
  • Они оба пытаются установить блокировку монитора на s; только один преуспеет.
  • То, что преуспевает, вызовет поворот park при сохранении блокировки.
  • Другой терпеливо ждет, пока блокировка не будет выпущена, чтобы он мог ее приобрести.

Нет ничего (кроме убийства JVM), который будет сообщать ните, который не удалось припарковать, чтобы остановить ожидание s. Он будет ждать, пока он не сможет получить блокировку - это произойдет только после того, как первый поток завершит цикл на park.

Решение состоит не в том, чтобы входить в park вообще. Вместо этого автомобиль должен отключить флаг occupied с помощью нового метода unpark(Car). Этот метод также должен быть синхронизирован в целях видимости памяти по потокам.

Теперь поток данных выглядит следующим образом:

  • Два потока одновременно захватывают одно и то же пятно, s.
  • Они оба пытаются установить блокировку монитора на s; только один преуспеет.
  • Тот, который преуспевает, устанавливает occupied = true и сразу же возвращает, а затем освобождает блокировку s
  • Другой получает блокировку на s и видит, что место занято.

Кстати, для этого вам даже не нужен метод synchronized. Вы можете использовать метод AtomicBoolean compareAndSet, который позволяет вам атомарно проверять значение AtomicBoolean и устанавливать его только в том случае, если его текущее значение - то, что вы ожидаете это будет. Таким образом, return occupied.compareAndSet(false, true) означает "атомно проверить текущее значение, если оно ложно, тогда установите его в true и верните true, если оно истинно, сохраните его как есть и верните false". Такое поведение полезно, но немного более продвинуто.

Ответ 3

Отметьте места парковки с помощью AtomicBoolean:

class Spot{
  public final AtomicBoolean flag = new AtomicBoolean(false);
}

Некоторые, где еще в коде, есть гонка среди автомобильных нитей, чтобы захватить пятна:

if(spot.flag.compareAndSet(false,true)){
  // spot owned by current thread !!
  // for other threads, `compareAndSet` will fail because they expect it to be `false`.

  // visit store and buy stuff while car is parked.

  // time to go, release the spot
  spot.flag.set(false);
}else{
  // find another spot
}

Полный пример: http://ideone.com/dw3LnV

Этот подход не требует свободного времени и блокировки. Вы можете поместить спотовую спотовую логику в цикл while, и потоки будут продолжать бороться за флаг.

Дополнительная литература: http://www.ibm.com/developerworks/library/j-jtp11234/

Ответ 4

Я думаю, вы не получите большую картину о синхронизации, Luca. Проблема с циклом ожидания. Что касается самой синхронизации, это можно сделать только на объектах:

  • имеющий метод synchronized, синхронизирует его с this
  • имеющий блок кода synchronized(someObject) {...}, синхронизирует его на someObject

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

Вы можете, например, синхронизировать только паркую часть в методе парковки:

void park(Car c) {
    System.out.println(c + " parking at " + this);
    //don't care how this is implemented, just keep in mind
    //that it will enter in a loop until the owner came back.
    synchronized (this) {
        occupied = false;
    }
}