设计模式之单例模式

来源:互联网 发布:linux rsyslog配置 编辑:程序博客网 时间:2024/04/30 19:23

1、什么是单例模式?

          单例模式是一种常用的软件设计模式,是软件设计模式中最简单的形式之一,目的是为了使类的一个对象成为系统中的唯一实例。

2、为什么需要单例模式?

           单例模式是为了保证程序运行期间某些对象只有一个实例,比如代表打印资源的打印机、例如window只能打开一个任务管理器(ps :可以试下ctrl + shift +esc打开)。试想如果windows打开的多个任务管理器显示的内容一致,那么多余的将会浪费系统资源,造成不必要的浪费。再试想,如果系统中许多地方都在使用同一个类进行某些操作,而构建这个类是一个重量级的操作,那么多个地方使用都要先构建一次,使用完后进行销毁。在如果这个类占用内存比较大,这样频繁的创建销毁会导致频繁的gc,从而使程序变卡变慢。


3、单例模式怎么写?

        单例模式的三个要点:
        a)、构造函数私有
        b)、单例类持有自己的静态引用
        c)、提供静态方法返回单例

3.1、饿汉式写法

package org.yamikaze.singleton;/** * 饿汉式单例模式 * 优点是写法简单,且在多线程环境下也能正常工作 * 但缺点也很明显,在类加载的时候单列对象就产生了。 * 如果该单例对象比较重量级的话,在使用它之前会占用不必要的空间与资源 * @author yamikaze */public class HungrySingleton {private static HungrySingleton instance = new HungrySingleton();private HungrySingleton() {}public static HungrySingleton getInstance() {return instance;}}

3.2、懒汉式写法

package org.yamikaze.singleton;/** * 懒汉式单例 * 这种写法的单例比饿汉式写法要好一点,在单线程环境下也能正常工作。 * 但在多线程环境下有可能得到的两个对象不相等 * 即getInstance() == getInstance()为false。 * @author yamikaze * */public class LazySingleton {private static LazySingleton instance;private LazySingleton() {}public static LazySingleton getInstance() {if(instance == null) {instance = new LazySingleton();}return instance;}}
想测试这种情况使用以下代码:

@Testpublic void testLazySingleton01() {ExecutorService es = Executors.newCachedThreadPool();for(int i = 0; i < 100; i++) {es.execute(new Runnable() {@Overridepublic void run() {LazySingleton instance = LazySingleton.getInstance();System.out.println(instance.hashCode());}});}}
输出结果:

可以看到有一个输出结果不一致,所以这一种写法在多线程环境不安全。 

3.3 懒汉式写法-静态方法加锁

可以将3.2的写法稍微变化以下,改为下面这种
public class LazySingleton {private static LazySingleton instance;private LazySingleton() {}public synchronized static LazySingleton getInstance() {if(instance == null) {instance = new LazySingleton();} return instance;}}
这种方法解决了3.2中在多线程情况下获取的对象不想等的情况,但加上了锁意味着同一时刻只能有一个线程在访问该方法。其他线程只能阻塞在这儿。那这意味着线程的上下文切换,也会增加不必要的开销。当然将synchronized关键字加载if条件语句块上也是一样的。

3.4 懒汉式写法-双重检查

package org.yamikaze.singleton;/** * 懒汉式-双重检查 * @author yamikaze * */public class DoubleCheckSingleton {/** * 1、给实例加上volatile关键字 */private static volatile DoubleCheckSingleton instance;private DoubleCheckSingleton() {}public static DoubleCheckSingleton getInstance() {if(instance == null) {/** * 2、初始化加上synchronized块,并再次检验 */synchronized (DoubleCheckSingleton.class) {if(instance == null) {instance = new DoubleCheckSingleton();}}}return instance;}}
这种写法比3.3的写法要好上一些。但要在JAVA 5以后才能达到应有的效果。但这种方式也加了锁,在多线程环境下,加锁就意味着线程上下文切换,于是就有了下一种写法。

3.5 懒汉式-静态内部类

