[-设计模式知几何-] 建立型05-单例模式

打算开始打理个人公众号了,最新内容和独家秘籍。走过路过来捧个场,关注公众号:编程之王java

本文收录于公众号编程之王: 文章内存地址sjms-o-05
如何获取更多知识干粮,详见 <<编程之王食用规范1.0>>android


1.介绍与思考

单例模式:保证一个类仅有一个实例,并提供一个访问他的全局访问点编程


1.1:单例与设计原则

单例模式像一个奇葩,和设计原则格格不入。如今对象的建立,所以无接口拓展可言
依赖倒置原则接口隔离原则迪米特原则里氏替换原则合成复用原则研究无从谈起。
单例没法派生本身的族系,全部修改都要在本体中进行,违反开放封闭原则
单例是典型的大包干,功能的集聚地,可能存在职责太重,而违反单一职责原则设计模式

  • 既然单例彻底不遵照七大设计原则,它为什么能在设计模式中立足?

设计原则旨在协调一个软件实体(类、模块、函数)之间的结构关系 。
而单例每每只是一个类,没有本身的族系和朋友圈,它就像孤独而至高的
其次是由于它真的很是简单和好用。没有抽象的族系拓展,让它能够很容易被理解。安全


1.2:单例优点与劣势
---->[优点]----
[1].全局内存中只需有一个实例对象,减少内存开销
[2].使用一个对象提供访问,避免对稀缺资源的多重占用
[3].私有化构造,提供全局的惟一访问点,严格控制访问

---->[劣势]----
[1].无接口拓展可言,全部修改都要在本体中进行
[2].可能存在职责太重,而违反单一职责原则
复制代码

1.3:本文例子

若是上线一个世界程序,一个World对象占据内存10G
世界不能随便去new,如何不让上层没法主动建立World对象,
World对象占据内存太大,服务器没法支撑多个世界对象,须要提供惟一World对象bash

  • 关于单例的几个要点:
[1].私有构造:将类的构造私有化,从而限制外界访问。
[2].延迟加载:当且仅当第一次获取单例对象是才会建立对象。
[3].线程安全:多线程时不会建立多个该类对象。
[4].防反序列化:反序列化不会建立多个该类对象。
[5].防反射:反射不会建立多个该类对象。
复制代码

1、单例的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
        });
    }
}
复制代码

3、单例下的反序列化与反射

单例的价值在于一个程序中只用一个该对象实例
若是有恶意份子经过反射建立了另外一个世界会怎么样?

1.单例的测试

经过debug看出两次获取的都是同一个世界,这就是单一实例

单例测试.png

public class God {
    public static void main(String[] args) {
        World world1 = World.getInstance();
        World world2 = World.getInstance();
    }
}
复制代码

2.经过反射建立实例

可见world3的内存地址已经不同了,说明出现了第二个世界,也就是单例的失效

反射测试.png

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.经过反序列化建立对象

若是你的单例类有序列化的需求(如,单例对象本地存储,单例对象网络传输) 反序列化造成的实例也并不是原来的实例

反序列化.png

---->[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来控制序列化对象实例

反序列化的防治.png

---->[World]-------------
//解决反序列化建立实例的问题,readResolve建立的对象会直接替换io流读取的对象
private Object readResolve() throws ObjectStreamException {
    return getInstance();
}
复制代码

4、结尾小述

1.单例抉择
[1] 肯定以及确定不会在单线程中用到的单例对象,能够用单线程的懒汉
[2] 单例对象不大,并不介意在类加载时实例化对象,枚举首选,其次是饿汉
[3] 若是要在多线程的时候彻底防反射,双检锁模式不能够。可以使用静态初始化的几种模式,在建立对象时进行非空校验便可
复制代码
2.常见的单例
java.util.Calendar 标准单例,经过Calendar.getInstance方法获取对象
java.lang.System 彻底单例,不提供外部构造方法,所有以静态方法提供服务
android.view.LayoutInflater 标准单例 ,经过LayoutInflater.from(Context)方法获取对象
复制代码

后记:捷文规范

1----本文由张风捷特烈原创,转载请注明
2----若是有什么想要交流的,欢迎留言。也能够加微信:zdl1994328
3----我的能力有限,若有不正之处欢迎你们批评指证,一定虚心改正
4----看到这里,我在此感谢你的喜欢与支持,扫码关注-编程之王

icon_wx_200.png
相关文章
相关标签/搜索