Patterns: Singleton (Синглтон) 2


Вирішив зробити дві корисні речі відразу і, по-перше, не вживатиму алкоголь як мінімум до нового року, а, по-друге, взявся ще раз перечитати про патерни проектування від “Банди Чотирьох” і зробити невеличкий цикл статей на цю тему. Знаю, що нічого нового не придумаю, але ще кілька україномовних статей на тему програмування зайвими не будуть. А почнемо ми із патерну, який знають, мабуть, абсолютно всі – патерну Синглтон (Singleton, Одиночка, Одинак). Між іншим, існує також віскі з назвою Singleton. кажуть, досить непогане (сам поки не куштував).

Отже, що за патерн Синглтон такий.

Singleton UML

Singleton UML

Якщо коротко, то цей патерн – спосіб гарантовано завжди мати один-єдиний екземпляр якогось класу і не більше. Знаєте, бувають такі люди, що напиваються суто наодинці, одинаки 🙂 Нащо їм шукати компанію, якщо можна просто відкоркувати пляшку і вперед? Так само і тут: клас-синглтон може бути інстанційовано лише один раз, а для цього він повинен відповідати двом вимогам: по-перше, конструктор класу має бути приватним, що не дозволить створити “собутильника” нашому одинаку; по-друге мусить існувати якась глобальна точка входу, яка повертатиме той єдиний екземпляр, бо без конструктора отримати його просто так буде нелегко.

Спробуєю унаочнити картину на прикладі дядька Миколи, який і є уособленням того одинака-синглтона, бо випиває виключно наодинці. Клас Singleton репрезентує дядька Миколу та його ідеологію: він має приватний конструктор і тому ніхто не може звернутися до дядька Миколи інакше як через статичний метод getInstance(), який і є тією єдиною глобальною “точкою входу”. У цьому методі Микола перевіряє чи він уже культурно відпочиває чи ні. Якщо Микола не зайнятий споживанням алкоголю, то він з радістю перехилить стаканчик за програмування, у іншому разі відшиє набридливих товаришів по чарці.

public class Singleton {
    private static Singleton uncleMykola;
    private Singleton() {
        System.out.println("Ну, за програмування!");
    }

    public static Singleton getInstance() {
        if (uncleMykola == null) {
            uncleMykola = new Singleton();
        } else {
            System.out.println("Ідіть нафіг, мені не потрібна компанія!");
        }
        return uncleMykola;
    }
}

Щоб переконатися у принциповості дядька Миколи достатньо отакого коду:

public class Main {
    public static void main(String[] args) {
        Singleton mykola = Singleton.getInstance(); //Сам Микола
        Singleton convive = Singleton.getInstance(); //Ще якийсь любитель випити
    }
}

Все наче виглядає просто. Як бачимо, поки Микола ще не випив, то будь-які перипетії у житті змушують його пошукати оковиту, але коли, виражаючись словами Цезаря, Рубікон перейдено, достукатися до свідомості Миколи стає неможливо.

Тепер розглянемо кілька нюансів. Синглтони бувають двох типів: одні готові перехилити чарку з самісінького ранку (eager ініціалізація), а інші не шукають собі ґудза, доки життя не змусить випити (lazy ініціалізація). У нашому прикладі Микола ініціалізується за типом lazy (тобто конструктор викликається тільки тоді, коли хтось звертається до методу getInstance() – це ще нормально), а от дядя Вова – той взагалі, схоже, народився зі стаканом у руках:

public class SingletonVova {
    private final static SingletonVova UNCLE_VOVA = new SingletoneVova();
    private SingletonVova() {
        System.out.println("Ранок. Треба похмелитися...");
    }

    public static SingletonVova getInstance() {
        return UNCLE_VOVA;
    }
}

Тут все те ж саме, тільки ініціалізується екземпляр класу відразу, не чекаючи поки хтось його покличе випити: продер очі і вже шукає де б то горло промочити 😉 Якщо дядько Микола ще може жити відносно нормальним життям, поки його не чіпають, то при eager ініціалізації діє принцип “хзранку не випив – день пропав” і тому не варто нею зловживати, особливо якщо достеменно не відомо коли буде потрібен екземпляр нашого синглтона і чи буде потрібен взагалі. Це той випадок коли ледарство є перевагою 🙂

Ситуація з ледачими синглтонами може ускладнитися наприклад у тому випадку, якщо бажаючих скласти компанію за чаркою декілька і ми маємо справу із багатопоточним середовищем. Звісно, наш одинак слатиме лісом всіх, хто захоче йому “впасти на хвіст”, але коли один дзвонить на мобільник, другий махає рукою з-за паркана а третій вже стукає у двері то це зробити непросто. Тому треба якось синхронізувати всіх цих “товаришів” і не дати їм розірвати нашого Миколу на частини. Нехай заходять до Миколи по-одному:

public static Singleton getInstance() {
        synchronized (Singleton.class) { //Панове, не всі одразу!
            if (uncleMykola == null) {
                uncleMykola = new Singleton();
            } else {
                System.out.println("Ідіть нафіг, мені не потрібна компанія!");
            }
        }
    return uncleMykola;
}

Для цього ми використали блок synchronized і тепер дядько Микола не може спілкуватися одночасно з кількома людьми (що і не дивно для особи, яка так зловживає алкоголем). Тим не менш, настриливі сусіди і досі товчуться у нього на подвір’ї і толочать квіти, чекаючи своєї черги бути посланими лісом. Щоб вберегти від винищення клумбу, розбиту дружиною Миколи – тіткою параскою (бідненька і так вже натерпілася від чоловіка) треба здійснити попереднью перевірку стану Миколи ще до того як пускати сусіда на подвір’я.

public static Singleton getInstance() {
        if (uncleMykola == null) {
            synchronized (Singleton.class) { //Панове, не всі одразу!
                if (uncleMykola == null) {
                    uncleMykola = new Singleton();
                } else {
                    System.out.println("Ідіть нафіг, мені не потрібна компанія!");//Сам Микола
                }
            }
        } else {
            System.err.println("Ідіть нафіг, він вже лика не в'яже!");//А це вже Параска
        }
    return uncleMykola;
}

Отож, як тільки хтось захоче потривожити дядька Миколу, першим ділом тета Параска перевірить як почувається чоловік і, якщо той уже встиг закласти за комір, буде матюкатися у System.err. Якщо ж Микола ще при тямі, вона змушена буте пропустити нахабу до чоловіка (розуміючи чим це закінчиться у результаті). На жаль, присутність Параски не може спинити Миколу від зловживання алкоголем, зате хоч квіти залишаться цілі. Таким чином для правильної реалізації синглтону у багатопоточному середовищі слід двічі перевіряти чи є вже екземпляр нашого класу чи нема. Перевірка перед блоком synchronized (Double-Checked Locking) гарантує що ніхто не буде зайвий раз заходити у синхронізований блок і тому синглтон працюватиме швидше. Але виглядає так, що внаслідок специфіки різних версій Java цей код не може гарантувати повної безпеки для старих версій Java варто змінну-синглтон оголошувати як volatile:

private static volatile Singleton uncleMykola;

Насправді якщо сусідів не так багато і Микола не є дуже популярним дядьком (тобто метод getInstance() викликатиметься нечасто), то синхронізованим можна зробити увесь метод і не городити городів. Або ж змусити саму Параску наливати сто грам і відганяти непроханих гостей, зробивши її статичним внутрішнім класом. У такому випадку спроба докричатися до Миколи замінюється на звертання до Параски (“Поклич чоловіка!”) і таким чином ми знову отримуємо і ледачу ініціалізацію і безпечну роботу в режимі багатопоточності.

public class Singleton {
    private Singleton() {
        System.out.println("Ну, за програмування!");
    }

    private static class Paraska {
        private static final Singleton HUSBAND = new Singleton();
    }

    public static Singleton getInstance() {
        return Paraska.HUSBAND;
    }
}

Нюанси виникнуть і якщо доведеться серіалізувати та десеріалізувати синглтон; тоді треба мати на увазі, що доведеться подбати аби при десеріалізації Миколи не отримати кілька його клонів (інакше нещасній тітці Парасці гарантовано нервовий зрив). Популярним шляхом уникнути проблем із серіалізацією є використання для реалізації синглтона такої конструкції як enum:

public enum Singleton {
        MYKOLA;

        public static Singleton getInstance() {
            return Singleton.MYKOLA;
        }
    }
public class Main {
    public static void main(String[] args) {
        Singleton mykola = Singleton.getInstance();
        Singleton convive = Singleton.getInstance();
        System.out.println(mykola+" завжди радий випити з "+convive);
    }
}

Про серіалізацію енамів подбає сама JVM, тому можна бути впевненим, що проблем із братами-близнбками Миколи у параски не виникне. Метод getInstance() тут використовується більше для зручності і для того, щоб виклик екземпляра синглтона був всюди однаковим, але в принципі ніхто не заважає написати і отак:

Singleton mykola = Singleton.MYKOLA;

Це далеко не всі проблеми, які можуть виникнути у любителя побухати наодинці. Зокрема, якщо програма виконується у кількох JVM одночасно, то теж треба бути дуже обережним, аби не сталося щось типу того як було у фільмі “З легким паром”. Але це вже тема зовсім окермої розмови, яка виходить за рамки нашої алкогольної статті. Наостанок наведу кілька прикладів того де використовується синглтон у Java (посилання на сайт Oracle):

За ваше здоров’я

alco

Почитайте ще оце:


Залиште коментар

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

2 thoughts on “Patterns: Singleton (Синглтон)