《HeadFirst设计模式》第五章-单例模式

1.声明

设计模式中的设计思想、图片和部分代码参考自《Head First设计模式》作者Eric Freeman & Elisabeth Freeman & Kathy Siezza & Bert Bates。

在这里我只是对这本书进行学习阅读,并向大家分享一些心得体会。

2.单例模式

2.1单例简介

单例模式就是让一个类实例化的对象只要一份,这样做的好处就是,例如在某些场合下,我们不希望这个类有多个对象,例如工具类、配置表、线程池(防止无限创建线程)。

Java的静态变量也可以做到单例,但是,有个缺陷是,静态变量会随着类加载而加载,那么如果没有用到的话,就会造成资源的浪费。

2.2经典的单利模式-懒汉式

懒汉式代码:

//单例模式-懒汉式
public class Singleton {
	
	//静态成员
	private static Singleton uniqueInstance;
	//私有化构造方法,不让外部创建对象
	private Singleton() {}
	//获得本类实例的方法
	public static Singleton getInstance() {
		if (uniqueInstance == null) {
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}

}

使用方式:

public class SingletonClient {
	public static void main(String[] args) {
		//获取唯一对象
		Singleton singleton = Singleton.getInstance();
		System.out.println(singleton.getConfig());
	}
}

懒汉式的缺陷:

看完代码,看上去确实蛮有道理的,但是设想,如果多个线程调用getInstance(),会不会出现线程不同步的问题呢?

线程不同步的问题猜想:

《HeadFirst设计模式》第五章-单例模式_第1张图片

如果真如上图猜想的那样,那么就无法保证创建的对象是唯一的了。

但那仅仅是个猜想,为了考证,我们用代码验证一下。

懒汉式代码:

//单例模式-懒汉式
public class Singleton {
	
	//静态成员
	private static Singleton uniqueInstance;
	//私有化构造方法,不让外部创建对象
	private Singleton() {}
	
	//获得本类实例的方法
	public static Singleton getInstance() throws Exception {
		if (uniqueInstance == null) {
			//为了放大程序创建Singleton的时间,这里强行让程序睡眠300毫秒
			Thread.sleep(300);
			uniqueInstance = new Singleton();
		}
		return uniqueInstance;
	}
	
	//其他有用的方法,例如获取配置
	public String getConfig() {
		String config = "懒汉式的配置";
		return config;
	}
}

懒汉式的代码,为了放大程序执行的效果,这里所作的唯一改动就是让线程睡眠了300毫秒。

测试代码:

public class SingletonClient {
	public static void main(String[] args) {
		//模拟10个线程同时创建对象
		for (int i=0; i<10; i++) {
			//开启新线程
			new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						//获取唯一对象
						Singleton singleton = Singleton.getInstance();
						//输出对象的身份证(哈希码值)
						System.out.println(singleton.hashCode());
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}).start();
		}
		
	}
}

输出结果:

1534375319
422411000
1832745745
923214033
349353316
78345198
1428845299
616366847
686205869
1370164158

结论:

从结果可以看出,创建的对象就没有一个是相同的,当然我们这里对new Singleton()的执行时间做出了方法处理,如果没有进行线程睡眠,那么有可能创建的10个对象是完全相同的。但是这也足以说明,懒汉式并不是线程安全的。

那么如何才能解决这个线程安全问题呢?

问题的关键在于,if语句块中,线程1已经通过了创建对象的权限拦截(if拦截),但是还没有来得及创建对象,线程2也通过了权限拦截。那么我们将getInstance()设计为同步方法不就可以解决这个问题了?

设计为同步方法确实能解决问题,但是效率很低,下面介绍的两种方法,都可以解决单例模式的线程安全问题。

2.3经典的单例模式-饿汉式

饿汉式从根本上解决问题,在懒汉式中,各个线程都有创建Singleton的可能,但是在饿汉式中,Singleton直接被设计成为静态变量,全局唯一,getInstance()方法,只负责返还创建好的实例,不再负责对象的创建。

饿汉式代码:

//单例模式-饿汉式
public class Singleton {
	
	//全局唯一
	private static Singleton uniqueInstance = new Singleton();
 
	private Singleton() {}
 
	public static Singleton getInstance() throws Exception {
		//为了放大程序创建Singleton的时间,这里强行让程序睡眠300毫秒
		Thread.sleep(300);
		return uniqueInstance;
	}
	
	//其他有用的方法,例如获取配置
	public String getConfig() {
		String config = "饿汉式的配置";
		return config;
	}
}

代码测试(注意要重新导Singleton的包):

