Опубликован: 10.10.2010 | Доступ: свободный | Студентов: 3217 / 301 | Оценка: 4.14 / 3.32 | Длительность: 13:16:00
ISBN: 978-5-9963-0444-8
Специальности: Системный архитектор
Лекция 5:

Использование Java RMI

< Лекция 4 || Лекция 5: 12345 || Лекция 6 >

Компиляция и выполнение сервера и клиента

Подготовив отдельные фрагменты, мы можем сформировать и выполнить наше распределенное приложение, но для этого потребуется несколько действий. Для начала необходимо компилировать исходные классы. Далее, нужно компилировать класс удаленного объекта ( ...Impl ), используя компилятор rmic (утилита J2SE ) для формирования класса-заглушки (о котором говорилось в предыдущем разделе). Этот класс должен быть доступен для клиента (либо локально, либо путем загрузки по сети), чтобы дать возможность устанавливать удаленное соединение с серверным объектом. В зависимости от параметров командной строки, передаваемых rmic,может быть сгенерировано несколько файлов. В Java 1.1 rmic формирует два класса - класс-заглушку и класс-каркас ( skeleton ). В Java 2 класс-каркас больше не требуется. Параметр командной строки -v1.2 указывает, что rmic следует создать только класс-заглушку.

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

rmic -v1.2 com.asw.rmi.ex1.BillingServiceImpl

которая сгенерирует файл BillingServiceImpl_Stub.class.

Следующий этап - запуск реестра RMI, который зарегистрирует удаленный объект. Командная строка

rmiregistry

запускает реестр RMI на локальной машине. В окне командной строки в ответ на эту команду никакого текста отображаться не будет. Типичная ошибка заключается в том, что если не запустить реестр RMI прежде чем попытаться привязать удаленный объект к реестру, будет сгенерировано исключение java.rmi.ConnectException,которое указывает, что программа не может соединиться с реестром.

Чтобы удаленный объект мог принимать удаленные вызовы методов, необходимо связать объект с именем в реестре RMI. Для этого нужно запустить серверное приложение из командной строки. В нашем случае командная строка выглядит так:

java com.asw.rmi.ex1.BillingServiceImpl

В результате отображается сообщение об инициализации BillingService.

Теперь клиентское приложение может соединиться с удаленным объектом, выполняющимся на локальной машине localhost. Команда

java com.asw.rmi.ex1.BillingClient

соединит BillingClient с объектом BillingServiceImpl.

Если серверное приложение выполняется не на клиенте, можно указать IP -адрес или доменное имя компьютера-сервера в качестве параметра командной строки при выполнении клиента. Например, чтобы осуществить доступ к компьютеру с IP -адресом 192.168.1.1, введем команду

java com.asw.rmi.ex1.BillingClient 192.168.1.1

Несколько слов о синхронизации

Теперь, после того как нами реализовано первое приложение с использованием RMI, настало время немного поговорить об одной из особенностей, связанной с применением этой технологии.

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

Непонимание этого факта может привести к ошибкам при программировании серверных объектов, которые затем будет очень сложно локализовать и устранить. Для иллюстрации рассмотрим один из возможных сценариев выполнения нашего приложения, в случае если два клиента одновременно вызывают метод addMoney для одной и той же карты (на приведенной схеме (таблица 5.1) события разворачиваются во времени - время течет сверху вниз):

Таблица 5.1. Схема выполнения метода addMoney двумя потоками
t Клиент 1 Клиент 2
t1 Вызов метода сервера addMoney с номером карты 1 и суммой 10 Вызов метода сервера addMoney с номером карты 1 и суммой 12
t2 Double d = (Double)hash.get(card); Double d = (Double)hash.get(card);
t3 if (d!=null) hash.put(card,new Double(d.doubleValue()+money))
t4 if (d!=null) hash.put(card,new Double(d.doubleValue()+money));
t5

Итак, в момент t1 оба клиента вызывают метод addMoney для карты с номером 1 и с разными суммами начисления (мы предполагаем, что карта с таким номером существует). Далее по каким-то причинам выполнение потоков, в которых происходит работа методов addMoney для первого и второго клиентов, происходит с разными скоростями. В момент времени t2 первый клиент получает из хэша значение баланса карты - второй делает то же самое. В момент времени t3 первый клиент изменяет значение баланса, прибавляя к нему свое начисление, и возвращает баланс в хэш. Ту же самую операцию, но только в момент времени t4 - позднее, чем первый - делает второй клиент. В результате приведенного сценария в хэше окажется значение баланса, измененное вторым клиентов, а изменения, которые сделал первый клиент, просто потеряются - т.е. созданное нами приложение отработает неправильно.

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

public synchronized void addMoney(String card, double money) throws RemoteException {
  
  Double d = (Double)hash.get(card);
  
  if (d!=null) hash.put(card,new Double(d.doubleValue()+money));
  else throw new NotExistsCardOperation();
}
Листинг 5.5.

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

Может показаться, что, введя синхронизацию на уровне методов, мы решили проблему, однако это не так. Чтобы убедиться в этом, достаточно рассмотреть одновременное выполнение двух разных методов - addMoney и subMoney - с теми же самыми предположениями о скоростях выполнения. Несложно догадаться, что в приведенном примере блокировка должна выполняться не на уровне методов, а на уровне ресурсов - в нашем случае хэш-таблицы. Для окончательного устранения нежелательного эффекта код всех методов изменения баланса должен быть переписан как-нибудь так2Поступив таким образом, мы фактически превратили наше приложение в строго последовательное. В действительности нам вовсе не обязательно было синхронизировать доступ ко всей хэш-таблице, достаточно было бы синхронизации доступа к конкретной карте - при этом операции с разными картами могли бы выполняться параллельно (пример 5.6).

public void addMoney(String card, double money) throws RemoteException{
    synchronized (hash) {
      Double d = (Double) hash.get(card);

      if (d != null)
        hash.put(card, new Double(d.doubleValue() + money));
      else throw new NotExistsCardOperation();
    }
  }
Листинг 5.6.
< Лекция 4 || Лекция 5: 12345 || Лекция 6 >
Алмаз Мурзабеков
Алмаз Мурзабеков
Прохожу курс "Построение распределенных систем на Java" в третьей лекции где описывается TCPServer вылетает эта ошибка
"Connection cannot be resolved to a type"


Java version 1.7.0_05