设计模式 | 飞机票 |
---|---|
三大工厂模式 | 登机入口 |
策略模式 | 登机入口 |
委派模式 | 登机入口 |
模板方法模式 | 登机入口 |
观察者模式 | 登机入口 |
单例模式 | 登机入口 |
原型模式 | 登机入口 |
代理模式 | 登机入口 |
装饰者模式 | 登机入口 |
适配器模式 | 登机入口 |
建造者模式 | 登机入口 |
责任链模式 | 登机入口 |
享元模式 | 登机入口 |
组合模式 | 登机入口 |
门面模式 | 登机入口 |
桥接模式 | 登机入口 |
中介者模式 | 登机入口 |
迭代器模式 | 登机入口 |
状态模式 | 登机入口 |
解释器模式 | 登机入口 |
备忘录模式 | 登机入口 |
命令模式 | 登机入口 |
访问者模式 | 登机入口 |
软件设计7大原则和设计模式总结 | 登机入口 |
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式属于创建型模式
顾名思义饿汉式单例是在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线程还没出现以前就被实例化了,不可能存在访问安全问题
没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好
类加载的时候就初始化,不管用与不用都占着空间,如果项目中有大量单例对象,则可能会浪费大量内存空间
package com.zwx.design.pattern.singleton.hungry;
public class HungrySingleton {
private static final HungrySingleton hungrySigleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance(){
return hungrySigleton;
}
}
或者也可以利用静态代码块的方式实现饿汉式单例
package com.zwx.design.pattern.singleton.hungry;
public class HungryStaticSingleton {
private static final HungryStaticSingleton hungrySigleton;
static {
hungrySigleton = new HungryStaticSingleton();
}
private HungryStaticSingleton() {
}
public static HungryStaticSingleton getInstance(){
return hungrySigleton;
}
}
这两种写法都非常的简单,也非常好理解,饿汉式适用在单例对象较少的情况
懒汉式单例的特点是:被外部类调用的时候内部类才会加载
package com.zwx.design.pattern.singleton.lazy;
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public static LazySingleton getInstance(){
if(null == lazySingleton){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
上面的写法是最简单的一种懒汉式单例写法,但是存在线程安全问题,多线程情况下会有一定几率返回多个单例对象,这明显违背了单例对象原则,那么如何优化上面的代码呢?答案就是加上synchronized关键字
package com.zwx.design.pattern.singleton.lazy;
import com.zwx.design.pattern.singleton.hungry.HungrySingleton;
public class LazySingleton {
private static LazySingleton lazySingleton = null;
private LazySingleton() {
}
public synchronized static LazySingleton getInstance(){
if(null == lazySingleton){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
示例2的写法仅仅是在getInstance()方法上面加了synchronized关键字,其他地方没有任何变化。用 synchronized 加锁,在线程数量比较多情况下,如果CPU分配压力上升,会导致大批量线程出现阻塞,从而导致程序运行性能大幅下降。那么,有没有一种更好的方式,既兼顾线程安全又提升程序性能呢?答案是肯定的。接下来就在介绍一种双重检查锁(double-checked locking)单例写法
package com.zwx.design.pattern.singleton.lazy;
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazySingleton = null;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance(){
if(null == lazySingleton){
//1
synchronized (LazyDoubleCheckSingleton.class){
//2
if(null == lazySingleton){
//3
lazySingleton = new LazyDoubleCheckSingleton();//4
}
}
}
return lazySingleton;//5
}
}
这里的写法将同步放在了方法里面的第一个非空判断之后,这样可以确保对象不为空的时候不会被阻塞,但是第二个非空判断的意义是什么呢?我们假设线程A首先获得锁,进入了第3行,还没有释放锁的时候,线程B又进来了,这时候因为线程还没有执行对象初始化,所以判空成立,会进入第2行等待获得锁,这时候当线程A释放锁之后,线程B会进入到第3行,这时候因为第二个判空判断对象不为空了,所以就会直接返回,如果没有第2个判空,这时候就会产生新的对象了,所以需要两次判空!
大家可能注意到这里的变量定义上加了volatile关键字,为什么呢?这是因为DCL在可能会存在失效的情况:
第4行代码:lazySingleton = new LazyDoubleCheckSingleton();
大致存在以下三步:
(1)、分配内存给对象
(2)、初始化对象
(3)、将初始化好的对象和内存地址建立关联(赋值)
而这3步由于CPU指令重排序,不能保证一定按顺序执行,假如线程A正在执行new的操作,第1步和第3步都执行完了,但是第2步还没执行完,这时候线程B进入到方法中的第1行代码,判空不成立,所以直接返回了对象,而这时候对象并没有初始化完全,所以就会报错了,解决这个问题的办法就是使用volatile关键字,禁止指令重排序(jdk1.5之后),保证按顺序执行上面的三个步骤。想要详细了解volatile关键字是如何解决重排序问题的,可以点击这里。
package com.zwx.design.pattern.singleton.lazy;
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton(){
}
public static final LazyInnerClassSingleton getInstance(){
return LazyHolder.LAZY;
}
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
上面的写法巧妙的利用了内部类的特性,LazyHolder里面的逻辑需要等到外面方法调用时才执行。
这种写法看起来很完美,没有加锁,也保证了懒加载,但是这种单例模式也有问题,那就是可以被反射或者序列化破坏单例,下面我们写一个反射破坏单例的例子
package com.zwx.design.pattern.singleton.lazy;
import java.lang.reflect.Constructor;
public class LazyInnerClassSingletonTest {
public static void main(String[] args) throws Exception {
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor constructor = clazz.getDeclaredConstructor(null);
constructor.setAccessible(true);
Object o1 = constructor.newInstance();
Object o2 = LazyInnerClassSingleton.getInstance();
System.out.println(o1 == o2);//false
}
}
上面这个结果输出的结果为false,说明产生了2个对象,当然,要防止反射破坏单例很简单,我们可以把上面例子中的构造方法加一个判断就可以了:
private LazyInnerClassSingleton(){
//防止反射攻击
if(null != LazyHolder.LAZY){
throw new RuntimeException("不允许构造多个实例");
}
}
这样虽然防止了反射破坏单例,但是依然可以被序列化破坏单例,下面就让我们验证一下序列化是如何破坏单例的!
首先对上面的类实现序列化接口
public class LazyInnerClassSingleton implements Serializable
接下来开始对单例对象类进行序列化和反序列化测试:
package com.zwx.design.pattern.singleton.lazy;
import com.zwx.design.pattern.singleton.seriable.SeriableSingleton;
import java.io.*;
import java.lang.reflect.Constructor;
public class LazyInnerClassSingletonTest {
public static void main(String[] args) throws Exception {
LazyInnerClassSingleton s1 = null;
LazyInnerClassSingleton s2 = LazyInnerClassSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("LazyInnerClassSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("LazyInnerClassSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (LazyInnerClassSingleton)ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);//false
}catch (Exception e){
e.printStackTrace();
}
}
}
这时候输出结果为false,说明产生了2个对象,那么我们应该如何防止序列化破坏单例呢?我们可以对LazyInnerClassSingleton类加上readResolve方法就可以防止序列化破坏单例
package com.zwx.design.pattern.singleton.lazy;
import java.io.Serializable;
public class LazyInnerClassSingleton implements Serializable {
private LazyInnerClassSingleton(){
//防止反射攻击
if(null != LazyHolder.LAZY){
throw new RuntimeException("不允许构造多个实例");
}
}
//防止序列化破坏单例
private Object readResolve(){
return LazyHolder.LAZY;
}
public static final LazyInnerClassSingleton getInstance(){
return LazyHolder.LAZY;
}
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
这是因为JDK源码中会检验一个类中是否存在一个readResolve()方法,如果存在,则会放弃通过序列化产生的对象,而返回原本的对象,也就是说,在校验是否存在readResolve()方法前产生了一个对象,只不过这个对象会在发现类中存在readResolve()方法后丢掉,然后返回原本的单例对象,保证了单例的唯一性,这种写法虽然保证了单例唯一,但是过程中类也是会被实例化两次,假如创建对象的频率增大,就意味着内存分配的开销也随之增大,那么有没有办法从根本上解决问题呢?那么下面就让继续介绍一下注册式单例
注册式单例就是将每一个实例都保存到某一个地方,然后使用唯一的标识获取实例
package com.zwx.design.pattern.singleton.register;
public class ContainerSingleton {
private ContainerSingleton(){
}
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;
}
return ioc.get(className);
}
}
}
容器式写法适用于创建实例非常多的情况,便于管理。但是,是非线程安全的,spring中的单例就是属于此种写法
package com.zwx.design.pattern.singleton.register;
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;
}
}
枚举式单例是《Effective java》一书中推荐的写法,这种写法避免了上面的内部类写法中存在的问题(虽然结果唯一,但是过程产生了多个实例对象),是一种效率较高的写法
ThreadLocal不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全
package com.zwx.design.pattern.singleton.threadlocal;
public class ThreadLocalSingleton {
private ThreadLocalSingleton() {
}
private static final ThreadLocal<ThreadLocalSingleton> singleton =
new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance(){
return singleton.get();
}
}
测试
package com.zwx.design.pattern.singleton.threadlocal;
import com.zwx.design.pattern.singleton.ExectorThread;
import com.zwx.design.pattern.singleton.ExectorThread3;
public class ThreadLocalSingletonTest {
public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
ThreadLocalSingleton singleton = ThreadLocalSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + singleton);
}
});
t1.start();
}
}
反复测试可以发现同一个线程获得的对象是唯一的,不同对象则不唯一
单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用,单例模式的写法很多,大家可以根据自己的业务需求选择合适自己的单例方式