14

用一个通俗易懂的例子彻底说清楚单例模式

 4 years ago
source link: http://www.cnblogs.com/zhuhuix/p/13030646.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

目录

一、背景

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

二、单例模式

1、概念

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

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

单例模式有三大要点:

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

    return APP_INSTANCE;

    -- }

2、网站计数的单例实现

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

Nf2ARfR.png!web
  • 登录线程的实现

    我们先创建一个登录线程类,用于登录及登录成功后调用单例对象进行计数。

/**
 * 单例模式的应用--登录线程
 *
 * @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个线程并发登录过程中,获取到了同一个对象引用地址,即该单例模式是有效的。

3MnM7f3.png!web

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个线程并发登录过程中,获取到了四个对象引用地址,该单例模式失效了。

qMBrEnZ.png!web

对代码进行分析:

// 未使用线程同步
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个线程并发登录过程中,获取到了同一个对象引用地址,即该单例模式有效了。

jeuYFz6.png!web

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个线程并发登录过程中,该单例模式是有效的。

eEVnEnA.png!web

三、总结

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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK