Java设计模式之单例模式的七种写法

什么是单例模式?


单例模式是一种常见的设计模式,单例模式的写法有很多种,这里主要介绍三种: 懒汉式单例模式、饿汉式单例模式、登记式单例 。


单例模式有以下特点:


1、单例类只能有一个实例。

2、单例类必须自己创建自己唯一的实例。

3、单例类必须给所有其它对象提供这一实例。


单例模式确保某各类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能,每台计算机可以有若干个打印机,但只能有一个Printer spooler,以避免两个打印作业同时输出到打印机中,每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态.


在将单例之前,要做一次基础知识的科普行动,大家都知道Java类加载器加载内容的顺序:

1、从上往下(Java的变量需要声明才能使用)

2、先静态后动态(对象实例化) (静态块和static关键字修饰在实例化以前分配内存空间)

3、先属性后方法(成员变量不能定义在方法中,只能定义在class下)


懒汉式单例(4种写法)

懒汉式顾名思义:需要用到的时候才会初始化

饿汉式:不管用不用先实例化

注册登记式:相当于有一个容器装载实例,在实例产生之前会先检查一下容器看有没有,如果有就直接取出来使用,如果没有就先new一个放进去,然后在后面的人使用,Spring IOC就是一种典型的注册登记单例


第一种写法:

/**
 * Created by xingyuchao on 2018/1/20.
 * 懒汉式单例类,在第一次使用的时候实例化自己
 */
public class Singleton {

    //1.第一步先将构造方法私有化
    private Singleton(){}

    //2.然后声明一个静态变量保存单例的引用
    private static Singleton single = null;

    //3.通过提供一个静态方法来获得单例的引用
    public static Singleton getInstance(){
        if(single == null){
            single = new Singleton();
        }
        return single;
    }
}
Singleton1通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Signleton1的唯一实例只能通过getInstance()方法访问。

事实上,通过Java反射机制是能否实现实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效,此问题在此处不做讨论

但是以上懒汉式单例的实现没有考虑线程安全问题,它是非线程安全的,并发环境下可能出现多个Singleton1实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法的改造,保证了懒汉式单例的线程安全.

第二种写法:在getInstance()方法上加同步
/**
 * Created by xingyuchao on 2018/1/29.
 * 懒汉式单例类,保证线程安全
 */
public class Singleton2 {

    //1.第一步先将构造方法私有化
    private Singleton2(){}

    //2.然后声明一个静态变量保存单例的引用
    private static Singleton2 single = null;

    //3.通过提供一个静态方法来获得单例的引用
    // 为了保证线程环境下正确访问,给方法上加上同步锁synchronized
    public static synchronized Singleton2 getInstance(){
        if(single == null){
            single = new Singleton2();
        }
        return single;
    }
}
第三种写法:双重检测机制

/**
 * Created by xingyuchao on 2018/1/29.
 * 懒汉式单例类,保证线程安全  双重检测机制
 */
public class Singleton3 {

    //1.第一步先将构造方法私有化
    private Singleton3(){}

    //2.然后声明一个静态变量保存单例的引用
    private static Singleton3 single = null;

    //3.通过提供一个静态方法来获得单例的引用
    // 为了保证线程环境下的另一种实现方式,双重锁检查
    public static synchronized Singleton3 getInstance(){
        if(single == null){
            single = new Singleton3();
        }
        return single;
    }
}
第四种:静态内部类
/**
 * Created by xingyuchao on 2018/1/29.
 * 懒汉式单例类,通过静态内部类实现
 */
public class Singleton4 {

    //1. 先声明一个静态内部类
    //内部类的初始化,需要依赖主类
    //也就是说,当JVM加载Singleton4类的时候LazyHolder类也会被加载
    //只是目前还没有被实例化,需要等主类先实例化后,内部类才开始实例化
    private static class LazyHolder{
        //final是为了防止内部类将这个属性值覆盖掉
        private static final Singleton4 INSTANCE = new Singleton4();
    }

    //2. 将默认构造方法私有化
    private Singleton4(){}

    //3.同样提供静态方法获取实例

    //当getInstance方法第一次被调用的时候,它第一次读取LazyHolder.INSTANCE,内部类LazyHolder类得到初始化;
    // 而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton4的实例,由于是静态的域,因此只会在
    // 虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。这个模式的优势在于,getInstance方法并没有被同步,
    // 并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

