03.单例模式详解(上)

说在前面

        不要为了套用设计模式而使用设计模式,而是,在业务上遇到问题时,很自然地想到设计模式作为一种解决方案

单例模式

        是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点,属于创建型模式

适用场景

        确保任何情况下都绝对只需要一个实例的场景。例如:ServletContext、ServletConfig、ApplicationContext、DBPool都是属于单例的

常见的几种写法

  • 饿汉式单例
  • 懒汉式单例
  • 注册式单例
  • ThreadLocal单例

饿汉式单例

/**
 * 饿汉式单例
 * 1、构造器私有,防止被别人使用new关键字创建对象
 * 2、提供一个对外方法,供外部调用
 *
 * @author wcj
 * @description
 * @date 2019/8/16 17:37
 */
public class HungryMan {
    /**
     * 第一种写法:静态成员创建该类实例
     * 写成final是为了防止被反射获取后,进行变量的覆盖
     */
    private static final HungryMan HUNGRY_MAN = new HungryMan();
    /**
     * 第二种写法:在静态块中创建该类实例
     */
//    static {
//        HUNGRY_MAN = new HungryMan();
//    }
    private HungryMan() {
    }
    /**
     * 提供唯一一个对外的方法,供外部调用该类实例
     *
     * @return
     */
    public static HungryMan getInstance() {
        return HUNGRY_MAN;
    }
}

       在类的加载时就创建了实例。饿汉式单例优点是本身就是线程安全的。缺点是浪费了内存空间,因为这类可能不会被用到,当然在一个类这么做的时候,浪费的是很少的,如果所有类都这么做,那么就非常严重了,所以还有一种单例模式:懒汉式单例

懒汉式单例

public class LazyMan {
    private static LazyMan lazyMan = null;
    private LazyMan() {
    }
    public static LazyMan getInstance() {
        if (lazyMan == null) {
            lazyMan = new LazyMan();
        }
        return lazyMan;
    }
}
/**
 * 懒汉式单例第一次优化
 *
 * @author wcj
 * @description
 * @date 2019/8/17 10:52
 */
public class OptimizeLazyMan {
    private static OptimizeLazyMan lazyMan = null;
    private OptimizeLazyMan() {
    }
    /**
     * 给方法块上添加synchronized关键字,可以防止多线程情况下拿到的不是同一对象
     * 缺点:虽然在jdk1.6以后对synchronized进行了性能优化,但是还是存在一定的性能问题
     * 可以使用双重检查锁稍加优化
     *
     * @return
     */
    public synchronized static OptimizeLazyMan getInstance() {
        if (lazyMan == null) {
            lazyMan = new OptimizeLazyMan();
        }
        return lazyMan;
    }
}
/**
 * 懒汉式单例第二次优化
 *
 * @author wcj
 * @description
 * @date 2019/8/17 10:52
 */
public class OptimizeLazyMan02 {
    private static OptimizeLazyMan02 lazyMan = null;
    private OptimizeLazyMan02() {
    }
    /**
     * 因为之前的是用synchronized修饰静态方法,是类锁,效率较低
     * 使用双重检查锁机制,可以稍加优化
     *
     * @return
     */
    public static OptimizeLazyMan02 getInstance() {
        if (lazyMan == null) {
            synchronized (OptimizeLazyMan02.class){
                if(lazyMan == null){
                    lazyMan = new OptimizeLazyMan02();
                }
            }
        }
        return lazyMan;
    }
}
/**
 * 模拟执行线程,去创建单例对象
 * @author wcj
 * @description
 * @date 2019/8/17 10:55
 */
public class ExecuteThread implements Runnable {
    @Override
    public void run() {
        OptimizeLazyMan02 instance = OptimizeLazyMan02.getInstance();
        System.out.println(Thread.currentThread().getName() + ":" + instance);
    }
}
public class SingletonTest {
    public static void main(String[] args) {
        //生成两个线程去访问得到对象,看是否是一致
        Thread t1 = new Thread(new ExecuteThread());
        Thread t2 = new Thread(new ExecuteThread());
        t1.start();
        t2.start();
    }
}

        第一次的优化,在静态方法上添加了synchronized关键字,可以有效的保证多线程的安全,缺点是修饰的是静态方法,锁的范围较大,性能较差。第二次的优化是采用了双重检查锁机制,不在静态方法上使用锁,而在方法内部。

为什么第二种优化需要两层if判断

       当两个线程都同时进入第一层if判断的时候,线程A获得锁,进行new后并释放锁后,线程B进入锁后,还是会再次进行new对象,这样就会产生new对象两次,且可能造成非单例的情况产生,所以为了避免此种情况,需要再加一层if判断。

        而外层的if判断是为了减少锁竞争,当对象不为空的时候,就不需要进行锁竞争,不然每个线程调用该方法时,都会进行锁的不必要竞争。下图是线程级别的debug,当线程1进入的时候,线程0就处于monitor状态了,等待线程1的释放。

延伸:synchronized修饰普通方法和静态方法的区别

  • 修饰普通方法时,锁是对象锁,也就是this。当该类中有多个普通方法被Synchronized修饰(同步),那么这些方法的锁都是这个类的一个对象this。多个线程访问这些方法时,如果这些线程调用方法时使用的是同一个该类的对象,虽然他们访问不同方法,但是他们使用同一个对象来调用,那么这些方法的锁就是一样的,就是这个对象,那么会造成阻塞
  • 修饰静态方法时,锁是类锁,也就是类名.class。这个范围就比对象锁大。这里就算是不同对象,但是只要是该类的对象,就使用的是同一把锁。多个线程调用该类的同步的静态方法时,都会阻塞。

