廖雪峰->单例
public class Singleton {
// 静态字段引用唯一实例:
private static final Singleton INSTANCE = new Singleton();
// 通过静态方法返回实例:
public static Singleton getInstance() {
return INSTANCE;
}
// private构造方法保证外部无法实例化:
private Singleton() {
}
}
public class Singleton {
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
private Singleton() {
}
}
但这种写法在多线程下是不行的,可能会创建多个实例对象。
此时,有两种方式:方法加锁或二次检查(DCL)
方法加锁:严重影响并发性能
public synchronized static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
二次检查/双重检查:由于Java的内存模型,双重检查在这里不成立。
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
因为JVM内存模型(JMM)允许‘无序写入’。即在JVM执行指令过程中,指令的执行顺序有可能是乱序的。如代码mInstance = new Singleton();其实这行代码做了3件事:
1,为单例对象分配内存空间
2,将mInstance引用变量指向刚分配好的内存空间(此时,mInstance已经是非null的了)
3,为单例对象通过mInstance调用其类的构造方法进行初始化。
在JVM执行过程中,2,3步的执行顺序是不确定的,可能是颠倒的。颠倒的情况下,在并发时,就有可能发生严重的错误,当线程一执行到步骤2而未执行步骤3时(实例未初始化,但引用变量mInstance已经非null),如果此时被线程2获取到了CPU的使用权,线程2在执行mInstance的非null判断时,将会认为mInstance为非null而直接返回引用,但其实此时是未初始化的,如果线程2使用这个mInstance引用,系统就会报错(因为mInstance未初始化却被使用了)。
在JDK版本较高(>1.5)的情况下,可以通过使用volatile关键字来避免这种情况
如果没有特殊的需求,使用Singleton模式的时候,最好不要延迟加载,这样会使代码更简单。
public enum World {
// 唯一枚举:
INSTANCE;
private String name = "world";
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
枚举类也完全可以像其他类那样定义自己的字段、方法,这样上面这个World类在调用方看来就可以这么用:
String name = World.INSTANCE.getName();
使用枚举实现Singleton还避免了第一种方式实现Singleton的一个潜在问题:即序列化和反序列化会绕过普通类的private构造方法从而创建出多个实例,而枚举类就没有这个问题。
避免直接在方法上加锁,降低锁的粒度,提高性能。第一次判断if (INSTANCE == null)可以检查是否存在实例,存在则直接返回实例对象,避免进入同步锁。
第二次判断if (INSTANCE == null)是为了避免在第一次判断到进入同步锁期间,有对象实例被创建出来,此时检查对象实例,如果依旧为null就创建实例,否则不创建直接返回。
我们定义一个枚举类:
public enum Color {
RED, GREEN, BLUE;
}
编译器编译出的class大概如下:
public final class Color extends Enum { // 继承自Enum,标记为final class
// 每个实例均为全局唯一:
public static final Color RED = new Color();
public static final Color GREEN = new Color();
public static final Color BLUE = new Color();
// private构造方法,确保外部无法调用new操作符:
private Color() {}
}
枚举类其实就是一个特殊的类,它继承了Enum类,并被声明为final,表示其不能被继承。
类中的每个实例都是用static final声明的静态常量,是全局唯一的,且构造方法用private声明,确保外部无法调用new来创建实例对象。由于是static的所以所有的对象只会加载一次且线程安全,这就保证了实例的单例性。
单例有个问题就是一旦实现了 Serializable接口后,就不再是单例的了,因为每次反序列化调用readObject()时都会创建一个新对象。而枚举类为了保证其定义的枚举变量在JVM中是唯一的,对序列化和反序列化做了一些特殊处理。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。
枚举类的序列化问题
public static void main(String[] args) throws Exception{
Constructor constructor = SingleTest.class.getDeclaredConstructor();
constructor.setAccessible(true);
SingleTest s1 = SingleTest.getSingleTest();
SingleTest s2 = SingleTest.getSingleTest();
SingleTest s3 = (SingleTest) constructor.newInstance();
System.out.println("输出结果为:"+s1.hashCode()+"," +s2.hashCode()+","+s3.hashCode());
}
解决方法:
在单例中构建一个私有构造方法:
private static boolean flag = false;
private SingleTest(){
synchronized(SingleTest.class){
if(flag == false){
flag = !flag;
}else {
throw new RuntimeException("单例模式被侵犯!");
}
}
}
解决方法:
//在单例类中加入这个方法即可
private Object readResolve(){
return instance;
}
破坏单例及防护方法
反射破坏单例