Java常用设计模式的实例学习系列-单例模式-8种-以打印机为例

超级链接: Java常用设计模式的实例学习系列-绪论

参考:《HeadFirst设计模式》


其实关于单例模式的代码网上有很多,但是很多都是直接以Singleton类名所编写的示例代码。

通过参考这些代码,可能落实到真正的代码中会遇到些困难,例如枚举式单例模式

所以本文以实例打印机为例,站在各位巨人的肩膀上,再次进行单例模式的整理与总结。

1.单利模式存在的意义

在很多应用中,对于某一种对象,我们最多只需要其一个实例存在。比方说:

  • 操作系统的打印机进程。
  • Java项目的单个数据库的连接池对象。
  • Windows操作系统的垃圾回收站。
  • 项目的配置类。
  • 。。。

为什么上述对象,至多只需要一个实例存在呢?主要原因:

  1. 避免资源浪费。例如:打印机的打印吞吐量取决于打印机硬件本身,多个打印机进程的存在除了占用资源,毫无意义。
  2. 避免多实例存在产生错误。例如:多个打印机进程共同操作一个打印机,那么同时打印很多文件时,可能因为资源挣争夺二产生错误。

关于单例模式的定义概括如下:

  • 单例模式In Java:保证同一个Java进程中,某些类的实例对象至多只有一个,并提供一个访问它的全局访问点的设计模式。

2.实现单例模式的要点

要保证单例,只需要保证以下三点:

  1. 对外统一提供一个公共方法用于获取唯一的实例对象:一般定义为getInstance()方法。
  2. 不允许其他类对当前类进行实例化:当前类的构造函数或实例化方法必须是private的。
  3. 当前类需要创建实例对象:需要实现当前类的构造函数或实例化方法。

2.1.懒汉式与饿汉式

  • 饿汉式懒汉式的区别在于:对象实例化的时机。
  • 饿汉式:类加载阶段进行对象实例化,速记:开始就加载
  • 懒汉式:getInstance首次调用时进行对象实例化,速记:用时再加载延时加载

3.几种单例模式的Java实现

下面以打印机为应用场景,进行几种单例模式的编码。

3.0.工具类

为了方便测试,编写了工具类PrinterUtil,此工具类有以下四个作用:

  • 提供计数器统计getInstance()方法被调用的次数。
  • 提供计数器统计实际初始化的实例对象个数。
  • 提供自定义线程池,用于测试。
  • 配置一些全局变量。

工具类PrinterUtil.java如下:

/**
 * <p>工具类:测试相关</P>
 *
 * @author hanchao
 */
public class PrinterUtil {
    /**
     * 测试相关:调用getInstance方法次数统计
     */
    public static LongAdder CALL_COUNTER = new LongAdder();

    /**
     * 测试相关:对象实例化次数统计
     */
    public static LongAdder ACTUAL_COUNTER = new LongAdder();

    /**
     * 测试相关:线程池
     */
    public static ExecutorService getThreadPool() {
        return new ThreadPoolExecutor(0, 1024,
                6L, TimeUnit.SECONDS,
                new SynchronousQueue<>(), new ThreadFactory() {
            private AtomicInteger threadNumber = new AtomicInteger(1);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "design-pattern-thread-" + threadNumber.getAndIncrement());
            }
        });
    }

    /**
     * 测试相关:最大线程量
     */
    public static Integer MAX_THREAD = 1000;
}

3.1.饿汉式(静态变量初始化)[推荐使用]

示例代码:

/**
 * <p>饿汉式(静态变量初始化)[推荐使用]</P>
 *
 * @author hanchao
 */
@Slf4j
public class Printer01 {

    /**
     * 私有构造函数:防止外部类创建实例
     */
    private Printer01() {
        log.info("Connect to physical printer...");
        PrinterUtil.ACTUAL_COUNTER.increment();
    }

    /**
     * 类初始化时调用私有构造函数,完成对象实例化
     */
    private static Printer01 INSTANCE = new Printer01();

    /**
     * 对外统一的实例获取方法
     */
    public static Printer01 getInstance() {
        PrinterUtil.CALL_COUNTER.increment();
        try {
            Thread.sleep(10L);
        } catch (InterruptedException e) {
            log.error("error!");
        }
        return INSTANCE;
    }
}

