设计模式——单例模式(懒汉、饿汉)

定义

保证一个类仅有一个实例,并提供一个全局访问点

类型

创建型

适用场景

想保证任何情况下都绝对只有一个实例。(数据库连接池。。。。。)

优点

1、在内存里只有一个实例,减少了内存开销。

2、可以避免对资源的多重占用

3、设置全局访问点,严格控制访问。

缺点

1、没有接口,扩展困难

重点

  1. 私有构造器
  2. 线程安全
  3. 延迟加载
  4. 序列化与反序列化安全
  5. 反射

懒汉模式

public class LazySingleton {
    private static LazySingleton lazySingleton=null;
    private LazySingleton(){

    }
    public static LazySingleton getInstance(){
        //线程不安全
        if(lazySingleton==null){
            lazySingleton=new LazySingleton();
        }
        return lazySingleton;
    }
}

为什么说线程不安全呢?我们来测试一下

public class T implements Runnable{
    @Override
    public void run() {
        LazySingleton lazySingleton=LazySingleton.getInstance();
        System.out.println(Thread.currentThread().getName()+" "+lazySingleton);
    }
}
public class Test {
    public static void main(String[] args) {
        Thread t1=new Thread(new T());
        Thread t2=new Thread(new T());
        t1.start();
        t2.start();
        System.out.println("ok");
    }
}

我们分别在LazySingleton类的if(lazySingleton==null)处、T类的system处、Test的System处打上断点并设置为Thread

(如何操作左键打好断点、在断点上右键)

首先我们直接运行

表面上看,我们得到的是一个对象,但是真相是怎样的?

现在我们用debug的方式去执行。

首先我们选择Thread-0单步执行

可以看到它首先去判断lazySingleton是否为null,这个时候他是null,我们继续单步执行。

可以看到达17行是还为null因为他的赋值还没有完成。

这时,我们切换到Thread-1上,单步。

这个时候还为空,这是因为Thread-0还没赋值完成,单步进入17行。

然后我们切汇Thread-0单步。

这时已经完成为503。

我们切回Thread-1。

这个时候他还没有执行但是已经是504了,我们单步执行。

执行完成后编号为505。

这就说明在多线程的情况下这种懒汉模式会生成不止一个实例。违背了初衷。

我们切回Thread-0。

这时lazySinleton已经被Thread-1重新赋值了为505。

我们让线程执行完。

从控制台看到,两个线程拿到的还是同一个对象。但是通过刚才的debug,可以知道其实是不同实例,Threa-1进行重新赋值,然后才进行return的。

怎么输出不同的实例呢?

选择Thread-0单步执行到17行。

 

再切换到Thread-1单步执行到17行。

我们让Thread直接执行。

在切回Thread-0。

Thread-0这时是504继续执行完。

最后就可以获得两个不同的实例对象。

所以就算是控制台输出相同的对象,但是在具体过程中也可能会产生不同的对象。

所以获得相同对象、和获得不同对象具有随机性。这与我们使用单例模式的初衷相违背。

改进

在getInstance上加上synchronized,让这个方法成为同步方法。

如果锁加在静态方法上,锁的就是这个类的class文件,如果这个方法不是静态方法,则锁的是堆内存上的对象。

public  static LazySingleton getInstance(){
    synchronized(LazySingleton.class){
        if(lazySingleton==null){
            lazySingleton=new LazySingleton();
        }
        return lazySingleton;
    }
}
public synchronized static LazySingleton getInstance(){
    if(lazySingleton==null){
        lazySingleton=new LazySingleton();
    }
    return lazySingleton;
}

 

这两种方法是等价的。

这时候我们debug当Thread-0进入getInstance方法Thread-1是不能进入的。这就保证我们始终生成的是一个实例。

 

Double Check(双重从检查锁)

单线程情况下2,3步骤是可以颠倒的。但是在多线程情况先就会出现问题(重排序问题)

当出现重排序问题,线程0分配内存空间,并设置instance指向内存空间,但是此时并没有初始化对象。而此次线程1来了,它首先判断instance是否为null,而此次instance已经指向内存空间,所以不为null,但是当线程1初次访问对象就会出现问题。

处理方法:

1、不允许线程0出现重排序(使用volatile修饰变量)

