设计模式中的设计思想、图片和部分代码参考自《Head First设计模式》,作者Eric Freeman & Elisabeth Freeman & Kathy Siezza & Bert Bates。
在这里我只是对这本书进行学习阅读,并向大家分享一些心得体会。
单例模式就是让一个类实例化的对象只要一份,这样做的好处就是,例如在某些场合下,我们不希望这个类有多个对象,例如工具类、配置表、线程池(防止无限创建线程)。
Java的静态变量也可以做到单例,但是,有个缺陷是,静态变量会随着类加载而加载,那么如果没有用到的话,就会造成资源的浪费。
懒汉式代码:
//单例模式-懒汉式
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(),会不会出现线程不同步的问题呢?
线程不同步的问题猜想:
如果真如上图猜想的那样,那么就无法保证创建的对象是唯一的了。
但那仅仅是个猜想,为了考证,我们用代码验证一下。
懒汉式代码:
//单例模式-懒汉式
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()设计为同步方法不就可以解决这个问题了?
设计为同步方法确实能解决问题,但是效率很低,下面介绍的两种方法,都可以解决单例模式的线程安全问题。
饿汉式从根本上解决问题,在懒汉式中,各个线程都有创建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
可见,懒汉式确实解决了线程安全的问题,当然还有另外一种解决方案,如下。
此方法和同步方法的目的一致,都是保证对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更新后的值)。
本文提到的两中单例设计模式分别是懒汉式和饿汉式。
优点:线程安全
缺点:无法延迟初始化,变量是否用到都会占用内存空间
优点:延迟初始化
缺点:线程不安全
静态内部类模式代码:
//内部类单例模式
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帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
确保一个类只有一个实例,并提供一个全局访问点。
类图:
volatile关键字:https://baijiahao.baidu.com/s?id=1595669808533077617&wfr=spider&for=pc
静态内部类设计模式与饿汉式的区别:https://www.cnblogs.com/zhaoyan001/p/6365064.html