测试代码:

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = PrinterUtil.getThreadPool();
        CountDownLatch latch = new CountDownLatch(1);

        for (int i = 0; i < PrinterUtil.MAX_THREAD; i++) {
            executorService.submit(() -> {
                Printer01.getInstance();
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    log.error("Error !");
                }
            });
        }
        latch.countDown();

        executorService.shutdown();
        Thread.sleep(1000L);

        System.out.println("在1s内,调用getInstance次数:" + PrinterUtil.CALL_COUNTER.sum());
        System.out.println("在1s内,实际实例化的对象个数:" + PrinterUtil.ACTUAL_COUNTER.sum());
    }

测试结果:

2019-06-27 14:45:26,782  INFO [main-1] pers.hanchao.designpattern.singleton.Printer01:20 - Connect to physical printer...1s内,调用getInstance次数:10001s内,实际实例化的对象个数:1

优点:

  • 代码实现简单,无需考虑线程安全。

缺点:

  • 在未调用之前就实例化,那么从实例化到对象真正被调用这段时间,其实是对资源的一种浪费。
  • 未实现延时加载,可能实例对象早早就加载到内存中,但是从始至终都未使用,更是对资源的浪费。

适用性分析:

  • 资源充足,无需考虑通过延时加载进行资源节省的场景。
  • 实例对象很早并且必定会被使用的场景。

3.2.饿汉式(静态代码块初始化)[推荐使用]

  • 第二种饿汉式本质与第一种是一致的,其唯一区别是:第一种把对象实例化放在静态变量,第二种把对象实例化放在静态代码块

示例代码:

/**
 * <p>饿汉式(静态代码块初始化)[推荐使用]</P>
 *
 * @author hanchao
 */
@Slf4j
public class Printer02 {

    /**
     * 私有构造函数:防止外部类创建实例
     */
    private Printer02() {
        log.info("Connect to physical printer...");
        PrinterUtil.ACTUAL_COUNTER.increment();
    }

    private static Printer02 INSTANCE;

    /**
     * 类初始化时调用私有构造函数,完成对象实例化
     */
    static {
        //静态代码块写法的优点:可以进行更多的操作
        log.info("Do more thing...");
        INSTANCE = new Printer02();
    }

    /**
     * 对外统一的实例获取方法
     */
    public static Printer02 getInstance() {
        PrinterUtil.CALL_COUNTER.increment();
        try {
            Thread.sleep(10L);
        } catch (InterruptedException e) {
            log.error("error!");
        }
        return INSTANCE;
    }
}

测试代码:

  • 参考3.1.饿汉式的测试代码。

测试结果:

2019-06-27 14:46:17,593  INFO [main-1] pers.hanchao.designpattern.singleton.Printer02:31 - Do more thing... 
2019-06-27 14:46:17,597  INFO [main-1] pers.hanchao.designpattern.singleton.Printer02:20 - Connect to physical printer... 
在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:1

优点:

  • 代码实现简单,无需考虑线程安全。

缺点:

  • 在未调用之前就实例化,那么从实例化到对象真正被调用这段时间,其实是对资源的一种浪费。
  • 未实现延时加载,可能实例对象早早就加载到内存中,但是从始至终都未使用,更是对资源的浪费。

适用性分析:

  • 资源充足,无需考虑通过延时加载进行资源节省的场景。
  • 实例对象很早并且必定会被使用的场景。
  • 相比于静态变量饿汉式。此种方式更方便在实例化阶段做更多的处理。

3.3.懒汉式(无视线程安全)[不能使用]

示例代码:

/**
 * <p>懒汉式(无视线程安全)[不能使用]</P>
 *
 * @author hanchao
 */
@Slf4j
public class Printer03 {

    /**
     * 私有构造函数:防止外部类创建实例
     */
    private Printer03() {
        log.info("Connect to physical printer...");
        PrinterUtil.ACTUAL_COUNTER.increment();
    }

    private static Printer03 INSTANCE;

    /**
     * 对外统一的实例获取方法
     */
    public static Printer03 getInstance() {
        //调用getInstance时再进行实例化,但是并没有考虑并发情况
        if (null == INSTANCE) {
            INSTANCE = new Printer03();
        }
        try {
            Thread.sleep(10L);
        } catch (InterruptedException e) {
            log.error("error!");
        }
        PrinterUtil.CALL_COUNTER.increment();
        return INSTANCE;
    }
}

