Java中单例模式是一个很重要的设计模式,主要作用是保证在Java程序中,某个类只有一个实例存在。单例模式避免了对象的重复创建,减少了内存的开销。
单例模式有以下特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式有很多种写法:
1.懒汉模式
public class SingleTon {
private static SingleTon singleTon;
private SingleTon() {
// TODO Auto-generated constructor stub
}
public static SingleTon getInstance() {
if (singleTon == null) {
singleTon = new SingleTon();
}
return singleTon;
}
}
懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例,当线程A执行到if判断,singleTon此时为空,线程A此时让出cpu,线程B开始执行,线程B执行的时候,singleTon此时为空,线程Bnew出来一个SingleTon,此时线程A接着执行,线程A也会new出来一个SingleTon,这样就出现了多个实例。
改进的话可以考虑:
(1).在getInstance方法加锁:
public class SingleTon {
private static SingleTon singleTon;
private SingleTon() {
// TODO Auto-generated constructor stub
}
public static synchronized SingleTon getInstance() {
if (singleTon == null) {
singleTon = new SingleTon();
}
return singleTon;
}
}
加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题,依然不够完美。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。
(2).双重检索模式
public class SingleTon {
private static SingleTon singleTon;
private SingleTon() {
// TODO Auto-generated constructor stub
}
public static SingleTon getInstance() {
if (singleTon == null) {
synchronized (SingleTon.class) {
if (singleTon == null)
singleTon = new SingleTon();
}
}
return singleTon;
}
}
双重索引的模式改变了加锁的位置,只有在singleTon为空的时候,才会进行线程同步,减少了线程同步的开销,但是这样还是会存在一个问题:Java中的指令重排优化。所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。主要问题是出在singleTon = new SingleTon();这句代码上面,这句代码大概做了3件事:
1.给 instance 分配内存调用 Singleton
2.的构造函数来初始化成员变量
3.将instance对象指向分配的内存空间
java指令集乱序会导致执行的过程可能是123或者是132,如果是132的执行顺序,则在 3 执行完毕、2 未执行之前,被另外一个抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
解决方案可以是将 instance 变量声明成 volatile 就可以了。
private volatile static SingleTon singleTon;
volatile的一个语义是禁止指令重排序优化,也就保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。
(3).静态内部类
public class SingleTon {
private SingleTon() {
// TODO Auto-generated constructor stub
}
private static class SingleTonHolder {
private static SingleTon instance = new SingleTon();
}
public static SingleTon getInstance() {
return SingleTonHolder.instance;
}
}
2.饿汉模式
public class SingleTon {
private static SingleTon singleTon = new SingleTon();
private SingleTon() {
// TODO Auto-generated constructor stub
}
public static SingleTon getInstance() {
return singleTon;
}
}
饿汉模式是最简单的一种实现方式,饿汉模式在类加载的时候就对实例进行创建,实例在整个程序周期都存在。它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
3.枚举模式
public enum SingletonMenu {
singleTon;
}
上面提到的实现单例的方式(枚举法除外)都有共同的缺点:
1)每次反序列化一个序列化的对象时都会创建一个新的实例。
public class Client {
public static void main(String[] args) {
// TODO Auto-generated method stub
SingleTon singleTon = SingleTon.getInstance();
File file = new File("/Users/qianxin/Desktop/tmp.txt");
try {
file.createNewFile();
// 序列化过程
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleTon);
oos.flush();
oos.close();
fos.close();
// 反序列化过程
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
SingleTon st1 = (SingleTon) ois.readObject();
ois.close();
fis.close();
System.out.println(st1 == singleTon);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
打印出来的是false
public class Client {
public static void main(String[] args) {
// TODO Auto-generated method stub
SingletonMenu singleTon = SingletonMenu.singleTon;
File file = new File("/Users/qianxin/Desktop/tmp.txt");
try {
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleTon);
oos.flush();
oos.close();
fos.close();
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
SingletonMenu st1 = (SingletonMenu) ois.readObject();
ois.close();
fis.close();
System.out.println(st1 == singleTon);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
打印出来的是true
2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
public static void main(String[] args) {
// TODO Auto-generated method stub
Class<?> singleTonClass = SingleTon.class;
for (int i = 0; i < 10; i++) {
SingleTon singleTon;
try {
Constructor cons = singleTonClass.getDeclaredConstructor(null);
cons.setAccessible(true);
singleTon = (SingleTon) cons.newInstance(null);
System.out.println(singleTon);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
打印:
cn.xports.singleton.SingleTon@8391b0c
cn.xports.singleton.SingleTon@5d1eb50b
cn.xports.singleton.SingleTon@b0014f0
cn.xports.singleton.SingleTon@325e9e34
cn.xports.singleton.SingleTon@61e481c1
cn.xports.singleton.SingleTon@6102d81c
cn.xports.singleton.SingleTon@1ba4806
cn.xports.singleton.SingleTon@6cce82cc
cn.xports.singleton.SingleTon@69ed56e2
cn.xports.singleton.SingleTon@5ce345c2
而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。