JUC并发编程 --单例模式

Posted kuzhongyk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC并发编程 --单例模式相关的知识,希望对你有一定的参考价值。

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

注意:

  1. 单例类只能有一个实例。

  2. 单例类必须自己创建自己的唯一实例。

  3. 单例类必须给所有其他对象提供这一实例。


使用场景:

  1. 要求产生唯一序列号。

  2. WEB中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。

  3. 创建的一个对象需要消耗的资源过多,比如I/O与数据库的连接等。


饿汉式单例模式

优点:没有加锁,执行效率会提高。

缺点:类加载时就初始化,浪费内存。

package com.ning.single;
/** * @author 16790 * 饿汉式单例模式 */public class Hungry {
private Hungry(){}
private static Hungry hungry=new Hungry();
public static Hungry getInstance(){ return hungry; }
}


懒汉式单例模式
package com.ning.single;
/** * @author 16790 * 懒汉式单例模式 不支持多线程,在多线程下,这种模式不安全 */public class LazyMan {
private LazyMan(){}
private static LazyMan lazyMan;
public static LazyMan getInstance(){ if (lazyMan==null){ lazyMan=new LazyMan(); } return lazyMan; }}


DCL 懒汉式单例模式(DCL即双检锁 double-checked locking)

这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

package com.ning.single;
/** * @author 16790 * DCL懒汉式单例模式 */public class DCLLazyMan { private DCLLazyMan(){}    //这里为什么要使用volatile?    /**      dclLazyMan=new DCLLazyMan();这个不是原子操作,要防止指令重排      为什么不是原子操作?      1.分配内存空间      2.执行构造方法,初始化对象      3.把这个对象指向这个空间    */ private static volatile DCLLazyMan dclLazyMan; public static DCLLazyMan getInstance(){ if (dclLazyMan==null){        //同步代码块 synchronized (DCLLazyMan.class){ dclLazyMan=new DCLLazyMan(); } } return dclLazyMan; }}


静态内部类

这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

package com.ning.single;
/** * @author 16790 * 静态内部类 */public class StaticClass {
private StaticClass(){}
public static StaticClass getInstance(){ return InnerClass.STATIC_CLASS;    }    //内部类 public static class InnerClass{ private static final StaticClass STATIC_CLASS=new StaticClass(); }}


枚举

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。

package com.ning.single;
/** * @author 16790 * 枚举 */public enum EnumSingle {    INSTANCE; public EnumSingle getInstance(){ return INSTANCE;    }}



单例不安全,反射和序列化可以破坏单例


使用反射创建对象

package com.ning.single;
import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;
/** * @author 16790 * 懒汉式单例模式 */public class LazyMan {
private LazyMan(){}
private static LazyMan lazyMan;
public static LazyMan getInstance(){ if (lazyMan==null){ lazyMan=new LazyMan(); } return lazyMan; }}
class test{ public static void main(String[] args) throws Exception { LazyMan lazyMan=LazyMan.getInstance(); Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null); constructor.setAccessible(true); LazyMan lazyMan1 = constructor.newInstance(); System.out.println(lazyMan); System.out.println(lazyMan1); }}
测试结果:com.ning.single.LazyMan@1b6d3586com.ning.single.LazyMan@4554617c

分析:从结果来看,创建了两个单例对象,这样就破坏了单例模式。破坏的原因是将构造方式的private的检查,通过constructor.setAccessible(true)进行了屏蔽。


解决办法:将构造方法的调用次数设置一个标志,来进行表示调用次数,超过一次之后,在调用构造方法直接抛出异常。

package com.ning.single;
import java.lang.reflect.Constructor;
/** * @author 16790 * DCL懒汉式单例模式 */public class DCLLazyMan { private static boolean flag=false;
private DCLLazyMan(){ synchronized (DCLLazyMan.class){ if (flag==false){ flag=true; }else{ throw new RuntimeException("不能创建多个实例"); } } }
private static volatile DCLLazyMan dclLazyMan;
public static DCLLazyMan getInstance(){ if (dclLazyMan==null){ synchronized (DCLLazyMan.class){ dclLazyMan=new DCLLazyMan(); } } return dclLazyMan; }}class Test{ public static void main(String[] args) throws Exception{ Constructor<DCLLazyMan> constructor = DCLLazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true); DCLLazyMan dclLazyMan = constructor.newInstance(); DCLLazyMan dclLazyMan2= constructor.newInstance();
System.out.println(dclLazyMan); System.out.println(dclLazyMan2); }}