测试代码:

  • 参考3.1.饿汉式的测试代码。

测试结果:

2019-06-27 14:50:41,799  INFO [design-pattern-thread-2-15] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer... 
2019-06-27 14:50:41,802  INFO [design-pattern-thread-3-16] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer... 
2019-06-27 14:50:41,801  INFO [design-pattern-thread-6-19] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer... 
2019-06-27 14:50:41,801  INFO [design-pattern-thread-4-17] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer... 
2019-06-27 14:50:41,801  INFO [design-pattern-thread-5-18] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer... 
2019-06-27 14:50:41,802  INFO [design-pattern-thread-1-14] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer... 
2019-06-27 14:50:41,802  INFO [design-pattern-thread-7-20] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer... 
2019-06-27 14:50:41,802  INFO [design-pattern-thread-8-21] pers.hanchao.designpattern.singleton.Printer03:22 - Connect to physical printer... 
在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:8

优点:

  • 无。

缺点:

  • 根本未考虑采取措施保证线程安全。
  • 线程不安全,在并发环境下,产生了多个实例对象。

适用性分析:

  • 线程不安全,不可用。

3.4.懒汉式(同步方法保证,线程安全,资源争夺)[不能使用]

示例代码:

/**
 * <p>懒汉式(同步方法保证,线程安全,资源争夺)[不能使用]</P>
 *
 * @author hanchao
 */
@Slf4j
public class Printer04 {

    /**
     * 私有构造函数:防止外部类创建实例
     */
    private Printer04() {
        log.info("Connect to physical printer...");
        PrinterUtil.ACTUAL_COUNTER.increment();
    }

    private static Printer04 INSTANCE;

    /**
     * 对外统一的实例获取方法(同步方法)
     */
    public static synchronized Printer04 getInstance() {
        //调用getInstance时再进行实例化,但是并没有考虑并发情况
        if (null == INSTANCE) {
            INSTANCE = new Printer04();
        }
        try {
            Thread.sleep(10L);
        } catch (InterruptedException e) {
            log.error("error!");
        }
        PrinterUtil.CALL_COUNTER.increment();
        return INSTANCE;
    }
}

测试代码:

  • 参考3.1.饿汉式的测试代码。

测试结果:

2019-06-27 14:54:42,587  INFO [design-pattern-thread-1-14] pers.hanchao.designpattern.singleton.Printer04:22 - Connect to physical printer... 
在1s内,调用getInstance次数:99
在1s内,实际实例化的对象个数:1

优点:

  • 通过同步方法保证了线程安全。
  • 保证线程安全的措施的代码实现相对简单,因为只需要在原有方法上添加synchronized关键字。

缺点:

  • 因为同步方法锁住了整个方法,所以效率很低,从测试结果来看,1s之内,getInstance()实际只被调用了99次,远少于其他方式的1000个。

适用性分析:

  • 效率太低,不可用,

3.5.懒汉式(同步代码块,单重校验,线程不安全)[不能使用]

示例代码:

/**
 * <p>懒汉式(同步代码块,单重校验,线程不安全)[不能使用]</P>
 *
 * @author hanchao
 */
@Slf4j
public class Printer05 {

    /**
     * 私有构造函数:防止外部类创建实例
     */
    private Printer05() {
        log.info("Connect to physical printer...");
        PrinterUtil.ACTUAL_COUNTER.increment();
    }

    private static Printer05 INSTANCE;

    /**
     * 对外统一的实例获取方法
     */
    public static Printer05 getInstance() {
        //调用getInstance时再进行实例化,单重校验线程不安全
        if (null == INSTANCE) {
            synchronized (Printer05.class) {
                INSTANCE = new Printer05();
            }
        }
        try {
            Thread.sleep(10L);
        } catch (InterruptedException e) {
            log.error("error!");
        }
        PrinterUtil.CALL_COUNTER.increment();
        return INSTANCE;
    }
}

测试代码:

  • 参考3.1.饿汉式的测试代码。

测试结果:

2019-06-27 15:00:05,769  INFO [design-pattern-thread-1-14] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer... 
2019-06-27 15:00:05,773  INFO [design-pattern-thread-11-24] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer... 
2019-06-27 15:00:05,774  INFO [design-pattern-thread-10-23] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer... 
2019-06-27 15:00:05,774  INFO [design-pattern-thread-9-22] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer... 
2019-06-27 15:00:05,775  INFO [design-pattern-thread-8-21] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer... 
2019-06-27 15:00:05,775  INFO [design-pattern-thread-7-20] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer... 
2019-06-27 15:00:05,775  INFO [design-pattern-thread-6-19] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer... 
2019-06-27 15:00:05,776  INFO [design-pattern-thread-5-18] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer... 
2019-06-27 15:00:05,777  INFO [design-pattern-thread-4-17] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer... 
2019-06-27 15:00:05,778  INFO [design-pattern-thread-3-16] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer... 
2019-06-27 15:00:05,778  INFO [design-pattern-thread-2-15] pers.hanchao.designpattern.singleton.Printer05:22 - Connect to physical printer... 
在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:11

优点:

  • 尝试去通过同步代码块保证线程安全。

缺点:

  • 线程不安全,在并发环境下,产生了多个实例对象。

适用性分析:

  • 线程不安全,不可用。

3.6.懒汉式(同步代码块,双重校验,线程安全)[推荐使用]

示例代码:

/**
 * <p>懒汉式(同步代码块,双重校验,线程安全)[推荐使用]</P>
 *
 * @author hanchao
 */
@Slf4j
public class Printer06 {

    /**
     * 私有构造函数:防止外部类创建实例
     */
    private Printer06() {
        log.info("Connect to physical printer...");
        PrinterUtil.ACTUAL_COUNTER.increment();
    }

    private static Printer06 INSTANCE;

    /**
     * 对外统一的实例获取方法
     */
    public static Printer06 getInstance() {
        //调用getInstance时再进行实例化,单重校验线程不安全,资源争夺
        if (null == INSTANCE) {
            synchronized (Printer06.class) {
                if (null == INSTANCE) {
                    INSTANCE = new Printer06();
                }
            }
        }
        try {
            Thread.sleep(10L);
        } catch (InterruptedException e) {
            log.error("error!");
        }
        PrinterUtil.CALL_COUNTER.increment();
        return INSTANCE;
    }
}

测试代码:

  • 参考3.1.饿汉式的测试代码。

测试结果:

2019-06-27 15:04:57,221  INFO [design-pattern-thread-1-14] pers.hanchao.designpattern.singleton.Printer06:20 - Connect to physical printer... 
在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:1

优点:

  • 线程安全。
  • 延时加载,节省资源。
  • 通过双重校验保证线程安全,效率较高。

缺点:

  • 线程安全保障措施的代码实现稍显复杂。

适用性分析:

  • 需要考虑通过延时加载进行资源节省的场景。
  • 不确定实例对象何时使用或者是否使用的场景。

3.7.内部类[推荐使用]

示例代码:

/**
 * <p>内部类[推荐使用]</P>
 *
 * @author hanchao
 */
@Slf4j
public class Printer07 {

    /**
     * 私有构造函数:防止外部类创建实例
     */
    private Printer07() {
        log.info("Connect to physical printer...");
        PrinterUtil.ACTUAL_COUNTER.increment();
    }

    /**
     * 将唯一实例放在静态内部类的静态变量中。静态内部类的静态变量在外部类被加载时不会立即实例化,
     * 只有在调用getInstance方法时才会加载静态内部类,进而完成静态内部类的静态变量的初始化。
     */
    private static class PrinterUtil07Holder {
        private static Printer07 INSTANCE = new Printer07();
    }

    /**
     * 对外统一的实例获取方法
     */
    public static Printer07 getInstance() {
        PrinterUtil.CALL_COUNTER.increment();
        try {
            Thread.sleep(10L);
        } catch (InterruptedException e) {
            log.error("error!");
        }
        return PrinterUtil07Holder.INSTANCE;
    }
}

测试代码:

  • 参考3.1.饿汉式的测试代码。

测试结果:

2019-06-27 15:11:55,177  INFO [design-pattern-thread-2-15] pers.hanchao.designpattern.singleton.Printer07:20 - Connect to physical printer... 
在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:1

优点:

  • 线程安全。
  • 延时加载,节省资源。
  • 通过内部类保证线程安全,效率较高。
  • 线程安全保障措施的代码实现十分简单。

