单例模式的应用场景
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并 提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。 例如,国家主席、公司 CEO、部门经理等。在 J2EE 标准中,ServletContext、 ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接 池也都是单例形式。
饿汉式
/**单例模式之 饿汉式
* @author gege
* @Description
* @date 2019/3/15 14:19
*/
public class EagerSingleton {
//私有化构造方法
private EagerSingleton(){}
//全局提供一个单例实例
private static final EagerSingleton EAGER_SINGLETON_INSTACE = new EagerSingleton();
//对方提供一个访问接口
public static EagerSingleton getInstance(){
return EAGER_SINGLETON_INSTACE;
}
}
我们先来回顾一下,类加载的顺序
1、先静态 后动态
2、先属性、后方法
3、先上后下
分析:
饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题。
优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存,有可能占着茅 坑不拉屎。
Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例。
下面我们就来做测试,这种模式正常情况是单例且并发时也是安全的。我这里想通过反射来访问构造方法实例化对象
@Test
public void getInstance() {
Class<EagerSingleton> clazz = EagerSingleton.class;
try {
Constructor<EagerSingleton> constructor= clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);//强制访问 强吻
//实例化对象
EagerSingleton eagerSingleton= constructor.newInstance();
EagerSingleton eagerSingleton2= constructor.newInstance();
EagerSingleton eagerSingleton3 = EagerSingleton.getInstance();
//输出
System.out.println(eagerSingleton);
System.out.println(eagerSingleton2);
System.out.println(eagerSingleton3);
} catch (Exception e) {
e.printStackTrace();
}
}
控制台打印:
eager.EagerSingleton@61e4705b
eager.EagerSingleton@50134894
eager.EagerSingleton@2957fcb0
结论出乎意料,通过反射强制访问私有构造 可以创建多个实例。此处改如何改进呢?敬请期待
其实我们只需在构造内添加一个判断
然后我们在执行刚刚的代码
我们用静态代码块来实现一下单例
/**
* @author gege
* @Description 静态代码块的机制 实现单例模式
* @date 2019/3/15 14:54
*/
public class StaticEagerSingleton {
//私有化构造方法
private StaticEagerSingleton(){}
//全局提供一个单例实例
private static final StaticEagerSingleton EAGER_SINGLETON_INSTACE ;
static {
EAGER_SINGLETON_INSTACE = new StaticEagerSingleton();
}
//对方提供一个访问接口
public static StaticEagerSingleton getInstance(){
return EAGER_SINGLETON_INSTACE;
}
以上两种饿汉式单例都很简单,也没什么区别。下面我们来看看懒汉式
懒汉式
简单的懒汉式代码实现
/**
* @author gege 懒加载模式来实现基本的单例
* @Description
* @date 2019/3/15 15:04
*/
public class SimpleLazySingleton {
private SimpleLazySingleton(){}
private static SimpleLazySingleton simpleLazySingleton;
public static SimpleLazySingleton getInstance(){
if(simpleLazySingleton==null)
simpleLazySingleton= new SimpleLazySingleton();
return simpleLazySingleton;
}
}
代码中把new 的动作推迟了,下面我们来测试
@Test
public void getInstance() {
SimpleLazySingleton simpleLazySingleton = SimpleLazySingleton.getInstance();
SimpleLazySingleton simpleLazySingleton1 = SimpleLazySingleton.getInstance();
SimpleLazySingleton simpleLazySingleton2 = SimpleLazySingleton.getInstance();
System.out.println(simpleLazySingleton);
System.out.println(simpleLazySingleton1);
System.out.println(simpleLazySingleton2);
}
控制台
lazy.SimpleLazySingleton@61e4705b
lazy.SimpleLazySingleton@61e4705b
lazy.SimpleLazySingleton@61e4705b
看起来没问题
下面我们来尝试多线程并发来访问
首先我们写一个并发工具类(不是重点,不做详解)
/**
* @author gege
* @Description
* @date 2019/3/15 15:08
*/
public class SimpleLazySingletonTest {
@Test
public void getInstance() {
SimpleLazySingleton simpleLazySingleton = SimpleLazySingleton.getInstance();
SimpleLazySingleton simpleLazySingleton1 = SimpleLazySingleton.getInstance();
SimpleLazySingleton simpleLazySingleton2 = SimpleLazySingleton.getInstance();
System.out.println(simpleLazySingleton);
System.out.println(simpleLazySingleton1);
System.out.println(simpleLazySingleton2);
}
//通过反射创建
@Test
public void getInstanceProxy() {
Class<SimpleLazySingleton> clazz = SimpleLazySingleton.class;
try {
Constructor<SimpleLazySingleton> constructor= clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);//强制访问 强吻
//实例化对象
SimpleLazySingleton simpleLazySingleton= constructor.newInstance();
SimpleLazySingleton simpleLazySingleton2= constructor.newInstance();
SimpleLazySingleton simpleLazySingleton3 = SimpleLazySingleton.getInstance();
//输出
System.out.println(simpleLazySingleton);
System.out.println(simpleLazySingleton2);
System.out.println(simpleLazySingleton3);
} catch (Exception e) {
e.printStackTrace();
}
}
@Test//并发情况下
public void concurrentInstance(){
}
}
@Test//并发情况下
public void concurrentInstance(){
try {
ConcurrentExecutor.execute(new ConcurrentExecutor.RunHandler() {
public void handler() {
System.out.println(System.currentTimeMillis() + ": " + SimpleLazySingleton.getInstance());
}
},10,6);
}catch (Exception e){
e.printStackTrace();
}
}
控制台
1552635063133: lazy.SimpleLazySingleton@7e541f7f
1552635063133: lazy.SimpleLazySingleton@36bb9ec9
1552635063133: lazy.SimpleLazySingleton@36bb9ec9
1552635063135: lazy.SimpleLazySingleton@36bb9ec9
1552635063135: lazy.SimpleLazySingleton@36bb9ec9
1552635063136: lazy.SimpleLazySingleton@36bb9ec9
1552635063136: lazy.SimpleLazySingleton@36bb9ec9
1552635063136: lazy.SimpleLazySingleton@36bb9ec9
1552635063136: lazy.SimpleLazySingleton@36bb9ec9
1552635063136: lazy.SimpleLazySingleton@36bb9ec9
虽然我是执行好几次才出现不同的实例,但出现一次就足以说明此单例存在线程安全问题
如果你会用idea多线程debug,你可以这么写,
/**
* @author gege
* @Description
* @date 2019/3/15 15:41
*/
public class ExectorThread implements Runnable {
public void run() {
SimpleLazySingleton simpleLazySingleton = SimpleLazySingleton.getInstance();
System.out.println(System.currentTimeMillis() + ": " +simpleLazySingleton);
}
}
@Test//并发情况下
public void concurrentInstance1(){
Thread t1 = new Thread(new ExectorThread());
Thread t2 = new Thread(new ExectorThread());
t1.start();
t2.start();
System.out.println("end");
}
然后用debug,手动控制3个线程的执行顺序也会出现多个实例的情况
Connected to the target VM, address: '127.0.0.1:65296', transport: 'socket'
1552636537928: lazy.SimpleLazySingleton@192fe644
1552636560953: lazy.SimpleLazySingleton@214b7af5
end
Disconnected from the target VM, address: '127.0.0.1:65296', transport: 'socket'
Process finished with exit code 0
经过测试 此单例出现线程安全的问题,在多线程并发情况下会出现多个实例,该如何优化呢?
我们自需在 getInstance 方法上加上 synchronized 关键字,使这个方法变成线程同步方法:
这时候,我们再来调试。当我们将其中一个线程执行并调用 getInstance()方法时,另一 个线程在调用 getInstance()方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻 塞。直到第一个线程执行完,第二个线程才恢复 RUNNING 状态继续调用 getInstance() 方法。
完美的展现了 synchronized 监视锁的运行状态,线程安全的问题便解决了。但是,用 synchronized 加锁,在线程数量比较多情况下,如果 CPU 分配压力上升,会导致大批 量线程出现阻塞,从而导致程序运行性能大幅下降。那么,有没有一种更好的方式,既 兼顾线程安全又提升程序性能呢?答案是肯定的。我们来看双重检查锁的单例模式:
/**
* @author gege 懒加载模式来实现基本的单例
* @Description 该如何优化呢?
* @date 2019/3/15 15:04
*/
public class SimpleLazySingleton2 {
private SimpleLazySingleton2(){}
private static SimpleLazySingleton2 simpleLazySingleton;
//在此处方法上加上同步锁
public static SimpleLazySingleton2 getInstance(){
if(simpleLazySingleton==null){
synchronized (SimpleLazySingleton2.class){
if(simpleLazySingleton==null)simpleLazySingleton= new SimpleLazySingleton2();
}
}
return simpleLazySingleton;
}
}
当第一个线程调用 getInstance()方法时,第二个线程也可以调用 getInstance()。当第一 个线程执行到 synchronized 时会上锁,第二个线程就会变成 MONITOR 状态,出现阻 塞。此时,阻塞并不是基于整个 LazySimpleSingleton 类的阻塞,而是在 getInstance() 方法内部阻塞,只要逻辑不是太复杂,对于调用者而言感知不到。 但是,用到 synchronized 关键字,总归是要上锁,对程序性能还是存在一定影响的。难 道就真的没有更好的方案吗?当然是有的。我们可以从类初始化角度来考虑,看下面的 代码,采用静态内部类的方式:
/**
* @author gege 懒加载模式来实现基本的单例
* @Description 静态内部类实现单例模式巧妙的运用了 静态内部类只有在该类被调用的时候才会加载,这个时候才开始实例化单例对象
* @date 2019/3/15 15:04
*/
public class SimpleLazySingleton3 {
//默认使用 SimpleLazySingleton3 的时候,会先初始化内部类
//如果没使用的话,内部类是不加载的
private SimpleLazySingleton3(){
if(InnerClass.SINGLETONINSTANCE!=null)
throw new RuntimeException("单例已被破坏");
}
public static SimpleLazySingleton3 getInstance(){
在返回结果以前,一定会先加载内部类
return InnerClass.SINGLETONINSTANCE;
}
//默认不加载
private static class InnerClass{
//每一个关键字都不是多余的
//static 是为了使单例的空间共享
//保证这个方法不会被重写,重载
private final static SimpleLazySingleton3 SINGLETONINSTANCE = new SimpleLazySingleton3();
}
}
上面我们已经提到过反射破坏单例及相应的解决办法
序列化破坏单例
下面我们来看一种反序列化破坏单例及解决办法
当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时 再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存, 即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当 于破坏了单例,来看一段代码:
@Test
public void getInstance() {
SeriablesSingleton seriablesSingleton =SeriablesSingleton.getInstance();
System.out.println(seriablesSingleton);
try {
ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream("SeriablesSingleton.obj"));
objectOutput.writeObject(seriablesSingleton);
ObjectInput objectInput = new ObjectInputStream(new FileInputStream("SeriablesSingleton.obj"));
SeriablesSingleton seriablesSingletonF = (SeriablesSingleton)objectInput.readObject();
System.out.println(seriablesSingletonF);
}catch (Exception e){
}
}
运行结果:
serializable.SeriablesSingleton@61e4705b
serializable.SeriablesSingleton@1e81f4dc
运行结果中,可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两 次,违背了单例的设计初衷。那么,我们如何保证序列化的情况下也能够实现单例?其 实很简单,只需要增加 readResolve()方法即可。来看优化代码:
/**
* @author gege
* @Description
* @date 2019/3/18 13:50
*/
public class SeriablesSingleton implements Serializable {
//序列化就是说把内存中的状态通过转换成字节码的形式
//从而转换一个 IO 流,写入到其他地方(可以是磁盘、网络 IO)
//内存中状态给永久保存下来了
//反序列化
//讲已经持久化的字节码内容,转换为 IO 流
//通过 IO 流的读取,进而将读取的内容转换为 Java 对象
//在转换过程中会重新创建对象 new
public final static SeriablesSingleton INSTANCE = new SeriablesSingleton();
private SeriablesSingleton(){}
public static SeriablesSingleton getInstance(){
return INSTANCE;
}
//针对反序列化破话单例的问题
private Object readResolve(){
return INSTANCE;
}
}
看控制台打印:
serializable.SeriablesSingleton@61e4705b
serializable.SeriablesSingleton@61e4705b
虽然在单例类里面添加一个readResolve方法,但查看jdk源码的知,其实还是实例化了的,只是没把实例化的对象返回上来。那如果,创建对象的动作发生频率增大,就 意味着内存分配开销也就随之增大,难道真的就没办法从根本上解决问题吗?下面我们 来注册式单例也许能帮助到你。
注册式单例
注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标 识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。先来看枚举 式单例的写法,来看代码,创建 EnumSingleton 类:
/**
* @author gege
* @Description 注册式单例之枚举单例
* @date 2019/3/18 14:41
*/
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
下面我们就来测试一下吧!
@Test
public void newInstance(){
EnumSingleton enumSingleton =EnumSingleton.INSTANCE;
enumSingleton.setData(new Object());
System.out.println(enumSingleton.getData());
try {
ObjectOutput objectOutput = new ObjectOutputStream(new FileOutputStream("enumSingleton.obj"));
objectOutput.writeObject(enumSingleton);
ObjectInput objectInput = new ObjectInputStream(new FileInputStream("enumSingleton.obj"));
EnumSingleton enumSingletonF = (EnumSingleton)objectInput.readObject();
System.out.println(enumSingletonF.getData());
}catch (Exception e){
}
}
控制台打印
java.lang.Object@61e4705b
java.lang.Object@61e4705b
true
竟然和我们的预想期是一样的,这是极力推荐的一种单例模式
注册式单例另一种写法,容器缓存的写法
/**
* @author gege
* @Description 注册式单例另一种写法,容器缓存的写法
* @date 2019/3/18 15:41
*/
public class ContainerSingleton {
private ContainerSingleton (){}
public Map<Class,Object> map = new HashMap<Class,Object>();
public Object get (Class clazz) {
synchronized (map) {
Object obj = map.get(clazz);
if (obj == null) {
try {
obj = clazz.newInstance();
} catch (Exception e) {
}
}
return obj;
}
}
}
单例模式小结 单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。 单例模式看起来非常简单,实现起来其实也非常简单。但是在面试中却是一个高频面试 题。