Избранные Материалы

567

Linux :: Релиз Ubuntu 10.04 / Lucid Lynx

8789

Linux :: Тест-Драйв Synapse IM Alpha

Linux :: Тест-Драйв Synapse IM Alpha

4614

Ext GWT (GXT) :: Интеграция WYSIWYG-редактора TinyMCE

Ext GWT (GXT) :: Интеграция WYSIWYG-редактора TinyMCE

2792

Информационные технологии :: В ногу со временем

Информационные технологии :: В ногу со временем

3184

Фриланс :: Стоит ли?

Фриланс :: Стоит ли?

Java :: Единичный экземпляр приложения

Постановка задачи

Необходимо ограничить количество одновременно выполняемых экземпляров приложения одним единственным. Причин для разработки такой функциональности может быть множество. Здесь мы не будем на них останавливаться, а лишь приведем универсальный и самый правильный, с нашей точки зрения, способ решения данной задачи.

Размышления

Сперва рассмотрим возможные варианты решения задачи, с которыми мы ознакомились во время поиска собственного решения.

В общем, решение данной проблемы, похоже, сводится к двум вариантам: “классический” с использованием 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) {
                // а вот и сам "фокус":
                // создаем поток...
                new Thread(new Runnable(){
                    public void run() {
                        while (true) {
                            try {
                                // ... и проверяем валидность блокировки
                                //     внутри него...
                                if (lock.isValid()) {};
                                // ... а затем засыпаем "навечно"
                                Thread.sleep(Long.MAX_VALUE);
                            } catch (InterruptedException e) {
                                // игнорируем
                            }
                        }
                    }
                }).start();
            }
            return lock != null;
        } catch (Exception ex) {
            // игнорируем, если мы ничего не в силах поделать -
            // пользователь должен сам позаботиться о том,
            // чтобы не запускать на выполнение более одного экземпляра приложения
        }
        return true;
    }

(голосов: 3, рейтинг: 4.67 из 5)
Войдите, чтобы поставить оценку.

2 комментария »

vanoas [2010-04-30 13:19:57 ]   [0] Войдите, чтобы выставить оценку

Было б неплохо запускать поток как демон :)

Войдите, чтобы ответить
kion [2010-04-30 21:12:02 ]   [0] Войдите, чтобы выставить оценку

Это еще зачем? :)

Войдите, чтобы ответить
 
 
Войдите, чтобы оставить комментарий