当CPU执行时,会将命令解析成JVM指令,逐步完成以下的步骤

  • 分配内存给对象
  • 初始化对象
  • 将初始化对象和内存建立连接,也就是赋值
  • 用户的初次访问

其中可能造成指令重排的问题,指令顺序每次可能都不一样,可以引用到volatile关键字用以解决指令重排的问题

静态内部单例模式

/**
 * 全程没有用到synchronized关键字,性能最优
 * 内部类单例模式
 *
 * @author wcj
 * @description
 * @date 2019/8/17 16:07
 */
public class InnerLazyMan {
    /**
     * 虽然构造方法私有了,但是反射还是可以创建的
     */
    private InnerLazyMan() {
    }
    /**
     * 当用户调用时,内部类才会执行
     * 这是JVM底层的执行逻辑,完全的避免了线程安全问题
     *
     * @return
     */
    public static final InnerLazyMan getInstance() {
        return InnerMan.INNER_LAZY_MAN;
    }
    /**
     * 这个内部类是饿汉式的,但是如果外部类没有调用getInstance(),那么内部类是不会被执行的
     */
    private static class InnerMan {
        private static final InnerLazyMan INNER_LAZY_MAN = new InnerLazyMan();
    }
}

上图中的代码,虽然内部类是饿汉式的,但是只有外部类方法被调用才会执行,是一种特殊的懒汉式加载。但是还会有一种情形,虽然构造器被私有了,但是可以通过反射方式获得对象

public class InnerSingletonTest {
    public static void main(String[] args) {
        try {
            //通过反射得到对象
            Class<InnerLazyMan> lazyManClass = InnerLazyMan.class;
            //getDeclaredConstructor()返回所有构造器,包括public的和非public的,当然也包括private的
            //getConstructor()返回访问权限是public的构造器
            Constructor<InnerLazyMan> constructor = lazyManClass.getDeclaredConstructor();
            //值为true表示反射的对象在使用时应该取消访问权限检查,值为false表示开启
            constructor.setAccessible(true);
            InnerLazyMan innerLazyMan = constructor.newInstance();
            InnerLazyMan instance = InnerLazyMan.getInstance();
            //判断两个对象是否是同一个对象
            System.out.println(innerLazyMan==instance);
        }catch (Exception r){
            r.printStackTrace();
        }
    }
}
输出的是:false,代表是这两个不是同一对象

要解决上述的情况,可以在构造器中处理,如下图所示

private InnerLazyMan() {
    if(InnerMan.INNER_LAZY_MAN!=null){
        throw new RuntimeException("不能创建更多的实例了");
    }
}

虽然上述的功能比较完备了,不能通过反射的方式进行创建对象了,但是还有一种方式可以得到对象,那就是序列化

通过序列化方式得到对象

/**
 * 序列化式的创建单例模式,其实就是一个简单的单例模式
 *
 * @author wcj
 * @description
 * @date 2019/8/17 16:43
 */
public class SerializableLazyMan implements Serializable {
    private static SerializableLazyMan serializableLazyMan = null;
    private SerializableLazyMan() {
    }
    public static SerializableLazyMan getInstance() {
        if (serializableLazyMan == null) {
            serializableLazyMan = new SerializableLazyMan();
        }
        return serializableLazyMan;
    }
}
/**
 * 序列化单例测试类
 * @author wcj
 * @description
 * @date 2019/8/17 16:43
 */
public class SerializableTest {
    public static void main(String[] args) {
        SerializableLazyMan s1;
        SerializableLazyMan s2 = SerializableLazyMan.getInstance();
        FileOutputStream fileOutputStream;
        try {
            //将s2写入序列化中
            fileOutputStream = new FileOutputStream("1.txt");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(s2);
            objectOutputStream.flush();
            objectOutputStream.close();
            //将对象反序列化赋给s1
            FileInputStream fileInputStream = new FileInputStream("1.txt");
            ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
            s1 = (SerializableLazyMan) objectInputStream.readObject();
            objectInputStream.close();
            System.out.println(s1);
            System.out.println(s2);
            //比较两个对象是否为同一对象
            System.out.println(s1 == s2);
        } catch (Exception e) {

        }
    }
}
输出的是:false

这时我们得知,序列化可以破坏单例模式的,得到的对象并不是同一对象,那么如何避免呢?可以在单例类中添加以下代码

/**
 * 添加readResolve方法
 * @return
 */
private Object readResolve() {
    return serializableLazyMan;
}

思考,为什么添加了这段代码,再运行测试类时,就是返回true,为同一对象呢?ObjectInputStream类中的源码如下

从上述源码可知,readObject0()中的readOrdinaryObject()方法中是判断该类是否可以被初始化,可以就new一个新的对象,那么自然输出的就是false了,被初始化后,又检查类中是否包含了readResolve()方法,包含则覆盖要被序列化类的对象,这样就可以保持对象的一致性。

重写readResolve(),只不过是覆盖了反序列化中的对象,其实对象还是创建了两次,这是发生在JVM层次中的,相对来说较为安全,之前反序列化出来的对象会被GC回收

因为序列化会破坏单例,所以还有一种单例模式是注册式单例。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芦蒿炒香干

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值