Проблеми при реалізації шаблону Одинак
В цій статті я хочу знову повернутися до теми, піднятої в першому пості, який присвячений шаблону Одинак (Singleton).
Власне, в чому проблема спитаєте ви? Є шаблон, є реалізація! Є одне але…
Якщо уважно придивитися до реалізації
Лістинг 1.
то неважко помітити, що метод, який повертає екземпляр помічений модифікатором synchronized . Тобто, кожен раз при отриманні об’екту класу Singleton в багатопоточному середовищі потоки будуть очікувати своєї черги (вікна) при доступі до методу. Крім того, зайві такти процесора будуть витрачатися на перевірку _instance == null , коли такі засоби застереження потрібні всього лише один раз.
Тепер давайте подумаємо про оптимізацію цього методу. Розглянемо таку реалізацію:
Лістинг 3.
Може здатися, що це допоможе вирішити цю проблему. Але це тільки в теорії. На практиці є конкретна реалізація віртуальних машин Java, які реалізують створення нового об’єкту таким чином:
Лістинг 4.
Тобто, якщо другий потік витіснить перший в момент, коли виділено пам’ять під екземпляр, але конструктор не викликаний, то все одно в нас буде існувати два об’єкти.
Звісно, не всі реалізації JVM працюють саме так, але Sun JDK 1.3 та IBM SDK for Java technology, version 1.3 діють таким чином.
Ускладнимо попередній варіант введенням проміжної змінної:
Лістинг 5.
1. Потік 1 входить в метод getInstance().
2. Оскільки instance рівна null, потік 1 входить в synchronized-блок //1.
3. Локальна змінна inst отримує значення змінною instance, яке становить null //2.
4.Оскільки inst = null, потік 1 входить в synchronized-блок //3.
5. Потік 1 починає виконувати код в //4, в результаті чого змінна inst вже не пуста, перед моментом виклику конструктора Singleton.
6. Потік 1 витісняється потоком 2.
7. Потік 2 входить в метод getInstance().
8. Оскільки instance = null, потік 2 робить спробу ввійти в synchronized-блок //1, але оскільки цей блок утримується першим потоком, тому 2-й потік блокується.
9. Перший потік завершує створення об’єкта //4.
10. Перший потік присвоює повністю створений об’єкт змінній instance (//5) і виходить з обох синхронізованих блоків.
11. Потік 1 повертає екземпляр.
12. Потік 2 продовжує роботу і присвоює змінну instance змінній inst.
13. Потік 2 бачить, що instance не пуста, повертає її.
Ключовий момент тут //5. Проблема виникає тоді, коли теорія і правтика ідуть наперекір одне одному.
Код в цьому лістингу не працює через поточну модель пам’яті. Специфікація мови Java говорить, що код із синхронізованого блоку не може бути переміщений поза синхронізований блок. Але про зворотнє перенесення не сказано нічого.
Компілятор побачить тут чодову можливість оптимізувати код: він видалить код в //4 та //5, об’єднає і напише його в такому вигляді:
Лістинг 6.
Тобто, це нас приведе до проблеми, на яку страждає код із лістингу 3. Крім цього, вам не здається, що ми дійсно займаємося оптимізацією коду із Лістингу 1, а не його ускладненням?
Тут рекомендують скористатися ключовим словом volatile:
Лістинг 7.
Але існують два заперечення:
Але в даному випадку втрачаємо ліниву ініціалізацію (Lazy Initialization) – об’єкт буде створений під час ініціалізації класу classloader-ом.
Як приклад, можна привести реальну реалізацію Одинака в системі Jira – промисловій системі управління заявками (Issue Tracker):
Використовуючи диспетчеризацію методів, в залежності від конкретного об’єкту, будуть викликатися різні методи повернення екземпляру: при першому виклику відбудеться створення об’єкту, надалі цей об’єкту буде просто повертатися. По ідеї, цей метод має працювати швидше, ніж використовуючи синхронізацію.
P.S. Вітаю тих, хто дочитав до кінця!
public final class Singleton { private static Singleton _instance = null; private Singleton() {} public static synchronized Singleton getInstance() { if (_instance == null) _instance = new Singleton(); return _instance; } }
public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { instance = new Singleton(); } } return instance; }
Лістинг 2.
Як бачимо, що фрагмент створення нового екземпляру синхронізується в багатопоточному середовищі, але давайте розглянемо наступну ситуацію:аа
- Потік 1 викликає метод getInstance() і визначає, що instance = null;
- Потік 1 входить в блок if, але витісняється потоком 2;
- Потік 2 викликає метод getInstance() і визначає, що instance = null;
- Потік 2 входить в блок if, створює об’єкт і присвоює його змінній instance;
- Потік 2 повертає об’єкт із методу;
- Потік 1 продовжує роботу, створює об’єкт і присвоює його змінній instance;
- Потік 1 повертає об’єкт із методу;
public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { //1 if (instance == null) //2 instance = new Singleton(); //3 } } return instance; }
mem = allocate(); //Виділення пам'яті під об'єкт instance = mem; //Зверніть увагу, що змінна вже не null //але об'єкт не ініціалізований ctorSingleton(instance); //Виклик конструктора
public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { //1 Singleton inst = instance; //2 if (inst == null) { synchronized(Singleton.class) { //3 inst = new Singleton(); //4 } instance = inst; //5 } } } return instance; }
public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { //1 Singleton inst = instance; //2 if (inst == null) { synchronized(Singleton.class) { //3 //inst = new Singleton(); //4 instance = new Singleton(); } //instance = inst; //5 } } } return instance; }
public final class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; }
- Цьому коду властиві ті ж проблеми, як і кодам із лістингів 3 та 6;
- Allen Holub підмітив, що це ключове слово призводить до проблем з продуктивністю на багатопроцесорних платформах.
Рішення
Існує два надійних варіанти реалізації Одинаків:- Змиритися з синхронізацією методу getInstance() (Лістинг 1);
- Використовувати статичні поля:
class Singleton { private Vector v; private boolean inUse; private static Singleton instance = new Singleton(); private Singleton() { v = new Vector(); inUse = true; //... } public static Singleton getInstance() { return instance; } }
package com.atlassian.jira; public class ComponentManager { private static final ComponentManager COMPONENT_MANAGER = new ComponentManager(); /** * Retuns a singleton instance of this class. * * @return a singleton instance of this class */ public static ComponentManager getInstance() { return COMPONENT_MANAGER; } }
Альтернатива
Тут пропонується наступний варіант реалізації:public final class Singleton { private static volatile Strategy strategy = new CreateAndReturnStrategy(); private static Singleton instance; private static interface Strategy { Singleton getInstance(); } private static class ReturnStrategy implements Strategy { public final Singleton getInstance() { return instance; } } private static class CreateAndReturnStrategy implements Strategy { public final Singleton getInstance() { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); strategy = new ReturnStrategy(); } } return instance; } } private Singleton() {} public static Singleton getInstance() { return strategy.getInstance(); } }
Related posts:
- Шаблон Одинак (Singleton Pattern) Вітаю всіх! Перший пост у своєму блозі я вирішив присвятити...
- Два методи “поламати” ваш Singleton Сьогодні хочу написати про ще декілька нюансів при роботі із...
- Від класів із приватним конструктором по замовчуванню не можна наслідуватися Як відомо, в Java щоб заборонити успадкування від даного класу...
- Захисне копіювання (Defensive copying) Захисне копіювання об’єктів застосовується тоді, коли клас містить об’єктні поля,...
- Незмінні об’єкти в Java (immutable objects) Незмінні об’єкти – (англ.: immutable objects) це просто об’єкти, стан...
Related posts brought to you by Yet Another Related Posts Plugin.







My LinkedIn Account
Останні коментарі