Многопоточность, ожидание освобождение потока

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

public class Main {

    public static void main(String[] args) throws InterruptedException {
        final int min = 1000;
        final int max = 10000;
        final int rnd = DelayTakeoff.rnd(min,max);

        Band newBand = new Band();
        Passenger passenger = new Passenger("Пассажирский");
        Cargo cargo = new Cargo("Грузовой");
        newBand.addPlane(passenger);
        newBand.addPlane(cargo);

        DelayTakeoff thread1 = new DelayTakeoff(passenger.name, passenger,newBand,5000,rnd);
        DelayTakeoff thread2 = new DelayTakeoff(cargo.name, cargo,newBand,10000,rnd);

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

public class DelayTakeoff extends Thread {
    private final Band object;
    private final int delay;
    private final int rndDelay;
    
    DelayTakeoff(String name,Plane plane, Band object, int delay, int rndDelay) {
        this.setName(name);
        this.object = object;
        this.delay = delay;
        this.rndDelay = rndDelay;
    }

    public int getDelay() {
        return delay;
    }

    public int getRndDelay() {
        return rndDelay;
    }

    public static int rnd(int min, int max) {
        max -=min;
        return (int) (Math.random() * ++ max) + min;
    }

    @Override
    public void run() {
        try {
            System.out.println("Прогрев начался: " + getName());
            Thread.sleep(getRndDelay());
            System.out.println("Прогрев завершился: " + getName());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (object) {
            try {
                System.out.println("Взлетает: " + getName());
                Thread.sleep(getDelay());
                System.out.println("Взлетел: " + getName());
            } catch (InterruptedException e1) {}
                object.notify();
            try {
                object.wait();
                } catch (InterruptedException e2) {}
        }
    }
}

Реализовал такое решение, но мне кажется оно не верно, но вывод соответсвует действительности.

public class Main {

public static void main(String[] args) throws InterruptedException {
    final int min = 1000;
    final int max = 10000;
    final int rnd = DelayTakeoff.rnd(min,max);

    Band newBand = new Band();
    Band newBandTwo = new Band();
    Passenger passenger = new Passenger("Пассажирский");
    Cargo cargo = new Cargo("Грузовой");
    Cargo cargo1 = new Cargo("222");
    newBand.addPlane(passenger);
    newBandTwo.addPlane(cargo);
    newBandTwo.addPlane(cargo1);

    DelayTakeoff thread1 = new DelayTakeoff(passenger.name, passenger,newBand,5000,rnd);
    DelayTakeoff thread2 = new DelayTakeoff(cargo.name, cargo,newBandTwo,10000,rnd);
    DelayTakeoff thread3 = new DelayTakeoff(cargo1.name, cargo, newBand, 2000, rnd);

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

Вывод:

Прогрев начался: Пассажирский
Прогрев начался: Грузовой
Прогрев начался: 222
Прогрев завершился: Грузовой
Прогрев завершился: Пассажирский
Прогрев завершился: 222
Взлетает: Грузовой
Взлетает: 222
Взлетел: 222
Взлетает: Пассажирский
Взлетел: Пассажирский
Взлетел: Грузовой

Ответы (1 шт):

Автор решения: kami

Каждая из полос - это независимый разделяемый ресурс, за который конкурируют самолеты в попытке взлететь.
При этом попытки занять этот ресурс не должны приводить к непроизводительному простою самолета в ожидании (вдруг другая полоса в это время освободится).

Для управления разделяемыми ресурсами создадим контейнер, который выдаст "наружу" только необходимый минимум, позволяющий (в рамках данной задачи) занять полосу и освободить её:

class Airport { // это и есть наш контейнер
    private final List<Band> bands = new ArrayList<>(); // все полосы аэропорта

    Airport(int bandCount) { // создаем в конструкторе и распоряжаемся сами
        for (int i = 0; i < bandCount; i++) {
            bands.add(new Band("band " + i));
        }
    }

    @Nullable
    public Band tryLockBand() {
        Band result = null;
        for (Band band : bands) {
            if (band.tryLock()) {
                result = band;
                break;
            }
        }
        if (result != null) System.out.println("Полоса " + result.name + " зарезервирована");
        return result;
    }

    public void unlockBand(@NotNull Band band) {
        band.unlock();
        System.out.println("Полоса " + band.name + " освобождена");
    }

    static class Band {
        public final String name;
        private final Lock lock = new ReentrantLock(); // полоса "сама" следит за возможностью её занять.

        Band(String name) {this.name = name;}

        private boolean tryLock() {return lock.tryLock();}

        private void unlock() {lock.unlock();}
    }
}

Комментарии для класса Airport, думаю, излишни - два основных метода tryLockBand и unlockBand отвечают за получение (первой из свободных) и высвобождение полосы. Сами полосы выступают неотъемлимой частью аэропорта, поэтому описаны как вложенный класс (по большому счёту - правильно было бы убрать из описания Band модификатор static, но тогда вполне справедливо появится hint).

Что касаемо самолета - он не может заранее знать, какая из полос ему станет доступна, поэтому зарезервировать полосу под себя он будет пытаться через аэропорт:

class Plane extends Thread {
    public final String name;
    private final Airport airport;

    Plane(String name, Airport airport) {
        this.name = name;
        this.airport = airport;
    }

    @Override
    public void run() {
        try {
            System.out.println("Прогрев начался: " + name);
            Thread.sleep((long) (Math.random() * 1000) + 500);
            System.out.println("Прогрев завершился: " + name);
            System.out.println("Резервируем полосу для: " + name);

            Airport.Band band;
            do {
                band = airport.tryLockBand();
                Thread.sleep(100);
            } while (band == null);
            // now we get band. let`s take off!!!

            try {
                System.out.println("Взлетает: " + name + " с полосы " + band.name);
                Thread.sleep((long) (Math.random() * 1000) + 500);
                System.out.println("Взлетел: " + name);
            } finally {
                airport.unlockBand(band);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

По большому счёту - комментарии тут тоже излишни, вывод в консоль выполняет эту задачу.

Ну и, напоследок - "основной" код:

public class Main {

    public static void main(String[] args) {

        Airport airport = new Airport(2); // у аэропорта будет 2 полосы

        List<Plane> planes = new ArrayList<>();
        for (int i = 0; i < 15; i++) { // пусть пытаются взлететь 15 самолетов
            planes.add(new Plane("Plane " + i, airport));
        }

        planes.forEach(Thread::start); // стартуем работу наших независимых самолетов
        planes.forEach(plane -> {
            try {
                plane.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }); // и подождем, пока они все не взлетят.
    }
}

P.S. на самом деле - этот код не совсем (а, вернее, совсем не) отображает реальность: не самолёт (борт) должен выбирать полосу, а аэропорт по готовности борта и с учетом метеоусловий и других параметров выделяет ему полосу.

→ Ссылка