【设计模式学习笔记】之 单例模式
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判断内部加入同步块,在同步块中继续判断是否为空,为空则初始化对象返回,缺点:麻烦,代码可读性差
- 同步获取实例方法:将获取实例方法同步,虽然解决了并发访问的问题,但效率偏低,每一次调用都要同步阻塞等待锁释放
- 静态内部类初始化:静态内部类会在类加载的顺序初始化该类中的成员变量(该实例),这样一来,的确可以获取到实例并避免了初始化多个对象的问题,基本等同饿汉式,缺点是代码可读性最差