Выполнение потоков в очереди в Java

У меня есть это упражнение:

Разработать многопоточное приложение. Используйте возможности java.util.concurrent.

НЕ ИСПОЛЬЗОВАТЬ: синхронизированный, BlockingQueue, BlockingDeque

Все сущности, желающие получить доступ к ресурсу, должны быть потоками. Используйте возможности ООП.

И моя задача:

Бесплатный CashDesk. В ресторане быстрого питания есть несколько касс. Клиент стоит в очереди у определенной кассы, но может перейти в другую кассу, если уменьшение или исчезновение очереди там.

Вот мое решение https://github.com/NikitaMitroshin/FreeCash

public class Restaurant {
    private static Restaurant instance = null;
    private static ReentrantLock lock = new ReentrantLock();
    private String name;
    private ArrayList<CashDesk> cashDesks;

    private Restaurant(String name) {
        this.name = name;
        cashDesks = new ArrayList<>();
    }

    public static Restaurant getInstance(String name) {
        lock.lock();
        try {
            if (instance == null) {
                instance = new Restaurant(name);
            }
        } finally {
            lock.unlock();
        }
        return instance;
    }

    public void addCashDesk(CashDesk cashDesk) {
        cashDesks.add(cashDesk);
    }

    public String getName() {
        return name;
    }

    public List<CashDesk> getCashDesks() {
        return Collections.unmodifiableList(cashDesks);
    }
}

Код клиента:

public class Client extends Thread {
    private final static Logger LOG = Logger.getLogger(Client.class);
    private Restaurant restaurant;
    private CashDesk cashDesk;
    private String name;
    private int itemsInOrder;

    public Client(Restaurant restaurant, int itemsInOrder, String name) {
        this.restaurant = restaurant;
        this.itemsInOrder = itemsInOrder;
        this.name = name;
    }

    public String getClientName() {
        return name;
    }

    public int getItemsInOrder() {
        return itemsInOrder;
    }

    @Override
    public void run() {
        System.out.println("Client " + name + " comes to restaurant " + restaurant.getName());
        this.cashDesk = chooseCashDesk();
        System.out.println("Client " + getClientName() + " choosed the cashDesk#"+ cashDesk.getNumber());
        cashDesk.addClient(this);
        while (true) {
            if (cashDesk.getLock().tryLock()) {
                try {
                    cashDesk.serveClient(this);
                } catch (ResourceException e) {
                    LOG.error("ResourceException!!! ", e);
                } finally {
                    cashDesk.getLock().unlock();
                    break;
                }
            } else {
                if (canChooseAnotherCashDesk()) {
                    cashDesk.removeClient(this);
                }
            }
        }
        cashDesk.removeClient(this);
        System.out.println("Client " + getClientName() + " leaves restaurant");
    }

    private CashDesk chooseCashDesk(){
        CashDesk result = restaurant.getCashDesks().get(0);
        for (CashDesk cashDesk : restaurant.getCashDesks()) {
            if(cashDesk.getClients().size() < result.getClients().size()) {
                result = cashDesk;
            }
        }
        return result;
    }

    private boolean canChooseAnotherCashDesk() {
        CashDesk result = chooseCashDesk();
        if(result.getClients().size() + 1 < cashDesk.getClients().size()) {
            cashDesk = result;
            cashDesk.addClient(this);
            System.out.println("Client " + getClientName() + " moved to cashDesk#" + cashDesk.getNumber());
            return true;
        }
        return false;
    }
}

Код CashDesk:

public class CashDesk {

    private ReentrantLock lock = new ReentrantLock();
    private LinkedList<Client> clients;
    private int number;
    private int timeOfService;

    public CashDesk(int number, int timeOfService) {
        clients = new LinkedList<>();
        this.number = number;
        this.timeOfService = timeOfService;

    }

    public void serveClient(Client client) throws ResourceException {
        System.out.println("Client "+client.getClientName() + " is serving on cashDesk#"+getNumber());
        try {
            client.sleep(timeOfService * client.getItemsInOrder());
        } catch (InterruptedException e) {
            throw new ResourceException("InterruptedException!!!", e);
        }
        System.out.println("Client "+client.getClientName() + " is served");
    }

    public List<Client> getClients() {
        return Collections.unmodifiableList(clients);
    }

    public void addClient(Client client) {
        clients.add(client);
    }

    public void removeClient(Client client) {
        clients.remove(client);
    }

    public int getNumber() {
        return number;
    }

    public ReentrantLock getLock() {
        return lock;
    }
}

Код бегуна:

public class RestaurantRunner {

    public static void main(String[] args) {
        Restaurant restaurant = Restaurant.getInstance("Mcdonalds");
        CashDesk cashDesk1 = new CashDesk(1, 140);
        CashDesk cashDesk2 = new CashDesk(2, 250);

        restaurant.addCashDesk(cashDesk1);
        restaurant.addCashDesk(cashDesk2);


        new Client(restaurant, 100, "client50").start();
        Random random = new Random();
        for (int i = 1; i < 8; i++) {

            int randNumbOfItems = random.nextInt(10) + 1;
            Client client =  new Client(restaurant, randNumbOfItems, "client"+i);
            client.start();
        }
    }
}

И у меня есть проблемы с этим. Вот что я получаю после запуска моего приложения

Client client1 comes to restaurant Mcdonalds
Client client1 choosed the cashDesk#1
Client client1 is serving on cashDesk#1
Client client3 comes to restaurant Mcdonalds
Client client3 choosed the cashDesk#2
Client client3 is serving on cashDesk#2
Client client5 comes to restaurant Mcdonalds
Client client5 choosed the cashDesk#1
Client client6 comes to restaurant Mcdonalds
Client client6 choosed the cashDesk#2
Client client4 comes to restaurant Mcdonalds
Client client4 choosed the cashDesk#1
Client client50 comes to restaurant Mcdonalds
Client client50 choosed the cashDesk#2
Client client7 comes to restaurant Mcdonalds
Client client7 choosed the cashDesk#1
Client client2 comes to restaurant Mcdonalds
Client client2 choosed the cashDesk#2
Client client1 is served
Client client5 is serving on cashDesk#1
Client client1 leaves restaurant
Client client3 is served
Client client3 leaves restaurant
Client client50 is serving on cashDesk#2
Client client5 is served
Client client5 leaves restaurant
Client client7 is serving on cashDesk#1
Client client7 is served
Client client7 leaves restaurant
Client client6 moved to cashDesk#1
Client client6 is serving on cashDesk#1
Client client2 moved to cashDesk#1
Client client6 is served
Client client6 leaves restaurant
Client client2 is serving on cashDesk#1
Client client2 is served
Client client2 leaves restaurant
Client client4 is serving on cashDesk#1
Client client4 is served
Client client4 leaves restaurant
Client client50 is served
Client client50 leaves restaurant

Итак, как видите, очередь обслуживания нарушена.

Когда client3 обслуживается, client6 должен начать обслуживание, но client50 делает это. И когда client5 обслуживается, client4 должен начать обслуживать, но client7 делает это. И когда client7 обслуживается, я не знаю почему, но client6 переходит на cashDesk#1 и начинает обслуживать, несмотря на то, что client4 должен начать обслуживать.

Я новичок в многопоточности, поэтому мне нужен совет, как правильно настроить работу моего приложения

2 ответа

Решение

Вы говорили об очередях в заголовке, но не использовали их в своем коде. Фактически, когда первый клиент (client5) приходит в cashdesk1, cashdesk блокируется для обслуживания этого клиента.

  //client code
          while (true) {
        if (cashDesk.getLock().tryLock()) {  //the cashdesk is locked
            try {
                cashDesk.serveClient(this);

Между тем приходят другие клиенты, так как есть время обслуживания. Итак, client4 и client7 ждут в кассе
Когда client5 обслуживается, client5 снимает блокировку

  //client code
   cashDesk.getLock().unlock();

Таким образом, следующий, который будет обслужен, первым захватит блокировку, и, поскольку это бесконечный цикл, вы не можете знать, в какой позиции в вашем коде находится каждый клиент. Таким образом, client7 получает его первым, прежде чем client4. Более того, читая ваш вывод, client2 тоже захватывает его.
Я предлагаю вам снять блокировку и использовать переменную, чтобы указать порядок

 //CashDesk
  Client current=null;
  public void nextClient() 
  {
    if(clients.size()==0)
        current=null;
    else
        current = clients.get(0);
 } 

Заменить часть после кода

 while (true) {
        if (cashDesk.getLock().tryLock()) {
            try {
                cashDesk.serveClient(this);
            } catch (ResourceException e) {
                LOG.error("ResourceException!!! ", e);
            } finally {
                cashDesk.getLock().unlock();
                break;
            }
        } else {
            if (canChooseAnotherCashDesk()) {
                cashDesk.removeClient(this);
            }
        }
    }

от

  while (true) {
        if(cashDesk.current==null)
             cashDesk.nextClient();
        if (current==this) {
            try {
                cashDesk.serveClient(this);
            } catch (ResourceException e) {
                LOG.error("ResourceException!!! ", e);
            } finally {
                cashDesk.nextClient();
                break;
            }
        } else {
            if (canChooseAnotherCashDesk()) {
                cashDesk.removeClient(this);
            }
        }
    }

Итак, как видите, очередь обслуживания нарушена.

Это не беспокоит. Он работает так, как задумано вами.

Документация ReentrantLock:

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

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

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

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

Другие вопросы по тегам