设计模式(一): 单例模式

在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:

某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
省去了new操作符,降低了系统内存的使用频率,减轻GC压力。
有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
创建单例很简单,总共三点:

私有构造器
声明一个私有的静态变量
提供一个对外的公共的静态方法访问该变量,如果该变量没有对象,则创建该对象
但是单例有很多种,至于哪一种才是天下无敌的,那要看你系统需要哪种,只有适合才是最好的。好了上代码。

第一种:懒汉式(线程不安全)
/**
 * 懒汉式
 * 线程不安全
 * @author Hacfqx
 *
 */
public class SingletonDemo1{
    //声明一个私有的静态变量
    private static SingletonDemo1 instance = null;
    //私有构造器
    private SingletonDemo1(){}
    //提供一个对外的公共的静态方法访问该变量,如果该变量没有对象,则创建该对象
    public static SingletonDemo1 getInstance(){
        if (null == instance) {
            instance = new SingletonDemo1();
        }
        return instance;
    }
}


在单线程模式下可以使用这个模式创建,但是多线程场景下就万万不可了,该单例模式是线程不安全的,当系统创建的时候回出现多个instance。

第二种:懒汉式(线程安全)
/**
 * 懒汉式
 * 线程安全
 * @author Hacfqx
 *
 */
public class SingletonDemo2{
    
    //声明一个私有的静态变量
    private static SingletonDemo2 instance = null;
    
    //私有构造器
    private SingletonDemo2(){}
    
    //此处增加同步锁synchronized
    public static synchronized SingletonDemo2 getInstance(){
        if (null == instance) {
            instance = new SingletonDemo2();
        }
        return instance;
    }
}
该写法在多线程环境能很好的工作,但是效率很低每次获取单例的时候都要先获取锁,这时候性能极其低下。不建议这样做


第三种:饿汉式  (我好饿,我不管,我就要先拥有这个instance,线程安全但是效率比较低)
public class SingletonDemo3 {   
     private static SingletonDemo3 instance = new SingletonDemo3();
     private SingletonDemo3() {}
     public static SingletonDemo3 getInstance(){
        return instance;
     }
}

这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
第四种:双重校验锁
/**
 * synchronized锁
 * 
 * @author Hacfqx
 *
 */
public class SingletonDemo4 {
    private static SingletonDemo4 instance = null;
 
    private SingletonDemo4() {
    }
 
    /**
     * 双重校验
     * 将synchronized关键字加在了内部,
     * 也就是说当调用的时候是不需要加锁的,
     * 只有在instance为null,并创建对象的时候才需要加锁,
     * 性能有一定的提升。
     * 但是,这样的情况,还是有可能有问题的,
     * 看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,
     * 也就是说instance = new Singleton();语句是分两步执行的。
     * 但是JVM并不保证这两个操作的先后顺序,
     * 也就是说有可能JVM会为新的Singleton实例分配空间,
     * 然后直接赋值给instance成员,然后再去初始化这个Singleton实例。
     * 这样就可能出错了
     * @return
     */
    public static SingletonDemo4 getInstance() {
        if (null == instance) {
            synchronized (instance) {
                if (null == instance) {
                    instance = new SingletonDemo4();
                }
            }
        }
        return instance;
    }
}

第五种:静态内部类
/**
 * 单例模式使用内部类来维护单例的实现,
 * JVM内部的机制能够保证当一个类被加载的时候,
 * 这个类的加载过程是线程互斥的。
 * 这样当我们第一次调用getInstance的时候,
 * JVM能够帮我们保证instance只被创建一次,
 * 并且会保证把赋值给instance的内存初始化完毕,
 * 这样我们就不用担心上面的问题。
 * 同时该方法也只会在第一次调用的时候使用互斥机制,
 * 这样就解决了低性能问题。
 * 
 * @author Hacfqx
 *
 */
 
public class SingletonDemo4 {
 
