由于单例模式只生成一个实例, 减少了系统性能开销(如: 当一个对象的产生需要比较多的资源时, 如读取配置, 产生其他依赖对象, 则可以通过在应用启动时直接产生一个单例对象, 然后永久驻留内存的方式来解决)
实现要点
单例模式主要追求三个方面性能
主要有五种实现方式,懒汉式(延迟加载,使用时初始化),饿汉式(声明时初始化),双重检查,静态内部类,枚举。
由于没有同步,多个线程可能同时检测到实例没有初始化而分别初始化,从而破坏单例约束。
public class Singleton {
private static Singleton instance;
private Singleton() {
};
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
由于对象只需要在初次初始化时需要同步,多数情况下不需要互斥的获得对象,加锁会造成巨大无意义的资源消耗
public class Singleton {
private static Singleton instance;
private Singleton() {
};
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种方法对比于上面的方法确保了只有在初始化的时候需要同步,当初始化完成后,再次调用getInstance不会再进入synchronized块。
NOTE
由于在同步块外的if语句中可能有多个线程同时检测到instance为null,同时想要获取锁,所以在进入同步块后还需要再判断是否为null,避免因为后续获得锁的线程再次对instance进行初始化
注意:volatile并不保证操作的原子性,例如即使count声明为volatile类型,count++操作被分解为读取->写入两个操作,虽然读取到的是count的最新值,但并不能保证读取与写入之间不会有其他线程再次写入,从而造成逻辑错误
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
};
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这种方式基于单ClassLoder机制,instance在类加载时进行初始化,避免了同步问题。饿汉式的优势在于实现简单,劣势在于不是懒加载模式(lazy initialization)
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
};
public static Singleton getInstance() {
return instance;
}
}
由于内部类不会在类的外部被使用,所以只有在调用getInstance()方法时才会被加载。同时依赖JVM的ClassLoader类加载机制保证了不会出现同步问题。
public class Singleton {
private Singleton() {
};
public static Singleton getInstance() {
return Holder.instance;
}
private static class Holder{
private static Singleton instance = new Singleton();
}
}
参见枚举类解析
- 线程安全
由于枚举类的会在编译期编译为继承自java.lang.Enum的类,其构造函数为私有,不能再创建枚举对象,枚举对象的声明和初始化都是在static块中,所以由JVM的ClassLoader机制保证了线程的安全性。但是不能实现延迟加载
- 序列化
由于枚举类型采用了特殊的序列化方法,从而保证了在一个JVM中只能有一个实例。
public enum Singleton {
INSTANCE;
public String error(){
return "error";
}
}
对于枚举类,该破解方法不适用。
import java.lang.reflect.Constructor;
public class TestCase {
public void testBreak() throws Exception {
Class clazz = (Class) Class.forName("Singleton");
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance1 = constructor.newInstance();
Singleton instance2 = constructor.newInstance();
System.out.println("singleton? " + (instance1 == instance2));
}
public static void main(String[] args) throws Exception{
new TestCase().testBreak();
}
}
对于枚举类,该破解方法不适用。
该测试首先需要声明Singleton为实现了可序列化接口public class Singleton implements Serializable
public class TestCase {
private static final String SYSTEM_FILE = "save.txt";
public void testBreak() throws Exception {
Singleton instance1 = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(SYSTEM_FILE));
oos.writeObject(instance1);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SYSTEM_FILE));
Singleton instance2 = (Singleton) ois.readObject();
System.out.println("singleton? " + (instance1 == instance2));
}
public static void main(String[] args) throws Exception{
new TestCase().testBreak();
}
}
JVM中存在两种ClassLoader,启动内装载器(bootstrap)和用户自定义装载器(user-defined class loader),在一个JVM中可能存在多个ClassLoader,每个ClassLoader拥有自己的NameSpace。一个ClassLoader只能拥有一个class对象类型的实例,但是不同的ClassLoader可能拥有相同的class对象实例,这时可能产生致命的问题。
对于序列化与反序列化,我们需要添加一个自定义的反序列化方法,使其不再创建对象而是直接返回已有实例,就可以保证单例模式。
我们再次用下面的类进行测试,就发现结果为true。
public final class Singleton {
private Singleton() {
}
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
private Object readResolve() throws ObjectStreamException {
// instead of the object we're on,
// return the class variable INSTANCE
return INSTANCE;
}
public class TestCase {
private static final String SYSTEM_FILE = "save.txt";
public void testBreak() throws Exception {
Singleton instance1 = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(SYSTEM_FILE));
oos.writeObject(instance1);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(SYSTEM_FILE));
Singleton instance2 = (Singleton) ois.readObject();
System.out.println("singleton? " + (instance1 == instance2));
}
public static void main(String[] args) throws Exception {
new TestCase().testBreak();
}
}
}
方式 | 优点 | 缺点 |
---|---|---|
饿汉式 | 线程安全, 调用效率高 | 不能延迟加载 |
懒汉式 | 线程安全, 可以延迟加载 | 调用效率不高 |
双重检测锁式 | 线程安全, 调用效率高, 可以延迟加载 | - |
静态内部类式 | 线程安全, 调用效率高, 可以延迟加载 | - |
枚举单例 | 线程安全, 调用效率高 | 不能延迟加载 |
测试结果:
在不考虑延迟加载的情况下,枚举类型获得了最好的效率,懒汉模式由于每次方法都需要获取锁,所以效率最低,静态内部类与双重检查的效果类似。考虑到枚举可以轻松有效的避免序列化与反射,所以枚举是较好实现单例模式的方法。
public class TestCase {
private static final String SYSTEM_FILE = "save.txt";
private static final int THREAD_COUNT = 10;
private static final int CIRCLE_COUNT = 100000;
public void testSingletonPerformance() throws IOException, InterruptedException {
final CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
FileWriter writer = new FileWriter(new File(SYSTEM_FILE), true);
long start = System.currentTimeMillis();
for (int i = 0; i < THREAD_COUNT; ++i) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < CIRCLE_COUNT; ++i) {
Object instance = Singleton.getInstance();
}
latch.countDown();
}
}).start();
}
latch.await();
long end = System.currentTimeMillis();
writer.append("Singleton 共耗时: " + (end - start) + " 毫秒\n");
writer.close();
}
public static void main(String[] args) throws Exception{
new TestCase().testSingletonPerformance();
}
}
static关键字的作用是把类的成员变成类相关,而不是实例相关,static块会在类首次被用到的时候进行加载,不是对象创建时,所以static块具有线程安全性
- 普通初始化块
当Java创建一个对象时, 系统先为对象的所有实例变量分配内存(前提是该类已经被加载过了), 然后开始对这些实例变量进行初始化, 顺序是: 先执行初始化块或声明实例变量时指定的初始值(这两处执行的顺序与他们在源代码中排列顺序相同), 再执行构造器里指定的初始值.
静态初始化块
又名类初始化块(普通初始化块负责对象初始化, 类初始化块负责对类进行初始化). 静态初始化块是类相关的, 系统将在类初始化阶段静态初始化, 而不是在创建对象时才执行. 因此静态初始化块总是先于普通初始化块执行.
执行顺序
系统在类初始化以及对象初始化时, 不仅会执行本类的初始化块[static/non-static], 而且还会一直上溯到java.lang.Object类, 先执行Object类中的初始化块[static/non-static], 然后执行其父类的, 最后是自己.
顶层类(初始化块, 构造器) -> … -> 父类(初始化块, 构造器) -> 本类(初始化块, 构造器)
小结
static{} 静态初始化块会在类加载过程中执行;
{} 则只是在对象初始化过程中执行, 但先于构造器;
内部类访问权限
非静态内部类
静态内部类
匿名内部类
如果(方法)局部变量需要被匿名内部类访问, 那么该局部变量需要使用final修饰.