博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
单例模式(四)
阅读量:6515 次
发布时间:2019-06-24

本文共 12205 字,大约阅读时间需要 40 分钟。

 

单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

 

介绍

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当您想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

应用实例: 1、一个党只能有一个主席。 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

优点: 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。 2、避免对资源的多重占用(比如写文件操作)。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景: 1、要求生产唯一序列号。 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

应用场景: 保证一个类仅有一个实例, 并提供一个访问它的全局访问点。

Spring 中的单例模式完成了后半句话, 即提供了全局的访问点 BeanFactory。 但没有从构造器级别去
控制单例, 这是因为 
Spring 管理的是是任意的 Java 对象。 Spring 下默认的 Bean 均为单例。

归类 特点 穷举
创建型模式 保证从系统启动到系统终止, 全过程只会产生一个实
例。
当我们在应用中遇到功能性冲突的时候, 需要使用单
例模式。
配置文件、 日历、 IOC 容器

常用单例模式写法: 饿汉式、 懒汉式、 注册式、 序列化。 

单例模式:初衷就是为了使资源能够共享,只需要赋值或者初始化一次,大家都能够重复利用。

 

饿汉式:绝对的线程安全,在线程还没出现以前就已经实例化了,不可能存在访问安全问题。

在实例使用之前,不管你用不用,我都先new出来再说,避免了线程安全问题。

