保证一个类仅有一个实例,并提供一个访问它的全局访问点。让类自身负责保存它的唯一实例,这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。例如在全局只有一个工厂,这个工厂可以生产你注册进去的任意组件,那么这个工厂就适合使用单例模式来创建。
1.常用单例方式
public class Singleton1 {
private Singleton1(){}
private static Singleton1 instance = null;
public static Singleton1 getInstance(){
if(instance == null){
instance = new Singleton1();
}
return instance;
}
}
这是最常用的单例模式,这里设置了构造方法为private,使得其他类无法实例化它,然后在内部提供静态的实例,并提供外部可以访问的单一访问点getInstance方法,保证只有在全局没有实例时才会创建新的实例。
2.可以进行注册的单例
public class Singleton2 {
protected Singleton2(){}
private static Singleton2 instance = null;
private static Map<String, Singleton2> repository= new HashMap<>();
private static Singleton2 lookup(String name){
return repository.get(name);
}
public static void register(String name, Singleton2 instance){
repository.put(name, instance);
}
public static Singleton2 getInstance(){
if(instance == null){
String name = System.getProperty("SINGLETON");
instance = lookup(name);
}
return instance;
}
}
public class ConcreteSingleton2 extends Singleton2{
public ConcreteSingleton2(){
register("ConcreteSingleton2", this);
}
@Test
public void test(){
//初始化时必须这么做
new ConcreteSingleton2();
System.setProperty("SINGLETON", "ConcreteSingleton2");
//实际使用时
Singleton2 s = Singleton2.getInstance();
System.out.println(s);;
}
}
这是设计模式书中的注册Singleton,Singleton父类提供注册、检索、获取单件的方法,子类可以使用父类提供的方法向父类提供的单件注册表注册自己,然后整个系统就可以通过实现新的子类、向系统注册这个子类、设置环境变量,最后通过总的获取方法来获得你在环境变量中指定的单件实例。
3.使用synchronized同步的单例(线程安全)
以上所讲的两种方式是设计模式书中的单例方法,但是在Java中,多线程程序非常常见,所以需要考虑多线程下该如何做,最简单的方式是直接使用Java提供的synchronized关键字来改造方法1中的单例方法。
public class Singleton3 {
private Singleton3(){}
private static Singleton3 instance = null;
public static synchronized Singleton3 getInstance(){
if(instance == null){
instance = new Singleton3();
}
return instance;
}
}
4.使用static域的单例(线程安全)
public class Singleton4 {
private Singleton4(){}
private static Singleton4 instance = new Singleton4();
public static Singleton4 getInstance(){
return instance;
}
}
利用static域也可以达到线程安全的目的,但是这种方式导致无法按需创建单例对象。static域会随着类的初始化而初始化,故当类被JVM加载时,这个static域就会被初始化,从而保证了调用static方法之前就已经存在了实例,这保证了线程安全性。
5.使用内部类创建(线程安全)
public class Singleton5 {
private Singleton5(){}
private static class Holder{
public static Singleton5 instance = new Singleton5();
}
public static Singleton5 getInstance(){
return Holder.instance;
}
}
这种方式在《Java并发编程实战》中称为“延长初始化占位类模式”,通过一个单独的内部静态类来初始化instance。JVM会推迟Holder的初始化操作,直到开始使用这个类时才会初始化,并且由于是通过静态变量初始化instance的,故不需要额外的同步,当其他类调用getInstance时,就会加载Holder,并初始化这个类、构造instance。这里的类内部static类不会在外部类被加载时就被初始化,只有第一次使用时才会被初始化(也就是调用外部的getInstance方法时)。
6.强烈不建议用的方式(双重检查加锁DCL、非多线程安全)
public class Singleton6 {
private Singleton6() {}
private static Singleton6 instance = null;
public static Singleton6 getInstance() {
if (instance == null) {
synchronized (Singleton6.class) {
if (instance == null) {
instance = new Singleton6();
}
}
}
return instance;
}
}
这种方式是通过在创建实例之前先检查对象是否存在,然后再在同步后继续判断对象是否存在,此时再进行创建新的对象。但是这种创建方式会存在如下问题:任何一个对象的创建都大致分为三个步骤——申请内存,初始化对象,对象指针引用对象,这之后就可以使用这个对象指针来引用这个对象了,但是,由于Java即时编译器存在指令重排的过程,故可能会导致创建步骤变为申请内存,对象指针引用对象,初始化对象。假设线程A申请内存,并将对象指针引用至当前内存,但是初始化过程相对复杂并耗时,线程B获取了这个对象的引用,检查不为null,就使用了这个对象(这个对象还正在进行初始化过程),从而导致了未定义的错误。故,不能使用这种方式进行编码,即使要使用,也要将instance定义为volatile。