设计模式-单例设计模式

概述

我们来看下一个类如何才能只被实例化一次。我们知道一般实例化对象时,都会调用类的构造方法。如果构造方法为public,那么肯定每个人都能实例化这个类,也就无法保证该类对象的唯一性。所以这个类的构造方法必须为private,不能向外界提供。但是这样我们就无法调用它的构造方法,也就无法实例化对象了,那么由谁来实例化呢?想必你也想到了,由类自身调用,因为这时候也只有它自身能调用构造方法了

实现

饿汉式

public class Singleton {
	public static Singleton singleton = new Singleton();
	private Singleton(){
		System.out.println("singleton init");
	}
	public static Singleton getInstance(){
		return singleton;
	}
	//测试
	public static void main(String[] args) {
		Singleton s1 = Singleton.getInstance();
		Singleton s2 = Singleton.getInstance();
		System.out.println(s1 == s2);
	}
}
output:
singleton init
true

缺点:类加载完就会创建实例对象。

懒汉式(懒加载)

public class Singleton {
	public static Singleton singleton = null;
	private Singleton(){
		System.out.println("singleton init");
	}
	public static Singleton getInstance(){
		if(singleton == null){
			singleton = new Singleton();
		}
		return singleton;
	}
	//测试
	public static void main(String[] args) {
		Singleton s1 = Singleton.getInstance();
		Singleton s2 = Singleton.getInstance();
		System.out.println(s1 == s2);
	}
}
output:
singleton init
true

线程安全的懒汉式

//测试
public static void main(String[] args) {
	Thread t1 = new Thread(new Runnable() {
		
		@Override
		public void run() {
			// TODO Auto-generated method stub
			Singleton.getInstance();
		}
	});
	Thread t2 = new Thread(new Runnable() {
		
		@Override
		public void run() {
			// TODO Auto-generated method stub
			Singleton.getInstance();
		}
	});
	t1.start();
	t2.start();
}
output:
singleton init
singleton init
解决方法一(同步方法)
public synchronized static Singleton getInstance(){
		if(singleton == null){
			singleton = new Singleton();
		}
		return singleton;
}

用synchronized修饰这个方法,相当于给这个方法加了把锁,只要有线程进入到这个方法里面,那么这个锁就会被锁上,这时其他的线程想要执行这个方法时,一看,呦,厕所门关着待会再来上。只有当里面的线程执行完这个方法后,这个锁才会打开,其他线程才能进入。这样就很好的避免前面重复创建对象的问题。synchronized虽然解决了这个问题,但是synchronized会降低程序执行效率。

解决方法二(同步代码块)
public static Singleton getInstance(){
		if(singleton == null){
			synchronized (Singleton.class) {
				if(singleton == null){
					singleton = new Singleton();
				}
			}
		}
		return singleton;
}
  • 同步代码块synchronized 后面的括号中需要一个对象,可以任意,这里我们用了Singleton的类对象Singleton.class
  • 方法中进行了两次对象是否为空的判断,一次在同步代码块外面,一次在里面。因此称之为双重检验锁模式(Double Checked Locking Pattern)
  • 第一个if判断,如果没有实例化则加锁

现在我们的双重检验锁模式既解决了在多线程中重复创建对象问题,又提高了代码执行效率,同时还是懒加载模式,是不是已经非常完美了?

问题出现在:

singleton = new Singleton();

在jvm中执行该对象的创建语句需要三步:

  1. 在栈内存中创建singleton 变量,在堆内存中开辟一个空间用来存放Singleton实例对象,该空间会得到一个随机地址,假设为0x0001
  2. 对Singleton实例对象初始化
  3. 将singleton变量指向该对象,也就是将该对象的地址0x0001赋值给singleton变量,此时singleton就不为null了

程序的运行其实就是CPU在一条条执行指令,有的时候CPU为了提高程序运行效率会打乱部分指令的执行顺序,也就是所谓的指令重排序

当CPU执行时,是无法保证一定是按照123的顺序执行,也可能由于指令重排序的优化,会以132的顺序执行

假设现在有两个线程A、B,CPU先切换到线程A,当执行上述创建对象语句时,假设是以132的顺序执行,当线程A执行完3时(执行完第3步后singleton就不为null了),突然停住了,CPU切换到了线程B去调用getInstance方法,由于singleton此时不为null,就直接返回了singleton,但此时步骤2是还没执行的,返回的对象还是未初始化的,这样程序也就出问题了。那有什么方法解决吗?很简单只要用volatile修饰singleton变量就可以了。

public static volatile Singleton singleton = null;
  • volatile可以禁止指令重排
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作

两种比较简洁的单例模式,即用静态内部类和枚举来实现

静态内部类

private Singleton(){
		System.out.println("singleton init");
	}
	private static class SingletonHolder{
		private static final Singleton INSTANCE = new Singleton();
	}
	public static Singleton getInstance(){
		return SingletonHolder.INSTANCE;
}

可以看到,我们在Singleton 类内部定义一个SingletonHolder 静态内部类,里面有一个对象实例,当然由于Java虚拟机自身的特性,只有调用该静态内部类时才会创建该对象,从而实现了单例的延迟加载,同样由于虚拟机的特性,该模式是线程安全的,并且后续读取该单例对象时不会进行同步加锁的操作,提高了性能。

枚举实现

枚举使用
public enum Person {
	person1;
	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}
//测试
Person.person1.setName("jaixinxiao");
System.out.println(Person.person1.getName());

output:
jiaxinxiao

person1就是我们这个枚举类的一个对象实例,也就是说,如果你要获取一个Person对象,不用再像上面那样调用new Person()来创建对象,直接获取这个枚举类中的person1就可以了。

这样你就只能获取到一个Person实例对象了。可能有的人有疑惑了,不对啊,难道我就不能再new一个吗?这个是不能的,因为枚举类的构造方法是私有掉的,你是无法调用到的并且你也无法通过反射来创建该实例,这也是枚举的独特之处。

如果这个Person的name需要在对象创建时就初始化好,那该怎么办呢?很简单,就和普通类一样只要在里面定义一个构造方法,传入name参数就可以了

public enum Person {
	person1("张三");
	Person(String name){
		this.name = name;
	}
	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
}

可以看到就和普通类一样,我们在枚举类中定义了一个入参为name的构造方法,注意这构造方法前面虽然没有加权限修饰的,但并不表示它的权限是默认的,前面提到枚举类中的构造方法是私有的,即使你强行给它加个public,编辑器也会报错。

枚举类实例的创建也是线程安全的,所以使用枚举来实现单例也是一种比较推荐的方法,但是如果你是做Android开发的,你需要注意了,因为使用枚举所使用的内存是静态变量的两倍之上,如果你的应用中使用了大量的枚举,这将会影响到你应用的性能,不过一般用的不多的话还是没什么关系的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值