4.2 单例模式

序言

日常工作中,有些对象我们只需要实例化一个,例如缓存、线程池、日志等对象。这些对象如果实例化多个时,抛去浪费资源不说,而且还可能造成程序错误。例如同事A和同事B需要操作同一个缓存对象,同事A向缓存写入数据,同事B从缓存读取数据。但是他们各自都创建了一个缓存对象。此时同事A将数据写入缓存A,同事B从缓存B读取数据,此时同事B一定是读取不到数据的,这就产生了错误。

产生该错误的原因就是同事A和同事B操作的是不同的缓存(如图1所示),解决方法就是让同事A和同事B操作同一个缓存对象即可(如图2所示)。

图片说明                                 图片说明

由此引出了我们本章的主角:单例模式。下面请跟着我揭开单例模式的神秘面纱。

定义

单例模式的定义很简单:

确保一个类只有一个实例,并对外提供一个全局接口以供访问。

类图

图片说明

示例代码

// 定义单例
public class Sinleton {
    private static Sinleton instance = null;

    // 私有构造函数,避免外部类直接new
    private Sinleton() {}

    public static Sinleton getInstance() {
        // instance未初始化时进行初始化,否则直接返回
        if (instance == null) {
            instance = new Sinleton();
        }
        return instance;
    }
}

// 验证
public class Client {
    public static void main(String[] args) {
        Sinleton sinleton1 = Sinleton.getInstance();
        Sinleton sinleton2 = Sinleton.getInstance();
        System.out.println(sinleton1 == sinleton2); // true,说明Sinleton只有一个实例
    }
}

上述代码是一个最简单的单例模式,主要有几点:

  1. 私有化构造函数,避免外部类依旧可以new对象
  2. 对外暴露全局接口
  3. 实现延时加载(也叫懒加载),即在需要时才进行实例化。所以这种模式也称为懒汉模式

请再次重新阅读上述代码,思考代码是否存在问题,存在怎样的问题?

聪明的你一定会发现上述代码在多线程下是不安全的,因为可能同时有多个线程判断 instance == nulltrue , 从而造成多个实例化结果。下面我们来验证一下:

public class Client {
    public static void main(String[] args) throws InterruptedException {
        // 用来存储实例化结果,方便查看
        Set<Sinleton1> sinletonSet = new HashSet<>();
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                sinletonSet.add(Sinleton1.getInstance());
            }).start();
        }
        // 等待子线程执行完毕
        Thread.sleep(2000);

        // 输出实例(此处可能会由于多线程问题导致有多个实例)
        sinletonSet.forEach(System.out::println);
    }
}

上述代码执行完,如果输出了多个内容,则说明了线程不安全,导致产生了多个实例。如果你的输出只有一个结果,那就多执行几次,一定会有问题的

问题原因就是上面提到的:可能同一时刻有多个线程判断 instance == nulltrue,从而执行new Singleton()导致生成多个实例。知道了问题的原因,我们就来看看如何解决该问题。

一提到多线程,大家应该会很快想到通过锁机制保证线程安全。此处我们借助 synchronized 加锁保证线程安全。

面试考点:请同学自行查阅保证线程安全的方式有哪些

public class Sinleton {
    private static Sinleton instance = null;

    // 私有构造函数,避免外部类直接new
    private Sinleton() {}

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

然后请各位继续执行上面的验证代码,自行验证是否只会产生一个实例。虽然加锁能保证线程安全,但是加锁、释放锁会降低系统的吞吐量,那让我们想想能不能不通过加锁就能保证线程安全呢?

面试考点:什么是乐观锁、悲观锁。乐观锁其实没有真正的锁

我们想想懒汉模式为什么会出问题,是因为每个线程都有创建实例的权利,所以会存在线程安全问题。那如果我们把创建线程的权利回收,只有 Singleton 自己才能创建实例,那是不是就没有线程安全问题了呢?

public class Sinleton {
    // 初始化时就创建实例,避免线程安全问题
    private static Sinleton instance = new Sinleton();

    private Sinleton1() {}

    public static Sinleton getInstance() {
        return instance;
    }
}

然后请各位继续执行上面的验证代码,自行验证是否只会产生一个实例。验证完成你会发现只会产生一个实例,所以这段代码是线程安全的。这段代码有个好听的名字就是饿汉模式

面试考点:

  1. 懒汉和饿汉模式代码书写
  2. 哪种是线程安全的

单例模式就讲这么多,更多关于单例模式问题,请在本章节最后一章“常见面试题”中进行查看。

全部评论

相关推荐

VirtualBool:都去逗他了?
点赞 评论 收藏
分享
小厂面经,也是我的处女面(30min)1.自我介绍2.spring&nbsp;boot的自动装配原理(好多类和接口的单词都忘了全称是啥了,就说了记得的单词,流程应该说对了吧)3.有用过redis吗?主要是用在实现什么功能(说了技术派用redis的zset来实现排行榜)5.有了解过Redisson吗?讲一下对于分布式锁的了解以及在什么场景下应用(说了秒杀场景)6.对mysql有了解吗?包括它的索引优化和创建(把想起来的全说了)7.了解设计模式吗?比如单例模式,为什么要使用单例模式,它的优点是什么(昨天刚看的设计模式)8.工厂模式有了解吗?主要的使用场景是?(也是昨天刚看的)9.场景题:有7个服务器,需要在早上十点定时的向数据库中的用户表中的用户发短信,如果做到发送的消息不重复,且如果发送失败了需要知道是到哪个用户失败了,这样下次就直接从这个用户开始(我答了用spring&nbsp;task来实现定时,用分布式锁来保证只有一份服务器可以发送消息,用消息队列来存储消息,然后用消息确认机制来保证错误信息的记录,以及在数据库或者业务层面完成消息消费的幂等性)10.场景题:如果在系统启动的时间就将数据库的所有用户相关的信息都读到一个hashmap中(这个没啥思路,没答好)27届的投了一个星期终于有一个面试了,大部分公司都只招26的
inari233:已oc,拒了
查看9道真题和解析
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务