1.介绍与思考
单例模式:
保证一个类仅有一个实例,并提供一个访问他的全局访问点
1.1:单例与设计原则
单例模式像一个奇葩,和设计原则格格不入。现在对象的创建,因此无接口拓展可言
依赖倒置原则
、接口隔离原则
、迪米特原则
、里氏替换原则
、合成复用原则
研究无从谈起。
单例无法派生自己的族系,所有修改都要在本体中进行,违反开放封闭原则
单例是典型的大包干,功能的集聚地,可能存在职责过重,而违反单一职责原则
既然单例完全不遵守七大设计原则,它为何能在设计模式中立足?
设计原则旨在协调
一个软件实体(类、模块、函数)
之间的结构关系 。
而单例往往只是一个类,没有自己的族系和朋友圈,它就像孤独而至高的王
。
其次是因为它真的非常简单和好用。没有抽象的族系拓展,让它可以很容易被理解。
1.2:单例优势与劣势
---->[优势]---- [1].全局内存中只需有一个实例对象,减小内存开销 [2].使用一个对象提供访问,避免对稀缺资源的多重占用 [3].私有化构造,提供全局的唯一访问点,严格控制访问 ---->[劣势]---- [1].无接口拓展可言,所有修改都要在本体中进行 [2].可能存在职责过重,而违反单一职责原则 复制代码
1.3:本文例子
如果上线一个世界程序,
一个World对象占据内存10G
世界不能随便去new,如何不让上层无法主动创建World对象,
World对象占据内存太大,服务器无法支撑多个世界对象,需要提供唯一World对象
关于单例的几个要点:
[1].私有构造:将类的构造私有化,从而限制外界访问。 [2].延迟加载:当且仅当第一次获取单例对象是才会创建对象。 [3].线程安全:多线程时不会创建多个该类对象。 [4].防反序列化:反序列化不会创建多个该类对象。 [5].防反射:反射不会创建多个该类对象。 复制代码
一、单例的n种形式--形式上的一切都仅是开始而已
1.极简单例(饿汉)
作为静态变量直接创建,最大的缺点是单例对象为没有延迟加载性
public class World { private final static World sWorld = new World(); //[1]私有化构造 private World() { initWorld();//初始化世界 System.out.println("世界已创建"); } private void initWorld() { } //[2]返回内部静态实例 public static World getInstance() { return sWorld; } }
2.单线程懒加载(懒汉)
最大的缺点是线程不安全,怎么个不安全法,且听我细细道来。
public class World { private static World sWorld = null; //[1]私有化构造 private World() { initWorld();//初始化世界 System.out.println("世界已创建"); } private void initWorld() { } //[2]返回内部静态实例 public static World getInstance() { if (sWorld==null){ sWorld=new World(); } return sWorld; } }
之所以称为单例,是因为在多次调用getInstance获取实例时是相同实例,且构造只执行一次
public class Client { public static void main(String[] args) { World world = World.getInstance(); World world2 = World.getInstance(); World world3 = World.getInstance(); System.out.println(world);//World@41cf53f9 System.out.println(world2);//World@41cf53f9 System.out.println(world3);//World@41cf53f9 } }
之所以说线程不安全,因为多线程下
sWorld==null
可能被多次通过,所以实例化多个对象。
演示一下,在一个Machine的Runnable对象中调用了World.getInstance()
来获取World对象
public class Machine implements Runnable { public void run() { World.getInstance(); } }
这时在Client中创建1000个线程去使用这个World,千人同时在线,每个用户一个访问线程
如果不作线程安全处理,就会创建多个世界,如果一个世界的渲染需要10G内存,结果可想而知,这样单例就没有意义了。
public class Client { public static void main(String[] args) { for (int i = 0; i < 1000; i++) { new Thread(new Machine()).start(); } } }
如果你会多线程调试,可以自己干预一下线程的执行。
3.懒汉双检锁
第一检--该对象是否非空,
为空才进行同步锁定
第二检--该对象是否非空,为空才创建实例
public class World { private volatile static World sWorld; //[1]私有化构造 private World() { initWorld();//初始化世界 System.out.println("世界已创建"); } private void initWorld() { } //[2]返回内部静态实例 public static World getInstance() { if (sWorld == null) {//判断非空后--执行 synchronized (World.class) {//加锁,保证多线程下的单例 if (sWorld == null) {//非空,创建实例 sWorld = new World(); } } } return sWorld; } }
这样无论多少个线程World都只会创建一次。虽然synchronized同步会影响一丢丢性能
不过进行了双检,只要有sWorld被创建了,是不会走同步的,测试了一下10000000
个线程通过第一检的也就10几个,所以这样挺完美的。
关于
指令重排序
一些时候
指令重排序
会将2和3步骤调换来提高性能。但并非百分百都会重排序。
这在单线程中并没有什么威胁,但这里多线程中sWorld == null
如果发生重排序,sWorld
指向内存空间,就会非空,如果实例化还没有来及。
下一个线程进入就会获取到一个未初始化完成的对象,在使用它时会空指针异常。
解决方案很简单在实例声明时加上volatile关键字
即可。
4.静态内部类
原理:
Class对象的初始化锁
。和上面的功能基本,所以我喜欢这个
public class World { //[1]私有化构造 private World() { initWorld();//初始化世界 System.out.println("世界已创建"); } private void initWorld() { } //[3]返回内部静态实例 public static World getInstance() { return WorldHolder.sWorld; } //[2]创建内部类创建实例 private static class WorldHolder { private static final World sWorld = new World(); } }
5.至简--枚举
枚举默认私有化构造器,防反射,防反序列化。
public enum World { INSTANCE; World() { initWorld();//初始化世界 System.out.println("世界已创建"); } private void initWorld() { } }
关于枚举:下面是通过jad反编译得到的枚举源码,可见枚举在JVM的眼中也只是一个类而已,
并且私有化构造
+静态代码块初始实例
,天然的单例材料。由于静态代码块初始实例,所以不是懒加载
命令:jad -s .java -8 World.class
package com.toly1994.dp.creational.singleton.world.enum_; import java.io.PrintStream; public final class World extends Enum{ public static World[] values(){ return (World[])$VALUES.clone(); } public static World valueOf(String name){ return (World)Enum.valueOf(com/toly1994/dp/creational/singleton/world/enum_/World, name); } private World(String s, int i){ super(s, i); initWorld(); System.out.println("\u4E16\u754C\u5DF2\u521B\u5EFA"); } private void initWorld(){//私有化构造 } public static final World INSTANCE;//静态实例 private static final World $VALUES[]; static //静态代码块初始实例 { INSTANCE = new World("INSTANCE", 0); $VALUES = (new World[] { INSTANCE }); } }
三、单例下的反序列化与反射
单例的价值在于一个程序中只用一个该对象实例
如果有恶意份子通过反射创建了另一个世界会怎么样?
1.单例的测试
通过debug看出两次获取的都是同一个世界,这就是单一实例
public class God { public static void main(String[] args) { World world1 = World.getInstance(); World world2 = World.getInstance(); } }
2.通过反射
创建实例
可见
world3
的内存地址已经不一样了,说明出现了第二个世界,也就是单例的失效
public class God { public static void main(String[] args) { World world1 = World.getInstance(); World world2 = World.getInstance(); //通过反射创建 Class<World> worldClass = World.class; try { Constructor<World> constructor = worldClass.getDeclaredConstructor(null); constructor.setAccessible(true); World world3 = constructor.newInstance(); System.out.println(world3==world2);//false System.out.println(world1==world2);//true } catch (Exception e) { e.printStackTrace(); } } }
3.通过反序列化
创建对象
如果你的单例类有序列化的需求(如,单例对象本地存储,单例对象网络传输) 反序列化形成的实例也并非原来的实例
---->[World]------------- public class World implements Serializable { ---->[God]------------- public class God { public static void main(String[] args) { World world1 = World.getInstance(); World world2 = World.getInstance(); //通过反射创建 Class<World> worldClass = World.class; try { Constructor<World> constructor = worldClass.getDeclaredConstructor(null); constructor.setAccessible(true); World world3 = constructor.newInstance(); System.out.println(world3 == world2);//false System.out.println(world1 == world2);//true } catch (Exception e) { e.printStackTrace(); } //通过反序列化创建对象 try { //序列化输出 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("world.obj")); oos.writeObject(world1); //反序列化创建对象 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("world.obj")); World world4 = (World) ois.readObject(); ois.close(); System.out.println(world1 == world4);//false } catch (Exception e) { e.printStackTrace(); } } }
4.发序列化的解决方案
通过反序列化时的钩子函数:
readResolve
来控制序列化对象实例
---->[World]------------- //解决反序列化创建实例的问题,readResolve创建的对象会直接替换io流读取的对象 private Object readResolve() throws ObjectStreamException { return getInstance(); }
四、结尾小述
1.单例抉择
[1] 确定以及肯定不会在单线程中用到的单例对象,可以用单线程的懒汉 [2] 单例对象不大,并不介意在类加载时实例化对象,枚举首选,其次是饿汉 [3] 如果要在多线程的时候完全防反射,双检锁模式不可以。可使用静态初始化的几种模式,在创建对象时进行非空校验即可
2.常见的单例
java.util.Calendar 标准单例,通过Calendar.getInstance方法获取对象 java.lang.System 完全单例,不提供外部构造方法,全部以静态方法提供服务 android.view.LayoutInflater 标准单例 ,通过LayoutInflater.from(Context)方法获取对象