DesignPattern系列__10单例模式
单例模式介绍
单例模式,是为了确保在整个软件体统中,某个类对象只有一个实例,并且该类通常会提供一个对外获取该实例的public方法(静态方法)。
比如日志、数据库连接池等对象,通常需要且只需要一个实例对象,这就会使用单例模式。
单例模式的八种模式
- 饿汉式(静态常量)
- 饿汉式(静态代码块)
- 懒汉式(线程不安全)
- 懒汉式(同步方法)
- 懒汉式(同步代码块)
- 懒汉式(双重检查)
- 静态内部类
- 枚举
下面依次来说明一下:
饿汉式(静态常量)
通常,我们创建一个对象的方式就是new,但是,当我们考虑只创建一个实例的时候,就应该禁止外部来通过new的方式进行创建。同时,由于无法使用new,你应该考虑提供一个获取单例对象的方式给别人。
思路
1.将构造器私有化(防止外部new,但是对反射还是有局限)
2.类的内部创建对象
3.对外提供一个获取实例静态的public方法
代码实现:
public class singleton1 { public static void main(string[] args) { hungrysingleton hungrysingleton = hungrysingleton.getinstance(); hungrysingleton hungrysingleton1 = hungrysingleton.getinstance(); system.out.println(hungrysingleton == hungrysingleton1); } } class hungrysingleton { //1.私有化构造器 private hungrysingleton() { } // 2.类内部创建对象,因为步骤3是static的, // 所以实例对象是static的 private final static hungrysingleton instance = new hungrysingleton(); //3.对外提供一个获取对象的方法, // 因为调用方式的目的就是为了获取对象, // 所以该方法应该是static的。 public static hungrysingleton getinstance() { return instance; } }
运行程序显示,我们的确只创建了一个对象实例。
小结
优点:代码实现比较简单,在类加载的时候就完成了实例化,同时,该方式能够避免线程安全问题。
缺点:在类装载的时候就完成实例化,没有达到lazy loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
这种方式基于classloder机制避免了多线程的同步问题,不过, instance在类装载时就实例化,在单例模式中大多数都是调用getinstance方法, 但是导致类装载的原因有很多种, 因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到lazy loading的效果。
总结:这种单例模式可以使用,但是可能造成内存的浪费。
饿汉式(静态代码块)
该方式和第一种区别不大,只是将创建实例放在了静态代码块中。
由于无法使用new,你应该考虑提供一个获取单例对象的方式给别人。
思路
1.将构造器私有化(防止外部new,但是对反射还是有局限)
2.类的内部创建对象(通过静态代码块)
3.对外提供一个获取实例静态的public方法
代码实现:
public class singleton2 { public static void main(string[] args) { hungrysingleton hungrysingleton = hungrysingleton.getinstance(); hungrysingleton hungrysingleton1 = hungrysingleton.getinstance(); system.out.println(hungrysingleton == hungrysingleton1); } } class hungrysingleton { //1.私有化构造器 private hungrysingleton() { } // 2.类内部创建对象,因为步骤3是static的, // 所以实例对象是static的 private final static hungrysingleton instance; static { instance = new hungrysingleton(); } //3.对外提供一个获取对象的方法, // 因为调用方式的目的就是为了获取对象, // 所以该方法应该是static的。 public static hungrysingleton getinstance() { return instance; } }
小结
该方式只是将对象的创建放在静态代码块中,其优点和缺点与第一种方式完全一样。
总结:这种单例模式可以使用,但是可能造成内存的浪费。(同第一种)
懒汉式(线程不安全)
该方式的主要思想就是为了改善饿汉式的缺点,通过懒加载(在使用的时候再去加载),达到节约内存的目的。
由于无法使用new,你应该考虑提供一个获取单例对象的方式给别人。
思路
1.将构造器私有化(防止外部new,但是对反射还是有局限)
2.类的内部创建对象,懒加载,在使用的时候才去加载
3.对外提供一个获取实例静态的public方法
代码实现:
public class singleton3 { public static void main(string[] args) { testthread testthread = new testthread(); thread thread = new thread(testthread); thread thread1 = new thread(testthread); thread.start(); thread1.start(); } } class lazysingleton { //1.私有化构造器 private lazysingleton() {} //2.类的内部声明对象 private volatile static lazysingleton instance; //3.对外提供获取对象的方法 public static lazysingleton getinstance() { //判断类是否被初始化 if (instance == null) { //第一次使用的时候,创建对象 instance = new lazysingleton(); } return instance; } } class testthread implements runnable { @override public void run() { system.out.println("线程" + thread.currentthread().getname() + "开始执行"); try { //为了演示多线程情况 thread.sleep(100); } catch (interruptedexception e) { e.printstacktrace(); } lazysingleton instance = lazysingleton.getinstance(); system.out.println("线程" + thread.currentthread().getname() + "初始化对象" + instance.hashcode()); } }
执行程序后,发现了问题:
//运行结果: 线程thread-0开始执行 线程thread-1开始执行 线程thread-1初始化对象1391273746 线程thread-0初始化对象547686109
小结
优点:起到了懒加载的作用,但是只能在单线程情况下使用。
缺点:多线程下不安全,如果一个线程进入到if语句中阻滞(还未开始创建对象),另一线程进入并通过了if判断,则会创建多个实例,这一点就违背了单例的目的。
结论:实际情况下,不要使用这种方式。
懒汉式(线程安全,同步方法)
思路
同上一中方式一样,但是为了解决多线程安全问题,使用同步方法。
代码演示:
public class singleton4 { public static void main(string[] args) { testthread testthread = new testthread(); thread thread = new thread(testthread); thread thread1 = new thread(testthread); thread.start(); thread1.start(); } } class lazysingleton { //1.私有化构造器 private lazysingleton() {} //2.类的内部声明对象 private volatile static lazysingleton instance; //3.对外提供获取对象的方法 public synchronized static lazysingleton getinstance() { //判断类是否被初始化 if (instance == null) { //第一次使用的时候,创建对象 instance = new lazysingleton(); } return instance; } } class testthread implements runnable { @override public void run() { system.out.println("线程" + thread.currentthread().getname() + "开始执行"); try { //为了演示多线程情况 thread.sleep(100); } catch (interruptedexception e) { e.printstacktrace(); } lazysingleton instance = lazysingleton.getinstance(); system.out.println("线程" + thread.currentthread().getname() + "初始化对象" + instance.hashcode()); } }
运行结果如下所示:
线程thread-1开始执行 线程thread-0开始执行 线程thread-0初始化对象681022576 线程thread-1初始化对象681022576
小结
优点:起到了懒加载的效果,同时,解决了线程安全问题。
缺点:效率低下,每次想要获取对象的时候,去执行getinstance()都是通过同步方法。而且,初始化对象后,再次使用的时候,应该直接return这个对象。
总结:可以在多线程条件下使用,但是效率低下,不推荐。
懒汉式(线程安全,同步代码块)
思路
同样是为了解决多线程安全问题,不过采用的是同步代码块。
代码实现:
public class singleton5 { public static void main(string[] args) { testthread testthread = new testthread(); thread thread = new thread(testthread); thread thread1 = new thread(testthread); thread.start(); thread1.start(); } } class lazysingleton { //1.私有化构造器 private lazysingleton() {} //2.类的内部声明对象 private volatile static lazysingleton instance; //3.对外提供获取对象的方法 public static lazysingleton getinstance() { //判断类是否被初始化 if (instance == null) { //第一次使用的时候,创建对象 synchronized (lazysingleton.class) { instance = new lazysingleton(); } } return instance; } } class testthread implements runnable { @override public void run() { system.out.println("线程" + thread.currentthread().getname() + "开始执行"); try { //为了演示多线程情况 thread.sleep(100); } catch (interruptedexception e) { e.printstacktrace(); } lazysingleton instance = lazysingleton.getinstance(); system.out.println("线程" + thread.currentthread().getname() + "初始化对象" + instance.hashcode()); } }
代码看上去没有问题,那么运行效果如何呢:
//运行结果: 线程thread-1开始执行 线程thread-0开始执行 线程thread-1初始化对象1368942806 线程thread-0初始化对象1187311731
那么,我们发现,打脸了,多线程情况下,创建了两个对象,并未达到单例的目的。
小结
不推荐使用这种方式。
懒汉式(线程安全,双重检查机制)
思路
针对懒汉式的多线程问题,我们可谓是操碎了心:同步方法可以解决问题,但是效率太低了;同步代码块则根本不能保证多线程安全。如何能做到“鱼和熊掌兼得”呢?既然同步代码块的效率较好,那么我们就针对这个方式进行改良:双重检查机制,即在getinstance()内进行两次检查,第一次通过if判断后,初始化对象之前,进行同步并再次进行判断。这样做的目的:既能解决线程安全问题,同时避免第二次使用对象的时候还要执行同步的代码。
代码实现:
public class singleton6 { public static void main(string[] args) { testthread testthread = new testthread(); thread thread = new thread(testthread); thread thread1 = new thread(testthread); thread.start(); thread1.start(); } } class lazydoublechecksingleton { //1.私有化构造器 private lazydoublechecksingleton() {} //2.类的内部声明对象 private volatile static lazydoublechecksingleton instance; //3.对外提供获取对象的方法 public static lazydoublechecksingleton getinstance() { //判断类是否被初始化 if (instance == null) { //第一次使用,通过if判断 //加锁 synchronized (lazydoublechecksingleton.class) { //拿到锁后,初始化对象之前,再次进行判断 if (instance == null) { instance = new lazydoublechecksingleton(); } } } return instance; } } class testthread implements runnable { @override public void run() { system.out.println("线程" + thread.currentthread().getname() + "开始执行"); try { //为了演示多线程情况 thread.sleep(100); } catch (interruptedexception e) { e.printstacktrace(); } lazydoublechecksingleton instance = lazydoublechecksingleton.getinstance(); system.out.println("线程" + thread.currentthread().getname() + "初始化对象" + instance.hashcode()); } }
运行结果如下所示:
//运行结果: 线程thread-0开始执行 线程thread-1开始执行 线程thread-1初始化对象996963733 线程thread-0初始化对象996963733
小结
优点:
- 解决了上一种方式中的线程安全问题,同时实现了延迟加载的效果,节约内存;
- 第二次使用的时候,if判断为false,直接返回创建好的对象,避免进入同步代码,提高了效率;
结论:推荐使用这种方式,实际工作中也比较常见这种方式。
静态内部类
思路
为了实现多线程情况下安全,除了手工加锁,还有别的方式。现在,我们采用静态内部类的方式。这种方式利用了jvm加载类的机制来保证只初始化一个对象。
思路同样是私有化构造器,对外提供静态的公开方法;不同之处是,类的创建交给静态内部类来时实现。
代码实现
public class singleton7 { public static void main(string[] args) { testthread testthread = new testthread(); thread thread = new thread(testthread); thread thread1 = new thread(testthread); thread.start(); thread1.start(); } } class staticinnersingleton { // 1.构造器私有化 private staticinnersingleton() {} // 2.通过静态内部类来初始化对象 private static class innerclass { private static final staticinnersingleton instance = new staticinnersingleton(); } // 3.对外提供获取对象的方法 public static staticinnersingleton getinstance() { return innerclass.instance; } } class testthread implements runnable { @override public void run() { system.out.println("线程" + thread.currentthread().getname() + "开始执行"); try { //为了演示多线程情况 thread.sleep(100); } catch (interruptedexception e) { e.printstacktrace(); } staticinnersingleton instance = staticinnersingleton.getinstance(); system.out.println("线程" + thread.currentthread().getname() + "初始化对象" + instance.hashcode()); } }
运行结果:
线程thread-0开始执行 线程thread-1开始执行 线程thread-0初始化对象1326533480 线程thread-1初始化对象1326533480
ok,我们发现,这种方式达到了预期的效果。
小结
优点:
- 这种静态内部类的方式,通过类加载机制来保证了初始化实例时只有一个实例。
- 类的静态属性只有在第一次加载类的时候初始化,而jvm能保证线程安全,在类的初始化过程中,只有一个线程能进入并完成初始化。
- 静态内部类方式实现了懒加载的效果,这种方式不会在类staticinnersingleton加载的时候进行初始化,而是在第一次使用时调用getinstance()方法初始化,能够起到节约内次的目的。
- 该方式的getinstance()方法,通过调用静态内部类的静态属性返回实例对象,避免了每次调用时进行同步,效率高。
结论:线程安全,效率高,代码实现简单,推荐使用。
枚举
思路
在静态内部类的方式中,我们借用了jvm的类加载机制来实现了功能,同样,还可以借用java的枚举来实现单例模式。
public class singleton8 { public static void main(string[] args) { testthread testthread = new testthread(); thread thread = new thread(testthread); thread thread1 = new thread(testthread); thread.start(); thread1.start(); } } enum enumsingleton { instance; public void sayhi() { system.out.println("hi, " + instance); } } class testthread implements runnable { @override public void run() { system.out.println("线程" + thread.currentthread().getname() + "开始执行"); try { //为了演示多线程情况 thread.sleep(100); } catch (interruptedexception e) { e.printstacktrace(); } enumsingleton instance = enumsingleton.instance; system.out.println("线程" + thread.currentthread().getname() + "初始化对象" + instance.hashcode()); } }
运行结果如下:
线程thread-0开始执行 线程thread-1开始执行 线程thread-1初始化对象1134798663 线程thread-0初始化对象1134798663
小结
优点:这中方式需要在jdk1.5以上的版本中使用,利用枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。在《effective java》中提到了这种方式,其作者推荐。
结论:推荐使用。
未完待续