单例模式(四)
单例模式
单例模式(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<String,Object> ioc = new ConcurrentHashMap<String,Object>(); 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容器
您的资助是我最大的动力!
金额随意,欢迎来赏!