单例模式
单例模式的应用场景
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。在生活中应用也非常广泛。例如,国家主席、公司CEO等。
实现步骤:
- 私有化构造器
- 创建一个该对象的成员变量
- 创建一个外部可以访问的方法,用来获取该对象
饿汉式
先看代码:
public class HungrySingleton {
private final static HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton() {}
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
它还可以把初始化放在静态代码块中,如下
public class HungrySingleton {
private final static HungrySingleton INSTANCE;
private HungrySingleton() {}
static {
INSTANCE = new HungrySingleton();
}
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
饿汉式单例是在类加载的时候就初始化,并创建单例对象。线程安全,在线程还没出现的时候就已经初始化了,不存在访问安全的问题。
优点:不需要加锁,性能高。
缺点:这种方式在类加载的时候就初始化了,所以,不管你用不用这个类,它都已经被创建了,浪费了空间。
懒汉式
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton(){}
public synchronized LazySingleton getInstance() {
if(instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
懒汉式单例可以实现懒加载,在类第一次被调用的时候才会被初始化,但是存在线程安全问题。
优点:可以实现懒加载。
缺点:存在线程安全问题,需要加锁,性能比饿汉式低。
双重检查
用synchronized加锁,在线程数较多的情况下,性能会大幅下降。我们可以做双重检查锁的单例模式:
public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton instance;
private DoubleCheckSingleton(){
}
public static DoubleCheckSingleton getInstance() {
if(instance == null) {
synchronized (DoubleCheckSingleton.class) {
if(instance == null) {
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
静态内部类
public class StaticInnerSingleton {
private static class holder {
private static final StaticInnerSingleton instance = new StaticInnerSingleton();
}
private StaticInnerSingleton(){
}
public final static StaticInnerSingleton getInstance() {
return holder.instance;
}
}
静态内部类默然不加载,在被调用时会被加载,也就是返回结果以前,一定会先加载内部类。这样既可以保证线程是安全的,又可以实现懒加载。推荐使用。
反射破坏单例
虽然我们把构造方法私有化了,然而反射依然可以破坏这种访问机制,从而创建对象,看代码
public class Test {
public static void main(String[] args) throws Exception {
Class<StaticInnerSingleton> clz = StaticInnerSingleton.class;
Constructor<StaticInnerSingleton> constructor = clz.getDeclaredConstructor(null);
constructor.setAccessible(true);
StaticInnerSingleton staticInnerSingleton = constructor.newInstance(null);
System.out.println(staticInnerSingleton == StaticInnerSingleton.getInstance());
}
}
打印的结果大家应该猜的到了,用反射机制,通过对象的构造器又创建了一个新的对象,所以结果是false,破坏了单例模式。解决方案也比较简单,我们只要在构造器中判断一下,如果对象不为空就抛出一个异常,这样通过反射就不能成功创建对象了。如下:
private StaticInnerSingleton(){
if(holder.instance != null) {
throw new RuntimeException("对象已存在,不允许创建多个实例");
}
}
然后,我们再运行下之前的测试代码,结果如下:
这样就完美解决的反射破坏的问题。不过,我们知道Java创建对象有好几种方式,反射、new、clone、序列化等都可以,反射和new我们都解决了,clone我们没实现,所以也没问题,我在看下序列化行不行?
public class SerializeSingletonTest {
public static void main(String[] args) throws Exception {
StaticInnerSingleton s1 = null;
StaticInnerSingleton s2 = StaticInnerSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("StaticInnerSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("StaticInnerSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (StaticInnerSingleton)ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
当然,这段代码要跑得通,需要StaticInnerSingleton实现Serializable接口。我们看下打印的结果。
结果是false,明显地址不同,有两个对象,所以还是破坏了单例模式。为什么呢?我们来看下源码。
我们先进入readObject()方法
再进入readObject0(false)方法中
对象序列化,进入TC_OBJECT,我们再点开readOrdinaryObject
找到重点了。上图,红色的框中的代码,我们是熟悉的,通过反射创建了一个新的对象。那么怎么解决这个问题呢?我们继续往下面看
我们看到这里有个判断hasReadResolveMethod()方法,我们点进去看看
代码简单,再看下readResolveMethod是什么时候被赋值的?
根据readResolve方法名去寻找类中这个方法,如果有则调用这个方法。看到这里,我就有解决方案了,我们只需要加一个readResolve方法就OK了,把之前的代码修改一下,如下:
public class StaticInnerSingleton implements Serializable {
private static class holder {
private static final StaticInnerSingleton instance = new StaticInnerSingleton();
}
private StaticInnerSingleton(){
if(holder.instance != null) {
throw new RuntimeException("对象已存在,不允许创建多个实例");
}
}
public final static StaticInnerSingleton getInstance() {
return holder.instance;
}
public Object readResolve() {
return holder.instance;
}
}
再次运行下测试类,结果如下:
从结果中可以看出来,反序列化创建的对象还是跟之前的对象一样,但是通过源码,我们发现在调用readResolve方法之前,已经创建了一次对象,只是被readResolve方法覆盖了而已。那如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,有没有更好的解决方案呢?接下来我们看下注册式单例。
注册式单例
注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种方法:一为容器缓存,一为枚举登记。先看下枚举式单例的写法:
枚举单例
public enum EnumSingleton {
INSTANCE;
Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
测试代码:
public class Test {
public static void main(String[] args) {
EnumSingleton instance = EnumSingleton.getInstance();
instance.setData(new Object());
EnumSingleton instance1 = null;
try{
FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance);
oos.flush();
oos.close();
FileInputStream fileInputStream = new FileInputStream("EnumSingleton.obj");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
instance1 = (EnumSingleton)objectInputStream.readObject();
System.out.println(instance.getData());
System.out.println(instance1.getData());
System.out.println(instance.getData() == instance1.getData());
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果:
java.lang.Object@6d6f6e28
java.lang.Object@6d6f6e28
true
是不是很神奇,结果是true,说明它是单例的。我们分析下为什么,还是先看readObject()方法中调用的readObject0的源码
我们进入readEnum()方法看看
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
我们发现,它是根据类名和class对象来找到一个唯一的枚举对象。因此,枚举对象不可能会被加载多次。接下里我们测试下反射
private static void testByReflect() {
try {
Class<EnumSingleton> enumSingletonClass = EnumSingleton.class;
Constructor<EnumSingleton> declaredConstructor = enumSingletonClass.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingleton enumSingleton = declaredConstructor.newInstance(null);
System.out.println(enumSingleton == EnumSingleton.getInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
调用后,输出结果:
抛异常了,没有不带参的构造方法(因为我本地的反编译软件编译出来的东西有问题,就不展示了),所以反射也不能破坏枚举单例。其实即使有无参的构造方法也是没办法创建的,我们可以看下Constructor的newInstance()方法
标红的区域,会判断是否是枚举类型,如果是,则会抛出异常,反射是无法破坏枚举单例的,JDK已经做了处理。
容器缓存单例
public class ContainerSingleton {
private final static Map<String, Object> CONTAINER = new ConcurrentHashMap<>();
private ContainerSingleton() {
}
public synchronized final static Object getInstance(String name) {
if(!CONTAINER.containsKey(name)) {
Object o = null;
try {
Class<?> clazz = Class.forName(name);
o = clazz.newInstance();
CONTAINER.put(name, o);
} catch (Exception e) {
e.printStackTrace();
}
return o;
} else {
return CONTAINER.get(name);
}
}
}
容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的。可参考下Spring的bean管理
ThreadLocal 线程单例
ThreadLocal单例不能保证对象全局唯一,但是能保证对象在单个线程中是唯一的,而且线程安全。
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> SINGLETON = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){
}
public final static ThreadLocalSingleton getInstance() {
return SINGLETON.get();
}
}
测试代码
public class Test {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
new Thread(new ExectorThread()).start();
new Thread(new ExectorThread()).start();
}
private static class ExectorThread implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName() + ":" + ThreadLocalSingleton.getInstance());
}
}
}
打印结果
main:threadlocalsingleton.ThreadLocalSingleton@1b6d3586
main:threadlocalsingleton.ThreadLocalSingleton@1b6d3586
main:threadlocalsingleton.ThreadLocalSingleton@1b6d3586
main:threadlocalsingleton.ThreadLocalSingleton@1b6d3586
main:threadlocalsingleton.ThreadLocalSingleton@1b6d3586
Thread-0:threadlocalsingleton.ThreadLocalSingleton@dbefca
Thread-0:threadlocalsingleton.ThreadLocalSingleton@dbefca
Thread-0:threadlocalsingleton.ThreadLocalSingleton@dbefca
Thread-0:threadlocalsingleton.ThreadLocalSingleton@dbefca
Thread-1:threadlocalsingleton.ThreadLocalSingleton@6464d21
Thread-1:threadlocalsingleton.ThreadLocalSingleton@6464d21
Thread-1:threadlocalsingleton.ThreadLocalSingleton@6464d21
Thread-1:threadlocalsingleton.ThreadLocalSingleton@6464d21
从结果看到,每条线程的对象引用地址都是一样的。为什么会这样呢?这里需要了解ThreadLocal的实现了,ThreadLocal将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程间隔离的。
单例模式小结
单例模式可以保证内存里只有一个实例,减少了内存的开销;避免对资源的多重占用。原理很简单,但是了解全部细节也没那么容易。