设计模式之——单例模式
一般情况下,Java类的构造方法都是public的,这就意味着别人可以随意的创建该类的对象。但是有一种类型是在整个系统运行过程中只允许出现一个实例: 也就是只能创建一个对象。这类对象称为单例对象: JavaWeb的Servlet对象,MyBatis的SqlSessionFactory对象,Spring的BeanFactory对象都是单例的。单例对象发展成一个单例模式(Singleton Pattern).
单例模式是指确保一个类在任何情况下都绝对是只有一个实例,并提供一个全局访问方法。单例模式是创建型模式。生活中有很多单例对象: 总统, 主席,皇帝,公司的董事长等都是单例对象。“一国不能有二主”。单例模式的实现:
饿汉式单例模式
饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没有出现以前就实例化了,不可能存在访问安全问题。
优点: 没有枷锁,执行效率高,用户体验比懒汉式更好
缺点: 类加载的时候就初始化,不管用与不用都会占用空间,浪费内存。
package com.hanker.oop2;
/**
* 饿汉式单例模式
* @author kongfanyu
* 总统: 一个国家只能有一个总统
*/
public class President {
private static final President president = new President();//创建对象
//构造方法:注意私有化
private President() {
System.out.println("==创建总统对象===");
}
//返回单例对象
public static President getInstance() {
return president;
}
}
静态代码块单例模式
package com.hanker.oop2;
/**
* 饿汉式静态代码块单例模式
* @author kongfanyu
* 总统: 一个国家只能有一个总统
*/
public class President2 {
private static final President2 president ;
//静态代码块,在类加载的时候执行,优先级高于构造方法
static {
System.out.println("===静态代码块创建对象====");
president = new President2();
}
//构造方法:注意私有化
private President2() {
System.out.println("==创建总统对象===");
}
//返回单例对象
public static President2 getInstance() {
return president;
}
}
测试类:
package com.hanker.oop2;
public class PresidentTest {
public static void main(String[] args) {
//构造方法私有的,别的类不能访问
//President p1 = new President();
//不是创建对象,是获取别人创建好的对象
President p1 = President.getInstance();
President p2 = President.getInstance();
President p3 = President.getInstance();
//==比较的是地址说明指向的是同一个地址
System.out.println(p1 == p2);
System.out.println(p2 == p3);
//测试静态代码块单例模式
President2 p11 = President2.getInstance();
President2 p22 = President2.getInstance();
President2 p33 = President2.getInstance();
System.out.println(p11 == p22);
System.out.println(p22 == p33);
}
}
执行结果:
==创建总统对象===
true
true
===静态代码块创建对象====
==创建总统对象===
true
true
这两种写法都很简单,也很好理解,饿汉式单例模式用于单例对象较少的情况。下面看看性能更优的写法。面试的时候主要面试下面的写法。
懒汉式单例模式
特点: 被外部类调用的时候内部类才会加载,下面看看懒汉式单例模式的实现:
package com.hanker.oop2;
/**
* 懒汉式单例模式在外部需要使用的时候才进行实例化
* @author kongfanyu
*
*/
public class PresidentLazy {
private PresidentLazy() {
System.out.println("==懒汉式创建对象===");
}
//注意没有创建对象,静态属性
private static PresidentLazy lazy = null;
//当调用该方法获取实例的时候才创建对象
public static PresidentLazy getInstance() {
if(lazy == null) {
lazy = new PresidentLazy();
}
//返回对象
return lazy;
}
}
测试类:
package com.hanker.oop2;
public class PresidentLazyTest {
public static void main(String[] args) {
PresidentLazy p1 = PresidentLazy.getInstance();
PresidentLazy p2 = PresidentLazy.getInstance();
PresidentLazy p3 = PresidentLazy.getInstance();
System.out.println(p1 == p2);
System.out.println(p2 == p3);
}
}
但是懒汉式存在线程安全问题:
package com.hanker.oop2;
/**
* 懒汉式单例模式在外部需要使用的时候才进行实例化
* @author kongfanyu
*
*/
public class PresidentLazy {
private PresidentLazy() {
System.out.println("==懒汉式创建对象===");
}
//注意没有创建对象,静态属性
private static PresidentLazy lazy = null;
//当调用该方法获取实例的时候才创建对象
public static PresidentLazy getInstance() {
if(lazy == null) {
//模拟业务执行,让线程休眠500毫秒
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
lazy = new PresidentLazy();
}
//返回对象
return lazy;
}
}
测试类:
package com.hanker.oop2;
public class PresidentLazyTest {
static PresidentLazy p11; //声明一个引用
static PresidentLazy p22; //声明一个引用
public static void main(String[] args) {
//多线程环境下测试
new Thread() {
public void run() {
p11 = PresidentLazy.getInstance();
}
}.start();
new Thread() {
public void run() {
p22 = PresidentLazy.getInstance();
}
}.start();
try {
Thread.sleep(1200);//保证两个子线程执行结束
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("比较两个对象:" + (p11 == p22));
}
}
执行结果:
==懒汉式创建对象===
==懒汉式创建对象===
比较两个对象:false
如何解决问题:其实就是解决线程安全问题: 使用锁机制。
package com.hanker.oop2;
/**
* 懒汉式单例模式在外部需要使用的时候才进行实例化
* @author kongfanyu
*
*/
public class PresidentLazy2 {
private PresidentLazy2() {
System.out.println("==懒汉式创建对象===");
}
//注意没有创建对象,静态属性
private static PresidentLazy2 lazy = null;
//这就是双重检查,第一重是if(lazy==null) 如果不为null直接返回
//如果为null就进入到同步代码模块,只能一个线程执行保证安全性和效率。
public static PresidentLazy2 getInstance() {
if(lazy == null) {
synchronized (PresidentLazy2.class) {
if(lazy == null) {
//模拟业务执行,让线程休眠500毫秒
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//在锁的环境之内,只能有一个线程执行
lazy = new PresidentLazy2();
}
}
}
//返回对象
return lazy;
}
}
测试类:
package com.hanker.oop2;
public class PresidentLazy2Test {
static PresidentLazy2 p11; //声明一个引用
static PresidentLazy2 p22; //声明一个引用
public static void main(String[] args) {
//多线程环境下测试
new Thread() {
public void run() {
p11 = PresidentLazy2.getInstance();
}
}.start();
new Thread() {
public void run() {
p22 = PresidentLazy2.getInstance();
}
}.start();
try {
Thread.sleep(1200);//保证两个子线程执行结束
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("比较两个对象:" + (p11 == p22));
}
}
执行效果:
==懒汉式创建对象===
比较两个对象:true
静态内部类单例模式
静态内部类的单例模式的实现代码
package com.hanker.oop2;
//这种形式兼顾饿汉式单例模式的内存浪费问题和synchronized的性能问题
//完美的解决了这两个缺点
public class PresidentInnerClass {
//使用PresidentInnerClass的时候,默认先初始化内部类
//如果不使用,则内部类不加载
private PresidentInnerClass() {}
//static是为了使单例的空间共享,final保证这个方法不会被重写
public static final PresidentInnerClass getInsance() {
//在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}
//创建一个内部类,在内部类里创建外部类的实例
private static class LazyHolder{
private static final PresidentInnerClass LAZY = new PresidentInnerClass();
}
}
测试类:
package com.hanker.oop2;
public class PresidentInnerClassTest {
public static void main(String[] args) {
PresidentInnerClass p1 = PresidentInnerClass.getInsance();
PresidentInnerClass p2 = PresidentInnerClass.getInsance();
PresidentInnerClass p3 = PresidentInnerClass.getInsance();
System.out.println(p1 == p2);
System.out.println(p2 == p3);
}
}
序列化破坏单例
一个单例对象创建后,有时候需要将对象序列化然后写入磁盘,下次使用的时候再从磁盘中读取对象并进行反序列化,将其转化为对象。反序列化后的对象会重新分配内存,即重新创建对象。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,代码实现:
序列化的单例模式
package com.hanker.oop2;
import java.io.Serializable;
//反序列化导致破坏单例模式
public class PresidentSeriable implements Serializable{
//序列化就是把内存中的对象的状态通过转换成字节码的形式存储到磁盘
//从而转换一个I/O流,写入其他地方;内存中的状态数据会永久的保存下来。
//反序列化就是将已经持久化的字节码内容转换成I/O流
//通过I/O流读取,进而将读取的内容转换成java对象
//注意:在转换过程中会重新创建对象new
public static final PresidentSeriable INSTANCE = new PresidentSeriable();
private PresidentSeriable() {}
public static PresidentSeriable getInstance() {
return INSTANCE;
}
}
测试代码
package com.hanker.oop2;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class PresidentSeriableTest {
public static void main(String[] args) {
PresidentSeriable s1 = null; //声明引用
PresidentSeriable s2 = PresidentSeriable.getInstance();//获取单例对象
FileOutputStream fos = null; //输出流--出水管
FileInputStream fis = null; //输入流--进水管
try {
fos = new FileOutputStream("president.obj");
ObjectOutputStream oos =new ObjectOutputStream(fos);
oos.writeObject(s2);//把s2对象写到磁盘
oos.close();
//============从文件中读取数据=======
fis = new FileInputStream("president.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (PresidentSeriable) ois.readObject();//读取到的对象赋值s1
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
fos.close();
fis.close();
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
}
执行效果:
com.hanker.oop2.PresidentSeriable@3d4eac69
com.hanker.oop2.PresidentSeriable@55f96302
false
从运行结果可以看出,反序列化的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的初衷。那么,如何保证在序列化的情况下也能够实现单例模式呢,其实很简单,需要添加一个方法readResolve()方法即可:
package com.hanker.oop2;
import java.io.Serializable;
//反序列化导致破坏单例模式
public class PresidentSeriable implements Serializable{
//序列化就是把内存中的对象的状态通过转换成字节码的形式存储到磁盘
//从而转换一个I/O流,写入其他地方;内存中的状态数据会永久的保存下来。
//反序列化就是将已经持久化的字节码内容转换成I/O流
//通过I/O流读取,进而将读取的内容转换成java对象
//注意:在转换过程中会重新创建对象new
public static final PresidentSeriable INSTANCE = new PresidentSeriable();
private PresidentSeriable() {}
public static PresidentSeriable getInstance() {
return INSTANCE;
}
//添加的方法
private Object readResolve() {
return INSTANCE;
}
}
spring容器式单例
核心思想是: 我们创建一个容器,把所有的对象都放到容器中,在存储对象的时候:如果容器中已经存在了就不放,如果容器中不存在对象则放到容器。
package com.hanker.oop2;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ContainerSingleton {
//构造器私有
private ContainerSingleton() {}
//创建一个线程安全的map对象
private static Map<String,Object> ioc = new ConcurrentHashMap<>();
//存储单例对象
public static Object getBean(String className) {
synchronized (ioc) {
if(!ioc.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();//反射创建对象
ioc.put(className, obj);//放到容器
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}else {
//如果存在这个对象
return ioc.get(className);
}
}
}
}
测试类
package com.hanker.oop2;
public class ContainerSingletonTest {
public static void main(String[] args) {
//注意存放的是公有的构造方法
Object obj1 = ContainerSingleton.getBean("com.hanker.entity.Student");
Object obj2 = ContainerSingleton.getBean("com.hanker.entity.Student");
System.out.println(obj1);
System.out.println(obj2);
System.out.println(obj1 == obj2);
}
}
容器式单例模式适用于实例非常多的情况,便于管理。但是它是非线程安全的。其实Spring的ApplicationContext实现的容器就是单例模式的。