1.作用:
产生唯一实例,拒绝客户端程序员使用new关键字获取实例,即一个类只有一个实例。比如:有一个类用于读取配置文件生成一个Properties对象,只需要一个对象即可。如果每次用到就读取一次新建一个Properties实例,这样就会造成资源浪费,以及多线程的安全问题。单例模式区分懒汉式、饿汉式。
2.观点:
严格来说,单例模式并不应该算得上设计模式,纠结线程安全问题可以,但是纠结茴香豆的八种写法这就不好了吧,代码应该先实现,然后在保证效果的前提下再去提升效率,过度设计并不适合所有场景。
3.懒汉、饿汉共同点:
都私有化构造方法,有一个静态方法返回生成的唯一实例。
4.饿汉式【推荐】:
说明:需要单例的类,在classloader load进内存的时候,直接静态初始化自己的类本身,产生一个唯一对象,使用一个静态方法返回这个实例。
1 package com.mi.singleton;
2
3 /**
4 * 单例模式:饿汉
5 *
6 * 优点:单例模式中最简单,线程安全
7 */
8 public class Singleton1 {
9
10 // 私有化构造方法,防止客户端程序员new对象
11 private Singleton1() {
12 }
13
14 // 静态初始化,产生唯一实例
15 private static Singleton1 singleton = new Singleton1();
16
17 // 返回实例方法
18 public static Singleton1 getInstance() {
19 return singleton;
20 }
21
22 }
多线程下安全,那么就肯定是安全的了,所以特意写了个多线程测试类(线程类我也防止这个类代码里了)
1 package com.mi.singleton;
2
3 public class Test {
4
5 public static void main(String[] args) {
6
7 ThreadTest[] tt = new ThreadTest[10];
8 for(int i = 0 ; i < tt.length ; i++){
9 tt[i] = new ThreadTest();
10 }
11
12 for (int j = 0; j < tt.length; j++) {
13 (tt[j]).start();
14 }
15 }
16 }
17
18 class ThreadTest extends Thread{
19
20 @Override
21 public void run() {
22 //打印实例返回实例的hashcode
23 System.out.println(Singleton1.getInstance().hashCode());
24 }
25
26 }
输出:
644512395
644512395
644512395
644512395
644512395
644512395
644512395
644512395
644512395
644512395
由此可证,饿汉式单例模式只产生了一个实例且线程安全、简单易懂的。
懒汉式:
懒汉式的实现分为线程安全和不安全的情况,所以有几种实现方式,以下分别写出并测试:
1)普通【不推荐:thread unsafe】:
1 package com.mi.singleton;
2
3 /**
4 * 单例模式:懒汉式
5 *
6 * 原始版懒汉式
7 * 优点:所谓的省内存,没有在初始化这个类的同时初始化这个成员
8 * 缺点:多线程不安全,多个线程第一次访问这个方法,当时singleton没有初始化,那么都会去new一个对象
9 * 这样就没有满足单例
10 */
11 public class Singleton2 {
12
13 private Singleton2(){}
14 private volatile static Singleton2 singleton; //volatile关键字刷新缓存
15 public static Singleton2 getInstance(){
16 if(singleton == null){
17 try {
18 //当前线程睡眠,测试结果更明显
19 Thread.sleep(300);
20 } catch (InterruptedException e) {
21 e.printStackTrace();
22 }
23 singleton = new Singleton2();
24 }
25 return singleton;
26 }
27 }
修改ThreadTest中run方法中的Singleton1改为Singleton2,测试输出:
2049977720
1798105197
644512395
294592865
516266445
257688302
1483688470
1906199352
860588932
1388138972
2)同步方法实现【不推荐:speed slow】
1 package com.mi.singleton;
2
3 /**
4 * 单例模式:懒汉式
5 *
6 * 同步方法方式解决多线程访问问题
7 * 优点:线程安全
8 * 缺点:线程虽然安全了,但是多线程情况下,同步方法会导致阻塞,影响其他线程的速度
9 */
10 public class Singleton3 {
11
12 private Singleton3(){}
13 private static Singleton3 singleton;
14 public static synchronized Singleton3 getInstance(){
15 if(singleton == null){
16 try {
17 //当前线程睡眠,测试结果更明显
18 Thread.sleep(300);
19 } catch (InterruptedException e) {
20 e.printStackTrace();
21 }
22 singleton = new Singleton3();
23 }
24 return singleton;
25 }
26
27 }
修改ThreadTest中run方法中的Singleton2改为Singleton3,测试输出:
2049977720
2049977720
2049977720
2049977720
2049977720
2049977720
2049977720
2049977720
2049977720
2049977720
3)三检查方式【推荐:lazy load & thread safe】
1 package com.mi.singleton;
2
3 /**
4 * 单例模式:懒汉式
5 *
6 * 使用三检查方式
7 * 优点:解决了多线程访问问题,同时也解决了效率问题
8 * 缺点:代码可读性差
9 */
10 public class Singleton4 {
11
12 private Singleton4 (){}
13 private static Singleton4 singleton;
14 public static Singleton4 getInstance(){
15 if(singleton == null){ //判断,只有该对象还没有初始化的时候,才会进入
16 //同步锁,保证只有一个线程进入
17 synchronized(Singleton4.class){
18 //确保当前对象还没有被初始化,才去初始化
19 if(singleton == null){
20 try {
21 //当前线程睡眠,测试结果更明显
22 Thread.sleep(300);
23 } catch (InterruptedException e) {
24 e.printStackTrace();
25 }
26 singleton = new Singleton4();
27 }
28 }
29 }
30 return singleton;
31 }
32 }
修改ThreadTest中run方法中的Singleton3改为Singleton4,测试输出:
644512395
644512395
644512395
644512395
644512395
644512395
644512395
644512395
644512395
644512395
4)静态内部类方式【不推荐:代码可读性差】
1 package com.mi.singleton;
2
3 /**
4 * 单例模式:懒汉式
5 *
6 * 静态内部类的实现 优点:解决了多线程的安全问题 缺点:代码可读性不好
7 * 评价:该方法使用了静态内部类的初始化时机来初始化内部类中的对象,
8 * 感觉相当于饿汉式,而且 代码看起来简单,其实更麻烦了,可读性差,比较投机取巧
9 */
10 public class Singleton5 {
11
12 private Singleton5() {
13 }
14
15 private static class InnerClass {
16 private static Singleton5 singleton = new Singleton5();
17 }
18
19 public static Singleton5 getInstance() {
20 return InnerClass.singleton;
21 }
22
23 }
修改ThreadTest中run方法中的Singleton4改为Singleton5,测试输出:
1483688470
1483688470
1483688470
1483688470
1483688470
1483688470
1483688470
1483688470
1483688470
1483688470
总结:
懒汉式、饿汉式区别:懒汉式是有需要的时候初始化对象(个人认为内部类方式应该算在饿汉式中),而饿汉式是加载类的同时初始化对象
纯粹的懒汉式会导致并发访问的时候,出现初始化多次的问题,针对这个问题的解决方案有以下几种:
- 三检查:在获取对象方法中先判断是否为空,在if判断内部加入同步块,在同步块中继续判断是否为空,为空则初始化对象返回,缺点:麻烦,代码可读性差
- 同步获取实例方法:将获取实例方法同步,虽然解决了并发访问的问题,但效率偏低,每一次调用都要同步阻塞等待锁释放
- 静态内部类初始化:静态内部类会在类加载的顺序初始化该类中的成员变量(该实例),这样一来,的确可以获取到实例并避免了初始化多个对象的问题,基本等同饿汉式,缺点是代码可读性最差