//注:测试代码和懒汉式完全相同,但是注意Singleton要重新导包:hungry.Singleton
public class SingletonClient {
	public static void main(String[] args) {
		//模拟10个线程同时创建对象
		for (int i=0; i<10; i++) {
			//开启新线程
			new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						//获取唯一对象
						Singleton singleton = Singleton.getInstance();
						//输出对象的身份证(哈希码值)
						System.out.println(singleton.hashCode());
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}).start();
		}
		
	}
}

输出结果:

1832745745
1832745745
1832745745
1832745745
1832745745
1832745745
1832745745
1832745745
1832745745
1832745745

可见,懒汉式确实解决了线程安全的问题,当然还有另外一种解决方案,如下。

2.4"双重检查加锁"式同步

此方法和同步方法的目的一致,都是保证对new Singleton的同步,但不同的是,这种方法尽可能的少在getInstance()中使用同步,效率会比同步方法高很多。

"双重检查加锁"代码:

//使用双重锁的懒汉式
public class Singleton {
	
	private volatile static Singleton uniqueInstance;
 
	private Singleton() {}
 
	public static Singleton getInstance() throws Exception {
		if (uniqueInstance == null) {
			//此同步块保证了进入只有一个线程可以进入第二个if语句块
			synchronized (Singleton.class) {
				/*
				 * 此if语句块保证了:
				 * 线程1刚刚创建完Singleton后,
				 * 已经进入第一个if语句块的线程2,不会再执行第二个if语句块
				 */
				if (uniqueInstance == null) {
					Thread.sleep(300); //方便测试的睡眠代码
					uniqueInstance = new Singleton();
				}
			}
		}
		return uniqueInstance;
	}
}

代码测试(注意要重新导double_lock的包):

//注:测试代码和懒汉式完全相同,但是注意Singleton要重新导包:double_lock.Singleton
public class SingletonClient {
	public static void main(String[] args) {
		//模拟10个线程同时创建对象
		for (int i=0; i<10; i++) {
			//开启新线程
			new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						//获取唯一对象
						Singleton singleton = Singleton.getInstance();
						//输出对象的身份证(哈希码值)
						System.out.println(singleton.hashCode());
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}).start();
		}
		
	}
}

输出结果:

422411000
422411000
422411000
422411000
422411000
422411000
422411000
422411000
422411000
422411000

可见双重加锁确实也可以解决线程安全的问题。

volatile关键字简述:

单词直译:表示某人或某物是不稳定的、易变的。

volatile作为java中的关键词之一,用以声明变量的值可能随时会别的线程修改,使用volatile修饰的变量会强制将修改的值立即写入主存,主存中值的更新会使缓存中的值失效(非volatile变量不具备这样的特性,非volatile变量的值会被缓存,线程A更新了这个值,线程B读取这个变量的值时可能读到的并不是是线程A更新后的值)。

3.一些疑问

  • 对于一个好的OO设计来说,一个类就应该负责一方面的事情,单例模式在一定程度上违反了这个原则,因为单例模式不仅负责自己的生成,而且还负责自己的管理,但是由类来管理自己的对象这种情况并不少见,而且还可以使设计更简单,所以无伤大雅。
  • 单例类并不适合被继承,原因是:1.如果某个类想要被继承,那么构造方法肯定不能是private的,而是protect或public,那么这样的话,就不能保证创建对象的唯一性;2.单例的实现依靠类中的静态变量,这样的话,此类的子类都必须共享这个静态变量,这可能不是我们想要的。
  • 如果有多个类加载器,可能会出现多个单件并存的情况,所以我们需要指定同一个类加载器。

4.其他的单例模式 

本文提到的两中单例设计模式分别是懒汉式和饿汉式。

4.1懒汉式

优点:线程安全

缺点:无法延迟初始化,变量是否用到都会占用内存空间

4.2饿汉式

优点:延迟初始化

缺点:线程不安全

4.3静态内部类模式(最推荐使用)

静态内部类模式代码:

//内部类单例模式
public class Singleton {

	//内部类持有Singleton对象
	private static class Holder {
		//INSTANCE为final的,此引用不可以再指向其他对象
		private static final Singleton INSTANCE = new Singleton();
	}
	
	//私有化构造方法
	private Singleton() {
	}
	
	public static Singleton getInstance() {
		return Holder.INSTANCE;
	}
	
	//其他有用的方法,例如获取配置
	public String getConfig() {
		String config = "内部类单例模式的配置";
		return config;
	}
}

优点:线程安全,可延迟初始化

与饿汉式的对比:

这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

5.总结

单例模式

确保一个类只有一个实例,并提供一个全局访问点。

类图:

《HeadFirst设计模式》第五章-单例模式_第2张图片

参考链接

volatile关键字:https://baijiahao.baidu.com/s?id=1595669808533077617&wfr=spider&for=pc

静态内部类设计模式与饿汉式的区别:https://www.cnblogs.com/zhaoyan001/p/6365064.html

你可能感兴趣的:(设计模式)