Когда мне пришлось сильно углубиться в использование RMI, я поняла, какое большое значение играет умение правильно реализовать параллельность в самом графическом интерфейсе программы. На мое удивление в интернете не было достаточно материала на эту тему и особенно качественных примеров, хотя тема для любого программиста несоменно очень важная. Именно поэтому я решила поделиться своим опытом.
Кратко о RMI: программный интерфейс вызова удаленных методов в языке Java (источник). С помощью него можно например управлять данными на программе сервера с одного или множества компьютеров. Подробнее можно почитать на Хабре. Будем исходить из того, что с его основами вы уже знакомы. Необходимо так же иметь представление о нововведениях в Java 8, а именно — вам понадобятся лямбда-выражения. Хорошее объяснение есть здесь.
Возможности применения RMI очень разнообразны. С помощью него можно сделать, например, чат или программу для голосования на Java. В моем же примере будет простой счетчик с графической оболочкой, которая выглядит следующим образом:

Однако, прежде чем перейти к GUI, создадим сам RMI-объект счетчик и RMI-сервер, на котором он будет хранится.
Счетчик — интерфейс «Counter»:
Клас инициализации счетчика «CounterClass»:
Сервер для RMI-счетчика «Counter_Server»:
Так как я исхожу из того, что RMI вам уже знакомо, то не буду объяснять эти классы по строчкам. Краткое объяснение: метод «reset» приравнивает переменной «counter» значение 0 и возвращает его назад, метод «increment» увеличивает значение переменной «counter» на 1 и возвращает его назад. В сервере создаем свой регистр со скелетоном CounterClass. После этого сервер можно уже запустить.
Наконец, переходим к графике. Создадим класс Counter_Client_GUI, который создает сам фрейм с GUI и одновременно через главный метод берет стаб для удаленного управления счетчика из ранее созданного регистра:
Тут уже стоит объяснить некоторые строки:
Перед следующим шагом необходимо понять, для чего нужен в данном случае параллельный подход. Если мы реализуем обновления JLable самым обычным способом, например:
То это с огромной вероятностью приведет к постоянному замораживанию фрейма и при этом долгое время невозможно будет нажать ни на одну кнопку, хотя нажатия при этом регистрируются и при размораживании фрейма один за другим начнут исполняться, что приведет к хаосу. Это происходит из-за того, что в данном случае все действия с графической оболочкой будет принимать на себя лишь один единственный Thread — EventDispatchThread. И не стоит забывать, что ходя в данном примере клиент и сервер находятся на одном компьютере, управление счетчиком все-равно совершается удаленно, поэтому может возникнуть сбой в RMI регистре или же задержка доставки команды на сервер (кроме того это только пример, а в реальной программе клиент и сервер конечно же не находятся на localhost).
Теперь приступаем к самой важной части — описываем методы incrementClicked и resetClicked, вводя при этом необходимую параллельность:
Объяснение: для кадого нажатия на кнопку создаем новый Thread и запускаем его.
Внутри каждого Thread будет вот что:
EventQueue.invokeLater(...) — ключевой момент программы. EventQueue с английского «очередь событий» это функция, которая (содержится в Java) посылает задание текущего Thread-а на очередь к выполнению в главный Thread. В нашем случае задание это обновление счетчика
Стоит отметить, что в теле EventQueue.invokeLater(...) так же находится лямбда-выражение. В этой программе они применены для более лучшего и понятного внешнего вида кода.
Вот и все. Теперь параллельность работы с графической оболочкой программы обеспечена в любом случае даже при работе с RMI.
Спасибо за внимание!
Кратко о RMI: программный интерфейс вызова удаленных методов в языке Java (источник). С помощью него можно например управлять данными на программе сервера с одного или множества компьютеров. Подробнее можно почитать на Хабре. Будем исходить из того, что с его основами вы уже знакомы. Необходимо так же иметь представление о нововведениях в Java 8, а именно — вам понадобятся лямбда-выражения. Хорошее объяснение есть здесь.
Возможности применения RMI очень разнообразны. С помощью него можно сделать, например, чат или программу для голосования на Java. В моем же примере будет простой счетчик с графической оболочкой, которая выглядит следующим образом:

- JLable с актуальным значением счетчика
- JButton «Плюс» поднимает значение счетчика на единицу
- JButton «Сброс» сбрасывает значение счетчика на единицу
Однако, прежде чем перейти к GUI, создадим сам RMI-объект счетчик и RMI-сервер, на котором он будет хранится.
Счетчик — интерфейс «Counter»:
import java.rmi.Remote; import java.rmi.RemoteException; public interface Counter extends Remote { final String NAME = "Counter"; int reset() throws RemoteException; int increment() throws RemoteException; }
Клас инициализации счетчика «CounterClass»:
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class CounterClass extends UnicastRemoteObject implements Counter { private static final long serialVersionUID = 1L; private int counter; public CounterClass() throws RemoteException { } @Override public synchronized int reset() { this.counter = 0; return this.counter; } @Override public synchronized int increment() { this.counter++; return this.counter; } }
Сервер для RMI-счетчика «Counter_Server»:
import java.io.IOException; import java.rmi.AlreadyBoundException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class Counter_Server { public static void main(final String[] args) throws IOException, AlreadyBoundException { CounterClass counter = new CounterClass(); Registry localReg = LocateRegistry.createRegistry(Registry.REGISTRY_PORT); localReg.bind(Counter.NAME, counter); System.out.println("Counter-Server runs"); } }
Так как я исхожу из того, что RMI вам уже знакомо, то не буду объяснять эти классы по строчкам. Краткое объяснение: метод «reset» приравнивает переменной «counter» значение 0 и возвращает его назад, метод «increment» увеличивает значение переменной «counter» на 1 и возвращает его назад. В сервере создаем свой регистр со скелетоном CounterClass. После этого сервер можно уже запустить.
Наконец, переходим к графике. Создадим класс Counter_Client_GUI, который создает сам фрейм с GUI и одновременно через главный метод берет стаб для удаленного управления счетчика из ранее созданного регистра:
import Counter.Counter; public class Counter_Client_GUI extends JFrame { private static final long serialVersionUID = 1L; protected Counter counter; protected JLabel counterLabel; public Counter_Client_GUI(final Counter counter) { this.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); this.counter = counter; this.counterLabel = new JLabel("0", SwingConstants.CENTER); final JButton incrementButton = new JButton("Плюс"); final JButton resetButton = new JButton("Сброс"); incrementButton.addActionListener(this::incrementClicked); resetButton.addActionListener(this::resetClicked); this.setLayout(new GridLayout(0, 1)); this.add(this.counterLabel); this.add(incrementButton); this.add(resetButton); this.setSize(300, 200); this.setVisible(true); } public static void main(String[] args) throws RemoteException, NotBoundException { Registry reg = LocateRegistry.getRegistry("localhost"); Counter counter = (Counter) reg.lookup(Counter.NAME); new Counter_Client_GUI(counter); }
Тут уже стоит объяснить некоторые строки:
- incrementButton.addActionListener(this::incrementClicked) — лямбда-выражение, тело Listener описано в методе incrementClicked в этом же классе;
- resetButton.addActionListener(this::resetClicked) — лямбда-выражение, тело Listener описано в методе resetClicked в этом же классе;
- Registry reg = LocateRegistry.getRegistry(«localhost») — в данном примере и серер и клиент находятся на одном компьютере, поэтому вместо ссылки на регистр задаем «localhost».
Перед следующим шагом необходимо понять, для чего нужен в данном случае параллельный подход. Если мы реализуем обновления JLable самым обычным способом, например:
this.counterLabel.setText(String.valueOf(novoeZnacheniePeremennoiCounter));
То это с огромной вероятностью приведет к постоянному замораживанию фрейма и при этом долгое время невозможно будет нажать ни на одну кнопку, хотя нажатия при этом регистрируются и при размораживании фрейма один за другим начнут исполняться, что приведет к хаосу. Это происходит из-за того, что в данном случае все действия с графической оболочкой будет принимать на себя лишь один единственный Thread — EventDispatchThread. И не стоит забывать, что ходя в данном примере клиент и сервер находятся на одном компьютере, управление счетчиком все-равно совершается удаленно, поэтому может возникнуть сбой в RMI регистре или же задержка доставки команды на сервер (кроме того это только пример, а в реальной программе клиент и сервер конечно же не находятся на localhost).
Теперь приступаем к самой важной части — описываем методы incrementClicked и resetClicked, вводя при этом необходимую параллельность:
protected void incrementClicked(final ActionEvent ev) { new Thread(this::incrementOnGUI).start(); } protected void resetClicked(final ActionEvent ev) { new Thread(this::resetOnGUI).start(); }
Объяснение: для кадого нажатия на кнопку создаем новый Thread и запускаем его.
Внутри каждого Thread будет вот что:
protected void incrementOnGUI() { try { final int doAndGetIncrement= this.counter.increment(); final String newLabelText = String.valueOf(doAndGetIncrement); EventQueue.invokeLater(() -> this.counterLabel.setText(newLabelText)); } catch (final RemoteException re) { final String message = "Fehler: " + re.getMessage(); EventQueue.invokeLater(() -> JOptionPane.showMessageDialog(this, message)); } } protected void resetOnGUI() { try { final int doAndGetReset= this.counter.reset(); final String newLabelText = String.valueOf(doAndGetReset); EventQueue.invokeLater(() -> this.counterLabel.setText(newLabelText)); } catch (final RemoteException re) { final String message = "Fehler: " + re.getMessage(); EventQueue.invokeLater(() -> JOptionPane.showMessageDialog(this, message)); } }
EventQueue.invokeLater(...) — ключевой момент программы. EventQueue с английского «очередь событий» это функция, которая (содержится в Java) посылает задание текущего Thread-а на очередь к выполнению в главный Thread. В нашем случае задание это обновление счетчика
this.counterLabel.setText(newLabelText) или вывод сообщения об ошибке JOptionPane.showMessageDialog(this, message). Это обязательно необходимо для того, чтобы не возникло запутанности среди работы множества созданных Thread-ов. Например, метод будет считать в одном Thread-е в таблице количество строк, а другой Thread будет удалять строки. С большой вероятностью полученное число будет неправильным. В конечном итоге EventQueue содержит список задач, которые выполняются по очереди или по доступности, не мешая любой другой работе с графическим интерфейсом.Стоит отметить, что в теле EventQueue.invokeLater(...) так же находится лямбда-выражение. В этой программе они применены для более лучшего и понятного внешнего вида кода.
Вот и все. Теперь параллельность работы с графической оболочкой программы обеспечена в любом случае даже при работе с RMI.
Спасибо за внимание!