保证对象唯一 为了避免其他程序过多建立该类对象。先禁止其他程序建立该类对象 还为了让其他程序可以访问到该类对象,只好在本类中,自定义一个对象。 为了方便其他程序对自定义对象的访问,可以对外提供一些访问方式。 这三步怎么用代码体现呢? 将构造函数私有化。 在类中创建一个本类对象。 提供一个方法可以获取到该对象。
一、饿汉式(静态变量)
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}
二、饿汉式(静态常量)
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton() { }
public static Singleton getInstance() {
return INSTANCE;
}
}
三、饿汉式(静态代码块)
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}
上面三种写法本质上其实是一样的,但使用静态final的实例对象或者使用静态代码块依旧不能解决在反序列化、反射、克隆时重新生成实例对象的问题。 反射:可以通过setAccessible(true)来绕过 private 限制,从而调用到类的私有构造函数创建对象。
优点:写法比较简单,在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
四、懒汉式(线程不安全)
public class Singleton {
private static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance== null) {
instance = new Singleton();
} return instance;
}
}
优点:懒加载,只有使用的时候才会加载。
缺点:但是只能在单线程下使用。如果在多线程下,instacnce对象还是空,这时候两个线程同时访问getInstance()方法,因为对象还是空,所以两个线程同时通过了判断,开始执行new的操作。所以在多线程环境下不可使用这种方式。
五、懒汉式(线程安全,存在同步开销,效率低)
public class Singleton {
private static Singleton instance;
private Singleton() { }
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
} return instance;
}
}
优点:懒加载,只有使用的时候才会加载,获取单例方法加了同步锁,保正了线程安全。
缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。
六、懒汉式(线程假装安全,同步代码块)
public class Singleton {
private static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
优点:改进了第五种效率低的问题。
缺点:但实际上这个写法还不能保证线程安全,和第四种写法类似,只要两个线程同时进入了 if (singleton == null) { 这句判断,照样会进行两次new操作
七、DCL「双重检测锁:Double Checked Lock」 单例(假)
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
本例的亮点都在getInstance()方法上,可以看到在该方法中对instance进行了两次判空:第一层判断为了避免不必要的同步,第二层判断则是为了在null的情况下创建实例。对第六种单例的漏洞进行了弥补,但是还是有小问题的,问题就在instance = new Singleton();语句上。 这语句在这里看起来是一句代码啊,但实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了3件事情: 给Singleton的实例分配内存 调用Singleton()的 构造函数,初始化成员字段 将instance对象指向分配的内存空间(此时instance就不是null了) 但是,由于Java编译器运行处理器乱序执行,以及jdk1.5之前Java内存模型中Cache、寄存器到主内存会写顺序的规定,上面的第二和第三的顺序是无法保证的。也就是说,执行顺序可能是1-2-3也可能是1-3-2.如果是后者,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候instance因为已经在线程A内执行3了,instance已经是非null,所有线程B直接取走instance,再使用时就会出错,这就是DCL失效问题,而且这种难以跟踪难以重现的问题很可能会隐藏很久。
优点:线程安全;延迟加载;效率较高。
缺点:JVM编译器的指令重排导致单例出现漏洞。
八、DCL「双重检测锁:Double Checked Lock」 单例(真,推荐使用)
public class Singleton {
//volatile保证了:1 instance在多线程并发的可见性,2 禁止instance在操作时的指令重排序
private volatile static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
//第一次判空,保证不必要的同步
if (instance == null) {
//synchronized对Singleton加全局锁,保证每次只有一个线程创建实例
synchronized (Singleton.class) {
//第二次判空时为了在null的情况下创建实例
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
优点:线程安全;延迟加载;效率较高。
缺点:由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,略微的性能降低,但除非你的代码在并发场景比较复杂或者低于JDK6版本下使用,否则,这种方式一般是能够满足需求的。
九、静态内部类(推荐使用)
public class Singleton {
private Singleton() { }
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}
这种方式跟饿汉式方式采用的机制类似,但又有不同。 两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。 所以在这里,利用 JVM的 classloder 的机制来保证初始化 instance 时只有一个线程。JVM 在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化
优点:避免了线程不安全,延迟加载,效率高。
缺点:依旧不能解决在反序列化、反射、克隆时重新生成实例对象的问题。
十、枚举
public enum Singleton {
INSTANCE
}
枚举类单例模式是《Effective Java》作者 Josh Bloch 极力推荐的单例方法 枚举类类型是 final 的「不可以被继承」 构造方法是私有的「也只能私有,不允许被外部实例化,符合单例」 类变量是静态的 没有延时初始化,随着类的初始化就初始化了「从上面静态代码块中可以看出」 由 4 可以知道枚举也是线程安全的
优点:写法简单,不仅能避免多线程同步问题,而且还能防止反序列化、反射,克隆重新创建新的对象。
缺点:JDK 1.5之后才能使用。