单例模式(Singleton)
在系统设计过程中,经常有一些数据或者功能上要求在JVM的生命周期过程中,只存在一份,那么这个时候需要将某个类设计成单例(singleton)的。
比如,系统的数据字典通常为通过配置的方式存储在数据库中,系统运行过程中,如果需要读取数据字典,可以从数据库读取,但从数据库读取存在IO开销大的问题,并且数据字典运用比较广泛,所以读取的频率相对很高,数据库读取会直接降低系统的性能。
这个时候,会考虑系统启动时,直接一次性读取所有的数据字典,将数据加载到对象中,在内存中存储,这时候,我们再需要数据字典的数据,就可以直接从内存中获取了,而不需要每次都采用效率比较低的数据库读取。
此时,又会出现另外一个问题,我们希望系统中存储数据字典数据的对象在JVM中存在且只存在一份,否则仍然会出现多次读取数据库和内存浪费的情况。单例模式应运而生。
单例模式的原理
一个类之所以称之为单例的,需要满足以下的条件:
- 1、构造器私有化,让类外的任何地方都不可以进行对象创建
- 2、在本类内进行实例化,对外提供此对象的统一访问方式
单例模式的几种类型
-
1、饿汉式单例模式
-
需求:
我们有如下的db.properties配置文件,系统运行过程中,我们不希望每次需要配置文件的数据的时候,都读取此文件,而是通过单例模式的方式进行预先加载到对象中进行存储,需要时,直接从对象中获取相应属性。jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql:///test?&useUnicode=false&characterEncoding=utf8&useSSL=false jdbc.user=root jdbc.password=password
-
解决方案:
建立存储配置文件的类,类中定义相应的属性,并私有化构造器,在构造器中直接实例化此类对象实现数据的加载,或者在类对象定义时直接初始化。- code
@Data public class PropertyCache { private String driver ; private String url; private String user; private String password; //实例化的对象 private static PropertyCache instance = new PropertyCache(); //构造器中进行数据的加载 private PropertyCache(){ InputStream is = PropertyCache.class.getClassLoader().getResourceAsStream("db.properties"); Properties p = new Properties(); try { p.load(is); this.setDriver(p.getProperty("jdbc.driver")); this.setUrl(p.getProperty("jdbc.url")); this.setUser(p.getProperty("jdbc.user")); this.setPassword(p.getProperty("jdbc.password")); } catch (IOException e) { e.printStackTrace(); } } //对外提供统一的访问接口 public static PropertyCache getInstance(){ return instance; } }
- 测试代码
@Test public void test_singleton(){ PropertyCache instance = PropertyCache.getInstance(); PropertyCache instance1 = instance.getInstance(); System.out.println(instance == instance1); System.out.println(instance.getDriver()); System.out.println(instance.getUrl()); System.out.println(instance.getUser()); System.out.println(instance.getPassword()); }
- 测试结果
true com.mysql.jdbc.Driver jdbc:mysql:///test?&useUnicode=false&characterEncoding=utf8&useSSL=false root password
- code
-
2、懒汉式单例模式
- 需求
饿汉式中的instance,不想在lei加载的时候,就进行初始化,想在第一次真正需要的时候,再进行加载和数据的赋值,加快系统启动时的效率(lazy_loading) - 解决方案
在getInstance方法中进行instance的赋值,并进行数据的装载 - code
@Data public class LazyPropertyCache { private String driver ; private String url; private String user; private String password; //实例化的对象 private static LazyPropertyCache instance ; //构造器中进行数据的加载 private LazyPropertyCache(){ } //对外提供统一的访问接口 public static LazyPropertyCache getInstance(){ if(null == instance){ synchronized (LazyPropertyCache.class) { if(null == instance) { instance = new LazyPropertyCache(); init(); } } } return instance; } //db.properties配置文件中数据的抓取和赋值 private static void init(){ InputStream is = LazyPropertyCache.class.getClassLoader().getResourceAsStream("db.properties"); Properties p = new Properties(); try { p.load(is); getInstance().setDriver(p.getProperty("jdbc.driver")); getInstance().setUrl(p.getProperty("jdbc.url")); getInstance().setUser(p.getProperty("jdbc.user")); getInstance().setPassword(p.getProperty("jdbc.password")); } catch (IOException e) { e.printStackTrace(); } } }
- 测试代码
@Test public void test_singleton2(){ LazyPropertyCache instance1 = LazyPropertyCache.getInstance(); LazyPropertyCache instance2 = LazyPropertyCache.getInstance(); System.out.println(instance1 == instance2); System.out.println(instance1.getDriver()); System.out.println(instance1.getUrl()); System.out.println(instance1.getUser()); System.out.println(instance1.getPassword()); }
- 测试结果
true com.mysql.jdbc.Driver jdbc:mysql:///test?&useUnicode=false&characterEncoding=utf8&useSSL=false root password
- 注意
懒汉式就是在定义属性或者构造器内部进行instance的赋值,而留到getInstance方法内进行初始化,值得注意的式,同时调用getInstance的线程可能不只一个,如不做同步处理,则多个线程可能同时通过构造器构造出多个实例,就不能称为单例了 ,因此需要synchonize同步化处理。
另外,在同步块中,为啥需要进行2次if判断instance是否为null,大家可以深入考虑一下。
- 需求
-
3、枚举方式实现单例模式
- 疑问
以上饿汉式和懒汉式的单例模式定义好后,是否真的可以确保系统中永远最多只有一个对象,有没有什么办法破解这个单例,也就是生成多个实例对象?答案是有的,我们通过下面的测试来看一下: - 测试
@Test public void test_singleton3() throws Exception { PropertyCache instance = PropertyCache.getInstance(); //对象序列化入文件 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\instance"))); oos.writeObject(instance); //从文件读入进行反序列化 ObjectInputStream ois = new ObjectInputStream((new FileInputStream(new File("D:\\instance")))); PropertyCache instance2 = (PropertyCache) ois.readObject(); System.out.println(instance == instance2); System.out.println(instance2.getDriver()); System.out.println(instance2.getUrl()); System.out.println(instance2.getUser()); System.out.println(instance2.getPassword()); }
- 结果
false com.mysql.jdbc.Driver jdbc:mysql:///test?&useUnicode=false&characterEncoding=utf8&useSSL=false root password
- 解决方式
我们知道枚举这个结构,就是从jvm底层保证了唯一性,我们使用Enum来进行一下实验 - code
public enum EnumPropertyCache implements Serializable { INSTANCE; private String driver ; private String url; private String user; private String password; public String getDriver() { return driver; } public String getUrl() { return url; } public String getUser() { return user; } public String getPassword() { return password; } private void setDriver(String driver) { this.driver = driver; } private void setUrl(String url) { this.url = url; } private void setUser(String user) { this.user = user; } private void setPassword(String password) { this.password = password; } private EnumPropertyCache(){ InputStream is = PropertyCache.class.getClassLoader().getResourceAsStream("db.properties"); Properties p = new Properties(); try { p.load(is); this.setDriver(p.getProperty("jdbc.driver")); this.setUrl(p.getProperty("jdbc.url")); this.setUser(p.getProperty("jdbc.user")); this.setPassword(p.getProperty("jdbc.password")); } catch (IOException e) { e.printStackTrace(); } } }
- 测试代码和结果
@Test public void test_singleton4(){ EnumPropertyCache instance = EnumPropertyCache.INSTANCE; EnumPropertyCache instance2 = EnumPropertyCache.INSTANCE; System.out.println(instance == instance2); System.out.println(instance.getDriver()); System.out.println(instance.getUrl()); System.out.println(instance.getUser()); System.out.println(instance.getPassword()); }
true com.mysql.jdbc.Driver jdbc:mysql:///test?&useUnicode=false&characterEncoding=utf8&useSSL=false root password
- 序列化方式测试
@Test public void test_singleton3() throws Exception { EnumPropertyCache instance = EnumPropertyCache.INSTANCE; //对象序列化入文件 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File("D:\\instance"))); oos.writeObject(instance); //从文件读入进行反序列化 ObjectInputStream ois = new ObjectInputStream((new FileInputStream(new File("D:\\instance")))); EnumPropertyCache instance2 = (EnumPropertyCache) ois.readObject(); System.out.println(instance == instance2); System.out.println(instance2.getDriver()); System.out.println(instance2.getUrl()); System.out.println(instance2.getUser()); System.out.println(instance2.getPassword()); }
- 测试结果
true com.mysql.jdbc.Driver jdbc:mysql:///test?&useUnicode=false&characterEncoding=utf8&useSSL=false root password
- 疑问
从测试结果中可以看出,尽管经过了序列化和反序列化的过程,最终的instance都是同一个instance,也就是说jvm从底层保证了Enum枚举类型的唯一。