设计模式-单例模式

什么是单例模式

单例模式,理解起来十分简单。如果一个类只能创建一个对象(或者实例),那么这个类就是一个单例类,这种设计模式叫做单例设计模式,也叫做单例模式

为什么要使用单例

1. 处理资源访问冲突

public class Logger {
    private FileWriter writer;

    public Logger() throws IOException {
        File file = new File("/Users/***/log.txt");
        // true表示追加写入
        writer = new FileWriter(file, true);
    }

    public void log(String message) throws IOException {
        writer.write(message);
    }
}

// Logger类的应用示例:
public class UserController {
    private Logger logger = new Logger();

    public void login(String username, String password) // ...省略业务逻辑代码...
        logger.log(username + " logined!");
    }
}

public class OrderController {
    private Logger logger = new Logger();

    public void create(OrderVo order) {
        logger.log("Created an order: " + order.toString());
    }
}

上面这段代码显而易见的会存在并发问题,比如并发请求过来,分别调用UserController#login和OrderController#create方法,因为这两个Controller对象本身会创建各自的Logger对象,所以此时两个对象会将各自的日志同时写入到log.txt中,那就存在着日志覆盖的问题。

至于怎么理解互相覆盖的问题,可以参考下图,下图是个典型的多线程环境下对同一个共享变量+1导致的并发覆盖问题。

设计模式-单例模式

其实要解决上面的问题也简单,最先想到的就是给log()函数加上互斥锁,如Java的synchronized关键字,同一时刻只允许一个线程调用执行log()函数,具体代码如下

public class Logger {
    private FileWriter writer;

    public Logger() throws IOException {
        File file = new File("/Users/***/log.txt");
        // true表示追加写入
        writer = new FileWriter(file, true);
    }

    public void log(String message) throws IOException {
        synchronized (this) {
            writer.write(message);
        }
    }
}

先不说以上代码的正确性和合理性,这样设计真的可以解决互相覆盖的问题,或者说并发问题,答案是否定的,因为log()函数目前加的锁是对象锁,对象锁什么概念呢,也就是这个对象在不同的线程下调用加对象锁的代码段或者函数,那么这个代码段或者函数就必须顺序执行,就如上面的log()函数。但是,不同的对象并不共享同一把锁。因此在不同的线程下,通过不同的对象调用log()函数,锁并不会起作用,仍然有可能出现相互覆盖的情况。

设计模式-单例模式

其实上面的代码并不太合理,因为FileWriter内部本身就有一把对象锁,write的时候本身就已经加了对象级别的锁。但是同样解决不了日志覆盖的问题,原理同上。

那么,究竟应该如何解决这个问题?实际上最简单的方法就是将对象锁换成类级别的锁,代码如下:

public class Logger {
    private FileWriter writer;

    public Logger() throws IOException {
        File file = new File("/Users/***/log.txt");
        // true表示追加写入
        writer = new FileWriter(file, true);
    }

    public void log(String message) throws IOException {
        synchronized (Logger.class{
            writer.write(message);
        }
    }
}

除了换成类级别的锁以外,还可以采用并发队列的方法,如Java中的BlockingQueue, 多个线程往并发队列里写入日志,然后由一个线程读取队列里的数据并写入日志文件。

稍微复杂点的情况可能还需要用到分布式锁。不过实现一个安全可靠、无Bug、高性能的分布式锁,并不是一件容易的事情。

相较于以上两种解决方案,单例模式就简单多了,说白了就是只允许你创建一个对象,就不存在多个线程通过多个对象写日志的并发问题了。加上FileWriter本身是线程安全的,也就不需要再手打添加对象级别的锁了。Logger单例类具体代码如下:

public class Logger {
    private FileWriter writer;
    private static final Logger instance = new Logger();

    private Logger() {
        File file = new File("/Users/***/log.txt");
        // true表示追加写入
        writer = new FileWriter(file, true);
    }