    private SingletonDemo4() {
    }
 
    /**
     * 静态内部类来维护单例
     * @return
     */
    private static class SingeletonHolder{
        private final static SingletonDemo4 instance = new SingletonDemo4();
    }
    
    /**
     * 获取实例
     * @return
     */
    public static SingletonDemo4 getInstance(){
        return SingletonHolder.instance();
    }
}

第六种:枚举类单例
public enum EnumSingleton {
    INSTANCE;
    
    public void method(){
        //TODO
    }
}
该写法不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象(具体可以查看enum内部实现)。在实际项目中比较少见。

当然还有其他写法,一般单例都是五种写法。懒汉,恶汉,双重校验锁,枚举和静态内部类。
说到单例我会想到两个问题:
单例模式的序列化
单例模式的侵犯
下面是解决代码(我随便拿一个单例举例):

解决办法:

序列化单例,重写readResolve()方法
在私有构造器里判断intance,如存在则抛异常(防止反射侵犯私有构造器)
我把这两个放在同一个类里执行验证,所以下面需要各位亲手运行代码感受了。= =会有收获的。
public class SingletonDemo6 implements Serializable{  
      
    // 类初始化时,不初始化这个对象(延迟加载,真正用的时候再创建)  
    private static SingletonDemo6 instance;  
      
    private SingletonDemo6() {  
        // 防止反射获取多个对象的漏洞  
        if (null != instance) {  
            throw new RuntimeException("单例模式被侵犯!");  
        }  
    }  
      
    public static synchronized SingletonDemo6 getInstance() {  
        if (null == instance)  
            instance = new SingletonDemo6();  
        return instance;  
    }  
  
    // 防止反序列化获取多个对象的漏洞。  
    // 无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。  
    // 实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。  
    private Object readResolve() throws ObjectStreamException {    
        return instance;  
    }  

package com.zz.designpatterns.createpattern.singletonpattern;
 
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
 
public class ReflectAndSerialSingleton {
        public static void main(String[] args) throws Exception {  
            SingletonDemo6 sc1 = SingletonDemo6.getInstance();  
            SingletonDemo6 sc2 = SingletonDemo6.getInstance();    
            System.out.println(sc1);  
            System.out.println(sc2);
            System.out.println(sc1.equals(sc2)); // sc1,sc2是同一个对象  
             
             /**
              * 通过反序列化的方式构造多个对象(类需要实现Serializable接口)  
              */
            // 1. 把对象sc1写入硬盘文件  
            FileOutputStream fos = new FileOutputStream("object.txt");  
            ObjectOutputStream oos = new ObjectOutputStream(fos);  
            oos.writeObject(sc1);  
            oos.close();  
            fos.close();  
              
            // 2. 把硬盘文件上的对象读出来  
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));  
            // 如果对象定义了readResolve()方法,readObject()会调用readResolve()方法。从而解决反序列化的漏洞  
            SingletonDemo6 sc5 = (SingletonDemo6) ois.readObject();  
            // 反序列化出来的对象,和原对象,不是同一个对象。如果对象定义了readResolve()方法,可以解决此问题。  
            System.out.println(sc5);   
            ois.close(); 
            
            /**
             * 通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞)  
             */
            Class<SingletonDemo6> clazz = SingletonDemo6.class; 
            Constructor<SingletonDemo6> c = clazz.getDeclaredConstructor(null); 
            c.setAccessible(true); // 跳过权限检查 
            SingletonDemo6 sc3 = c.newInstance(); 
            SingletonDemo6 sc4 = c.newInstance(); 
            System.out.println(sc3);  // sc3,sc4不是同一个对象 
            System.out.println(sc4);
              
             
    }
}

这次总结让我更加加深了对单例模式,线程安全,反射侵犯以及序列化对单例的影响有了更加深刻的理解。总归一句话,没有强无敌的单例,适合系统才是最好的。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值