美文网首页互联网科技Java架构技术进阶老男孩的成长之路
用一个通俗易懂的实战案例,彻底搞懂单例模式

用一个通俗易懂的实战案例,彻底搞懂单例模式

作者: Java入门到入坟 | 来源:发表于2020-06-02 16:39 被阅读0次

    推荐阅读:

    一、背景

    • 在企业网站后台系统中,一般会将网站统计单元进行独立设计,比如登录人数的统计、IP数量的计数等。在这类需要完成全局统计的过程中,就会用到单例模式,即整个系统只需要拥有一个计数的全局对象。
    • 在网站登录这个高并发场景下,由这个全局对象负责统计当前网站的登录人数、IP等,即节约了网站服务器的资源,又能保证计数的准确性。

    二、单例模式

    1、概念

    单例模式是最常见的设计模式之一,也是整个设计模式中最简单的模式之一。

    单例模式需确保这个类只有一个实例,而且自行实例化并向整个系统提供这个实例;这个类也称为单例类,提供全局访问的方法。

    单例模式有三大要点:

    • 构造方法私有化;
      -- private Singleton() { }
    • 实例化的变量引用私有化;
      -- private static final Singleton APP_INSTANCE = new Singleton();
    • 获取实例的方法共有
      -- public static SimpleSingleton getInstance() {
      -- return APP_INSTANCE;
      -- }
    2、网站计数的单例实现

    实现单例模式有多种写法,这里我们只列举其中最常用的三种实现方式,且考虑到网站登录高并发场景下,将重点关注多线程环境下的安全问题。

    • 登录线程的实现
      我们先创建一个登录线程类,用于登录及登录成功后调用单例对象进行计数。
    /**
     * 单例模式的应用--登录线程
     *
     * @author zhuhuix
     * @date 2020-06-01
     */
    public class Login implements Runnable {
        // 登录名称
        private String loginName;
    
        public String getLoginName() {
            return loginName;
        }
    
        public void setLoginName(String loginName) {
            this.loginName = loginName;
        }
    
        @Override
        public void run() {
            // TODO 
            // 登录成功后调用单例对象进行计数
        }
    }
    
    • 主程序的实现
      编写一个主程序,利用多线程技术模拟10个用户并发登录,完成登录后输出登录人次计数。
    /**
     * 单例模式--主程序
     *
     * @author zhuhuix
     * @date 2020-06-01
     */
    public class App {
        public final static int num = 10;
    
        public static void main(String[] args) throws InterruptedException {
            Thread[] threads = new Thread[num];
    
            for (int i = 0; i < num; i++) {
                Login login = new Login();
                login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
                threads[i] = new Thread(login);
                threads[i].start();
            }
    
            for (int i = 0; i < threads.length; i++) {
                threads[i].join();
            }
    
            // TODO
            // 调用单例对象输出登录人数统计
    }
    
    2.1 饿汉模式
    • 在程序启动之初就进行创建( 不管三七二十一,先创建出来再说)。
    • 天生的线程安全。
    • 无论程序中是否用到该单例类都会存在。
    /**
     * 饿汉式单例模式
     *
     * @author zhuhuix
     * @date 2020-06-01
     */
    public class SimpleSingleton implements Serializable {
        // 单例对象
        private static final SimpleSingleton APP_INSTANCE = new SimpleSingleton();
        // 计数器
        private AtomicLong count = new AtomicLong(0);
    
        // 单例模式必须保证默认构造方法为私有类型
        private SimpleSingleton() {
        }
    
        public static SimpleSingleton getInstance() {
            return APP_INSTANCE;
        }
    
        public AtomicLong getCount() {
            return count;
        }
    
        public void setCount() {
            count.addAndGet(1);
        }
    
    }
    

    我们将饿汉模式的单例对象加入进登录线程及主程序中进行测试:

    /**
     * 单例模式的应用--登录线程
     *
     * @author zhuhuix
     * @date 2020-06-01
     */
    public class Login implements Runnable {
        // 登录名称
        private String loginName;
    
        public String getLoginName() {
            return loginName;
        }
    
        public void setLoginName(String loginName) {
            this.loginName = loginName;
        }
    
        @Override
        public void run() {
            // 饿汉式单例
            SimpleSingleton simpleSingleton=  SimpleSingleton.getInstance();
            simpleSingleton.setCount();
            System.out.println(getLoginName()+"登录成功:"+simpleSingleton.toString());
        }
    
    }
    
    /**
     * 单例模式--主程序
     *
     * @author zhuhuix
     * @date 2020-06-01
     */
    public class App {
        public final static int num = 10;
        public static void main(String[] args) throws InterruptedException {
            Thread[] threads = new Thread[num];
            for (int i = 0; i < num; i++) {
                Login login = new Login();
                login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
                threads[i] = new Thread(login);
                threads[i].start();
            }
            for (int i = 0; i < threads.length; i++) {
                threads[i].join();
            }
            System.out.println("网站共有"+SimpleSingleton.getInstance().getCount()+"个用户登录");
    
        }
    }
    

    输出如下:
    10个线程并发登录过程中,获取到了同一个对象引用地址,即该单例模式是有效的。

    2.2 懒汉模式
    • 在初始化时只进行定义。
    • 只有在程序中调用了该单例类,才会完成实例化( 没人动我,我才懒得动)。
    • 需通过线程同步技术才能保证线程安全。

    我们先看下未使用线程同步技术的例子:

    /**
     * 懒汉式单例模式--未应用线程同步技术
     *
     * @author zhuhuix
     * @date 2020-06-01
     */
    public class LazySingleton {
        // 单例对象
        private static LazySingleton APP_INSTANCE;
        // 计数器
        private AtomicLong count = new AtomicLong(0);
    
        // 单例模式必须保证默认构造方法为私有类型
        private LazySingleton() {
        }
    
        public static LazySingleton getInstance() {
            if (APP_INSTANCE == null) {
                APP_INSTANCE = new LazySingleton();
            }
            return APP_INSTANCE;
        }
    
        public AtomicLong getCount() {
            return count;
        }
    
        public void setCount() {
            count.addAndGet(1);
        }
    
      }
    
    /**
     * 单例模式的应用--登录线程
     *
     * @author zhuhuix
     * @date 2020-06-01
     */
    public class Login implements Runnable {
    
        ....
        @Override
        public void run() {
            // 饿汉式单例
            LazySingleton lazySingleton =LazySingleton.getInstance();
            lazySingleton.setCount();
            System.out.println(getLoginName()+"登录成功:"+lazySingleton);
        }
    
    }
    
    /**
     * 单例模式--主程序-
     *
     * @author zhuhuix
     * @date 2020-06-01
     */
    public class App {
        public final static int num = 10;
        public static void main(String[] args) throws InterruptedException {
            Thread[] threads = new Thread[num];
            for (int i = 0; i < num; i++) {
                Login login = new Login();
                login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
                threads[i] = new Thread(login);
                threads[i].start();
            }
            for (int i = 0; i < threads.length; i++) {
                threads[i].join();
            }
            System.out.println("网站共有" + LazySingleton.getInstance().getCount() + "个用户登录");
        }
    }
    

    输出结果:
    10个线程并发登录过程中,获取到了四个对象引用地址,该单例模式失效了。

    对代码进行分析:

    // 未使用线程同步
    public static LazySingleton getInstance() {
            // 在多个线程并发时,可能会有多个线程同时进入 if 语句,导致产生多个实例
            if (APP_INSTANCE == null) {
                APP_INSTANCE = new LazySingleton();
            }
            return APP_INSTANCE;
        }
    

    我们使用线程同步技术对懒汉式模式进行改进:

    /**
     * 懒汉式单例模式
     *
     * @author zhuhuix
     * @date 2020-06-01
     */
    public class LazySingleton {
        // 单例对象 ,加入volatile关键字进行修饰
        private static volatile LazySingleton APP_INSTANCE;
        // 计数器
        private AtomicLong count = new AtomicLong(0);
    
        // 单例模式必须保证默认构造方法为私有类型
        private LazySingleton() {
        }
    
        public static LazySingleton getInstance() {
            if (APP_INSTANCE == null) {
                // 对类进行加锁,并进行双重检查
                synchronized (LazySingleton.class) {
                    if (APP_INSTANCE == null) {
                        APP_INSTANCE = new LazySingleton();
                    }
                }
            }
            return APP_INSTANCE;
        }
    
        public AtomicLong getCount() {
            return count;
        }
    
        public void setCount() {
            count.addAndGet(1);
        }
    
      }
    

    再测试运行:
    10个线程并发登录过程中,获取到了同一个对象引用地址,即该单例模式有效了。

    2.3 枚举类实现单例模式

    《Effective Java》 推荐使用枚举的方式解决单例模式。这种方式解决了最主要的;线程安全、自由串行化、单一实例。

    /**
     * 利用枚举类实现单例模式
     *
     * @author zhuhuix
     * @date 2020-06-01
     */
    public enum EnumSingleton implements Serializable {
        // 单例对象
        APP_INSTANCE;
        // 计数器
        private AtomicLong count = new AtomicLong(0);
    
        // 单例模式必须保证默认构造方法为私有类型
        private EnumSingleton() {
        }
    
        public AtomicLong getCount() {
            return count;
        }
    
        public void setCount() {
            count.addAndGet(1);
        }
    
    }
    
    /**
     * 单例模式的应用--登录线程
     *
     * @author zhuhuix
     * @date 2020-06-01
     */
    public class Login implements Runnable {
        ...
        @Override
        public void run() {
             EnumSingleton enumSingleton = EnumSingleton.APP_INSTANCE;
             enumSingleton.setCount();
            System.out.println(getLoginName()+"登录成功:"+enumSingleton.toString());
    
        }
    }
    
    /**
     * 单例模式--主程序
     *
     * @author zhuhuix
     * @date 2020-06-01
     */
    public class App {
        public final static int num = 10;
        public static void main(String[] args) throws InterruptedException {
            Thread[] threads = new Thread[num];
            for (int i = 0; i < num; i++) {
                Login login = new Login();
                login.setLoginName("" + String.format("%2s", (i + 1)) + "号用户");
                threads[i] = new Thread(login);
                threads[i].start();
            }
            for (int i = 0; i < threads.length; i++) {
                threads[i].join();
            }
             System.out.println("网站共有"+EnumSingleton.APP_INSTANCE.getCount()+"个用户登录");
    
        }
    }
    
    

    输出如下:
    10个线程并发登录过程中,该单例模式是有效的。

    三、总结

    1. 文中首先说明了单例模式在网站计数的应用:创建唯一的全局对象实现统计单元的计数。
    2. 根据该需求,建立了Login登录线程类及App主程序,模拟多用户同步并发登录。
    3. 分别设计了饿汉模式、懒汉模式、枚举类三种不同的实现单例模式的方式。
    4. 在设计单例模式的过程中,特别要注意线程同步安全的问题,文中以懒汉模式列出了线程不同步的实际例子。
    5. 延伸思考:《Effective Java》为什么说实现单例模式的最佳方案是单元素枚举类型?

    相关文章

      网友评论

        本文标题:用一个通俗易懂的实战案例,彻底搞懂单例模式

        本文链接:https://www.haomeiwen.com/subject/gcauzhtx.html