`

详细分析Java单例的几种写法(一)

    博客分类:
  • Java
阅读更多

单例模式简介

单例模式是软件设计模式中最简单的一种设计模式。从名称中可以看出,单例的目的就是使系统中只能包含有一个该类的唯一一个实例。

单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”

Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”

对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。

本文将以Java语言来描述单例类的几种实现方式,以及分析给出的几种实现方式的优缺点和并用程序验证性能数据。

 

 

Java实例化的方式

      我们知道在Java语言中创建实例的方法大体上有4种:

   1、使用new关键字

 

import java.io.Serializable;
import java.util.Random;

/**
 * @author thinkpad
 *
 */
public class Singleton implements Serializable{
	
	private static final long serialVersionUID = -3868390997950184335L;
	
	private static Random random = new Random();
	
	private int id;
	
	private String name;
	
	private int [] data = {
			random.nextInt(),
			random.nextInt(),
			random.nextInt()
	};
	//default constructor  
	public Singleton(){
		
	}
	//public constructor
	public Singleton(int id){
		this.id = id * 2;
	}
	//private constructor
	private Singleton(int id,String name){
		this(id);
		this.name = name + "." + name;
	}
	
	@Override
	public String toString(){
		StringBuffer buf = new StringBuffer();
		buf.append(super.toString()).append("\n")
			.append("id :").append(id).append("\n")
			.append("name :").append(this.name).append("\n")
			.append("data array :").append(data.toString());
		return buf.toString();
	}
}

  最常见的使用new 关键字,创建一个对象

public class TestSingleton {

	public static void main(String[] args) {
		Singleton single = new Singleton(1);
	}
}

 

   2、使用反射

     使用反射能够突破JAVA中可见权限,本示例中调用了Singleton私有的构造方法。

   

import java.lang.reflect.Constructor;
import java.lang.reflect.Type;

/**
 * @author thinkpad
 *
 */
public class TestSingletonUseReflect {

	public static void main(String[] args) {
		try {
			Constructor<?>[] constructors = Singleton.class.getDeclaredConstructors();
			System.out.println(constructors.length);
			for(Constructor<?> con : constructors ) {
				Type[] types = con.getParameterTypes();
				if (types.length == 2){
					con.setAccessible(true);
					Singleton singleton = null;
					singleton = (Singleton) con.newInstance(1,"single1");
					System.out.println(singleton);
					
					Singleton singleton2 = null;
					singleton2 = (Singleton) con.newInstance(2,"single2");
					System.out.println(singleton2);
				}
			}
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}
 

 

   3、使用反序列化

    我们知道Java对象序列化(Object Serialization)将那些实现了Serializable接口的对象转换成一个字节序列,并可以通过反序列化将这个字节序列完全恢复成为原来的对象,因此反序列化提供了一个创建对象的方式。由于使用反序列化创建对象和使用new关键字创建对象有一些不同,反序列化过程中构造方法是没有被调用的(也不一定,若序列化对象父类没有实现Serializable接口,例如Object类,序列化过程中会递归调用父类的无参构造函数),而且其中的域的初始化代码也没有被执行。

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
 * @author thinkpad
 *
 */
public class TestSerializable {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Singleton single1 = new Singleton(100);
		Singleton single2 = new Singleton(200);
		
		try { 
			ByteArrayOutputStream buf = new ByteArrayOutputStream();
			ObjectOutputStream out = new ObjectOutputStream(buf);
			System.out.println("**********begin serializable**********");
			System.out.println(single1);
			System.out.println(single2);
			out.writeObject(single1);
			out.writeObject(single2);
			out.close();
			System.out.println("**********begin unserializable**********");
			ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));
			Singleton s1 = (Singleton)in.readObject();
			Singleton s2 = (Singleton)in.readObject();
			System.out.println(s1.toString());
			System.out.println(s2.toString());
			in.close();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} 
	}
}

 

   4、依赖注入(Spring DI)

    依赖注入从JVM的角度来看的话,应该不算是一种创建对象的方式。关于依赖注入的概念,请参考Spring官方网站。

 

Java单例的几种实现方式

       根据单例模式的定义及描述,要实现单例模式,有几个问题是需要考虑的。单例如何创建以及由谁创建,如何保证实例的唯一性,如何提供全局的访问点。由于Java语言中的实例化有多种,所以Java单例必须能够保证在各种条件下维持instance的唯一性,能够做到线程安全、延迟加载并且能够很好的抵抗反射攻击、反序列化攻击等。本文的以下内容将分析和验证常见的几种单例的设计方法,并用代码来验证效果(网上理论讲的很多,但是大部分都缺少实际的代码验证)。

       实现单例,必须是私有的构造器,并导出公有的静态实例,根据静态实例的初始化方式,可以分为饿汉型和懒汉型,根据实例的暴露方式可以分为直接暴露和工厂方法间接暴露。饿汉型在类加载的时候就完成实例化,懒汉型就是在实际使用的时候才进行实例化。因此饿汉型的单例是能够保证线程安全的(JVM来保证ClassLoader只加载一次,不考虑多个ClassLoader的情况),懒汉型的线程安全却需要额外的同步来控制;饿汉型不能实现懒汉式的延迟加载。

   

1、直接暴露-饿汉型(1)

      这种写法非常简单、粗暴,而且也能较好的满足需要,呵呵,有时候简单粗暴也是最能快速解决问题的。这种方式线程安全,但是缺乏灵活性,不容易扩展。

 

 

import java.io.Serializable;

public class DirectHungrySingleton implements Serializable{
	
	private static final long serialVersionUID = 6069877357741265707L;

	//实例对象
	public static final DirectHungrySingleton singleton = new DirectHungrySingleton();
	
	//私有的构造方法
	private DirectHungrySingleton(){
	}
}
    2、工厂方法-饿汉型
          采用工厂方法来暴露实例,对外提供了统一的API接口,扩展性和灵活性更强,而且也具有线程安全性,但是不适用需要延迟加载的场合。

 

 

import java.io.Serializable;

public class FactoryMethodHungrySingleton implements Serializable{
	
	private static final long serialVersionUID = 6069877357741265707L;

	//私有的实例对象
	private static final FactoryMethodHungrySingleton singleton = new FactoryMethodHungrySingleton();
	
	//私有的构造方法
	private FactoryMethodHungrySingleton(){
	}
	//公有工厂方法
	public static FactoryMethodHungrySingleton getInstance(){
		return singleton;
	}
}
   
3、工厂方法-懒汉型(一)  使用同步
     由于是采用延迟初始化的方式,无法通过JVM来保证实例的唯一性,因此在工厂方法上增加synchronized关键字保证线程安全。这种写法拥有了线程安全、延迟加载的优点,但带来了代码性能上的损失。无论该对象是否已经实例化,getInstance()方法时,必须要获得Class对象锁,加锁和解锁必然会损耗一定的性能。
 

 

 

import java.io.Serializable;

public class FactoryMethodLazySingleton implements Serializable{
	
	private static final long serialVersionUID = 6069877357741265707L;

	//私有的实例对象
	private static FactoryMethodLazySingleton singleton = null;
	
	//私有的构造方法
	private FactoryMethodLazySingleton(){
	}
	//4.增加线程安全性 【 在类对象上加锁,控制并发访问,防止由于实例化过程中(new对象和初始化过程中还未返回),其他线程调用getSingleton方法,导致存在多个实例】
	public static synchronized FactoryMethodLazySingleton getInstance(){
		if (singleton == null){
			singleton = new FactoryMethodLazySingleton();
		}
		return singleton;
	}
}
    

 

4、工厂方法-懒汉型(二) 使用双重检查(适用jdk1.5+)

    我们分析代码发现,只需要在实例化过程中控制不能让多个线程进入即可,根据锁定的粒度最小化原则,只需要在new 代码行外加锁即可。这是著名的双重检查写法,一些网站资料上显示只适用于jdk1.5+,我个人的理解是jdk1.5+后,volatile关键字功能的增强,能够保证多线程环境下变量读写的可见性。但是具体资料还未找到,请知道的朋友告知一下。

    

import java.io.Serializable;

public class FactoryMethodLazyDCSingleton implements Serializable{
	
	private static final long serialVersionUID = 6069877357741265707L;

	//私有的实例对象
	//使用volatile保证多线程情况下的可见性
	private static volatile FactoryMethodLazyDCSingleton singleton = null;
	
	//私有的构造方法
	private FactoryMethodLazyDCSingleton(){
	}
	//公有工厂方法
	public static FactoryMethodLazyDCSingleton getInstance(){
		//外层检查,避免实例化成功后进入同步块,提高性能
		if (singleton == null){
			synchronized(FactoryMethodLazyDCSingleton.class){
				//内层检查,避免多个线程进入后,重复实例化
				if (singleton == null)
					singleton = new FactoryMethodLazyDCSingleton();
			}
		}
		return singleton;
	}
}

 5、工厂方法-懒汉型(三)  使用内部静态类

    使用双重检查能够很好的满足线程安全和延迟加载,但是代码书写比较复杂、容易出错,在网上出现了一种使用内部静态类的写法,感觉非常优雅。它通过内部类的延迟加载特性实现lazy loading,同时借助JVM的类加载机制保证线程安全,是一种非常优雅的写法。

   

import java.io.Serializable;

public class FactoryMethodLazyInnerClassSingleton implements Serializable {

	private static final long serialVersionUID = 6069877357741265707L;
	
	//内部静态类
	private static class Holder {
		static final FactoryMethodLazyInnerClassSingleton singleton 
			= new FactoryMethodLazyInnerClassSingleton();
	}
	
	// 私有的构造方法
	private FactoryMethodLazyInnerClassSingleton() {
	}

	// 公有工厂方法
	public static FactoryMethodLazyInnerClassSingleton getInstance() {
		return Holder.singleton;
	}
}

  

  6、使用Enum (Jdk 1.5+)

     在jdk1.5中提供了Enum关键字来定义枚举类型,我们知道枚举类是一种特殊的类。它有以下这些特点,为了保证类型安全,不对外提供公有的构造方法,所有的枚举对象均在类加载时完成初始化,并且均为static final类型,这些特点完全类似于第1种写法,简单优雅,线程安全,并完全提供抵抗反序列化攻击的机制。

 

//jdk1.6 Enum类 源码第199行可见  无法对enum进行反序列化,保证了唯一性
/**
      * prevent default deserialization
      */
    private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
            throw new InvalidObjectException("can't deserialize enum");
    }

  

 

public enum EnumSingleton {
	
	//定义实例
	INSTANCE(1, "other");
	
	//定义其他域
	private int intField;

	private String otherField;

	private EnumSingleton(int intField, String otherField) {
		this.intField = intField;
		this.otherField = otherField;
		System.out.println("init the enum type");
	}

	@Override
	public String toString() {
		return new StringBuffer().append(super.toString()).append(intField)
				.append(otherField).toString();
	}
}
分享到:
评论

相关推荐

    java-单例模式几种写法

    单例模式是软件设计模式中的一种,用于控制类的实例化过程,确保一个类只有一个实例,并提供全局访问点。在Java中,实现单例模式有多种方法,每种方法都有其特点和适用场景。以下是对这六种常见单例模式实现方式的...

    Java设计模式之单例模式的七种写法

    在 Java 中,单例模式的写法有好几种,主要有懒汉式单例、饿汉式单例、登记式单例等。 懒汉式单例是一种常见的单例模式实现方式,它在第一次调用的时候实例化自己。下面是懒汉式单例的四种写法: 1、基本懒汉式...

    01 设计模式之单例模式.pdf

    在Java语言中,实现单例模式主要有以下几种方式: 1. 饿汉式(Eager Initialization) 饿汉式单例模式在类加载的时候就已经进行了实例化,因此它不需要考虑多线程同步的问题。这种方式在类加载时就完成了初始化,...

    2023年最新java面试大全

    【06期】单例模式有几种写法? 【07期】Redis中是如何实现分布式锁的? 【08期】说说Object类下面有几种方法呢? 【09期】说说hashCode() 和 equals() 之间的关系? 【10期】Redis 面试常见问答 【11期】分布式...

    java_len.zip_java len

    10. **设计模式**:常见的23种设计模式,如单例模式、工厂模式、观察者模式等,是解决常见问题的成熟解决方案。 11. **注解(Annotation)**:用于提供元数据,增强代码的功能和可读性。 12. **Lambda表达式**:...

    《Java基础入门》复习资料(打印).doc.docx

    Java是一种高级编程语言,由Sun Microsystems(现已被Oracle公司收购)于1995年发布。它被设计成跨平台的,能够在各种操作系统上运行,包括Windows、Mac OS和Linux。Java的基础入门学习通常涉及以下几个核心概念: ...

    JAVA 面试题

    6. **单例模式的几种写法:** 单例模式保证一个类只有一个实例,并提供一个全局访问点。常见的单例实现方式有饿汉式、懒汉式、枚举类型以及利用双重检查锁定机制等。 7. **web.xml加载顺序:** 在Web应用中,web....

    java技术储备,如何提升自己

    1. 单例模式:了解单例模式的几种写法,包括懒汉式、饿汉式、双重检查锁定式等。 2. JDK 中的设计模式:了解 JDK 中的设计模式,包括 IO 中的装饰模式和设配器模式等。 3. 框架中的设计模式:了解常用的设计模式,...

    春招&秋招面经

    以下是几种实现方式: 1. **懒汉式** ```java public class Singleton { private static Singleton singleton = new Singleton(); private Singleton() {} public static Singleton getSingleton() { ...

    Android SharedPreferences存储的正确写法

    本文将详细介绍如何正确地使用SharedPreferences,并提供一种优化的实现方式。 SharedPreferences的特点主要有以下几点: 1. **单例模式**:通过`Context.getSharedPreferences()`获取的SharedPreferences对象是...

    天舟通信笔试题1

    单例模式是一种设计模式,确保一个类只有一个实例,并提供全局访问点。常见的单例实现有饿汉式(静态常量)、懒汉式(线程安全的双重检查锁定)和枚举单例等。 5. **递归算法** 递归是函数或方法在定义时调用自身...

    C#面试资料,囊括了个大IT公司招聘时的习题

    C#是微软公司于2000年推出的一种面向对象的编程语言,它基于C++和Java的设计理念,支持类型安全和垃圾回收,拥有丰富的类库和强大的开发工具Visual Studio。面试中,面试官通常会考察以下几个方面的基础概念: 1. *...

    Lista-de-Contatos

    描述 "ListadeContatos" 是标题的另一种写法,进一步证实了这一点。标签 "Kotlin" 提示我们该项目是使用 Kotlin 语言编写的,这是一种由 JetBrains 开发的现代编程语言,特别适合 Android 应用开发。 Kotlin 知识点...

Global site tag (gtag.js) - Google Analytics