单例设计模式与其线程安全

一、单例设计模式的定义

单例设计模式,顾名思义就是用来保证一个类里只有一个实例化对象,且这个对象的实例化是自己进行的,而且容易被外界访问。

二、单例设计模式的使用场景

因为在单例设计模式中,系统只有一个实例化对象,所以其常用于系统中只允许一个实例化对象存在的场合,如窗口管理器、打印缓冲池、文件系统等等。

三、单例设计模式的分类

单例设计模式根据其使用的方法分为懒汉、饿汉、双重校验锁、静态内部类和枚举五种单例设计模式。

四、五种单例的详细介绍

①懒汉式

懒汉式单例,英文lazy loading,我们文化人把它叫做延迟加载,他是在需要时才创建对象,而不是随着系统运行或类加载器一加载的时候就创建对象。如果当使用场景的实例化对象会占很多资源的时候,常常使用这种方法来实现单例,它的好处就是节约内存资源,提高系统效率,下面来看代码:

public class Singleton {
	//定义一个Singleton类型对象,通过返回值提供给外界
	private static Singleton instance = null;
			
	//构造方法私有化
	private Singleton(){
		System.out.println("对象实例化完成!");
	}
	//提供方法,使用外界可以得到该类的对象
	public static Singleton getInstance(){
		if(instance == null){
			instance = new Singleton();
		}
		
		return instance; 
	}

}

 

饿汉单例在单线程环境下运行的时候,完全没有问题,因为多次调用getInstance()方法获得的对象均为Singleton的同一对象,但是在多线程环境下,我们这段代码还能保证线程安全么,下面来看测试代码:

 

/**
 * 此类是为了检测在多个线程下创建的对象是否为同一个对象
 * set的特性是只能存储不同的集合,因此判断是否为线程安全就看st集合里是一个还是多个对象即可
 * @author zhmm
 *
 */
public class TestThreadSingleton implements Runnable{
	public Set<Singleton> st = new HashSet<Singleton>();
	@Override
	public void run() {
		Singleton s = Singleton.getInstance();
		st.add(s);
	}
}
public class Test {
	public static void main(String[] args) {
		
		for(int i = 0;i < 20;i++){
			TestThreadSingleton thread = new TestThreadSingleton();
			new Thread(thread).start();
		}
	}
}

 

 

 

 

显然,这个时候Singleton的构造的方法不只调用了一次,也就是说,他有多个实例化对象,因此,他在多线程下是不安全的,要想它安全我们就要介绍我们新的单例模式的实现方式:双重校验锁。

②双重校验锁

英文名:double checked locking,它其实就是懒汉的升级版,在多线程环境下也是安全的,它其实就是分别在锁前后进行判空校验,避免了多个机会进入临界区的线程都可以创建对象,同时也避免了后来的线程在创建对象后未退出临界区的时候进行等待的情况发生。

我们来看一种解决方案:

public class Singleton {
	//定义一个Singleton类型对象,使用volatile关键字,通过返回值提供给外界
	private volatile static Singleton instance = null;
	
	//构造方法私有化
	private Singleton(){
		System.out.println("对象实例化完成!");
	}
	//提供方法,使用外界可以得到该类的对象
	public static Singleton getInstance(){
		synchronized (Singleton.class) {
			if(instance == null){
				instance = new Singleton();
			}
		}
		return instance; 
	}

}

测试代码和懒汉一样,测试结果:

仅仅只有一个对象被实例化,因此线程安全得到保障,但是,此时锁住了整个方法的代码,因此,会造成线程等待,大大影响了程序的效率,这个并不是双重校验,而是单重校验,所以我们需要在进入锁的时候进行判空操作,下面是合理代码:

public class Singleton {
	//定义一个Singleton类型对象,使用volatile关键字,通过返回值提供给外界
	private volatile static Singleton instance = null;
	
	//构造方法私有化
	private Singleton(){
		System.out.println("对象实例化完成!");
	}
	//提供方法,使用外界可以得到该类的对象
	public static Singleton getInstance(){
		if(instance == null){
			synchronized (Singleton.class) {
				if(instance == null){
					instance = new Singleton();
				}
			}
		}
		return instance; 
	}

}

