Важный акцент этой статьи:
- потокобезопасный одноэлементный шаблон
- Предотвратить клонирование объекта от нарушения одноэлементного шаблона Singleton
- Не позволяйте сериализации нарушать одноэлементный шаблон
одноэлементный шаблон
Что такое одноэлементный шаблон
Шаблон singleton — это шаблон творческого типа для управления экземплярами. Шаблон singleton гарантирует, что в вашем приложении будет не более одного экземпляра указанного класса.
Сценарий применения шаблона Singleton
- Класс конфигурации проекта
Класс, считывающий информацию о конфигурации проекта, можно сделать одноэлементным, поскольку его нужно прочитать только один раз, а поля с информацией о конфигурации обычно экономят ресурсы. Этот одноэлементный класс обеспечивает глобальный доступ к классам в приложении. Нет необходимости выполнять многократное чтение файла конфигурации несколько раз.
- Класс журнала приложений
Logger Logger вездесущ в вашем приложении. Он также должен быть инициализирован только один раз, но может использоваться везде.
- Анализ и отчетность
Если вы используете некоторые инструменты анализа данных, такие как Google Analytics. Вы можете заметить, что они разработаны как синглтоны, инициализируются только один раз, а затем доступны для каждого действия пользователя.
Класс, реализующий шаблон singleton
-
Установите для конструктора по умолчанию значение private. Не позволяйте другим классам инициализировать этот класс непосредственно из приложения.
-
Создайте статический метод public static. Этот метод используется для возврата одного экземпляра case.
-
Вы также можете выбрать загрузить ленивую дружелюбную инициализацию.
образец кода
Пример кода см. в следующих классах.
- org.byron4j.cookbook.designpattern.singleton.Singleton
public class Singleton {
private static Singleton instance;
// 构造器私有化
private Singleton(){
}
// 提供静态方法
public static Singleton getInstance(){
// 懒加载初始化,在第一次使用时才创建实例
if(instance == null){
instance = new Singleton();
}
return instance;
}
public void display(){
System.out.println("Hurray! I am create as a Singleton!");
}
}
Класс модульного теста:
package org.byron4j.cookbook.designpattern;
import org.byron4j.cookbook.designpattern.singleton.Singleton;
import org.junit.Test;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingletonTest {
@Test
public void test(){
final Set<Singleton> sets = new HashSet<>();
ExecutorService es = Executors.newFixedThreadPool(10000);
for(int i = 1; i <= 100000; i++){
es.execute(new Runnable() {
public void run(){
Singleton s = Singleton.getInstance();
sets.add(s);
}
});
}
System.out.println(sets);
}
}
Результат запуска выглядит следующим образом, и в результате создается несколько экземпляров Singleton:
[org.byron4j.cookbook.designpattern.singleton.Singleton@46b91344, org.byron4j.cookbook.designpattern.singleton.Singleton@1f397b96]
потокобезопасный одноэлементный шаблон
Потокобезопасность очень важна для одноэлементных классов. Приведенный выше класс Singleton не является потокобезопасным, поскольку в случае параллелизма потоков может быть создано несколько экземпляров Singleton.
Чтобы обойти эту проблему, мы можем получить метод getInstance с синхронизированным измененным словом синхронизации, поэтому принудительно поток ждет, пока предыдущий поток не завершится, поэтому избегая сцены, в то же время есть несколько потоков, обращающихся к методу.
public static synchronized Singleton getInstance() {
// Lazy initialization, creating object on first use
if (instance == null) {
instance = new Singleton();
}
return instance;
}
Это решает проблему безопасности потоков. но,synchronized
Ключевые слова имеют серьезные проблемы с производительностью. Мы можем дополнительно оптимизировать метод getInstance, чтобы синхронизировать экземпляр и уменьшить область действия метода:
public static Singleton getInstance() {
// Lazy initialization, creating object on first use
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
Отнимающее много времени сравнение трех методов модульного тестирования:
package org.byron4j.cookbook.designpattern;
import org.byron4j.cookbook.designpattern.singleton.Singleton;
import org.byron4j.cookbook.designpattern.singleton.SingletonSynchronized;
import org.byron4j.cookbook.designpattern.singleton.SingletonSynchronizedOptimized;
import org.junit.Test;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingletonTest {
@Test
public void test(){
final Set<Singleton> sets = new HashSet<>();
long startTime = System.currentTimeMillis();
ExecutorService es = Executors.newFixedThreadPool(10000);
for(int i = 1; i <= 100000; i++){
es.execute(new Runnable() {
public void run(){
Singleton s = Singleton.getInstance();
sets.add(s);
}
});
}
System.out.println("test用时:" + (System.currentTimeMillis() - startTime));
System.out.println(sets);
}
@Test
public void testSynchronized(){
final Set<SingletonSynchronized> sets = new HashSet<>();
long startTime = System.currentTimeMillis();
ExecutorService es = Executors.newFixedThreadPool(10000);
for(int i = 1; i <= 100000; i++){
es.execute(new Runnable() {
public void run(){
SingletonSynchronized s = SingletonSynchronized.getInstance();
sets.add(s);
}
});
}
System.out.println("testSynchronized用时:" + (System.currentTimeMillis() - startTime));
System.out.println(sets);
}
@Test
public void testOptimised(){
final Set<SingletonSynchronizedOptimized> sets = new HashSet<>();
long startTime = System.currentTimeMillis();
ExecutorService es = Executors.newFixedThreadPool(10000);
for(int i = 1; i <= 100000; i++){
es.execute(new Runnable() {
public void run(){
SingletonSynchronizedOptimized s = SingletonSynchronizedOptimized.getInstance();
sets.add(s);
}
});
}
System.out.println("testOptimised用时:" + (System.currentTimeMillis() - startTime));
System.out.println(sets);
}
}
Запустив тестовый пример, вывод будет следующим:
test用时:1564
[org.byron4j.cookbook.designpattern.singleton.Singleton@68eae58e]
testSynchronized用时:3658
[org.byron4j.cookbook.designpattern.singleton.SingletonSynchronized@36429a46]
testOptimised用时:2254
[org.byron4j.cookbook.designpattern.singleton.SingletonSynchronizedOptimized@21571826]
Видно, что производительность исходной реализации лучшая, но она не потокобезопасна; Synchronized блокирует весь метод getInstance, который может быть потокобезопасным, но производительность наихудшая; Сужение области синхронизации может повысить производительность.
Синглтон и объект клонирование
Также обратите внимание, что вариант с одним клоном относится к методу при правильном использовании класса:
package org.byron4j.cookbook.designpattern.singleton;
/**
* 单例模式实例
* 1. 构造器私有化
* 2. 提供静态方法供外部获取单例实例
* 3. 延迟初始化实例
*/
public class SingletonZClone implements Cloneable{
private static SingletonZClone instance;
// 构造器私有化
private SingletonZClone(){
}
// 提供静态方法
public static SingletonZClone getInstance(){
// 将同步锁范围缩小,降低性能损耗
if(instance == null){
synchronized (SingletonZClone.class){
if(instance == null){
instance = new SingletonZClone();
}
}
}
return instance;
}
/**
* 克隆方法--改为public
* @return
* @throws CloneNotSupportedException
*/
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public void display(){
System.out.println("Hurray! I am create as a SingletonZClone!");
}
}
По умолчанию, когда клон защищен, он становится общедоступным.Тестовый пример выглядит следующим образом:
@Test
public void testClone() throws CloneNotSupportedException {
SingletonZClone singletonZClone1 = SingletonZClone.getInstance();
SingletonZClone singletonZClone2 = SingletonZClone.getInstance();
SingletonZClone singletonZClone3 = (SingletonZClone)SingletonZClone.getInstance().clone();
System.out.println(singletonZClone1 == singletonZClone2);
System.out.println(singletonZClone1 == singletonZClone3);
System.out.println(singletonZClone2 == singletonZClone3);
}
Результат выглядит следующим образом:
true
false
false
Давайте посмотрим на объяснение API метода клонирования, Хотя значения атрибутов клонированного объекта могут быть одинаковыми, они не являются одним и тем же экземпляром объекта:
x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)
Метод clone возвращает копию экземпляра клонированного объекта, за исключением адреса памяти, остальные свойства совпадают, поэтому копия и клонированный объект не являются одним и тем же экземпляром. Видно, что метод clone уничтожает класс singleton, чтобы предотвратить эту проблему, нам нужно отключить метод clone и напрямую изменить его на:
/**
* 克隆方法--改为public
* @return
* @throws CloneNotSupportedException
*/
@Override
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
Синглтоны и проблемы с сериализацией
Механизм сериализации Java позволяет преобразовать состояние объекта в поток байтов, которые могут быть легко сохранены и переданы. Как только объект сериализован, вы можете определить его - превратить поток байтов в объект. Если класс Singleton сериализуется, могут быть созданы дубликаты объектов. Мы можем использовать крючки, чтобы объяснить эту проблему.
метод readResolve()
В спецификации Java есть введение в метод readResolve():
Для сериализуемых и внешних классов метод readResolve() позволяет классу заменять/разрешать объекты, считанные из потока. Реализуя метод readResolve, класс может напрямую управлять десериализованным экземпляром и типом. Определяется следующим образом:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
Метод readResolve вызывается, когда ObjectInputStream считывает объект из потока. ObjectInputStream проверит, определяет ли класс метод readResolve. Если определен метод readResolve, он будет вызываться для указания объекта результата, который будет возвращен после десериализации из потока. Возвращаемый тип должен совпадать с типом исходного объекта, иначе возникнет исключение ClassCastException.
@Test
public void testSeria() throws Exception {
SingletonZCloneSerializable singletonZClone1 = SingletonZCloneSerializable.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"));
oos.writeObject(singletonZClone1);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
SingletonZCloneSerializable test = (SingletonZCloneSerializable) ois.readObject();
ois.close();
System.out.println(singletonZClone1 == test);
}
Вывод теста: false; Это означает, что исходный экземпляр больше не находится при десериализации, что разрушит режим singleton.
Таким образом, мы можем переопределить метод readResolve, чтобы решить проблему сериализации, нарушающей синглтон:
Класс SingletonZCloneSerializableReadResolve добавляет метод readResolve:
/**
* 反序列化时返回instance实例,防止破坏单例模式
* @return
*/
protected Object readResolve(){
return getInstance();
}
Выполните тестовый пример:
@Test
public void testSReadResolve() throws Exception {
s = SingletonZCloneSerializableReadResolve.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"));
oos.writeObject(s);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
SingletonZCloneSerializableReadResolve test = (SingletonZCloneSerializableReadResolve) ois.readObject();
ois.close();
System.out.println(s == test);
}
Выведите true, эффективно предотвращая уничтожение синглтона при десериализации.
вы знаете?
-
Одноэлементные классы используются редко, и если вы собираетесь использовать этот шаблон проектирования, вы должны точно знать, что делаете. Это рискованно на платформах с ограниченными ресурсами, поскольку глобально создается только один экземпляр.
-
Обратите внимание на клонирование объектов. Одноэлементный шаблон требует тщательной проверки и предотвращает использование метода клонирования.
-
При многопоточном доступе необходимо обратить внимание на вопросы безопасности потоков.
-
Будьте осторожны с несколькими загрузчиками классов, это может сломать ваш одноэлементный класс.
-
Если одноэлементный класс сериализуем, он должен реализовать строгую типизацию.