定义:
单例模式指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点,属于创建型设计模式
应用场景:
-
需要频繁的创建一些类,使用单例模式可以降低系统的内存压力,减少GC
-
某些类创建实例时占用资源比较多,或实例化耗时较长,且经常使用。
-
频繁访问数据库或者文件的对象
-
对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套
UML类图:
通用写法:
public class Client{
public static void main(String[] args){
Singleton.getInstance();
}
static class Singleton(){
private static final Singleton instance = new Singleton();
private Singleton(){};
public static Singleton getInstance(){
return instance;
}
}
}
使用单例模式解决实际问题:
饿汉式单例写法的弊端:
上面的通用型写法就是标准的饿汉式写法。饿汉式单例写法在类加载的时候立即初始化,并且创建单例对象。他绝对线程安全,在线程还没有出现之前就实例化了,不可能存在访问安全问题。饿汉式还有一种写法,代码如下:
//饿汉式静态块单例模式
public class Singleton{
private static final Singleton singleton;
static{
singleton = new Singleton();
}
private Singleton(){};
public static Singleton getInstance(){
return singleton;
}
}
这种写法的静态块机制,非常简单也容易理解。饿汉式单例写法适用于单例对象比较少的情况,这样写可以保证绝对的线程安全。执行效率比较高。但是它的缺点明显,就是所有对象都会在类加载的时候创建实例化,这样一来,系统中就会有大量的实例化单例对象存在,而且单例对象的数量也不确定,则系统初始化时会造成大量的内存浪费,从而导致系统内存不可控。也就是说,不管对象用不用,都会占用空间,浪费了内存。
懒汉式写法:
//懒汉式单例在外部需要使用的时候才会进行实例化
public class LazySingleton{
private LazySingleton(){}
//静态块,公共内存区域
private static LazySingleton lazySingleton = null;
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
但这样写又带来一个新的问题,如果在多线程环境下,则会出现线程安全问题。我们来模拟一下,编写线程类ExectorThread
public class ExectorThread implements Runnable{
@Override
public void run(){
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + lazySingleton);
}
}
客户端测试代码:
public class LazySingletonTest{
public static void main(String[] args){
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
System.out.println("End");
}
}
运行多次出现的结果:
End
Thread-0:com.design.pattern.singleton.LazySingleton@92734f7
Thread-1:com.design.pattern.singleton.LazySingleton@6a2ff6fb
这意味着上面的单例模式存在着线程安全的隐患。那么结果是怎么产生的。我们分析一下:
如果两个线程在同一时间进入getInstance()方法,则会同时满足if(lazySingleton == null)的条件,创建两个对象。如果两个线程继续执行往下代码,则又可能后执行线程的结果覆盖先执行的线程的结果,如果打印发生在覆盖之前,则最终的结果就是一致的。如果发生在打印发生在覆盖之后,就会得到两个不同的结果。
当然也有可能没有发生并发,正常运行。
如何优化代码,使得懒汉式单例写法在线程环境下是安全的呢?来看下面代码,给getInstance()方法加上synchronized关键字,使这个方法变成线程同步方法。
public class LazySingleton{
private LazySingleton(){}
//静态块,公共内存区域
private static LazySingleton lazySingleton = null;
public synchronized static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
这样就解决了线程安全的问题。
双重检查锁单例写法:
上面通过给方法加synchronized的方法解决线程问题,但是在线程数据剧增的情况下,用synchronized加锁,则会导致程序性能的大幅下降。所以引入了双重检查的方式
public class Singleton{
private volatile static Singleton instance;
private Singleton(){};
public static Singleton getInstance(){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
return instance;
}
}
这样的写法其实与在方法上加synchronized并无差异,还是会造成大规模的阻塞。我们把判断条件往上提一级
public class LazyDoubleCheckSingleton{
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
if(instance == null){
synchronized(LazyDoubleCheckSingleton.class){
instance = new LazyDoubleCheckSingleton();
}
}
return instance;
}
}
运行代码后,还是会存在线程安全问题。这是为什么呐,其实如果两个线程同一时间都满足if(instance == null)条件,则两个线程都会执行 synchronized块中的代码,因此,还是会创建两次,再优化一下代码
public class LazyDoubleCheckSingleton{
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton(){}
public static LazyDoubleCheckSingleton getInstance(){
//检查是否要阻塞
if(instance == null){
synchronized(LazyDoubleCheckSingleton.class){
//检查是否要创建实例
if(instance == null){
instance = new LazyDoubleCheckSingleton();
//指令重排序的问题
}
}
}
return instance;
}
}
当第一个线程调用getInstance()方法时,第二个线程也可以调用,当第一个线程执行到synchronized时会上锁。第二个线程就会变成MONITOR状态,出现阻塞。此时,阻塞并不是基于整个LazyDoubleCheckSingleton类的阻塞,而是在getInstance()方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感觉不到。
双重检查锁单例写法虽然解决了线程安全的问题和性能问题,但是只要用到synchronized关键字就总是要上锁,对程序的性能还是存在一定的影响。我们从类初始化的角度考虑下面代码,采用静态内部类的方式:
//这种形式兼顾了饿汉式单例写法的内存浪给问题和synchronized的性能问题
//完美的屏蔽了这两个缺点
public class LazyStaticInnerClassSingleton{
//使用LazyInnerClassGeneral的时候,默认会初始化内部类
//如果没使用,则内部类是不加载的
private LazyStaticInnerClassSingleton(){};
//每一个关键字都不是多余的,static是为了使单例模式的空间共享,保证这个写法不会被重写,重载
private static LazyStaticInnerClassSingleton getInstance(){
//返回结果前,一定会先加载内部类
return LazyHolder.INSTANCE;
}
//利用了java本身的语法特点,默认不加载内部类
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
这种方式兼顾了饿汉式单例写法的内存浪费问题和synchronized的性能问题,内部类一定要在方法调用之前被初始化,巧妙的避免了线程安全问题。
还原反射破坏单例模式的事故现场:
上面的单例模式的构造方法除了加上private关键字,没有坐任何处理,我们如果用反射调用起构造方法,在调用getInstance方法,应该会有两个不同的实例。客户端代码如下:以LazyStaticInnerClassSingleton为例:
public static void main(String[] args){
try{
Class<?> clazz = LazyStaticInnerClassSingleton.class;
Constructor c = clazz.getDeclaredConstructor(null);
c.setAccessible(true);
Object o1 = c.newInstance();
Object o2 = c.newInstance();
//调用了两次构造方法,相当于new了两次,犯了原则性错误
System.out.println(o1 == o2);
}catch(Exception e){
e.printStackTrace();
}
}
运行结果
false
我们做一次优化,我们在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常,优化后代码如下:
//这种形式兼顾了饿汉式单例写法的内存浪给问题和synchronized的性能问题
//完美的屏蔽了这两个缺点
public class LazyStaticInnerClassSingleton{
//使用LazyInnerClassGeneral的时候,默认会初始化内部类
//如果没使用,则内部类是不加载的
private LazyStaticInnerClassSingleton(){
if(LazyHolder.INSTANCE != null){
throw new RuntimeException("不允许创建多个实例");
}
};
//每一个关键字都不是多余的,static是为了使单例模式的空间共享,保证这个写法不会被重写,重载
private static LazyStaticInnerClassSingleton getInstance(){
//返回结果前,一定会先加载内部类
return LazyHolder.INSTANCE;
}
//利用了java本身的语法特点,默认不加载内部类
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
测试结果如下:
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.design.pattern.singleton.Test.main(Test.java:11)
Caused by: java.lang.RuntimeException: 不允许创建多个实例
at com.design.pattern.singleton.LazyStaticInnerClassSingleton.<init>(LazyStaticInnerClassSingleton.java:10)
... 5 more
但是会抛出异常,不够优雅。
更加优雅的枚举式单例写法:
枚举式单例的标准写法,创建EnumSingleton类:
public enum EnumSingleton{
INSTANCE;
private Object data;
public Object getData(){
return data;
}
public void setData(Object data){
this.data = data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
客户端测试代码:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class TestEnumSingleton{
public static void main(String[] args){
try{
EnumSingleton instance1 = null;
EnumSingleton instance2 = EnumSingleton.getInstance();
instance2.setData(new Object());
FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(instance2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("EnumSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
instance1= (EnumSingleton) ois.readObject();
ois.close();
System.out.println(instance1.getData());
System.out.println(instance2.getData());
System.out.println(instance1.getData() == instance2.getData());
}catch(Exception e){
e.printStackTrace();
}
}
}
测试结果:
java.lang.Object@6acbcfc0
java.lang.Object@6acbcfc0
true
还原反序列化破坏单例模式的事故现场:
一个单例对象创建好后,有时候需要讲对象序列化然后写入磁盘,当下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化的对象会重新分配内存,即重新创建。如果序列化的目标为单例对象,则违背了单例模式的初衷,相当于破坏了单例模式:如下代码“
//反序列化破坏了单例模式
public class SeriableSingleton implements Serializable{
//序列化就是把内存中的状态通过转换成字节码的形式
//从而转换成一个I/O流,写入其他的地方
//内存中的状态会被永久保存下来
//反序列化就是将已经持久化的字节码内容转换为I/O流
//通过I/O流的读取,进而将读取的内容转换成为Java对象
//在转换过程中会重新创建对象
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
}
客户端测试代码:
package com.design.pattern.singleton;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class SeriableSingletonTest {
public static void main(String[] args) {
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton) ois.readObject();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
}catch (Exception e){
e.printStackTrace();
}
}
}
测试结果:
com.design.pattern.singleton.SeriableSingleton@7cd84586
com.design.pattern.singleton.SeriableSingleton@5a07e868
false
从运行结果可以知道,反序列化后的对象和手动创建的对象是不一致的,被实例化了两次,违背了单例模式的设计初衷。要想解决,其实增加一个readResolve()方法即可:代码如下:
//反序列化破坏了单例模式
public class SeriableSingleton implements Serializable{
//序列化就是把内存中的状态通过转换成字节码的形式
//从而转换成一个I/O流,写入其他的地方
//内存中的状态会被永久保存下来
//反序列化就是将已经持久化的字节码内容转换为I/O流
//通过I/O流的读取,进而将读取的内容转换成为Java对象
//在转换过程中会重新创建对象
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton(){}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
private Object readResolve(){
return INSTANCE;
}
}
结果如下:
com.design.pattern.singleton.SeriableSingleton@5a07e868
com.design.pattern.singleton.SeriableSingleton@5a07e868
true
从JDK源码来看,虽然增加readResolve()方法返回实例解决了单例模式被破坏的问题,但实际上单例对象被实例化了两次,只不过新创建的对象没有返回而已。
容器式单例写法:
public class ContainerSingleton{
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
public static Object getBean(String className){
synchronized(ioc){
if(!ioc.containsKey(className)){
Object obj = null;
try{
obj = Class.forName(className).newInstance();
}catch(Exception e){
e.printStackTrace();
}
return obj;
}else{
return ioc.get(className);
}
}
}
}
容器式单例写法适用于需要大量创建单例对象的场景,便于管理,但它是非线程安全的。
ThreadLocal单例详解
线程单例实现ThreadLocal,不能保证其创建的对象是全局唯一的,但能保证在单个线程中是唯一的,是线程安全的。看下面代码:
package com.design.pattern.singleton;
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton(){};
public static ThreadLocalSingleton getInstance(){
return threadLocalInstance.get();
}
}
测试代码:
package com.design.pattern.singleton;
public class ThreadLocalSingletonTest {
public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println("-----------------");
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
}
}
结果如下:
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
com.design.pattern.singleton.ThreadLocalSingleton@135fbaa4
-----------------
Thread-1:com.design.pattern.singleton.LazySingleton@33e329ce
Thread-0:com.design.pattern.singleton.LazySingleton@33e329ce
由测试可以知道,在主线程中无论调用多少次,获取的实例都是同一个,都在两个字线程中分别获取了不同的实例。我们知道单例模式为了达到线程安全的目的,会给方法上锁,以时间换空间。ThreadLocal将多有的对象放在ThreadLocalMap中,为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。
单例的优点:
-
单例模式可以保证内存里只有一个实例,减少了内存的开销
-
可以避免对资源的多重占用
-
单例模式设置全局访问点,可以优化和共享资源
单例模式缺点:
-
单例模式一般没有接口,扩展困难,如果要扩展,则出了修改原来的代码,没有第二种途径,违背开闭原则
-
在并发测试中,单例模式不利于代码的调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
-
单例模式的功能代码通常写在一个类中,如果功能设计的不合理,则很容易违背单一职责原则