package org.yamikaze.singleton;/** * 静态内部类的方式 * @author yamikaze * */public class StaticClassSingleton {private StaticClassSingleton() {}public static StaticClassSingleton getInstance() {return StaticClassSingletonHolder.INSTACNE;}private static class StaticClassSingletonHolder {private static final StaticClassSingleton INSTACNE = new StaticClassSingleton();}}
这种方式使用了类的加载机制保证了单例对象的唯一性。而类加载机制保证了在同一时刻只能有一个线程执行类初始化方法,这种方式比较常用。

3.6 枚举写法

package org.yamikaze.singleton;/** * 枚举写法 * JDK5 * @author yamikaze * */public enum EnumSingleton {INSTANCE;public void doSomething() {}}
这种写法是《Effective Java》推荐的写法,很简洁,而且无偿提供了序列化机制,在面对复杂的序列化以及反射攻击的时候,绝对防止多次实例化。但需要的JDK5以上。

4、单例模式真的只有一个实例吗?

    抛开上面的枚举写法,其他几种写法真的能完全做到单例吗?
    答案当然不是。

4.1 、通过反射可以获取到不同的实例

     这儿的单例类使用3.1写法中的类,测试代码如下:
@Testpublic void testGetInstanceByReflect() throws Exception  {HungrySingleton instance = HungrySingleton.getInstance();Constructor<HungrySingleton> con = HungrySingleton.class.getDeclaredConstructor();con.setAccessible(true);HungrySingleton instance2 = con.newInstance();System.out.println(instance == instance2);//false}
通过反射你甚至可以将不可变对象String的值改变,所以通过反射可以使得这个单例类毫无意义。

 4.2 、 通过序列化获取到不同的实例

    使用java原生序列化反序列化不会调用构造函数,从而绕过了构造函数,测试代码如下:
@Testpublic void writeSingletonToFile() throws Exception{ObjectOutputStream dos = new ObjectOutputStream(new FileOutputStream("G:/a.file"));HungrySingleton instance = HungrySingleton.getInstance();dos.writeObject(instance);if(dos != null) {dos.close();}}@Testpublic void testGetInstanceBySerializable() throws Exception {ObjectInputStream ois = new ObjectInputStream(new FileInputStream("G:/a.file"));HungrySingleton instance = HungrySingleton.getInstance();HungrySingleton instance2 = (HungrySingleton)ois.readObject();System.out.println(instance == instance2); //falseif(ois != null) {ois.close();}}
先运行write方法将对象写入到文件中,然后在运行test方法读取对象与getInstance方法获取的对象作对比。
ps:需要HungrySingleton实现Serializable接口。

这种方式是可以在单例类中添加以下方法避免的:
private Object readResolve() {return getInstance();}

4.3、自定义类加载器获取实例

    在java中,比较两个‘类’是否相等,只有在这两个类是由同一个类加载器加载的前提下比较才有意义。由于java的类加载机制基于双亲委派模型加载的,可以保证一个类只有一个类加载器加载。可以看下面的例子:
public static void main(String[] args) throws Exception {ClassLoader cl = new ClassLoader() {@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {try {//违背双亲委派模型,先自己加载,自己不能加载再交给父加载器加载String str = name.substring(name.lastIndexOf(".") + 1) + ".class";InputStream is = getClass().getResourceAsStream(str);if (is == null) {return super.loadClass(name);}byte[] b = new byte[is.available()];is.read(b);return defineClass(name, b, 0, b.length);} catch (IOException e) {throw new ClassNotFoundException(name);}}};// 如果不加上newInstance(),那么返回的只是类对象,而我们要使用类对象获取对应类的实例,就要加上newInstance()Object obj = cl.loadClass("cn.yamikaze.java.basic.HelloJava").newInstance();System.out.println(obj.getClass()); //class cn.yamikaze.java.basic.HelloJava// 结果为false,因为HelloJava是有不同的类加载器加载的System.out.println(obj instanceof cn.yamikaze.java.basic.HelloJava);}
所以可以自定义类加载器来加载单例类实例。



     最后欢迎评论留言指出不足之处,谢谢! (。☌ᴗ☌。) 萌萌哒

参考资料

1、《Effective java》第二章 创建和销毁对象
2、《深入理解java虚拟机--JVM高级特性与最佳实践》第七章虚拟机类加载机制






原创粉丝点击