public class LazyDoubleCheckSingleton {
    private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton=null;
    private LazyDoubleCheckSingleton(){

    }

    public  static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton==null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton==null){
                    lazyDoubleCheckSingleton=new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

2、允许线程0出现2,3步骤重排序,但是不允许线程1看到这个重排序(静态内部类)

public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton=new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
    private StaticInnerClassSingleton(){}
}

线程1不会看到重排序。

当线程0和线程1试图获取Class对象的初始化锁的时候(只有一个线程可以获得这个锁),假设线程0获得这个锁,线程0执行静态内部类的初始化,即使2,3之间存在重排序,线程1也是无法看到的。

类(包括接口)被立刻初始化的五种情况(假设这个类为A):

  1. 有一个A类型的实例被创建
  2. A类中的静态方法被调用
  3. A类中的静态成员被赋予值
  4. A类中的静态成员被使用(不是常量成员)
  5. A类是一个顶级类,并且这个类中有断言语句

饿汉模式

package com.design.pattern.creational.singleton.hungry;

import java.io.Serializable;

/**
 * 单例 饿汉式
 * @ClassName HungrySingleton
 * @Author chenchen
 * @Date 2019/8/25 21:23
 * @Version 1.0
 **/
public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    static{
        hungrySingleton =new HungrySingleton();
    }
    private HungrySingleton(){

    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    private Object readResolve(){
        return hungrySingleton;
    }
}

反射破坏单例模式

我们写一个测试类

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance=HungrySingleton.getInstance();
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("singleton"));
        oos.writeObject(instance);
        File file=new File("singleton");
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance=(HungrySingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance==newInstance);
    }
}

可以看到在经过序列化、反序列化之后不再是一个对象。这就违背了单例模式的初衷。

在原有代码下加入下面代码就会拿到相同对象(此方法名称必须为readResolve)

private Object readResolve(){
    return hungrySingleton;
}

为什么加入此方法后就能拿到相同对象?

我们看一下ObjectInputStream.readObject()方法。

public final Object readObject()
    throws IOException, ClassNotFoundException
{
    if (enableOverride) {
        return readObjectOverride();
    }

    // if nested read, passHandle contains handle of enclosing object
    int outerHandle = passHandle;
    try {
	//重点
        Object obj = readObject0(false);
        handles.markDependency(outerHandle, passHandle);
        ClassNotFoundException ex = handles.lookupException(passHandle);
        if (ex != null) {
            throw ex;
        }
        if (depth == 0) {
            vlist.doCallbacks();
        }
        return obj;
    } finally {
        passHandle = outerHandle;
        if (closed && depth == 0) {
            clear();
        }
    }
}

我们重点看一下readObject0()

在readObject0()方法中我们属于OBJECT类型,别的不关注。

这里将readOrdinaryObject方法的返回值作为checkResolve方法的参数。

先看readOridinaryObject

private Object readOrdinaryObject(boolean unshared)
    throws IOException
{
    if (bin.readByte() != TC_OBJECT) {
        throw new InternalError();
    }

    ObjectStreamClass desc = readClassDesc(false);
    desc.checkDeserialize();

    Class<?> cl = desc.forClass();
    if (cl == String.class || cl == Class.class
            || cl == ObjectStreamClass.class) {
        throw new InvalidClassException("invalid class descriptor");
    }

    Object obj;
    try {
	//如果desc.isInstantiable返回true就会返回一个新的对象否则就会返回null
      //重点1  
	obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(
            desc.forClass().getName(),
            "unable to create instance").initCause(ex);
    }

    passHandle = handles.assign(unshared ? unsharedMarker : obj);
    ClassNotFoundException resolveEx = desc.getResolveException();
    if (resolveEx != null) {
        handles.markException(passHandle, resolveEx);
    }

    if (desc.isExternalizable()) {
        readExternalData((Externalizable) obj, desc);
    } else {
        readSerialData(obj, desc);
    }

    handles.finish(passHandle);
    //重点2
    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
//重点3
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            handles.setObject(passHandle, obj = rep);
        }
    }

    return obj;
}

重点1

obj = desc.isInstantiable() ? desc.newInstance() : null;

如果desc.isInstantiable返回true就会返回一个新的对象否则就会返回null。

