Java Singleton设计模式

它是Java中最简单的设计模式之一。

如果有人问我哪种设计模式好,那么我会很自豪地说Singleton。

但是,当他们深入询问单身人士的概念时,我感到很困惑。

真的单身是那么困难吗?

确实不是,但是它有许多我们需要了解的场景(尤其是初学者)。

定义:

在所有情况下,该类只应允许一个实例,并且我们应提供对该实例的全局访问点。

定义就像1,2,3和A,B,C,D一样简单。

让我们看看如何实现Singleton类。

我们如何确保对象始终都是一个?

提示:将对象创建逻辑仅放在一个位置,并且不允许用户每次尝试执行该逻辑时都只能执行一次。

对象创建逻辑->它是什么
我们如何用Java创建对象?

是的,使用构造函数,我们不应该允许用户每次尝试访问构造函数并执行它。
但是我们应该这样做一次,至少要得到一个对象。

那么,如何确保构造函数只能访问和执行一次?

  1. 防止在类外部访问构造函数,以使任何外部人员都无法创建实例。
    如何使它->如何防止类外部的方法访问?
    简单,将make方法作为私有权限,类似地将构造函数作为私有权限。
  2. 防止构造函数在类中多次执行。
    如何制作->这有多种实现方式,下面以示例来看。

如果满足以上两个条件,则我们班级将始终有一个对象。 该类称为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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值