背景:在日常开发中,有时候为了节约资源,有时需要确保系统中某个类只有唯一一个实例,当这唯一一个实例创建成功后,我们无法再创建一个同类型的其他对象,所有操作只能基于这一个实例。为了确保对象唯一性,可以通过单例模式进行。
单例模式三个要点:1、某个类只能有一个实例;2、它必须自行创建这个实例(私有构造函数);3、自行向整个系统提供这个实例。
例一:提供一个服务器的负载均衡器,将访问进行分发。由于集群中的服务器需要动态删减,且客户服务端请求需要统一分发,因此需要确保这个负载均衡器的唯一性,只能用一个负载均衡器来负责。否则将会带来服务器状态的不一致以及请求分配冲突等问题
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class LoadBalancer {
//负载均衡器,是一个单例类,其中包含一个list存储服务器信息的集合serverList。每次从serverList中取一台服务器响应客户请求
private static LoadBalancer instance = null;//静态私有成员变量,存储唯一实例
private List serverList = null;//服务器集合
//私有构造函数
private LoadBalancer(){
System.out.println("初始化LoadBalancer");
serverList = new ArrayList();
}
//共有静态成员方法,返回唯一实例
public static LoadBalancer getLoadBalancer(){
if(instance == null){
instance = new LoadBalancer();
}
return instance;
}
//增加服务器
public void addServer(String server){
serverList.add(server);
}
//删除服务器
public void removeServer(String server){
serverList.remove(server);
}
//使用random类随机获取服务器
public String getServer(){
Random random = new Random();
int i = random.nextInt(serverList.size());
return (String)serverList.get(i);
}
}
public class LoadBalancerTest {
public static void main(String[] args){
LoadBalancer loadBalancer1,loadBalancer2,loadBalancer3,loadBalancer4;
loadBalancer1 = LoadBalancer.getLoadBalancer();
loadBalancer2 = LoadBalancer.getLoadBalancer();
loadBalancer3 = LoadBalancer.getLoadBalancer();
loadBalancer4 = LoadBalancer.getLoadBalancer();
if (loadBalancer1 == loadBalancer2 && loadBalancer2 == loadBalancer3 && loadBalancer3 == loadBalancer4){
System.out.println("服务器负载均衡器具有唯一性!");
}
loadBalancer1.addServer("1");
loadBalancer1.addServer("2");
loadBalancer1.addServer("3");
loadBalancer1.addServer("4");
for(int i = 0; i < 10; i++){
String server = loadBalancer1.getServer();
System.out.println("分发到:" + server);
}
}
}
上述代码通过私有构造函数使外部不能访问,那么就不能通过new来构造一个对象。new对象时会进行初始化,初始化的时候会调用构造函数。然后通过getLoadBalancer 方法返回一个唯一的实例。
顺便复习下对象初始化的过程:静态代码块、构造代码块,构造方法
对于例一来说,会存在一个问题,在代码执行 instance = new LoadBalancer() 时,如果时间过长,有大量初始化工作要完成。此时,在多线程环境中,如果再调用一次 instance = new LoadBalancer() ,由于instance尚未创建成功,仍为null值,判断条件(instance== null)为真值,因此代码instance= new LoadBalancer()将再次执行,导致最终创建了多个instance对象,这违背了单例模式的初衷,也导致系统运行发生错误。
此问题有两种解决方案,在正式介绍这两种解决方案之前我们先介绍一下单例模式的的两种不同实现方式,饿汉式和懒汉式。
饿汉式单例
package EagerSingletonModel;
public class EagerSingleton {
private static final Person instance = new Person();
private EagerSingleton(){}
public static Person getInstance(){
return instance;
}
}
package EagerSingletonModel;
public class EagerSingletonTest {
public static void main(String[] args){
Person eagerSingleton1,eagerSingleton2,eagerSingleton3;
eagerSingleton1 = EagerSingleton.getInstance();
eagerSingleton2 = EagerSingleton.getInstance();
eagerSingleton3 = EagerSingleton.getInstance();
if(eagerSingleton1 == eagerSingleton2 && eagerSingleton2 == eagerSingleton3 ){
System.out.println("true");
}else {
System.out.println("false");
}
}
}
package EagerSingletonModel;
public class Person {
private String name = "A";
}
当类被加载的时候,饿汉模式会将instance 变量初始化一个Person对象。之后都用这个实例对象。这样不会出现创建多个单例对象的问题,可确保单例对象的唯一性。
懒汉式单例
懒汉式在第一次调用getInstance()方法时进行实例化,在类加载的时候不会进行实例化操作。未避免多个线程同时调用,可以使用synchronized关键字。
package LazySingleModel;
public class LazySingle {
private static LazySingle instance = null;
private LazySingle(){}
synchronized public static LazySingle getInstance(){
if(instance == null) {
instance = new LazySingle();
}
return instance;
}
}
这样写能够保证不会出现并发的问题,但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。事实上,无须对整个getInstance() 进行锁定,只需要对instance = new LazySingle(); 进行锁定就可以了。如下
package LazySingleModel;
public class LazySingle {
//有问题的代码
private static LazySingle instance = null;
private LazySingle(){}
public static LazySingle getInstance(){
if(instance == null) {
synchronized(LazySingle.class) {
instance = new LazySingle();
}
}
return instance;
}
}
问题貌似得以解决,事实并非如此。如果使用以上代码来实现单例,还是会存在单例对象不唯一。
假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在synchronized中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-CheckLocking)。使用双重检查锁定实现的懒汉式单例类完整代码如下所示:
package LazySingleModel;
public class LazySingle {
private volatile static LazySingle instance = null;
private LazySingle(){}
public static LazySingle getInstance(){
//第一次判断
if(instance == null) {
synchronized(LazySingle.class) {
//第二次判断
if(instance == null) {
instance = new LazySingle();
}
}
}
return instance;
}
}
需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理,且该代码只能在JDK 1.5及以上版本中才能正确执行。由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
饿汉式单例类与懒汉式单例类比较
饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。
懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。
饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。可见,无论是饿汉式单例还是懒汉式单例都存在这样那样的问题,有没有一种方法,能够将两种单例的缺点都克服,而将两者的优点合二为一呢?答案是:Yes!下面我们来学习这种更好的被称之为Initialization Demand Holder (IoDH)的技术。
Initialization Demand Holder (IoDH)
在IoDH中,我们在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用,实现代码如下所示:
package IoDHSingle;
public class Singleton {
private Singleton() {
}
private static class HolderClass {
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return HolderClass.instance;
}
public static void main(String args[]) {
Singleton s1, s2;
s1 = Singleton.getInstance();
s2 = Singleton.getInstance();
System.out.println(s1==s2);
}
}
编译并运行上述代码,运行结果为:true,即创建的单例对象s1和s2为同一对象。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。
通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH)。
通过枚举实现单例
普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
枚举序列化:在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject等方法。
public class InstanceDemo {
/**
* 构造方法私有化
*/
private InstanceDemo(){
}
/**
* 返回实例
* @return
*/
public static InstanceDemo getInstance() {
return Singleton.INSTANCE.getInstance();
}
/**
* 使用枚举方法实现单例模式
*/
private enum Singleton {
instance;
private InstanceDemo instance;
/**
* JVM保证这个方法绝对只调用一次
*/
Singleton() {
instance = new InstanceDemo();
}
public InstanceDemo getInstance() {
return instance;
}
}
}