单例模式作为一种创建型模式,在日常开发中用处极广,我们先来看一一段代码:
// 构造函数protected Calendar(TimeZone var1, Locale var2) { this.lenient = true; this.sharedZone = false; this.nextStamp = 2; this.serialVersionOnStream = 1; this.fields = new int[17]; this.isSet = new boolean[17]; this.stamp = new int[17]; this.zone = var1; this.setWeekCountData(var2); }// 提供 Calendar 类实例的方法public static Calendar getInstance(){ return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT)); }
看过上一篇博客Java设计模式(5:工厂模式详解)的朋友应该熟悉这段来自JDK
中Calendar
类的代码,这就是单例模式的一种实现:
Calendar
类的构造函数被protected
修饰,保证其不能被其他包下的类访问。getInstance()
方法提供了获得Calendar
类实例化对象的方法。
从上述代码来看,我们可以认定实现单例模式需要满足两个基本原则:
- 类的构造函数私有化。
- 该类需要提供一个获得实例的全局访问点。
所以可以得出结论:单例模式是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局的访问点。
得出结论后,再来看看实现。在java
语言当中,两种方式构建单例模式:饿汉式单例和懒汉式单例。
一、饿汉式单例
// 饿汉式单例public class HungrySingleton { // 构造函数私有化 private HungrySingleton() {} private static final HungrySingleton singleton = new HungrySingleton(); // 提供一个全局的访问点 public static HungrySingleton getInstance(){ return singleton; }}
饿汉式单例是在类加载的时候就立即初始化,并且创建了单例对象。在上述代码中,当HungrySingleton
类在被类加载器加载时,它的实例对象singleton
就已经创建完成了;并且根据类的加载机制,我们明白:singleton
作为HungrySingleton
类中的一个静态的声明对象,在HungrySingleton
类第一次被类加载器加载时就已经创建完成,并且只会创建这一次。这就保证了无论getInstance()
方法被调用多少次,返回的都是同一个singleton
实例;保证了线程的绝对安全,不会出现访问安全的问题。
但也正式因为singleton
实例在HungrySingleton
类第一次被类加载器加载时就已经创建完成,若getInstance()
方法不被任何地方调用,那么singleton
实例就会一直占着内存空间,白白浪费了资源。所以引申出了另一种构建单例模式的方式:懒汉式单例
二、懒汉式单例
懒汉式单例的特点是只有在类的全局访问点被访问的时候,类的实例化对象才会创建。
// 懒汉式单例public class LazySingleton { // 构造函数私有化 private LazySingleton() {} private static LazySingleton lazySingleton = null; // 全局访问点 public static LazySingleton getInstance(){ if (lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; }}
在上述代码中,只有当getInstance()
方法被调用时,才会去创建lazySingleton
实例。这样就解决了饿汉式模式中的资源占用问题,但同样引申出了另一个问题:线程安全问题。
我们先来创建一个属于我们自己的线程类LazyThread
:
// 线程public class LazyThread implements Runnable { @Override public void run() { LazySingleton instance = LazySingleton.getInstance(); // 打印 线程名字 和 instance实例的内存地址 System.out.println(Thread.currentThread().getName() + ":" +instance); }}
调用:
// 创建两个线程public static void main(String[] args) { Thread thread1 = new Thread(new LazyThread()); Thread thread2 = new Thread(new LazyThread()); thread1.start(); thread2.start();}
我们采用debug模式调试一下,先和下图一般,在LazySingleton
类中打一个断点。
再用鼠标右键点击断点的位置(红色圆点的位置),打开如下图的框之后,先选择红框中的Thread
模式,再点击蓝框中的Done
按钮。
做完上述的操作之后,我们来用debug模式运行一下main方法
上图红框中内容就是我们所创建的两个线程,目前是Thread-0
线程在运行。我们将Thread-0
线程运行到lazySingleton = new LazySingleton()
这行代码的位置(图1),然后切换为Thread-1
线程,并将Thread-1
线程同样运行到此位置(图2):
图1:
图2:
最后:切换回Thread-0
线程,并全部放开,让代码一直运行下去;并对Thread-1
做出同样的操作。打印出结果:
通过结果可以看出,两个线程获得的lazySingleton
实例所对应的内存地址不相同,显然不符合单例模式中的只有一个实例的原则。
那有什么办法可以保证懒汉式模式在线程环境下安全呢?有,而且很简单,加锁。我们来给getInstance()
方法加上锁:
// 懒汉式public class LazySingleton { // 私有化构造函数 private LazySingleton() {} private static LazySingleton lazySingleton = null; // 加锁 public synchronized static LazySingleton getInstance(){ if (lazySingleton == null){ lazySingleton = new LazySingleton(); } return lazySingleton; }}
我们再用上述的方式来debug调试一下:
在线程Thread-1
进入getInstance()
方法内部的时候,线程Thread-0
处于MONITOR
锁监控的状态。将线程Thread-1
运行完后,Thread-0
进入getInstance()
方法内部,状态更新为RUNNING
运行状态。
而此时我们可以看出lazySingleton
已经有值了,所以我们将线程Thread-0
运行完后,两个线程会打印出一样的结果:
由结果我们可以看出,在给getInstance()
方法加上锁之后,线程安全的问题便解决了。但依然可以继续来优化这段懒汉式单例模式的代码。
// 懒汉式public class LazySingleton { // 私有化构造函数 private LazySingleton() {} // volatile 关键字 解决重排序的问题 private volatile static LazySingleton lazySingleton = null; public static LazySingleton getInstance(){ if (lazySingleton == null){ // 锁代码块 synchronized (LazySingleton.class) { if (lazySingleton == null){ lazySingleton = new LazySingleton(); } } } return lazySingleton; }}
这种方式被称为双重检查锁,它有着以下两点的好处:
- 线程由基于
LazySingleton
整个类的阻塞变为在getInstance()
方法内部的阻塞。锁的颗粒度变得更细,锁的代码块变得更小了。 - 第一重的
if
判断,直接分流了一部分在lazySingleton
实例化后在进入getInstance()
方法的线程,提高了效率。
但是,只要涉及到加锁的问题,对程序的性能或多或少都有影响,那么有没有不加锁的方式呢?当然也是有的,那就是以类的初始化角度来考虑,使用内部类的方式。
三、静态内部类实现单例模式
// 懒汉式模式 和 饿汉式模式 兼顾public class InnerClassSingleton { // 私有化构造函数 private InnerClassSingleton(){} public static InnerClassSingleton getInstance(){ return SingletonHolder.singleton; } // 静态内部类 private static class SingletonHolder{ private static final InnerClassSingleton singleton = new InnerClassSingleton(); }}
这种方式兼顾了懒汉式模式和饿汉式模式,根据类的加载机制来说,静态内部类SingletonHolder
不会随着外部类InnerClassSingleton
的加载而加载,只会在被调用时才会加载。
这里外部类InnerClassSingleton
在被类加载器加载后,并不会去进一步加载SingletonHolder
类,从而也不会去实例化singleton
,也就避免了资源浪费的情况。而在getInstance()
方法第一次被调用时,内部类SingletonHolder
才会加载,SingletonHolder
类中声明的静态对象singleton
才会被实例化;后面每一次调用getInstance()
方法时,返回的都是此singleton
对象,保证了只有一个实例化对象的原则。
四、用反射的方式来破坏单例
讲完单例模式的几种实现方式之后,我们来讲一讲破坏单例的方式;虽然日常开发中不会怎么用到,但对面试来说,可以说是一个必考点。多了解了解,总会有意想不到的用处。
public static void main(String[] args) { try { // 用反射获得 InnerClassSingleton 类的实例 Class clazz = InnerClassSingleton.class; Constructor constructor = clazz.getDeclaredConstructor(null); // 强制访问 constructor.setAccessible(true); InnerClassSingleton instance1 = (InnerClassSingleton)constructor.newInstance(); // 单例模式获取 InnerClassSingleton instance2 = InnerClassSingleton.getInstance(); System.out.println("利用反射得到的实例对象:"+instance1); System.out.println("单例模式的实例对象:"+instance2); }catch (Exception e){ e.printStackTrace(); }}
上述的测试代码,我分别用反射的方式和单例的方式来获得InnerClassSingleton
类的实例,最后打印出来,看一看结果:
可以看出,两次创建的InnerClassSingleton
类的实例又不相同了。那怎么杜绝这种办法呢?我们可以来优化一下上述的静态内部类的代码:
// 懒汉式模式 和 饿汉式模式 兼顾public class InnerClassSingleton { // 私有化构造函数 private InnerClassSingleton(){ if (SingletonHolder.singleton != null){ throw new RuntimeException("不能以这种方式来获得实例对象......"); } } public static InnerClassSingleton getInstance(){ return SingletonHolder.singleton; } // 静态内部类 private static class SingletonHolder{ private static final InnerClassSingleton singleton = new InnerClassSingleton(); }}
主要看私有构造函数中的代码,我们将这里做了限制,当被外界调用时,直接抛出异常!测试的结果也如我们所愿:
五、用序列化的方式破坏单例
除了反射之外,用序列化的方式也能破坏单例,达到创建不一样的类的实例的效果。
先将InnerClassSingleton
类实现序列化接口:
// 懒汉式模式 和 饿汉式模式 兼顾public class InnerClassSingleton implements Serializable { // ....... 中间的代码查看上面的代码}
编写测试代码:
public static void main(String[] args) { try { InnerClassSingleton instance1 = InnerClassSingleton.getInstance(); FileOutputStream fos = new FileOutputStream("singleton.obj"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fos); objectOutputStream.writeObject(instance1); objectOutputStream.flush(); objectOutputStream.close(); fos.close(); FileInputStream fis = new FileInputStream("singleton.obj"); ObjectInputStream objectInputStream = new ObjectInputStream(fis); InnerClassSingleton instance2 = (InnerClassSingleton)objectInputStream.readObject(); objectInputStream.close(); fis.close(); System.out.println("利用单例获得实例:"+instance1); System.out.println("利用序列化获取的实例:"+instance2); }catch (Exception e){ e.printStackTrace(); }}
在上面的代码中,我们先获得InnerClassSingleton
类的实例instance1
,再将instance1
写入singleton.obj
文件当中;然后再从中取出来,转化为实例instance2
;最后将instance1
和instance2
打印出来:
可以看出,两次创建的InnerClassSingleton
类的实例又不相同了。那么这种方式的解决方案是什么呢?也不难,只需要加上一个方法就好了:
public class InnerClassSingleton implements Serializable { // ....... 代码省略 // 加上 readResolve() 方法 private Object readResolve(){ return SingletonHolder.singleton; } // 静态内部类 private static class SingletonHolder{ private static final InnerClassSingleton singleton = new InnerClassSingleton(); }}
再加上readResolve()
之后,再来测试一下:
可以看出,两次创建的实例完全相同,完美的解决了序列化的问题。那么为什么加上readResolve()
就会解决这个问题呢?这里和JDK
的源码有关,我这里就不贴源码了,不便于观看,我这里画了一个时序图,大家可以跟着这个时序图来对照JDK
源码进行查看,了解内情。
1、先从编写的测试代码里面进入ObjectInputStream
类中的readObject()
方法
2、实序图
以实序图来看,其实方法内部还是创建了一次InnerClassSingleton
类的实例,不过是后面用调用readResolve()
方法获得的InnerClassSingleton
类的实例将它替换掉了,所以打印出的结果依旧是相同的。总体来说,还是白白消耗了内存,那么再来看另一种创建单例的方式。
六、注册式单例
注册式单例又被称为登记式单例,大体分为枚举登记和容器缓存两种。
6.1 枚举登记
public enum EnumSingleton { INSTANCE; // 用来测试对象是否相同 private Object data; public Object getData() { return data; } public void setData(Object data) { this.data = data; } public static EnumSingleton getInstance(){ return INSTANCE; }}
6.1.1 序列化破坏
将上面的测试代码稍微更改一下:
public static void main(String[] args) { try { EnumSingleton instance1 = EnumSingleton.getInstance(); instance1.setData(new Object()); // ....... 查看 五、用序列化的方式破坏单例 的测试代码 EnumSingleton instance2 = (EnumSingleton)objectInputStream.readObject(); objectInputStream.close(); fis.close(); System.out.println("利用单例获得实例:"+instance1.getData()); System.out.println("利用序列化获取的实例:"+instance2.getData()); }catch (Exception e){ e.printStackTrace(); }}
结果:
由结果可以看出是可行的,那么原理是什么呢?通过上述实序图的方式查看源码:
1、ObjectInputStream
类中的readObject0()
方法:
private Object readObject0(boolean unshared) throws IOException { // ...... 省略代码 // 如果是枚举类 case TC_ENUM: return checkResolve(readEnum(unshared)); // ......}
2、readEnum()
方法
private Enum<?> readEnum(boolean unshared) throws IOException { // ...... if (cl != null) { try { // 通过Class对象 c1 和 类名 name 来获得唯一的枚举对象 @SuppressWarnings("unchecked") Enum<?> en = Enum.valueOf((Class)cl, name); result = en; } catch (IllegalArgumentException ex) { throw (IOException) new InvalidObjectException( "enum constant " + name + " does not exist in " + cl).initCause(ex); } if (!unshared) { handles.setObject(enumHandle, result); } } // ......}
通过查看源码发现,枚举类型其实通过Class 对象类和类名找到一个唯一的枚举对象;因此,枚举对象不可能被类加载器加载多次。
6.1.2 反射破坏
测试代码:
public static void main(String[] args) { try { Class clazz = EnumSingleton.class; Constructor constructor = clazz.getDeclaredConstructor(null); // 强制访问 constructor.setAccessible(true); EnumSingleton instance1 = (EnumSingleton)constructor.newInstance(); EnumSingleton instance2 = EnumSingleton.getInstance(); System.out......原文转载:http://www.shaoqun.com/a/828772.html
跨境电商:https://www.ikjzd.com/
Sunrate:https://www.ikjzd.com/w/2685
垂直电商:https://www.ikjzd.com/w/1450
笨鸟转运:https://www.ikjzd.com/w/1550
单例模式是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局的访问点。在java语言当中,有着两种方式构建单例模式:饿汉式单例和懒汉式单例。单例模式作为一种创建型模式,在日常开发中用处极广,我们先来看一一段代码://构造函数protectedCalendar(TimeZonevar1,Localevar2){this.lenient=true;this.sharedZone=false;t
春节台湾旅游 关于垦丁的超强交通攻略:http://www.30bags.com/a/419883.html
kkr:https://www.ikjzd.com/w/1340
e邮包:https://www.ikjzd.com/w/594.html?source=tagwish
2000亿美元关税听证会结束,将在9月底或10月初开征?:https://www.ikjzd.com/articles/5796
职场菜鸟如何成功入职:http://lady.shaoqun.com/a/111138.html
和别人老婆睡了一年多,小\:http://lady.shaoqun.com/a/387378.html
中美贸易战下,出口跨境电商或成"中国生产+越南制造"新模式:https://www.ikjzd.com/articles/5783
美国商标"EDEALYN"侵权,涉案产品为汽车坐垫:https://www.ikjzd.com/articles/5787
怎样"看女人"?一位父亲写给儿子的话:http://lady.shaoqun.com/m/a/43782.html
口述:老婆的男秘问我每周和老婆几次:http://lady.shaoqun.com/m/a/105021.html
跨境电商不要做红人营销,否则你的搜索量会让人窒息:https://www.ikjzd.com/articles/146035
为什么不推荐亚马逊新手卖家去做低价产品?:https://www.ikjzd.com/articles/146043
No comments:
Post a Comment