package com.tzy.Singleton;/** * @author Heaton * @date 2018/3/29 0029 21:26 * @describe 饿汉式单例 */public class Hungry {//它是在类加载的时候就立即初始化,并且创建单例对象    //优点:没有加任何的锁、执行效率比较高,    //在用户体验上来说,比懒汉式更好    //缺点:类加载的时候就初始化,不管你用还是不用,我都占空间    //浪费了内存,有可能占着茅坑不拉屎    private Hungry() {    }    private static final Hungry hungry = new Hungry();    public static Hungry getInstance() {        return hungry;    }}

懒汉式:默认加载的时候不实例化,在需要用到这个实例的时候才实例化,延时加载。

package Singleton;/** * @author Heaton * @date 2018/3/29 0029 21:54 * @describe 懒汉式单例 线程不安全 */public class LazyOne {    private LazyOne() {    }    //静态块,公共内存区域    private static LazyOne lazy = null;    public static LazyOne getInstance() {        //调用方法之前,先判断        //如果没有初始化,将其进行初始化,并且赋值        //将该实例缓存好        if (lazy == null) {            lazy = new LazyOne();        }        return lazy;    }}
package com.tzy.Singleton;import java.util.concurrent.CountDownLatch;/*** @author Heaton* @date 2018/3/29 0029 23:04* @describe 线程安全测试*/public class ThreadSafeTest {    public static void main(String[] args) {        int count = 200;        //发令枪,我就能想到运动员        CountDownLatch latch = new CountDownLatch(count);        long start = System.currentTimeMillis();        for (int i = 0; i < count;i ++) {            new Thread("线程"+i){                @Override                public void run() {                    try{                        try {                            // 阻塞                            // count = 0 就会释放所有的共享锁                            // 万箭齐发                            latch.await();                        }catch(Exception e){                            e.printStackTrace();                        }                        //必然会调用,可能会有很多线程同时去访问getInstance()                        Object obj = LazyOne.getInstance();                        System.out.println(this.getName()+"--"+System.currentTimeMillis() + ":" + obj);                    }catch (Exception e){                        e.printStackTrace();                    }                }            }.start(); //每循环一次,就启动一个线程,具有一定的随机性            //每次启动一个线程,count --            latch.countDown();        }        long end = System.currentTimeMillis();        System.out.println("总耗时:" + (end - start));    }}

发现问题,出现了不同的对象,单例就失败了,证明线程不安全,所以就有锁机制出来

package com.tzy.Singleton;/*** @author Heaton* @date 2018/3/29 0029 23:23* @describe 懒汉--二加锁*/public class LazyTwo {    private LazyTwo(){}    private static LazyTwo lazy = null;    public static synchronized LazyTwo getInstance(){        if(lazy == null){            lazy = new LazyTwo();        }        return lazy;    }}
package com.tzy.Singleton;/*** @author Heaton* @date 2018/3/29 0029 23:26* @describe 性能测试*/public class LazyTest {    public static void main(String[] args) {        long start = System.currentTimeMillis();        for (int i = 0; i < 200000000;i ++) {            Object obj = LazyOne.getInstance();        }        long end = System.currentTimeMillis();        System.out.println("总耗时:" + (end - start));        long start1 = System.currentTimeMillis();        for (int i = 0; i < 200000000;i ++) {            Object obj = LazyTwo.getInstance();        }        long end1 = System.currentTimeMillis();        System.out.println("总耗时:" + (end1 - start1));    }}

发现加锁好慢啊。。。。(线程倒是安全了)怎么办好纠结!!!

见证奇迹的时刻到了------

package com.tzy.Singleton;/*** @author Heaton* @date 2018/3/29 0029 23:28* @describe 超级饿汉666*///特点:在外部类被调用的时候内部类才会被加载    //内部类一定是要在方法调用之前初始化    //巧妙地避免了线程安全问题    //这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题    //完美地屏蔽了这两个缺点    //史上最牛B的单例模式的实现方式public class LazyThree {    private static boolean initialized = false;    //默认使用LazyThree的时候,会先初始化内部类    //如果没使用的话,内部类是不加载的    private LazyThree(){        synchronized (LazyThree.class){            if(initialized == false){                initialized = !initialized;            }else{                throw new RuntimeException("单例已被侵犯");            }        }    }    //每一个关键字都不是多余的    //static 是为了使单例的空间共享    //final 保证这个方法不会被重写,重载    public static final LazyThree getInstance(){        //在返回结果以前,一定会先加载内部类        return LazyHolder.LAZY;        //相当于第一次返回时发现内部类没加载就加载内部类,        //内部类里有共享的不可变的内部单例,而第二次调用时,        //返现有内部类,故而直接调用,实现单例,提高性能    }    //默认不加载    private static class LazyHolder{        private static final LazyThree LAZY = new LazyThree();    }}
package com.tzy.Singleton;import java.lang.reflect.Constructor;/*** @author Heaton* @date 2018/3/29 0029 23:39* @describe 超级饿汉反射测试*/public class LazyThreeTest {    public static void main(String[] args) {        try{            Class
clazz = LazyThree.class; //通过反射拿到私有的构造方法 Constructor c = clazz.getDeclaredConstructor(null); //强制访问 c.setAccessible(true); //暴力初始化 Object o1 = c.newInstance(); //调用了两次构造方法,相当于new了两次 //犯了原则性问题, Object o2 = c.newInstance(); System.out.println(o1 == o2);// Object o2 = c.newInstance(); }catch (Exception e){ e.printStackTrace(); } }}

锁住了反射调用,提供了安全的反射操作,并且性能是杠杠的哦

package Singleton;/*** @author Heaton* @date 2018/3/29 0029 23:26* @describe 性能测试*/public class LazyTest {    public static void main(String[] args) {        long start = System.currentTimeMillis();        for (int i = 0; i < 200000000;i ++) {            Object obj = LazyOne.getInstance();        }        long end = System.currentTimeMillis();        System.out.println("总耗时:" + (end - start));        long start1 = System.currentTimeMillis();        for (int i = 0; i < 200000000;i ++) {            Object obj1 = LazyTwo.getInstance();        }        long end1 = System.currentTimeMillis();        System.out.println("总耗时:" + (end1 - start1));        long start2 = System.currentTimeMillis();        for (int i = 0; i < 200000000;i ++) {            Object obj2 = LazyThree.getInstance();        }        long end2 = System.currentTimeMillis();        System.out.println("总耗时:" + (end2 - start2));    }}

注册登记式:每使用一次,都往一个固定的容器中去注册并且将使用过的对象进行缓存,下次去

取对象的时候,就直接从缓存中取值,以保证每次获取的都是同一个对象,(IOC的单例就是这样)

package com.tzy.Singleton;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;/*** @author Heaton* @date 2018/3/30 0030 13:48* @describe 注册试单例*///Spring中的做法,就是用这种注册式单例public class BeanFactory {    private BeanFactory(){}    //线程安全    private static Map
ioc = new ConcurrentHashMap
(); public static synchronized Object getBean(String className){ if(!ioc.containsKey(className)){ Object obj = null; try { obj = Class.forName(className).newInstance(); ioc.put(className,obj); } catch (Exception e) { e.printStackTrace(); } return obj; }else{ return ioc.get(className); } }}
package com.tzy.Singleton;/*** @author Heaton* @date 2018/3/30 0030 13:51* @describe spring提供一个对象,然后单例创建他*/public class Pojo {}
package com.tzy.Singleton;import java.util.concurrent.CountDownLatch;/*** @author Heaton* @date 2018/3/30 0030 13:49* @describe 注册式单例测试*/public class BeanFactoryTest {    public static void main(String[] args) {        int count = 200;        //发令枪,我就能想到运动员        CountDownLatch latch = new CountDownLatch(count);        long start = System.currentTimeMillis();        for (int i = 0; i < count;i ++) {            new Thread(){                @Override                public void run() {                    try{                        try {                            // 阻塞                            // count = 0 就会释放所有的共享锁                            // 万箭齐发                            latch.await();                        }catch(Exception e){                            e.printStackTrace();                        }                        //必然会调用,可能会有很多线程同时去访问getInstance()                        Object obj = BeanFactory.getBean("com.tzy.Singleton.Pojo");                        System.out.println(System.currentTimeMillis() + ":" + obj);                    }catch (Exception e){                        e.printStackTrace();                    }                }            }.start(); //每循环一次,就启动一个线程,具有一定的随机性            //每次启动一个线程,count --            latch.countDown();        }        long end = System.currentTimeMillis();        System.out.println("总耗时:" + (end - start));    }}

序列化和反序列化保证单例:重写readResolve()

package com.tzy.Singleton;import java.io.Serializable;/*** @author Heaton* @date 2018/3/30 0030 14:16* @describe 序列化式单例 *            业务主要解决需要修改的配置文件等信息, *            如果配置文件修改,主机宕机,那么内存中的配置文件就会丢失。 *            就需要读取到硬盘等储存上*///反序列化时导致单例破坏public class Seriable implements Serializable {    /*    一个类实现了 Serializable接口, 我们就可以把它往内存地写再从内存里读出而"组装"成一个跟原来一模一样的对象.    不过当序列化遇到单例时,这里边就有了个问题: 从内存读出而组装的对象破坏了单例的规则.    单例是要求一个JVM中只有一个类对象的, 而现在通过反序列化,一个新的对象克隆了出来.     */    //序列化就是说把内存中的状态通过转换成字节码的形式    //从而转换一个IO流,写入到其他地方(可以是磁盘、网络IO)    //内存中状态给永久保存下来了    //反序列化    //讲已经持久化的字节码内容,转换为IO流    //通过IO流的读取,进而将读取的内容转换为Java对象    //在转换过程中会重新创建对象new    public  final static Seriable INSTANCE = new Seriable();    private Seriable(){}    public static  Seriable getInstance(){        return INSTANCE;    }    /*private  Object readResolve(){        return  INSTANCE;    }*/}
package com.tzy.Singleton;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;/** * Created by Tom on 2018/3/8. */public class SeriableTest {    public static void main(String[] args) {        Seriable s1 = null;        Seriable s2 = Seriable.getInstance();        FileOutputStream fos = null;        try {            fos = new FileOutputStream("Seriable.obj");            ObjectOutputStream oos = new ObjectOutputStream(fos);            oos.writeObject(s2);            oos.flush();            oos.close();            FileInputStream fis = new FileInputStream("Seriable.obj");            ObjectInputStream ois = new ObjectInputStream(fis);            s1 = (Seriable)ois.readObject();            ois.close();            System.out.println(s1);            System.out.println(s2);            System.out.println(s1 == s2);        } catch (Exception e) {            e.printStackTrace();        }    }}

发现对象是不同的,在反序列化的时候。

所以我们可以重写readResolve()方法。

那么结果就会变为

readResolve()

 

总结一下:

单例模式就是个用户提供一个 独特且唯一的 实例。

饿汉式:上来就给用户一个实例,管你要不要,那么就会对用户产生负担(没有使用,内存消耗),像一个人买东西,你就给他

            购物车,袋子等等,他不一定会用到购物车啊。(好处就是线程安全,线程没有启用的时候,对象就有了)

懒汉式:比如用户买东西,你会问他需要什么,如果需要购物车,就看有没有,如果有就给他用,用完了归还,没有就给他

            造一个,用完了在归还。(那么如果同时来了2个用户,就需要造2个,相当于2个线程同时访问),会产生线程问题,

            有可能会造出2个,那么就破坏了单例,这时就需要给程序对象上锁。那么就会存在有一个线程等待的问题,会造成

            执行效率低下。

有没有可以解决不消耗内存的单例模式呢

加强版单例模式(饿汉加强)

我们知道静态内部类只有在外部类加载的时候才会创建,我们可以给静态内部类里,加入一个可以共用的内存空间,来储存

我们想要的这个单例。只有在用户需要的时候,我们在调用一个方法来加载我们的饿汉,饿汉被加载的时候会去加载静态内

部类,如果没有用户调用,那么静态内部类是不会被加载的,节约了内存。并且线程安全。

这样做的同时还有一个问题需要我们考虑,如果饿汉在创建的时候就加载了2个饿汉呢?也会存在不安全。这时我们可以锁住

反射对象,告诉程序,你创建2个就是不行,对饿汉的创建加入锁的机制,这样避免饿汉被反射创建两次,就可以达到线程安

全,使程序只是在创建饿汉上执行锁机制,一旦有了饿汉,那么程序运行时不在调用时访问锁,及达到线程安全,又达到效率

提高,而且不会产生空消耗。

IOC其实就是解决了内存消耗问题的map容器

 

 

 

转载于:https://www.cnblogs.com/ttzzyy/articles/9000360.html

你可能感兴趣的文章
继爆款超级账本后,IBM再次推出新产品
查看>>
贝壳金控赵文乐:基于 Spring Cloud 的服务治理实践
查看>>
Pyspider框架 —— Python爬虫实战之爬取 V2EX 网站帖子
查看>>
区域生长算法 C++实现
查看>>
数据分析-从入门到崩溃
查看>>
web.xml 中的listener、 filter、servlet 加载顺序
查看>>
MyBatis原理简介和小试牛刀
查看>>
js部分基础
查看>>
Docker 常用基础命令
查看>>
脏读,幻读,不可重复读解释和例子
查看>>
Day02 数值运算&条件判断
查看>>
Tomcat指定(JDK路径)JAVA_HOME而不用环境变量
查看>>
Bluemix专属版本落地中国 开放物联网和认知计算能力
查看>>
汤姆大叔的6道javascript编程题题解
查看>>
【世界知名量子科学家加盟阿里】施尧耘出任阿里云量子技术首席科学家
查看>>
DataCore对外出售其虚拟化软件产品
查看>>
说说云计算与移动管理
查看>>
T-Mobile美国使用28GHz频段测试5G
查看>>
如何缓解影子云服务安全风险?
查看>>
Bossies 2016:最佳开源大数据工具
查看>>