/**
 * Returns true if represented class is serializable/externalizable and can
 * be instantiated by the serialization runtime--i.e., if it is
 * externalizable and defines a public no-arg constructor, or if it is
 * non-externalizable and its first non-serializable superclass defines an
 * accessible no-arg constructor.  Otherwise, returns false.
 */
boolean isInstantiable() {
    return (cons != null);
}

cons是构造器类型Constructor

根据此方法的注释可以看出, 如果表示的类是可序列化/可外部化的并且可以由序列化运行时实例化则返回true。根据我们的情况此方法返回true。这里返回true上一步则会newInstance。通过反射创建对象,肯定和之前的对象不是同一个对象。

重点2

desc.hasReadResolveMethod()

/**
 * Returns true if represented class is serializable or externalizable and
 * defines a conformant readResolve method.  Otherwise, returns false.
 */
boolean hasReadResolveMethod() {
    return (readResolveMethod != null);
}

根据注释可以知道,如果这个类是可以序列化的且有readResolve方法返回true

重点3

Object invokeReadResolve(Object obj)
    throws IOException, UnsupportedOperationException
{
    if (readResolveMethod != null) {
        try {
            return readResolveMethod.invoke(obj, (Object[]) null);
        } catch (InvocationTargetException ex) {
            Throwable th = ex.getTargetException();
            if (th instanceof ObjectStreamException) {
                throw (ObjectStreamException) th;
            } else {
                throwMiscException(th);
                throw new InternalError(th);  // never reached
            }
        } catch (IllegalAccessException ex) {
            // should not occur, as access checks have been suppressed
            throw new InternalError(ex);
        }
    } else {
        throw new UnsupportedOperationException();
    }
}

根据反射得到这个方法(readResolve)。

到现在为止我们还没有找到对readResolve的定义。

在此类我们ctrl+f找一下readResolve可以找到对readResolve的赋值。

反射攻击

public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //反射攻击
    Class obj=HungrySingleton.class;
    Constructor constructor=obj.getDeclaredConstructor();
    HungrySingleton instance=HungrySingleton.getInstance();
    HungrySingleton object=(HungrySingleton)constructor.newInstance();
    System.out.println(instance);
    System.out.println(object);
    System.out.println(instance==object);
}

因为他的构造方法是私有的,所以报错了。

public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //反射攻击
    Class obj=HungrySingleton.class;
    Constructor constructor=obj.getDeclaredConstructor();
    //修改权限
    constructor.setAccessible(true);
    HungrySingleton instance=HungrySingleton.getInstance();
    HungrySingleton object=(HungrySingleton)constructor.newInstance();
    System.out.println(instance);
    System.out.println(object);
    System.out.println(instance==object);
}

 

通过修改构造方法的权限可以得到实例。

饿汉模式,是在类加载的时候就获得了实例。为了解决这个问题,我们在私有构造方法中添加了判断。

public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    static{
        hungrySingleton =new HungrySingleton();
    }
    private HungrySingleton(){
        if(hungrySingleton!=null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    private Object readResolve(){
        return hungrySingleton;
    }
}

这种方法对类加载时就把类创建好这种类是可以的。

对于不是在类加载时初始化好对象的类是不行的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java单例模式包括饿式和懒汉式两种实现方式。饿式是在类加载阶段就创建实例并持有,而懒汉式则是在需要时才创建实例。 饿模式是指在类加载阶段就创建出实例的,因此它的实例化过程相对于普通情况要早很多。这也是为什么叫“饿”的原因,就像一个饥饿的人对食物没有抵抗力,一下子就开始吃了一样。 懒汉模式是指在需要时才创建实例。这种方式的优点是节省了资源,只有在需要时才会进行实例化。但是它的缺点是在多线程环境下可能会导致多个线程同时创建实例的问题,需要进行额外的线程安全措施来解决这个问题。 总结来说,饿式适合在应用启动时就需要创建实例的情况,因为它的实例化过程早于普通情况。而懒汉式适合在需要时才创建实例的情况,可以节省资源。 需要注意的是,单例模式的使用要根据具体的适应场景来决定,不同的情况下选择不同的实现方式。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Java设计模式单例模式——饿式、懒汉式(初了解)](https://blog.csdn.net/m0_68062837/article/details/127307310)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Java多线程案例之单例模式饿懒汉)](https://blog.csdn.net/qq_63218110/article/details/128738155)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值