详解单例

Jul 20, 2016


单例

什么是单例?

  • 类只有一个唯一实例

Java中的单例

单例的两种方式:

  • 1.饱汉式单例
  • 2.饿汉式单例

饿汉式单例

什么是饿汉式单例?

  • 饿汉,就是实例对象一开始就已创建好(这里的开始指的是类初始化阶段);

经典写法如下:

public class HungrySingle {
	//声明一个WellFedSingle对象并初始化实例对象,设计成私有
	private static HungrySingle single = new HungrySingle();
	
	//重写私有构造方法,防止外部进行实例化(但是依旧可以通过反射进行实例化)
	private HungrySingle() {
		System.out.println("类进行实例化...");
	}
	
	//公开给外部一个方法,获取single对象
	public static HungrySingle getInstance() {
		return single;
	}

	public void testOfSingle(){
		System.out.println("这是HungrySingle类对象的一个测试方法...");
	}
}

这样一个饿汉式单例就完成了,但是这个类存在两个隐患和一个优势:

我们先通过javap -verbose命令查看此类的字节码文件:

Constant pool:
	... //过滤掉常量池
{
   static {};
	descriptor: ()V
	flags: ACC_STATIC
	Code:
		stack=2, locals=0, args_size=0
		0: new           #1                  // class cn/march/algo/string/HungrySingle
		3: dup
		4: invokespecial #10                 // Method "<init>":()V
		7: putstatic     #13                 // Field single:Lcn/march/algo/string/HungrySingle;
		10: return
		LineNumberTable:
		line 6: 0
		LocalVariableTable:
		Start  Length  Slot  Name   Signature

}

通过代码我们可以看到static HungrySingle single = new HungrySingle();这条语句其实转换成static语句块中执行

static{}语句块实际相当于clinit初始化方法(对应类加载过程的初始化阶段);在此static块中调用init方法(即类的构造方法,实例化类对象)

在JVM中规定:

  • 虚拟机会保证clinit方法在多线程环境下被正确的加锁,同步,如果多个线程同时去初始化一个类,那么只会有一个类执行clinit方法,其他线程都需要堵塞等待;

所以饿汉式单例是线程安全的;

优势:

  • 1.此类是线程安全;

缺点:

  • 2.类如果继承Serializable接口,能够被反序列化创建新的实例(如果类不重写readResolve方法做处理);
  • 3.能够通过反射攻击(即可以通过反射API访问类的资源(比如获取构造器Constructor对象进行实例化))

详情请参考Java单例讨论

饱汉式单例(懒汉)

什么是饱汉式单例?

  • 指类实例对象是一个懒加载的过程

经典写法如下:

public class WellFedSingle {
	//声明静态类实例对象,初始化为null
	private static WellFedSingle single = null;
	
	//重写构造方法,设为私有
	private WellFedSingle() {
		System.out.println("类进行实例化...");
	}

	public static WellFedSingle getInstance() {
		//进行判断,如果single为null(即还没有创建对象),进行实例化操作
		if(single == null){
			single = new WellFedSingle();
		}
		
		return single;
	}
}

这样一个饱汉式单例产生了,但是这个类有3个隐患:

隐患:

  • 1.线程不安全(PS:如下图);

图中HungrySingle类应该改为WellFedSingle类(因为图文件找不到了,见谅!!!)

single

线程A执行if(single == null){ … }的时候线程B此时也执行到if(single == null):

  • 1.而A并没有实例化,所以B正好完成实例化操作,B完成实例化操作之后,A并没有及时获取single状态也对single进行实例化操作;
  • 2.A完成实例化操作,但是B并没有及时响应获取single状态,所以B对single进行实例化操作;

线程A,B都产生了不同的single对象;

所以这就产生了线程安全问题;

解决方案:

//使用JDK1.5之后的volatile保证single对象的可见性,但是仅仅光声明volatile不行,还需同步处理
private static volatile WellFedSingle single = null;
//为什么要双重判断,因为如果没有 //1 的检查,那么所有的getInstance()都会进入锁争夺,会影响性能,因此加入了检查。 
public static WellFedSingle getInstance() {
	if(single == null) { //1
		synchronized(WellFedSingle.class) {
			//进行判断,如果single为null(即还没有创建对象),进行实例化操作
			if(single == null){
				single = new WellFedSingle();
			}
		}
	}
	return single;
}
  • 2.类如果继承Serializable接口,能够被反序列化创建新的实例(如果类不重写readResolve方法做处理);
  • 3.能够通过反射攻击(即可以通过反射API访问类的资源(比如获取构造器Constructor对象进行实例化))

分析内部类,枚举实现单例

内部类

经典写法如下:

public abstract class InnerSingle {
	//重写构造方法,设置为私有
	private InnerSingle(){
		System.out.println("类正在实例化...");
	}
	//编写一个静态内部类
	private static class InnerSingleHelper {
		//声明外部类对象并进行实例化赋值
		private static final InnerSingle innerSingle = new InnerSingle() {

		};
	}
	
	public static InnerSingle getInstance() {
		return InnerSingleHelper.innerSingle;
	}
}

这样一个内部类单例完成,这样写有2个优势,1大隐患

优势:

  • 1.线程安全,因为是类加载过程中就已经实例化(如饿汉)
  • 2.不能通过反射来进行实例化(因为定义为抽象类不能进行实例化)

隐患:

  • 3.类如果继承Serializable接口,能够被反序列化创建新的实例(如果类不重写readResolve方法做处理);
枚举实现单例

写法如下:

enum EnumSingle {

	INSTANCE(){

		@Override
		public void testOfSingle() {
			System.out.println("测试实例方法...");
		}
	
	};
	//定义一个实例抽象方法
	public abstract void testOfSingle();
}

通过javap查看字节码文件:

abstract class cn.march.algo.string.EnumSingle extends java.lang.Enum<cn.march.algo.string.EnumSingle> {
  		public static final cn.march.algo.string.EnumSingle INSTANCE;
  		static {};
  		public abstract void testOfSingle();
  		public static cn.march.algo.string.EnumSingle[] values();
  		public static cn.march.algo.string.EnumSingle valueOf(java.lang.String);
}

我通过javap -verbose查看static{ … }中字节码如下:

static {};
		Code:
		stack=4, locals=0, args_size=0
			0: new           #1                  // class cn/march/algo/string/EnumSingle
			3: dup
			4: ldc           #12                 // String INSTANCE
			6: iconst_0
			7: invokespecial #13                 // Method "<init>":(Ljava/lang/String;I)V
			10: putstatic     #17                 // Field INSTANCE:Lcn/march/algo/string/EnumSingle;
			13: iconst_1
			14: anewarray     #1                  // class cn/march/algo/string/EnumSingle
			17: dup
			18: iconst_0
			19: getstatic     #17                 // Field INSTANCE:Lcn/march/algo/string/EnumSingle;
			22: aastore
			23: putstatic     #19                 // Field ENUM$VALUES:[Lcn/march/algo/string/EnumSingle;
			26: return

由代码可以知道,用枚举实现单例是最安全且高效:

优势:

  • 1.线程安全,在静态块进行初始化(类比饿汉);
  • 2.不能通过反射实例化(因为EnumSingle是abstract类);
  • 3.不能通过反序列化实例化对象

好了先写到这,今天的目标完成了;

参考:Java枚举单例