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

1、背景

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

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

2、单例模式

一、概念

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

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

单例模式有三大要点:服务器

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

二、网站计数的单例实现

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

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

  • 登陆线程的实现
    咱们先建立一个登陆线程类,用于登陆及登陆成功后调用单例对象进行计数。
/**
 * 单例模式的应用--登陆线程
 *
 * @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个线程并发登陆过程当中,获取到了同一个对象引用地址,即该单例模式是有效的。ide

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

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;
    }

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

/**
 * 懒汉式单例模式
 *
 * @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个线程并发登陆过程当中,该单例模式是有效的。

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

3、总结

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