它是Java中最简单的设计模式之一。
如果有人问我哪种设计模式好,那么我会很自豪地说Singleton。
但是,当他们深入询问单身人士的概念时,我感到很困惑。
真的单身是那么困难吗?
确实不是,但是它有许多我们需要了解的场景(尤其是初学者)。
定义:
在所有情况下,该类只应允许一个实例,并且我们应提供对该实例的全局访问点。
定义就像1,2,3和A,B,C,D一样简单。
让我们看看如何实现Singleton类。
我们如何确保对象始终都是一个?
提示:将对象创建逻辑仅放在一个位置,并且不允许用户每次尝试执行该逻辑时都只能执行一次。
对象创建逻辑->它是什么
我们如何用Java创建对象?
是的,使用构造函数,我们不应该允许用户每次尝试访问构造函数并执行它。
但是我们应该这样做一次,至少要得到一个对象。
那么,如何确保构造函数只能访问和执行一次?
- 防止在类外部访问构造函数,以使任何外部人员都无法创建实例。
如何使它->如何防止类外部的方法访问?
简单,将make方法作为私有权限,类似地将构造函数作为私有权限。 - 防止构造函数在类中多次执行。
如何制作->这有多种实现方式,下面以示例来看。
如果满足以上两个条件,则我们班级将始终有一个对象。 该类称为Singleton,因为它在我们请求的所有时间都产生单个对象。
没有太多理论,我们现在将开始执行它。
创建单例对象的方法有很多:
方法1
- 急于初始化或使用前初始化
package com.kb.singleton;
public class EagerSingletonClass {
private static volatile EagerSingletonClass singletonInstance = new EagerSingletonClass();
//making constructor as private to prevent access to outsiders
private EagerSingletonClass() {
}
public static EagerSingletonClass getInstance(){
return singletonInstance;
}
}
EagerSingletonClass的实例在类启动时创建。 由于它是静态的,因此会在加载EagerSingletonClass的过程中加载并创建它。
- Junit测试类为上述类的单例测试。
package com.kb.singleton;
import static org.junit.Assert.*;
import org.junit.Test;
public class EagerSingletonClassTest {
@Test
public void testSingleton() {
EagerSingletonClass instance1 = EagerSingletonClass.getInstance();
EagerSingletonClass instance2 = EagerSingletonClass.getInstance();
System.out.println("checking singleton objects equality");
assertEquals(true, instance1==instance2);
}
}
优势:
此策略在加载类期间创建对象,因此从多线程方案中可以更快更安全。 我们只需要使实例具有可变性即可处理多线程方案。
坏处 :
这种策略会在类加载本身时创建实例,因此,如果我们不使用它,那么这将浪费整个时间和内存来创建实例。 因此,最好在需要时选择一种策略来创建实例。
什么时候使用以上策略?
只要我们100%确保在我们的应用程序中肯定使用了该对象。
要么 当物体不重时,我们可以管理速度和内存。
方法2
- 延迟初始化或在需要时初始化
与其在启动时创建对象,不如在需要时创建对象,这是很好的。 因此,让我们看看如何做到这一点:
package com.kb.singleton;
public class LazySingleton {
private static volatile LazySingleton singletonInstance = null;
//making constructor as private to prevent access to outsiders
private LazySingleton() {
}
public static LazySingleton getInstance(){
if(singletonInstance==null){
synchronized (LazySingleton.class) {
singletonInstance = new LazySingleton();
}
}
return singletonInstance;
}
}
在以上程序中,仅当通过getInstance()方法发出请求时,我们才创建了一个对象。
在此,在首次调用getInstance()的过程中,对象“ singletonInstance”将为null,并在条件变为true时执行if条件块并创建一个对象。
然后,对getInstance()方法的后续调用将返回相同的object。
但是,如果我们看一下多线程方案,问题就出在以下上下文中:2个线程t1和t2调用getInstance()方法,线程t1执行if(singletonInstance == null)并发现singletonInstance为null,因此它进入同步块以创建一个宾语。
但是在执行对象创建逻辑之前,如果线程t2执行if(singletonInstance == null),那么它还将发现singletonInstance为null,因此它还将尝试输入同步块,但是由于已经输入的第一个线程t1,它没有锁。
因此,线程t2等待线程t1完成同步块的执行。
因此线程t1执行并创建对象。 现在线程t2也正在等待同步块时进入同步块,并再次创建对象。
因此,两个线程创建了两个对象。 因此无法实现单例。
解决上述问题的方法是“ 双重检查锁定”。
它说在我们在同步块内执行对象创建的逻辑之前,请重新检查同步块内的实例变量。
这样,我们可以避免多个线程多次创建对象。
怎么样 ?
线程t1检查条件if(singletonInstance == null),第一次为true,因此它进入同步块,然后再次检查条件if(singletonInstance == null),这也为true,因此创建了对象。
现在线程t2进入方法getInstance()并假定它已在线程t1执行对象创建逻辑之前执行了if(singletonInstance == null)条件,然后t2也等待进入同步块。
在线程t1从同步块中出来之后,线程t2进入了同一个块,但是我们再次在其中有if条件if(singletonInstance == null)但线程t1已经创建了一个对象,它使条件变为false并进一步停止执行并返回相同的实例。
让我们看看如何在代码中完成它:
package com.kb.singleton;
public class LazySingletonDoubleLockCheck {
private static volatile LazySingletonDoubleLockCheck singletonInstance = null;
//making constructor as private to prevent access to outsiders
private LazySingletonDoubleLockCheck() {
}
public static LazySingletonDoubleLockCheck getInstance(){
if(singletonInstance==null){
synchronized (LazySingleton.class) {
if(singletonInstance ==null){
singletonInstance = new LazySingletonDoubleLockCheck();
}
}
}
return singletonInstance;
}
}
让我们做单元测试
package com.kb.singleton;
import static org.junit.Assert.*;
import org.junit.Test;
public class LazySingletonDoubleLockCheckTest {
@Test
public void testSingleton() {
LazySingletonDoubleLockCheck instance1 = LazySingletonDoubleLockCheck.getInstance();
LazySingletonDoubleLockCheck instance2 = LazySingletonDoubleLockCheck.getInstance();
System.out.println("checking singleton objects equality");
assertEquals(true, instance1==instance2);
//fail("Not yet implemented");
}
}
上面的实现是针对单例模式的最佳建议解决方案,它最适合于单线程,多线程等所有情况。
方法3
- 使用内部类的单例
让我们看一下下面的使用内部类创建对象的代码:
package com.kb.singleton;
public class SingletonUsingInnerClass {
private SingletonUsingInnerClass() {
}
private static class LazySingleton{
private static final SingletonUsingInnerClass SINGLETONINSTANCE = new SingletonUsingInnerClass();
}
public static SingletonUsingInnerClass getInstance(){
return LazySingleton.SINGLETONINSTANCE;
}
}
单元测试代码
package com.kb.singleton;
import static org.junit.Assert.*;
import org.junit.Test;
public class SingletonUsingInnerClassTest {
@Test
public void testSingleton() {
SingletonUsingInnerClass instance1 = SingletonUsingInnerClass.getInstance();
SingletonUsingInnerClass instance2 = SingletonUsingInnerClass.getInstance();
System.out.println("checking singleton objects equality");
assertEquals(true, instance1==instance2);
}
}
以上使用内部类创建对象的方法是创建单例对象的最佳方法之一。
在这里,除非并且直到有人尝试访问LazySingleton静态内部类的静态引用变量,否则将不会创建该对象。
因此,这还将确保在需要时以及在需要时创建对象。 而且实现起来非常简单。 从多线程进行也是安全的。
方法4
- 具有序列化和反序列化的Singleton
现在假设我们的应用程序是分布式的,我们序列化我们的单例对象并将其写入文件。 后来我们通过反序列化单例对象来阅读它。 取消序列化对象始终会创建一个文件内具有可用状态的新对象。 如果在写入文件后进行任何状态更改,然后尝试取消序列化对象,则将获得原始对象,而不是新的状态对象。 因此,在此过程中我们得到了2个对象。
让我们尝试通过程序来了解这个问题:
首先->使Singleton类可序列化以序列化和反序列化此类的对象。
第二件事->将对象写入文件(序列化)
第三件事->更改对象状态 第四件事-> de序列化对象
我们的单例课程如下:
package com.kb.singleton;
import java.io.Serializable;
public class SingletonSerializeAndDesrialize implements Serializable {
private int x=100;
private static volatile SingletonSerializeAndDesrialize singletonInstance = new SingletonSerializeAndDesrialize();
private SingletonSerializeAndDesrialize() {
}
public static SingletonSerializeAndDesrialize getInstance() {
return singletonInstance;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
}
序列化我们的对象,然后对状态进行一些更改,然后取消序列化。
package com.kb.singleton;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class SerializeAndDeserializeTest {
static SingletonSerializeAndDesrialize instanceOne = SingletonSerializeAndDesrialize.getInstance();
public static void main(String[] args) {
try {
// Serialize to a file
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
"filename.ser"));
out.writeObject(instanceOne);
out.close();
instanceOne.setX(200);
// Serialize to a file
ObjectInput in = new ObjectInputStream(new FileInputStream(
"filename.ser"));
SingletonSerializeAndDesrialize instanceTwo = (SingletonSerializeAndDesrialize) in.readObject();
in.close();
System.out.println(instanceOne.getX());
System.out.println(instanceTwo.getX());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}}
输出:
200
100
它清楚地表明,即使单身,我们也有2个不同的对象。 之所以发生这种情况,是因为反序列化会创建具有文件中可用状态的新实例。
如何克服这个问题? 意味着如何防止在反序列化期间创建新实例?
解决方案非常简单–在您的singleton类中实现以下方法:
Access_modifier Object readResolve() throws ObjectStreamException{
}
例:
Public Object readResolve() throws ObjectStreamException{
return modifiedInstance;
}
将其应用于上面的单例课程,则完整的单例课程如下:
package com.kb.singleton;
import java.io.ObjectStreamException;
import java.io.Serializable;
public class SingletonSerializeAndDesrialize implements Serializable {
private int x=100;
private static volatile SingletonSerializeAndDesrialize singletonInstance = new SingletonSerializeAndDesrialize();
private SingletonSerializeAndDesrialize() {
System.out.println("inside constructor");
}
public static SingletonSerializeAndDesrialize getInstance() {
return singletonInstance;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public Object readResolve() throws ObjectStreamException{
return singletonInstance;
}
}
现在运行上面的序列化和反序列化类,以检查两个实例的输出。
输出:
200
200
这是因为,在反序列化期间,它将调用readResolve()方法,并且在此返回现有实例,这将阻止创建新实例并确保单例对象。
- 小心序列号
在序列化之后和取消序列化之前,只要类结构发生更改。 然后,在反序列化过程中,它将找到一个不兼容的类,并因此引发异常:java.io.InvalidClassException:SingletonClass; 本地类不兼容:流classdesc serialVersionUID = 5026910492258526905,本地类serialVersionUID = 3597984220566440782
因此,为避免发生此异常,我们必须始终对可序列化的类使用序列号ID。 其语法如下:
private static final long serialVersionUID = 1L;
所以最后通过涵盖以上所有情况,单例类的最佳解决方案如下,我建议始终使用此解决方案:
package com.kb.singleton;
import java.io.Serializable;
public class FinalSingleton implements Serializable{
private static final long serialVersionUID = 1L;
private FinalSingleton() {
}
private static class LazyLoadFinalSingleton{
private static final FinalSingleton SINGLETONINSTANCE = new FinalSingleton();
}
public static FinalSingleton getInstance(){
return LazyLoadFinalSingleton.SINGLETONINSTANCE;
}
private Object readResolve() {
return getInstance();
}
}
翻译自: https://www.javacodegeeks.com/2014/05/java-singleton-design-pattern.html