【设计模式学习笔记】之 单例模式

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

总结:

  • 懒汉式、饿汉式区别:懒汉式是有需要的时候初始化对象(个人认为内部类方式应该算在饿汉式中),而饿汉式是加载类的同时初始化对象

  • 纯粹的懒汉式会导致并发访问的时候,出现初始化多次的问题,针对这个问题的解决方案有以下几种:

  1. 三检查:在获取对象方法中先判断是否为空,在if判断内部加入同步块,在同步块中继续判断是否为空,为空则初始化对象返回,缺点:麻烦,代码可读性差
  2. 同步获取实例方法:将获取实例方法同步,虽然解决了并发访问的问题,但效率偏低,每一次调用都要同步阻塞等待锁释放
  3. 静态内部类初始化:静态内部类会在类加载的顺序初始化该类中的成员变量(该实例),这样一来,的确可以获取到实例并避免了初始化多个对象的问题,基本等同饿汉式,缺点是代码可读性最差

 

posted @ 2018-02-07 10:53  东北小狐狸  阅读(233)  评论(0编辑  收藏  举报