结果:


序列化破坏单例模式

测试

package com.ning.single;
import java.io.*;import java.lang.reflect.Constructor;
/** * @author 16790 * DCL懒汉式单例模式 */public class SerializableTest implements Serializable { private static boolean flag=false;
private SerializableTest(){ synchronized (SerializableTest.class){ if (flag==false){ flag=true; }else{ throw new RuntimeException("不能创建多个实例"); } } }
private static volatile SerializableTest serializableTest;
public static SerializableTest getInstance(){ if (serializableTest==null){ synchronized (SerializableTest.class){ serializableTest=new SerializableTest(); } } return serializableTest; }}class sTest{ public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")); oos.writeObject(SerializableTest.getInstance());
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("tempFile"))); SerializableTest serializableTest1 = (SerializableTest) ois.readObject(); //判断是否是同一个对象 System.out.println(serializableTest1); System.out.println(SerializableTest.getInstance()); }}

结果:


输出的结果不一样说明:

通过对该类的序列化与反序列化得到的一个对象是一个新对象。

为什么序列化会破坏单列模式?

这里重点看一下readOrdinaryObject方法的代码:

private Object readOrdinaryObject(boolean unshared)        throws IOException //此处省略部分代码 Object obj; try {        /**        isInstantiable():如果一个serializable/externalizable的类可以        在运行时被实例化,那么该方法就返回true。         desc.newInstance:该方法通过反射的方式调用无参构造方法新建        一个对象。        */ obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception ex) { throw (IOException) new InvalidClassException( desc.forClass().getName(), "unable to create instance").initCause(ex);        }  //此处省略部分代码 if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { // Filter the replacement object if (rep != null) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1); } } handles.setObject(passHandle, obj = rep); }        } return obj; }

所以也可以解释为什么序列化可以破坏单例了?

序列化会通过反射调用无参数的构造方法创建一个新的对象。


防止序列化破坏单例模式:

只要在该类中定义readResolve()方法就可以解决该问题:

package com.ning.single;
import java.io.*;import java.lang.reflect.Constructor;
/** * @author 16790 * DCL懒汉式单例模式 */public class SerializableTest implements Serializable { private static boolean flag=false;
private SerializableTest(){ synchronized (SerializableTest.class){ if (flag==false){ flag=true; }else{ throw new RuntimeException("不能创建多个实例"); } } }
private static volatile SerializableTest serializableTest;
public static SerializableTest getInstance(){ if (serializableTest==null){ synchronized (SerializableTest.class){ serializableTest=new SerializableTest(); } } return serializableTest; }
public Object readResolve(){ return serializableTest; }}class sTest{ public static void main(String[] args) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile")); oos.writeObject(SerializableTest.getInstance());
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("tempFile"))); SerializableTest serializableTest1 = (SerializableTest) ois.readObject(); //判断是否是同一个对象 System.out.println(serializableTest1); System.out.println(SerializableTest.getInstance()); }}

原理:

还是在readOrdinaryObject方法中

//此处省略部分代码/**hasReadResolveMethod():如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回trueinvokeReadResolve():通过反射的方式调用要被反序列化的类的readResolve方法。*/ if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { // Filter the replacement object if (rep != null) { if (rep.getClass().isArray()) { filterCheck(rep.getClass(), Array.getLength(rep)); } else { filterCheck(rep.getClass(), -1); } } handles.setObject(passHandle, obj = rep); } } return obj; }


以上是关于JUC并发编程 --单例模式的主要内容,如果未能解决你的问题,请参考以下文章

Java并发编程系列之三JUC概述

一文总结 JUC 并发编程

JUC并发编程线程池及相关面试题 详解

手写笔记23:初探JUC并发编程

并发编程-阻塞队列&JUC常用工具

JUC并发编程与源码分析