假设有一个User类,User中的成员变量和成员方法有静态的和非静态的,创建对象user的过程如下:
public class User {
private String name = "Java";
public static int age = 10;
public String getName() {
return name;
}
}
- new User()创建对象,此时堆区域会为其分配内存空间并执行成员变量的初始化
- 调用构造方法
- 对象引用赋值
也可以通过反射、克隆、反序列化的手段创建对象,其中克隆和反序列化的手段不会执行第二步。
通过上面的介绍,我们知道创建对象会调用构造方法,而构造方法模式是public的,如果我们修改成private的就可以禁止外部的访问,new就失效了,然后我们在通过一个静态方法提供一个类内部创建好的实例给外界使用,每次使用都是这同一个实例,这就是单例模式。所以,单例模式具备以下三个特征:
- 私有化的构造函数
- 私有化的成员变量
- 公有化的静态方法
单例模式有很多种,接下来我们开始循序渐进的认识单例模式:
- 饿汉式,顾名思义,它很饿,我们用不用这个实例,它都存在着。正是因为它在类加载的时候就已经实例化了,所以它是线程安全的,但是浪费内存。Spring中IOC容器中的ApplicationContext本身就是典型的饿汉式单例模式。
/**
1. 典型饿汉式,线程安全,浪费内存
*/
public class Singleton1 {
private static final Singleton1 instance = new Singleton1();
private Singleton1() {}
public static Singleton1 getInstance() {
return instance;
}
}
饿汉式单例模式还有静态代码块的写法,如下:
/**
* 饿汉式,静态代码块方式
*/
public class Singleton2 {
private static final Singleton2 instance;
static {
instance = new Singleton2();
}
private Singleton2() {}
public static Singleton2 getInstance() {
return instance;
}
}
- 懒汉式单例模式,相较于饿汉式单例模式,它具备延迟加载的特性,但是不是线程安全的。
/**
* 典型懒汉式,延迟加载,线程不安全
*/
public class Singleton3 {
private static Singleton3 instance = null;
private Singleton3() {}
public static Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
我们可以使用synchronized关键字解决线程安全问题:
/**
* 典型懒汉式,延迟加载,线程安全但是由于synchronized关键字修饰了方法性能较低
*/
public class Singleton4 {
private static Singleton4 instance = null;
private Singleton4() {}
public synchronized static Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
synchronized关键字会导致程序性能下降,我们继续优化:
/**
* 典型懒汉式,延迟加载,线程安全,synchronized关键字修饰代码块,减少了同步范围,稍微提升了性能
* volatile的作用是禁止指令重排序以及保证内存可见性
*/
public class Singleton5 {
private static volatile Singleton5 instance = null;
private Singleton5() {}
public static Singleton5 getInstance() {
if (instance == null) {
synchronized (Singleton5.class) {
if (instance == null) {
instance = new Singleton5();
}
}
}
return instance;
}
}
双重检查锁单例模式中,synchronized关键字修饰代码块,减少了同步范围,稍微提升了性能,那么有没有更优秀的办法吗?我们可以使用静态内部类单例模式,由于静态内部类只有在外部类调用的时候才会加载,所以这个方法解决了浪费内存问题和synchronized性能降低问题
/**
* 静态内部类方式,静态内部类默认不加载,线程安全,延迟加载
*/
public class Singleton6 {
private Singleton6() {}
public static final Singleton6 getInstance() {
return InnerSingleton.instance;
}
//默认不加载,静态内部类和非静态内部类一样,只有在被外部类调用的时候才加载
private static class InnerSingleton {
private static final Singleton6 instance = new Singleton6();
}
}
上文说到,我们也可以通过反射的方式创建对象,通过反射可以获取到构造方法,进而可以通过newInstance方法实例化对象。
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<?> clazz = Singleton6.class;
//通过反射获取私有的构造方法
Constructor c = clazz.getDeclaredConstructor(null);
//强制访问
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
System.out.println(o1 == o2);
}
打印出结果是false,说明通过反射的方式可以破坏上面写的单例模式,那是因为可以通过反射的方式获取到构造方法,那么我们可以在构造方法里面做判断:
/**
* 静态内部类方式,静态内部类默认不加载,线程安全,延迟加载
*/
public class Singleton6 {
private Singleton6() {
if (InnerSingleton.instance != null) {
throw new RuntimeException("不允许重复创建实例");
}
}
public static final Singleton6 getInstance() {
return InnerSingleton.instance;
}
//默认不加载,静态内部类和非静态内部类一样,只有在被外部类调用的时候才加载
private static class InnerSingleton {
private static final Singleton6 instance = new Singleton6();
}
}
这样就完美规避了反射破坏单例模式的问题,但是,上文提到我们也可以通过序列化反序列化的方式创建对象:
public class SeriableSingleton implements Serializable {
private static final SeriableSingleton instance = new SeriableSingleton();
private SeriableSingleton() {}
public static SeriableSingleton getInstance() {
return instance;
}
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null;
fos = new FileOutputStream("SeriableSingletonobj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingletonobj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton) ois.readObject();
ois.close();
System.out.println(s1 == s2);
}
打印出结果是false,怎么办呢?看如下代码:
public class SeriableSingleton implements Serializable {
private static final SeriableSingleton instance = new SeriableSingleton();
private SeriableSingleton() {}
public static SeriableSingleton getInstance() {
return instance;
}
private Object readResolve() {
return instance;
}
}
添加了readResolve方法后,就打印为true了。因为readResolve方法时反序列化后用于返回新对象的方法,我们重写了这个方法,返回原来的实例。所以即使创建了新的对象也不返回。那还有更好的办法吗?枚举单例模式!
public enum EnumSingleton {
INSTANCE;
}
枚举单例模式在静态代码块中就给instance进行了赋值,是饿汉式单例模式的实现,并且在内部的构造方法中做了判断,防止反射破坏单例。枚举类型是通过类名和类对象找到一个唯一的枚举对象,因此,枚举对象不可能被类加载器加载多次,所以序列化也无法破坏单例模式。
接下来看一下容器式单例模式,这种模式适合实例比较多的情况:
public class ContainerSingleton {
private ContainerSingleton() {}
private static final Map<String, Object> ioc = new ConcurrentHashMap<>();
public static Object getBean(String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object obj = Class.forName(className).newInstance();
ioc.put(className, obj);
return obj;
}
return ioc.get(className);
}
}
}
最后,我们看一下线程单例模式:
/**
* ThreadLocal将所有的对象全部放在ThreadLocalMap中,为每个线程都提供一个对象,以空间换时间进行隔离
*/
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = ThreadLocal.withInitial(() -> new ThreadLocalSingleton());
private ThreadLocalSingleton(){}
public static ThreadLocalSingleton getInstance() {
return threadLocalInstance.get();
}
public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(ThreadLocalSingleton.getInstance());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(ThreadLocalSingleton.getInstance());
}
}).start();
}
}
无论主线程调用多少次,获取到的实例都是同一个,在两个子线程中分别获取到了不同的实例。那么是如何实现的呢?ThreadLocal把所有的对象放到ThreadLocalMap中,为每个线程都提供了一个对象,实际上是以时间换空间的做法。