Java :: Единичный экземпляр приложения
20 сентября 2008 Разработка 6 595 Комментарии (2)
Постановка задачи
Необходимо ограничить количество одновременно выполняемых экземпляров приложения одним единственным. Причин для разработки такой функциональности может быть множество. Здесь мы не будем на них останавливаться, а лишь приведем универсальный и самый правильный, с нашей точки зрения, способ решения данной задачи.
Размышления
Сперва рассмотрим возможные варианты решения задачи, с которыми мы ознакомились во время поиска собственного решения.
В общем, решение данной проблемы, похоже, сводится к двум вариантам: «классический» с использованием lock-файла и «альтернативный» с использованием сокетов. Рассмотрим оба варианта вкратце.
Вариант с использованием сокетов предполагает реализацию следующей идеи: при старте приложения, оно выступает в качестве «сервера-заглушки», пытаясь зарезервировать на локальном компьютере пользователя определенный (константный, закрепленный за приложением) сетевой порт; при этом второй экземпляр приложения при попытке повторно зарезервировать тот-же порт, потерпит неудачу и проинформирует пользователя, что приложение уже запущено. Такой подход, в принципе, неплох, довольно надежен и не так уж сложен в реализации. Но есть и минусы: хоть вероятность такой «накладки» довольно мала (всего доступных портов — 65536), она все же существует — порт, закрепленный за вашим приложением, может быть уже занят другим приложением (особенно если на машине пользователя, к примеру, запущен bittorrent-клиент, часто любящий использовать при своей работе некоторый, и подчас довольно широкий, диапазон портов); кроме того, это решение показалось мне не совсем «эстетически красивым» и более напоминающим «workaround», чем универсальное решение.
Вариант с использованием lock-файла куда более классичен: при старте приложения, оно создает тот самый специальный lock-файл, а при завершении своей работы — этот файл удаляет. Если при старте приложения оно обнаруживает, что файл существует, то пользователь информируется о том, что приложение уже запущено. Этот подход кажется более легким в реализации (и так оно и есть), однако он имеет «ахиллесову пяту», которая сводит все его преимущества на нет: при аварийном завершении приложения, lock-файл остается на диске и последующий запуск приложения ложно просигнализирует о том, что приложение уже запущено. Единственный выход, в таком случае, — ручное удаление lock-файла, что, согласитесь, не очень удобно для пользователя.
Усовершенствованный вариант этого же подхода предполагает не простое создание/удаление lock-файла, а его открытие для записи и блокировку при старте приложения и высвобождение этой блокировки при завершении работы приложения. При этом, блокировка на запись в lock-файл действует лишь тогда, когда приложение выполняется, а при его закрытии, будь то обычное или аварийное завершение работы, блокировка автоматически снимается. Таким образом, этот усовершенствованный подход защищен от слабости его упрощенного варианта, хоть и незначительно усложняет реализацию. Этот вариант показался нам наиболее универсальным и надежным, однако в процессе его реализации на Java мы столкнулись со следующей проблемой: даже при открытии lock-файла для записи и его последующей блокировке, виртуальная машина Java через некоторое время автоматически высвобождает блокировку, так как определяет, что она на самом деле не используется (вот вам пример отличной оптимизации последних реализаций JVM ;)). Таким образом, после запуска приложения, поначалу все работает отлично и второй экземпляр не запускается, исправно информируя пользователя о том, что приложение уже запущено, но по прошествии некоторого времени (в нашем случае — около 5-10 минут) блокировка с lock-файла снимается и возможность запустить второй экземпляр приложения все-же появляется, что делает данный подход к решению поставленной задачи, непридатным для использования.
Решение
После некоторых изысканий, направленных на поиск решения проблемы, автору данной заметки все-же удалось ее решить путем незначительной модификации вышеописанного подхода. Суть модификации состоит в следующем: после блокировки lock-файла, следует запустить на выпонение отдельный поток (thread) внутри приложения, единственной задачей которого является регулярная проверка валидности блокировки (метод isValid() класса FileLock); впрочем после первой же проверки имеет смысл «усыпить» поток на максимально возможное время, минимизировав таким образом используемые им системные ресурсы; при этом, JVM определяет блокировку как используемую и не высвобождает ее автоматически.
Код
Ниже следует пример кода, демострирующего реализацию этого решения.
...
private static final String LOCK_FILE_NAME = ".lock"; // имя lock-файла
private static File rootDir; // директория, в которой находится lock-файл
...
public static void main(String[] args) throws Throwable {
rootDir = ... // инициализация директории, в которой находится lock-файл
// проверка на присутсвие единственного выполняемого экземпляра приложения;
// естественно должна выполняться перед основной инициализацией приложения
// и реализацией его бизнесс-логики
singleAppInstanceCheck();
// основная инициализация приложения
...
// реализация основной функциональности приложения
...
}
private static void singleAppInstanceCheck() throws Throwable {
// проверка: запущен ли другой экземпляр приложения?
if (!lock()) { // если да, то...
// ... информируем об этом пользователя...
System.err.println("Приложение уже запущено!");
// ... и прекращаем работу
System.exit(1);
}
}
private static boolean lock() {
try {
// создаем блокировку
final FileLock lock = new FileOutputStream(
new File(rootDir, LOCK_FILE_NAME))
.getChannel().tryLock();
if (lock != null) {
// а вот и сам "фокус":
// создаем поток...
Thread t = new Thread(new Runnable(){
public void run() {
while (true) {
try {
// ... и проверяем валидность блокировки
// внутри него...
if (lock.isValid()) {};
// ... а затем засыпаем "навечно"
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
// игнорируем
}
}
}
});
// запускаем поток как "демон",
// чтобы не блокировать завершение выполнения основной части кода
t.setDaemon(true);
t.start();
}
return lock != null;
} catch (Exception ex) {
// игнорируем, если мы ничего не в силах поделать -
// пользователь должен сам позаботиться о том,
// чтобы не запускать на выполнение более одного экземпляра приложения
}
return true;
}