线程安全,也保证效率。

③饿汉式

饿汉,顾名思义就是饥不择食,就是当系统加载时,这个类的实例就会被创建,一劳永逸,因此不用考虑线程安全问题,其具体实现是靠在定义自身变量的时候就将其实例化。下面直接来看代码:

public class Singleton{
	private static Singleton instance = new Singleton();
	private Singleton(){
		System.out.println("该实例被创建!");
	}
	public static Singleton getInstance(){
		return instance;	
	}
}

线程安全!

但是,虽然饿汉单例是线程安全的,但是因为饿汉单例的实例是系统已加载时就被创建,因此它会长期存在于内存中,不管你需不需要它,它都在那儿不来不去,所以当实例化对象要是占用的内存较多时,就会很难受,降低系统的资源利用率,那么,有没有改进的方法呢?答案是有滴,请看单例的第四种实现方法:静态内部类法。

④静态内部类

静态内部类法其实就是在Singleton类的内部定义了一个静态内部类,在该内部类的里面进行创建单例对象,下面来看代码:

/*
 * 静态内部类
 */
public class Singleton{
	private static class SingletonHolder{
		private static Singleton instance = new Singleton();
	}
	private Singleton(){
		System.out.println("该实例被创建--静态内部类!");
	}
	public static Singleton getInstance(){
		return SingletonHolder.instance;	
	}
}

其实静态内部类和饿汉式一样,都是利用了class loader的机制保证的线程的安全,不同的是,饿汉式在Singleton类被架加载的时候就创建了一个实例对象,而静态内部类被加载的时候不会创建实例化对象,除非调用了getInstance()方法,因为当Singleton被加载的时候SingletonHolder并没有被主动调用,只有当调用getInstance方法的时候该静态内部类才会被装载,从而实例化对象,这样就实现了lazy loader,既保证了系统性能,也能保证线程安全。

要注意的是以上四种单例模式的Singleton要是实现了Serializable序列化接口的时候,如果这样的话那么可能会序列化生成很多个实例,因为readObject()方法一直返回的是一个崭新的对象,请在Singleton类中添加readResolve()方法来解决:

private Object readResolve(){
		System.out.println("readResolve()方法被调用了!");
		
		return getInstance();
	}

虽然这样的解决方案解决了序列化问题,但是无法解决能被反射的问题,name能有哪种方法既能实现序列化,又能避免被反射么,答案就是我们最后一种单例的实现方式--枚举单例

⑤枚举单例

因为反射能够破解单例模式,那么我们想要绝对实现单例模式的话,还有一种方法,就是枚举单例。枚举类型是在jdk1.5的时候被引进的,这种方式技能实现线程安全,也能防止反序列化重新创建对象,还能防止被反射攻击,总之,贼牛,下面,来欣赏这段代码:

public enum EnumSingleton {
	INSTANCE;
	private EnumSingleton(){}
}

 

emmmm,可能你感觉它有点短,现在举个简单的小demo来分析一下:

 

public class Person {
	int id;
	String name;
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	
}

 

public class TestEnumSingleton implements Runnable{

	public static Set<Person> st = new HashSet<Person>();
	@Override
	public void run() {
		Person p = EnumSingleton.INSTANCE.getPerson();
		st.add(p);
	}
}

 

public class TestEnumSingletonTest {
	public static void main(String[] args) {
		for(int i = 0;i < 20;i++){
			TestEnumSingleton thread = new TestEnumSingleton();
			new Thread(thread).start();
		}
		for(Person p:TestEnumSingleton.st){
			System.out.println(p);
		}
	}
}

 

/*
 * 枚举法创建单例,使其:
 * ①能保证线程安全
 * ②在每次反序列化一个序列化的实例的时候能保证只创建一个实例
 * ③防止使用反射机制创建多个实例
 */
// 这个方法是定义单例模式中需要完成的代码逻辑
public interface MySingleton {
    void doSomething();
}

public enum Singleton implements MySingleton {
    INSTANCE {
        @Override
        public void doSomething() {
            System.out.println("complete singleton");
        }
    };

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

运行结果:

所以:线程安全

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值