GOF23种设计模式——单例模式

GOF23种设计模式——单例模式

前言

       单例模式是23中设计模式中最简单的一种,只需要一个类就可以实现,是非常常用的设计模式之一。老司机开车,请扶好坐稳,前方到站-单例模式站。

单例模式的特点

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

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

       3、单例对象必须给其他所有对象提供这一实例;

单例模式的优点

1、只有一个对象,内存开至少、性能好;

2、避免对资源的多重占用;

3、在系统设置全局访问点,优先和共享资源访问。

单例类的实现步骤

       1、私有化构造器(可以防止外部使用创建对象的方式调用该类中的方法);

       2、定义一个类方法用于获得单例对象,返回值是这个类的对象;

       3、在类中定义一个singleton类型的类属性;

       4、定义一个实现类方法(在本类中使用的是getInstance()方法)。

单例类的实现之旅

单例模式的写法有饿汉式、懒汉式、双重校验锁、静态内部类、枚举五种类型,后续我将对这五种写法的优点、缺点以及实现方式一一进行介绍。下面正式发车……

饿汉式单例

饿汉式采用了一种简单粗暴地方式,在类加载的时候就对对象进行了初始化,代码如下:

public class Singleton {

        /**
	 * 饿汉式单例
	 * 
	 * 优点:类加载的时候创建一次实例,避免了多线程同步问题;
	 * 缺点:即使单例没被用到也会创建,浪费内存
	 */

        //3、在类中定义一个Singleton类型的类属性
	private static Singleton singleton = new Singleton();
	
        //1、私有化构造器
	private Singleton() {
		
	}
	
        //2、定义一个类方法用于获得单例对象,返回值是这个类的类型
	public static Singleton getInstance() {
		return singleton;
	}
}

 

由于饿汉式单例有一个很大的缺点,就是即使没有被用到也会创建对象,造成资源浪费,然后就产生了饿汉式单例的变种。代码如下:

public class Singleton{

        /**
	 * 饿汉式单例-变种
         * 
         * 将创建对象放在静态代码块中,只有在使用单例类的时候才会创建对象,同时也保证了线程安全
	 */
	private static Singleton singleton = null;
	
	static {
		singleton = new Singleton();
	}
	
	private Singleton() {
		
	}
	
	public static Singleton getInstance() {
		return singleton;
	}
}

懒汉式单例

懒汉模式是一种偷懒的模式,只有在使用单例类的时候才会去创建对象。解决饿汉式单例资源浪费的问题,但同时也产生了新的问题。代码如下:

public class Singleton{

        /**
	 * 懒汉式单例(非线程安全)
	 * 
	 * 优点:需要时才会去创建实例,节省资源
	 * 缺点:没有考虑线程安全问题,多个线程并发调用getInstance,可能会创建多个实例
	 */
	private static Singleton singleton = null;

    private Singleton(){
    
    }

    public static Singleton getInstance(){
        if(null == singleton){
            singleton = new Singleton();
        }
        return singleton;
    }
}

懒汉式单例可以再需要的时候就去创建对象,节省资源空间。但是如果两个线程同时到达if(null == singleton)处时,两个线程的singleton都是null,所以都会进入到方法体中去创建对象,此时就会出现两个不同的对象,这时这个单例就失去了它的作用。别急,有了问题自然就会有解决办法,这不懒汉式单例的变种就应运而生了。

public class Singleton{

        /**
	 * 懒汉式单例-变种(线程安全)
	 * 
	 * 缺点:性能问题,添加了synchronized的函数比一般方法慢得多,若多次调用getInstance,则累积的    
         *      性能损耗特别大
	 */
	private static Singleton singleton;
	
	private Singleton() {
		
	}
	
	public static synchronized Singleton getInstance() {
		if(singleton == null) {
			singleton = new Singleton();
		}
		return singleton;
	}
}

双重校验锁

我们再来讨论一下懒汉式单例加锁的问题,对于getInstance()方法,绝大多数的操作都是读操作,读操作基本上都是线程安全的,我们没必要让每个线程必须持有锁才能调用该方法,所以我们需要调整加锁的问题。由此也就产生了一种新的模式:双重校验锁模式。代码如下:

public class Singleton{

        /**
	 * 双重校验锁
	 * 
	 * 注意:volatile的一个语义是禁止指令重排序优化,也就保证了instance变量被赋值的时候对象已经        
         *      是初始化过的(jdk1.5之后支持)
	 */

    private static volatile Singleton singleton = null;

    private Singleton(){
    
    }

    public static Singleton(){

        if(null == singleton){
            synchronized(Singleton.class){
                if(null == singleton){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

双重校验锁模式是一种非常好的单例模式,解决了单例、性能、线程安全问题。上面的双重校验锁模式如果不加volatile关键字,就还是存在问题的。在多线程的情况下,可能会出现空指针问题。出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。那么什么是指令重排序呢?看下面一个小栗子:

public class Test{

    public void test1(){
    1    int x = 10;
    2    int y = 11;
    3    String singleton = "sing";
    }
}

上面的方法,我们在编写的时候是按1、2、3的顺序进行的,但是JVM会对它进行指令重排序,所以执行顺序可能是3、2、1或1、3、2等,不管是哪种执行顺序,JVM都会保证所有实例进行实例化。如果方法中操作比较多时,为了提高效率,JVM会在方法里边的属性未全部实例化时就返回对象。当某个线程获取锁进行实例化时,其他线程就直接获取实例使用,由于JVM指令重排序的原因,其他线程获取到的也许不是一个完成的对象,所以在使用实例的时候就会出现空指针异常的问题。

要解决双重校验锁模式带来的空指针异常问题,只需要使用volatile关键字,volatile关键字严格遵循happen-before原则,即在读操作前,写操作必须全部完成。

静态内部类

静态内部类单例模式又称单例持有者模式,实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性又static修饰,保证只被实例化一次,并且严格保证实例化顺序。

public class Singleton{

        /**
	 * 静态内部类
	 *
	 * 优点:既实现了线程安全,又省去了null的判断
	 */

    private Singleton(){
    
    }

    private static class SingletonHolder(){
        public static Singleton singleton = new Singleton();
    }

    public static Singleton getSingleton(){
        return SingletonHolder.singleton;
    }
}

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种设计模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

枚举

枚举类单例模式是effective java作者极力推荐的单例设计模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分地利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是是所有单例实现中唯一不会被破坏的单例实现模式。代码如下:

public class Singleton{

    private Singleton(){

    }

        /**
	 * 枚举类型是线程安全的,并且只会装载一次
	 */

    private enum SingletonEnum{
        INSTANCE;
        private final Singleton instance;
        
        SingletonEnum(){
            instance = new Singleton();
        }

        private Singleton getInstance(){
            return instance;
        }
    }

    public static Singleton getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
}

破坏单例模式的方法及解决方法

1、除枚举方式外,其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象,所以如果我们想要组织单例破坏,可以再构造方法中进行判断,若已有实例,则阻止生成新的实例,解决方法如下:

private Singleton(){
    if(null == singleton){
        throw new RuntimeException("实例已经存在,请通过getInstance()方法获取");
    }
}

2、如果单例类实现了序列化接口Serializabale,就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(),反序列化时直接返回先关返利对象。

public Object readResolve() throws ObjectStreamException{
    return singleton;
}

好了,单例模式站已经到达,各位乘客下一站代理模式站见哦~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值