    public static Logger getInstance() {
        return instance;
    }
    public void log(String message) throws IOException {
        writer.write(message);
    }
}

// Logger类的应用示例:
public class UserController {

    public void login(String username, String password) // ...省略业务逻辑代码...
        Logger.getInstance().log(username + " logined!");
    }
}

public class OrderController {

    public void create(OrderVo order) {
        Logger.getInstance().log("Created an order: " + order.toString());
    }
}

2. 表示全局唯一类

从业务概念上,如果某些数据在系统上就应该只保存一份,那就比较适合设计成单例类,如配置信息类。

再比如唯一递增ID号码生成器

public class IdGenerator {
    // AtomicLong是一个Java并发库中提供的一个原子变量类型, 
    // 它将一些线程不安全需要加锁的复合操作封装为了线程安全的原子操作,
    // 比如下面会用到的incrementAndGet().
    private AtomicLong id = new AtomicLong(0);
    private static final IdGenerator instance = new IdGenerator();

    private IdGenerator() {
    }

    public static IdGenerator getInstance() {
        return instance;
    }

    public long getId() {
        return id.incrementAndGet();
    }
}

怎么实现一个单例

主要考虑下面几个点:

  • 构造函数私有

  • 考虑对象创建时的线程安全问题

  • 考虑是否支持延迟加载

  • 考虑getInstance()性能是否高(是否加锁)

1. 饿汉式

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private static final IdGenerator instance = new IdGenerator();

    private IdGenerator() {
    }

    public static IdGenerator getInstance() {
        return instance;
    }

    public long getId() {
        return id.incrementAndGet();
    }
}

有人觉得懒汉式的实现方式不好,因为不支持延迟加载,如果示例占用资源多或者初始化耗时长,提前初始化是一种浪费资源的行为。

其实也分情况,像Web应用这种,将这种实例初始化提前到系统启动的时候未尝不是件好事,这样可以提前暴露出一些系统资源的问题,也可以防止程序运行过程中去初始化造成的一些性能问题。

2. 懒汉式

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private static IdGenerator instance;

    private IdGenerator() {
    }

    public static synchronized IdGenerator getInstance() {
        return instance;
    }

    public long getId() {
        return id.incrementAndGet();
    }
}

懒汉式其实就是懒在延迟加载,但是换来的就是获取实例的时候需要注意并发问题,这里简单的对整个方法加锁,会极大降低性能。

3. 双重监测

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);
    private volatile static IdGenerator instance;

    private IdGenerator() {
    }

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

    public long getId() {
        return id.incrementAndGet();
    }
}

双重监测需要注意的点,就是指令重排序,禁止重排的方法就是加上volatile关键字。为什么要这么做,简而言之就是。因为指令重排序,可能会导致 IdGenerator 对象被new 出来,并且赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。详细的可参考https://www.zhihu.com/question/35268028/answer/261226895

4. 静态内部类

public class IdGenerator {
    private AtomicLong id = new AtomicLong(0);

    private IdGenerator() {
    }

    private static class SingletonHolder {
        private static final IdGenerator instance = new IdGenerator();
    }

    public static IdGenerator getInstance() {
        return SingletonHolder.instance;
    }

    public long getId() {
        return id.incrementAndGet();
    }
}

静态内部类的方法,兼具懒加载和线程安全,完全交由Jvm来保证。SingletonHolder是一个静态内部类,当外部类加载的时候并不会创建SingletonHolder实例对象,只有调用getInstance方法时,SingletonHolder才会被记载,这个时候才会创建instance。

5. 枚举

public enum IdGenerator {
    INSTANCE;
    private AtomicLong id = new AtomicLong(0);

    public long getId() {
        return id.incrementAndGet();
    }
}

枚举是最简单的单例实现方式了,保证了实例创建的线程安全和实例的唯一性。

单例的替代方案

工厂模式或者IOC容器(如Spring


原文始发于微信公众号(子枫进阶之路):设计模式-单例模式

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/24479.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!