第1条:用静态工厂方法代替构造器
- 静态工厂方法与构造器不同的第一大优势在于,它们有名称。
- 静态工厂方法与构造器不同的第二大优势在于,不必在每次调用它们的时候都创建一个新对象。 它从来不创建对象。 这种方法类似于享元(Flyweight)模式 。 如果程序经常请求创建相同的对象,并且创建对象的代价很高,则这项技术可以极大地提升性能。
在此,列举单例模式的几种创建方式 :
① 饿汉式单例 :
public class Singleton{
//构造方法私有化
private Singleton(){}
//定义一个常量SINGLETON
private static final Singleton SINGLETON=new Singleton();
//单例类的外部通过getInstance()方法得到SINGLETON对象
public static Singleton getInstance(){
return SINGLETON;
}
}
判断得到的是否为同一个对象
public static void main(String[] args) {
System.out.println(Singleton.getInstance());
System.out.println(Singleton.getInstance());
}
com.company.Singleton@49e4cb85
com.company.Singleton@49e4cb85
显示得到的两个Singleton对象相同
由于饿汉类单例的对象是在类加载完成后就创建了的,所以当多线程并发调用getInstance()方法时得到的为已经创建好的对象,不存在线程不安全的问题,但会一直占用一部分内存。
② 懒汉式单例
public class Singleton{
//构造方法私有化
private Singleton(){}
//定义SINGLETON但不初始化,此时不占用内存
private static Singleton SINGLETON=null;
//当第一次调用getInstance()时初始化SINGLETON
public static Singleton getInstance(){
if(SINGLETON==null){
SINGLETON=new Singleton();
}
return SINGLETON;
}
}
懒汉式单例相对饿汉式单例少占用了初始化前的一部分内存,但懒汉式单例存在一个很严重的问题,就是在多个线程同时调用getInstance()方法时,可能存在创建了两个不同对象的情况。
假设现在存在线程A和线程B同时调用getInstance()方法,线程A先进入if判断语句后,刚刚好在初始化SINGLETON之前进入了阻塞状态,线程B又进入了if语句。线程B创建了SINGLETON,线程B结束。此时线程A开始执行,也创建了一个SINGLETON。这个时候,线程A,B创建的两个SINGLETON就不是同一个对象。
DCL双检锁解决线程安全问题
public static Singleton getInstance(){
if(SINGLETON==null){
synchronized(Singleton.class){
if(SINGLETON==null){
SINGLETON=new Singleton();
}
}
}
return SINGLETON;
}
利用双重判断SINGLETON是否为空,只有在第一次为空时才会调用synchronized同步锁,也就解决了同步锁对运行效率的影响。
但是,在某些特殊情况下,可能会发生初始化对象时的指令重排序问题,导致多线程时某个线程得到的单例类对象为空。
指令重排序问题
正常情况下,JVM初始化对象以以下三个步骤进行:
步骤 | 操作 |
---|---|
1 | 分配初始化对象的内存空间 |
2 | 初始化对象 |
3 | 设置instance指向步骤1中的内存空间 |
重排序后,JVM初始化对象的顺序变为了1,3,2。在多线程访问时,就存在下面的情况:
步骤 | 操作 |
---|---|
A1:分配初始化对象的内存空间 | |
A3设置instance指向步骤1中的内存空间(发生重排序) | |
线程A进入阻塞状态 | |
进入第一次if判断发现单例对象不为空,得到A3返回的地址 | |
线程B结束 | |
A3:初始化对象 |
(具体加载过程,可参看文章 https://blog.youkuaiyun.com/aataxuefeihong/article/details/125087815)
在上述过程中,线程B得到的是一个空的地址,也就发生了错误,解决方法为,将SINGLETON声明volatile,volatile的作用为
1. 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。
2. 禁止指令重排序
如此,指令重排序问题解决,除此之外,还可以用静态内部类解决线程安全问题
public class Singleton{
private Singleton(){}
private static class SingletonHolder{
private static final Singleton SINGLETON=new Singleton();
}
public static final Singleton getInstance(){
return SingletonHolder.SINGLETON;
}
}
此方法类似于饿汉式单例,只是将饿汉式单例中的类一旦加载就将对象初始化变为了调用时初始化
反射对单例模式的影响
单例模式的实现依赖于将单例类的构造方法私有化,只有单例类本身可以调用。但反射的特性可以得到类的私有方法,也就可以得到私有构造方法从而创建对象,导致单例类有了两个对象。
以饿汉式单例为例实现反射创建对象:
public class Main {
public static void main(String[] args) throws Exception {
Class<?> c=Singleton.class;
Constructor<?> constructor= c.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton= (Singleton) constructor.newInstance();
System.out.println(singleton);
System.out.println(Singleton.getInstance());
}
}
输出结果如下:
com.company.Singleton@49e4cb85
com.company.Singleton@2133c8f8;
反序列化对单例模式的影响
仍以饿汉式为例实现单例类的序列化与反序列化
public class Main {
public static void main(String[] args) throws Exception {
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("test.txt"));
objectOutputStream.writeObject(Singleton.getInstance());
File file=new File("test.txt");
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(file));
Singleton singleton= (Singleton) objectInputStream.readObject();
System.out.println(singleton);
System.out.println(Singleton.getInstance());
}
}
结果如下:
com.company.Singleton@6ea12c19
com.company.Singleton@153f5a29
可以参考的解决方法:可以在构造方法中加入判断,以及重写readResolve方法
public class Singleton implements Serializable{
private static final long serialVersionUID = 1L;
//构造方法私有化
private Singleton(){
if(SINGLETON==null){
SINGLETON = this;
}else{
throw new RuntimeException("singleton")
}
}
//定义SINGLETON但不初始化,此时不占用内存
private static volatile Singleton SINGLETON=null;
//当第一次调用getInstance()时初始化SINGLETON
public static Singleton getInstance(){
if(SINGLETON==null){
synchronized(Singleton.class){
if(SINGLETON==null){
SINGLETON=new Singleton();
}
}
}
return SINGLETON;
}
private Object readResolve(){
return SINGLETON;
}
}
(关于为何序列化的问题,可以参考:https://baijiahao.baidu.com/s?id=1705785565221395954&wfr=spider&for=pc)
③ 枚举类实现单例模式
在jdk1.5之后,新增加了一个枚举类,枚举类实现单例可以避免上述的线程安全,反射,序列化对单例模式的影响
public enum EnumSingleton {
ENUM_SINGLETON;
private int i=0;
public void test(String sleep) throws InterruptedException {
if("on".equals(sleep)){
Thread.sleep(2000);
}
System.out.println(Thread.currentThread()+"第"+(++i)+"次调用");
}
}
public class Main {
public static void main(String[] args) throws Exception {
new Thread(()->{
System.out.println(EnumSingleton.ENUM_SINGLETON);
try {
EnumSingleton.ENUM_SINGLETON.test("on");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println(EnumSingleton.ENUM_SINGLETON);
EnumSingleton.ENUM_SINGLETON.test("off");
}
}
ENUM_SINGLETON
ENUM_SINGLETON
Thread[main,5,main]第1次调用
Thread[Thread-0,5,main]第2次调用
通过调用次数可以看出,枚举类是线程安全的。
验证反射是否会影响枚举单例
public class Main {
public static void main(String[] args) throws Exception {
Class<?> c=EnumSingleton.class;
Constructor<?> constructor=c.getDeclaredConstructor();
constructor.setAccessible(true);
EnumSingleton singleton= (EnumSingleton) constructor.newInstance();
System.out.println(singleton);
System.out.println(EnumSingleton.ENUM_SINGLETON);
}
}
结果:
Exception in thread "main" java.lang.NoSuchMethodException: com.company.EnumSingleton.<init>()
验证反序列是否影响枚举单例
public class Main {
public static void main(String[] args) throws Exception {
ObjectOutputStream objectOutputStream=new ObjectOutputStream(new FileOutputStream("test.txt"));
objectOutputStream.writeObject(EnumSingleton.ENUM_SINGLETON);
File file=new File("test.txt");
ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream(file));
EnumSingleton singleton= (EnumSingleton)objectInputStream.readObject();
System.out.println(singleton);
System.out.println(EnumSingleton.ENUM_SINGLETON);
}
}
结果:
ENUM_SINGLETON
ENUM_SINGLETON