缺点:

  • 无。

适用性分析:

  • 需要考虑通过延时加载进行资源节省的场景。
  • 不确定实例对象何时使用或者是否使用的场景。

3.8.枚举[推荐使用]

示例代码一:枚举(普通版)[推荐使用]

/**
 * <p>枚举(普通版)[推荐使用]</P>
 *
 * @author hanchao
 */
public enum Printer08 {

    /**
     * 唯一实例
     */
    INSTANCE;

    /**
     * 枚举的构造方法保证私有性:防止外部类创建实例
     */
    Printer08() {
        System.out.println("Connect to physical printer...");
        PrinterUtil.ACTUAL_COUNTER.increment();
    }

    /**
     * 对外统一的实例获取方法(在枚举实现的单例模式中,此方法可以省略,直接通过Printer08.INSTANCE获取对象实例)
     */
    public Printer08 getInstance() {
        PrinterUtil.CALL_COUNTER.increment();
        try {
            Thread.sleep(10L);
        } catch (InterruptedException e) {
            System.out.println("error!");
        }
        return this;
    }
}

测试代码一:

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = PrinterUtil.getThreadPool();
        CountDownLatch latch = new CountDownLatch(1);

        for (int i = 0; i < PrinterUtil.MAX_THREAD; i++) {
            executorService.submit(() -> {
                //更简洁的写法:Printer08 instance = Printer08.INSTANCE;
                Printer08.INSTANCE.getInstance();
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    System.out.println("Error !");
                }
            });
        }
        latch.countDown();

        executorService.shutdown();
        Thread.sleep(1000L);

        System.out.println("在1s内,调用getInstance次数:" + PrinterUtil.CALL_COUNTER.sum());
        System.out.println("在1s内,实际实例化的对象个数:" + PrinterUtil.ACTUAL_COUNTER.sum());
    }

示例代码二:枚举(简洁版)[推荐使用]

/**
 * <p>枚举(简洁版)[推荐使用]</P>
 *
 * @author hanchao
 */
public enum Printer09 {

    /**
     * 唯一实例
     */
    INSTANCE;

    /**
     * 枚举的构造方法保证私有性:防止外部类创建实例
     */
    Printer09() {
        System.out.println("Connect to physical printer...");
        PrinterUtil.ACTUAL_COUNTER.increment();
    }
}

测试代码二:


    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = PrinterUtil.getThreadPool();
        CountDownLatch latch = new CountDownLatch(1);

        for (int i = 0; i < PrinterUtil.MAX_THREAD; i++) {
            executorService.submit(() -> {
                Printer09 instance = Printer09.INSTANCE;
                PrinterUtil.CALL_COUNTER.increment();
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    System.out.println("Error !");
                }
            });
        }
        latch.countDown();

        executorService.shutdown();
        Thread.sleep(1000L);

        System.out.println("在1s内,调用getInstance次数:" + PrinterUtil.CALL_COUNTER.sum());
        System.out.println("在1s内,实际实例化的对象个数:" + PrinterUtil.ACTUAL_COUNTER.sum());
    }

测试结果:

在1s内,调用getInstance次数:1000
在1s内,实际实例化的对象个数:1

优点:

  • 线程安全。
  • 延时加载,节省资源。
  • 通过枚举保证线程安全,效率较高。
  • 线程安全保障措施的代码实现十分简单。

缺点:

  • 枚举实现的方式本身可能不易理解。

适用性分析:

  • 需要考虑通过延时加载进行资源节省的场景。
  • 不确定实例对象何时使用或者是否使用的场景。

3.总结

资源充足的单例模式

  • 3.1.饿汉式(静态变量初始化)[推荐使用]
  • 3.2.饿汉式(静态代码块初始化)[推荐使用]

不可用的单例模式:

  • 3.3.懒汉式(无视线程安全)[不能使用]
  • 3.4.懒汉式(同步方法保证,线程安全,资源争夺)[不能使用]
  • 3.5.懒汉式(同步代码块,单重校验,线程不安全)[不能使用]

节省资源的单例模式:

  • 3.6.懒汉式(同步代码块,双重校验,线程安全)[推荐使用]
  • 3.7.内部类[推荐使用]
  • 3.8.枚举[推荐使用]
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值