天天看点

设计模式--单例模式5种实现方式的比较1、饿汉式单例(推荐)2、懒汉式单例3、静态内部类(推荐)4、双重检查锁5、枚举总结

1、饿汉式单例(推荐)

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {
    }
    public static Singleton getInstance() {
        return instance;
    }
}
           

注解:初始化静态的instance创建一次。如果我们在Singleton类里面写一个静态的方法不需要创建实例,它仍然会早早的创建一次实例。而降低内存的使用率。

缺点:没有延迟加载的效果,从而降低内存的使用率。

2、懒汉式单例

未加锁(线程不安全)

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

}
           

这段代码却存在了一个致命的问题,那就是当多个线程并行调用 getInstance() 的时候,就会创建多个实例,线程不安全

懒汉式加锁

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

}
           

 实现了线程安全,但它并不是那么高效,因为在任何时候只能有一个线程去调用 getInstance() 方法,但实际上加锁操作也是耗时的,我们应该尽量地避免使用它。

3、静态内部类(推荐)

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton() {
    }
    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
} 
           

注解:定义一个私有的内部类,在第一次用这个嵌套类时,会创建一个实例。而类型为SingletonHolder的类,只有在Singleton.getInstance()中调用,由于私有的属性,他人无法使用SingleHolder,不调用Singleton.getInstance()就不会创建实例。

优点:达到了lazy loading的效果,即按需创建实例。 

这种写法用 JVM 本身的机制保证了线程安全的问题,同时读取实例的时候也不会进行同步,没什么性能缺陷,还不依赖 JDK 版本。

缺点:享有特权的客户端可以借助 AccessibleObject.setAccessible 方法,通过反射机制来调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。

4、双重检查锁

public class Singleton {
    private volatile static Singleton singleton;
    private Singleton() {
    }
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
           

 很可惜,它是有问题。主要在于 singleton= new Singleton() 这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。 

1.给 singleton分配内存 

2.调用 Singleton 的构造函数来初始化成员变量 

3.将 singleton对象指向分配的内存空间(执行完这步 instance 就为非 null 了) 

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

5、枚举

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
}
           

创建枚举默认就是线程安全的,所以不需要担心 double checked locking,而且还能防止反序列化导致重新创建新的对象。但是在安卓中大量使用枚举是比较耗费内存的。

总结

有两个问题需要注意:

1.如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。

2.如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。

对第一个问题修复的办法是:

private static Class getClass(String classname)
        throws ClassNotFoundException{
        ClassLoader classLoader=Thread.currentThread().getContextClassLoader();

        if(classLoader==null)
        classLoader=Singleton.class.getClassLoader();

        return(classLoader.loadClass(classname));
        }
}
           

对第二个问题修复的办法是:

public class Singleton implements java.io.Serializable {
    public static Singleton INSTANCE = new Singleton();
    protected Singleton() {
    }
    private Object readResolve() {
        return INSTANCE;
    }
}
           

继续阅读