设计模式 -- 单例模式(Singleton)
写在前面的话:读书破万卷,编码如有神
--------------------------------------------------------------------
主要内容包括:
- 初始单例模式,包括:定义、结构、参考实现
- 体会单例模式,场景问题、不用模式的解决方案、使用模式的解决方案
- 理解单例模式,包括:认识单例模式、懒汉式和饿汉式实现、延迟加载的思想、缓存的思想、Java中缓存的基本实现、利用缓存来实现单例模式、单例模式的优缺点、在Java中一种更好的单例实现方式、单例和枚举
- 思考单例模式,包括:单例模式的本质、何时选用
参考内容:
1、《研磨设计模式》 一书,作者:陈臣、王斌
---------------------------------------------------------------------
1、初识单例模式
1.1、定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
1.2、结构和说明
Singleton: 负责创建Singleton类自己的唯一实例,并提供一个getInstance的方法,让外部来访问这个类的唯一实例。
1.3、示例代码
在Java中,单例模式的实现又分为两种:懒汉式、饿汉式,下面分别来看看这两种的实现代码:
(1)懒汉式示例代码如下:
1 /** 2 * 懒汉式单例实现的示例代码 3 */ 4 public class Singleton { 5 //定义一个变量来存储定义好的类实例 6 private static Singleton instance = null; 7 8 /** 9 * 私有化构造方法,可以在内部控制创建实例的个数 10 */ 11 private Singleton(){ 12 13 } 14 15 /** 16 * 定义一个方法来为客户端提供类实例 17 * @return 一个Singleton的实例 18 */ 19 public static synchronized Singleton getInstance(){ 20 //判断存储实例的变量是否有值 21 if(instance == null){ 22 //如果没有,就创建一个类实例,并把值赋值给存储类实例的变量 23 instance = new Singleton(); 24 } 25 return instance; 26 } 27 28 /** 29 * 单例模式也可以有自己的操作 30 */ 31 public void Operation(){ 32 33 } 34 35 //单例模式也可以有自己的属性 36 private String data; 37 38 public String getData() { 39 return data; 40 } 41 42 public void setData(String data) { 43 this.data = data; 44 } 45 }
(2)饿汉式示例代码如下:
1 /** 2 * 饿汉式单例实现的示例代码 3 */ 4 public class Singleton { 5 //定义一个变量来存储定义好的类实例 6 private static Singleton instance = new Singleton(); 7 8 /** 9 * 私有化构造方法,可以在内部控制创建实例的个数 10 */ 11 private Singleton(){ 12 13 } 14 15 /** 16 * 定义一个方法来为客户端提供类实例 17 * @return 一个Singleton的实例 18 */ 19 public static synchronized Singleton getInstance(){ 20 return instance; 21 } 22 23 /** 24 * 单例模式也可以有自己的操作 25 */ 26 public void Operation(){ 27 28 } 29 30 //单例模式也可以有自己的属性 31 private String data; 32 33 public String getData() { 34 return data; 35 } 36 37 public void setData(String data) { 38 this.data = data; 39 } 40 }
---------------------------------------------------------------------
2、体会单例模式
2.1、场景问题
考虑这样一个应用,读取配置文件的内容,在实际的项目中配置文件多采用xml格式或者properties格式的,现在要读取配置文件的内容,该如何实现呢?
2.2、不用模式的解决方案
假设系统采用的是properties格式的配置文件,读取配置文件的示例代码如下:
1 (1)jdbc.properties文件的内容 2 jdbcUser=xixixixixi 3 jdbcPassword=123456 4 5 (2)读取配置文件应用程序 6 import java.io.IOException; 7 import java.io.InputStream; 8 import java.util.Properties; 9 10 /** 11 * 读取应用配置文件 12 */ 13 public class AppConfig { 14 //数据库用户名 15 private String jdbcUser; 16 //数据库密码 17 private String jdbcPassword; 18 19 /** 20 * 构造函数 21 */ 22 public AppConfig(){ 23 //调用读取配置文件的方法 24 readConfig(); 25 } 26 27 /** 28 * 读取配置文件,把配置文件中的内容读取出来设置到属性上 29 */ 30 private void readConfig(){ 31 Properties p = new Properties(); 32 InputStream in = null; 33 34 try{ 35 in = AppConfig.class.getResourceAsStream("jdbc.properties"); 36 p.load(in); 37 38 //把配置文件中的内容读取出来设置到属性上 39 jdbcUser = p.getProperty("jdbcUser"); 40 jdbcPassword = p.getProperty("jdbcPassword"); 41 }catch(IOException e){ 42 e.printStackTrace(); 43 }finally{ 44 try { 45 in.close(); 46 } catch (IOException e) { 47 e.printStackTrace(); 48 } 49 } 50 } 51 52 public String getJdbcUser() { 53 return jdbcUser; 54 } 55 56 public String getJdbcPassword() { 57 return jdbcPassword; 58 } 59 } 60 61 (3)测试客户端 62 public class Client { 63 public static void main(String[] args) { 64 AppConfig appConfig = new AppConfig(); 65 66 String jdbcUser = appConfig.getJdbcUser(); 67 String jdbcPassword = appConfig.getJdbcPassword(); 68 69 System.out.println("jdbcUser = " + jdbcUser); 70 System.out.println("jdbcPassword = " + jdbcPassword); 71 } 72 } 73 74 (4)运行结果 75 jdbcUser = xixixixixi 76 jdbcPassword = 123456
2.3、有何问题
上面的实现很简单,很容易就实现要求的功能了。但是来仔细想想,有没有什么问题?
存在的问题:在客户端中是通过new一个AppConfig的实例来得到一个操作配置文件内容的对象,如果在系统运行中,有很多地方都需要使用配置文件的内容,也就是说很多地方都需要创建AppConfig对象的实例。----->换句话说,在系统运行期间,系统中会存在很多个AppConfig的实例对象,每一个AppConfig实例对象里面都封装这配置文件的内容,系统中有多个AppConfig实例对象,也就是说系统中会同时存在多份配置文件的内容,这样会严重浪费内存资源。
把问题抽象一下:在一个系统运行期间,某个类只需要一个类实例就可以了,那么该怎么实现呢?
2.4、使用单例模式来解决问题
(1)解决问题的思路
现在AppConfig类能被创建多个实例,问题的根源在于类的构造方法是公开的,也就是可以让类的外部来通过构造方法创建多个实例。换句话说,只要类的构造方法能让类的外部访问,就没有办法去控制外部来创建这个类的实例个数。
要想控制一个类只被创建一个实例,那么首要的问题就是要把创建实例的权限收回来,让类自身来负责自己类实例的创建工作,然后由这个类来提供外部可以访问这个类实例的方法,这就是单例模式的实现方式。
(2)使用单例模式重写示例(饿汉式)
1 (1)properties属性文件 2 jdbcUser=xixixixixi 3 jdbcPassword=123123 4 5 (2)采用单例模式实现读取配置文件程序 6 import java.io.IOException; 7 import java.io.InputStream; 8 import java.util.Properties; 9 10 /** 11 * 读取应用配置文件,单例模式实现 12 */ 13 public class AppConfig { 14 //数据库用户名 15 private String jdbcUser; 16 //数据库密码 17 private String jdbcPassword; 18 19 //定义一个变量来存储创建好的类实例 20 private static AppConfig instance = new AppConfig(); 21 22 /** 23 * 私有化构造函数 24 */ 25 private AppConfig(){ 26 //调用读取配置文件的方法 27 readConfig(); 28 } 29 30 /** 31 * 定义一个方法来为客户端提供AppConfig类的实例 32 * @return AppConfig类的实例 33 */ 34 public static AppConfig getInstance(){ 35 return instance; 36 } 37 38 /** 39 * 读取配置文件,把配置文件中的内容读取出来设置到属性上 40 */ 41 private void readConfig(){ 42 Properties p = new Properties(); 43 InputStream in = null; 44 45 try{ 46 in = AppConfig.class.getResourceAsStream("jdbc.properties"); 47 p.load(in); 48 49 //把配置文件中的内容读取出来设置到属性上 50 jdbcUser = p.getProperty("jdbcUser"); 51 jdbcPassword = p.getProperty("jdbcPassword"); 52 }catch(IOException e){ 53 e.printStackTrace(); 54 }finally{ 55 try { 56 in.close(); 57 } catch (IOException e) { 58 e.printStackTrace(); 59 } 60 } 61 } 62 63 public String getJdbcUser() { 64 return jdbcUser; 65 } 66 67 public String getJdbcPassword() { 68 return jdbcPassword; 69 } 70 } 71 72 (3)客户端 73 public class Client { 74 public static void main(String[] args) { 75 76 //获取读取应用配置文件的对象 77 AppConfig appConfig = AppConfig.getInstance(); 78 79 String jdbcUser = appConfig.getJdbcUser(); 80 String jdbcPassword = appConfig.getJdbcPassword(); 81 82 System.out.println("jdbcUser = " + jdbcUser); 83 System.out.println("jdbcPassword = " + jdbcPassword); 84 } 85 } 86 87 (4)运行结果 88 jdbcUser = xixixixixi 89 jdbcPassword = 123123
---------------------------------------------------------------------
3、理解单例模式
3.1、认识单例模式
(1)单例模式的功能
单例模式是用来保证这个类在运行期间只会被创建一个类实例,并且还提供了一个全局唯一访问这个类实例的访问点。
(2)单例模式的范围
目前Java里面实例的单例是一个虚拟机的范围,因为装载类的功能是虚拟机的。
3.2、延迟加载的思想
单例模式的懒汉式实现方式体现了延迟加载的思想,什么是延迟加载呢? 通俗点说,延迟加载就是一开始不要加载资源或者数据,等到要使用的时候再去加载,所以也称为 Lazy Load,这在实际开发中是一种很常见的思想,尽可能地节约资源。
3.3、缓存的思想
单例模式的懒汉式实现还体现了缓存的思想,简单讲就是,某些资源或者数据被频繁地使用,而这些资源或数据存在在系统外部,比如数据库、磁盘文件等,那么每次操作这些数据的时候都得从数据库或者硬盘上去获取,速度会很慢,将造成性能问题。
解决办法就是利用缓存: 把这些数据缓存到内存里面,每次操作的时候,先到内存里面找,看有没有这些数据,如果有,就直接使用;如果没有,就去获取它并设置到缓存中,下一次访问的时候就可以直接从内存中获取了,从而节省大量的时间。
(ps:缓存是一种典型的空间换时间的方案)
3.4、Java中缓存的基本实现
在实际开发中常用的缓存方法有很多,比如:memcached、ehcache等。
在Java开发中最常见的一种实现缓存的方式就是使用Map,基本步骤如下:
- 先到缓存里面查找,看看是否存在需要使用的数据
- 如果没有找到,那么就创建一个满足要求的数据,然后把这个数据设置到缓存中,以备下次使用。
- 如果找到了相应的数据,或者创建了相应的数据,那么久直接使用这个数据。
示例代码如下:
1 import java.util.HashMap; 2 import java.util.Map; 3 4 /** 5 * Java中缓存的基本实现示例 6 */ 7 public class JavaCache { 8 /** 9 * 缓存数据的容器 10 */ 11 private Map<String,Object> map = new HashMap<String,Object>(); 12 13 /** 14 * 从缓存中获取值 15 * @param key 设置时候的key值 16 * @return key对应的value值 17 */ 18 public Object getValue(String key){ 19 //先从缓存中去获取 20 Object obj = map.get(key); 21 22 //判断缓存里面是否有值 23 if(obj == null){ 24 //如果没有,那么就去获取相应的数据,比如读取数据库或者文件 25 //这里只是演示,所以直接写个假值 26 obj = key + ",value"; 27 28 //把获取的值设置回到缓存里面 29 map.put(key, obj); 30 } 31 32 //如果有值了,就直接使用 33 return obj; 34 } 35 }
3.5、单例模式的优缺点
(1)时间和空间
- 懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间。
- 饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。
(2)线程安全
- 不加同步的懒汉式是线程不安全的,如果有两个线程同时调用getInstance方法,那么可能会导致并发问题。
- 饿汉式是线程安全的,因此虚拟机保证只会装载一次,在装载类的时候是不会发生并发的。
(3)如何实现懒汉式的线程安全呢?
只要加上synchronized即可,如下:
1 public static synchronized Singleton getInstance()
但是这在方法上加同步控制,会降低整个访问速度,效率比较低。
(4)双重检查加锁
所谓双重检查加锁机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查;进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。
示例代码如下:
1 import java.io.IOException; 2 import java.io.InputStream; 3 import java.util.Properties; 4 5 /** 6 * 读取应用配置文件,单例模式实现(双重检查方式) 7 */ 8 public class AppConfig { 9 //数据库用户名 10 private String jdbcUser; 11 //数据库密码 12 private String jdbcPassword; 13 14 //定义一个变量来存储创建好的类实例 15 private volatile static AppConfig instance = null; 16 17 /** 18 * 私有化构造函数 19 */ 20 private AppConfig(){ 21 //调用读取配置文件的方法 22 readConfig(); 23 } 24 25 /** 26 * 定义一个方法来为客户端提供AppConfig类的实例 27 * @return AppConfig类的实例 28 */ 29 public static AppConfig getInstance(){ 30 //先检查实例是否存在,如果不存在则进入下面的同步块 31 if(instance == null){ 32 //同步块,线程安全的创建实例 33 synchronized(AppConfig.class){ 34 //再次检查实例是否存在,如果不存在才真正的创建实例 35 if(instance == null){ 36 instance = new AppConfig(); 37 } 38 } 39 } 40 41 return instance; 42 } 43 44 /** 45 * 读取配置文件,把配置文件中的内容读取出来设置到属性上 46 */ 47 private void readConfig(){ 48 Properties p = new Properties(); 49 InputStream in = null; 50 51 try{ 52 in = AppConfig.class.getResourceAsStream("jdbc.properties"); 53 p.load(in); 54 55 //把配置文件中的内容读取出来设置到属性上 56 jdbcUser = p.getProperty("jdbcUser"); 57 jdbcPassword = p.getProperty("jdbcPassword"); 58 }catch(IOException e){ 59 e.printStackTrace(); 60 }finally{ 61 try { 62 in.close(); 63 } catch (IOException e) { 64 e.printStackTrace(); 65 } 66 } 67 } 68 69 public String getJdbcUser() { 70 return jdbcUser; 71 } 72 73 public String getJdbcPassword() { 74 return jdbcPassword; 75 } 76 }
双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存的,从而确保多个线程能正确的处理该变量。
3.6、在Java中一种更好的单例实现方式
下面这种方式既能够实现延迟加载,又能够实现线程安全(采用类级内部类的方式)。
示例代码:
1 public class Singleton { 2 3 /** 4 * 私有化构造方法 5 */ 6 private Singleton(){ 7 8 } 9 10 /** 11 * 类级的内部类,也就是静态的成员内部类,该内部类的实例与外部类的实例没有绑定关系, 12 * 而且只有被调用到时才会装载,从而实现延迟加载 13 */ 14 private static class SingletonHolder{ 15 /** 16 * 静态初始化器,由JVM来保证线程安全 17 */ 18 private static Singleton instance = new Singleton(); 19 } 20 21 public static Singleton getInstance(){ 22 return SingletonHolder.instance; 23 } 24 }
上面的代码使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了延迟加载和线程安全。
相关的基础知识:
(1)类级内部类
- 什么是类级内部类? 简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。
- 类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。
- 类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
- 类级内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载。
(2)多线程缺省同步锁的知识
在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况下,JVM已经隐含地为我们执行了同步,这些情况下就不用自己再来进行同步控制了,这些情况包括:
- 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
- 访问final字段时
- 在创建线程之前创建对象时
- 线程可以看见它将要处理的对象时
3.7、单例和枚举
3.8、思考单例模式
(1)单例模式的本质: 控制实例数目。
(2)何时选用单例模式
当需要控制一个类的实例只能有一个,而且客户只能从一个全局访问点访问它时,可以选用单例模式。
---------------------------------------------------------------------
---------------------------------------------------------------------
---------------------------------------------------------------------