    //此处加final是为了防止子类重写父类方法
    public static final Singleton4 getInstance(){
        return LazyHolder.INSTANCE;
    }
}

饿汉式单例(1种写法)

/**
 * Created by xingyuchao on 2018/1/29.
 * 饿汉式单例类,在类初始化时,已经自行初始化,不会产生线程安全问题
 */
public class Singleton5 {

    //1.同样也是将默认构造方法私有化
    private Singleton5(){}

    //2.声明静态变量,在类初始化之前就初始化变量,将对象引用保存
    //相反的如果这个单例对象一直没使用,那么内存空间也就被浪费掉了
    private static final Singleton5 singleton = new Singleton5();

    //3.开放静态方法,获取实例
    public static Singleton5 getSingleton(){
        return singleton;
    }

}

枚举式单例(1种写法)

public class DBConnection {}
/**
 * Created by xingyuchao on 2018/1/29.
 * 枚举式单例
 */
public enum Singleton6 {


    DATASOURCE;

    private DBConnection connection = null;

    private Singleton6() {
        connection = new DBConnection();
    }

    public DBConnection getConnection() {
        return connection;
    }
}

/**
 * Created by xingyuchao on 2018/1/29.
 */
public class Main {

    public static void main(String[] args) {
        DBConnection dbConnection1 = Singleton6.DATASOURCE.getConnection();

        DBConnection dbConnection2 = Singleton6.DATASOURCE.getConnection();

        System.out.println(dbConnection1 == dbConnection2); //true true  结果表明两次获取返回了相同的实例。
    }
}
这种方式不仅能避免多线程同步问题,而且能防止反射创建新的对象,可谓是很坚强的壁垒不过这种方式用的极少

为什么枚举会满足线程安全、序列化等标准。参考: http://blog.csdn.net/gavin_dyson/article/details/70832185


登记注册式单例

/**
 * Created by xingyuchao on 2018/1/29.
 * 登记式单例:类似spring里面的方法,将类名注册,下次直接从里面获取
 *
 * 登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记簿)中,对于已经登记过的实例,则从Map直接获取,对于没有登记的,则先登记,然后返回
 *
 * 内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在被装载的时候就被实例化了
 */
public class Singleton7 {

    private static Map<String,Singleton7> map = new HashMap<>();

    static{
        Singleton7 singleton7 = new Singleton7();
        map.put(singleton7.getClass().getName(),singleton7);
    }

    //保护的默认构造
    protected Singleton7(){}

    //静态工程方法,返回此类的唯一实例
    public static Singleton7 getInstance(String name) throws Exception {
        if(name == null){
            name = Singleton7.class.getName();
        }

        if(map.get(name) == null){
            map.put(name,(Singleton7)Class.forName(name).newInstance());
        }

        return map.get(name);
    }
}


测试:

public class Test {
    
    public static void main(String[] args) throws Exception{
        //启动100个线程同时去抢占cpu ,有可能产生并发,观察并发情况下是否为同一个对象实例
        int count = 100;

        //发令枪
        CountDownLatch latch = new CountDownLatch(count);

        for (int i = 0; i < count; i++){
            //Lambda简化后
            new Thread(()->{
                System.out.println(System.currentTimeMillis() + ":" + Singleton4.getInstance());
            }).start();

            latch.countDown();
        }
        
        latch.await(); //开始法令,抢占cpu
    }


}
结果:


分布式环境下的单例

有两个问题需要注意:

1. 如果单例类由不同的类装载器装载,那边可能存在多个单例类的实例。假定不是远端存取,例如有一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就会有各自的实例

解决:指定classloader
private static Class getClass(String classname) throws ClassNotFoundException{
        ClassLoader classloader = Thread.currentThread().getContextClassLoader();
        
        if(classloader == null){
            classloader = Singleton.class.getClassLoader();
        }
        
        return (classloader.loadClass(classname));
    }

2. 如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎么样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那么就会有多个类的实例

public class Singleton implements Serializable {
    
    public static Singleton singleton = new Singleton();
    
    protected Singleton(){}
    
    private Object readResolve(){
        return singleton;
    }
}


猜你喜欢

转载自blog.csdn.net/yuchao2015/article/details/79113854
今日推荐