一.概述
1.定义
所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
2.单例模式特点
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
3.单例模式的8种凡是
- 1) 单例模式保证了 系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
- 2) 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使 用new
- 3) 单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或 耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数 据库或文件的对象(比如数据源、session工厂等)
- 优点:由于单例模式只生成了一个实例,所以能够节约系统资源,减少性能开销,提高系统效率,同时也能够严格控制客户对它的访问。
- 缺点:也正是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,这样扩展起来有一定的困难。同时需要需要额外的处理线程安全。网上有这样观点jvm堆中的单例对象不会被垃圾收集,单例类本身如果长时间不用会不会被收集? (也有观点认为能够被回收,这里自己没有做深入探究,暂时不知对错)
二. 常用单例模式说明
1.饿汉式(静态常量)
(1) 创建方式
- 1) 构造器私有化 (防止 new )
- 2) 类的内部创建对象,加载时创建
- 3) 向外暴露一个静态的公共方法。getInstance
2.代码实现
package com.singleton;
/**
* @author zhourui
* @date 2020/2/1 14:21
* @description 饿汉式(静态常量)
* <p>
* 1) 构造器私有化 (防止 new )
* 2) 类的内部创建对象
* 3) 向外暴露一个静态的公共方法。getInstance
* 4) 代码实现
* </p>
*/
public class Singleton01 {
//构造器私有化,防止直接new
private Singleton01() {
}
//类内部静态属性,类加载时创建实例
private static Singleton01 singleton01 = new Singleton01();
//提供一个public获取这个类的实例
public static Singleton01 getInstance() {
return singleton01;
}
}
(3) 优缺点说明
- 1) 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
- 2) 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存 的浪费
- 3) 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载 时就实例化,在单例模式中大多数都是调用getInstance方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到lazy loading的效果
- 4) 结论:这种单例模式可用,可能造成内存浪费
2. 懒汉式(线程不安全)
(1) 创建方式
- 1) 构造器私有化 (防止 new )
- 2) 静态类属性,用于装载实例(懒加载),只有在调用getInstance时创建
- 3) 向外暴露一个静态的公共方法。getInstance
(2) 代码实现
package com.singleton;
/**
* @author zhourui
* @date 2020/2/1 14:36
* @description 懒汉式(线程不安全)
*/
public class Singleton02 {
//静态类属性,用于装载实例
private static Singleton02 singleton02;
//私有构造器,防止直接被new
private Singleton02() {
}
//提供静态共有方法获取实例
public static Singleton02 getInstance() {
if (singleton02 == null) {
singleton02 = new Singleton02();
}
return singleton02;
}
}
(3) 优缺点说明
- 1) 起到了Lazy Loading的效果,但是只能在单线程下使用。
- 2) 如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以 在多线程环境下不可使用这种方式
- 3) 结论:在实际开发中,不要使用这种方式.
3.懒汉式(线程安全,同步方法)
(1) 代码实现
package com.singleton;
/**
* @author zhourui
* @date 2020/2/1 14:36
* @description 懒汉式(线程安全 , 同步方法)
*/
public class Singleton03 {
//静态类属性,用于装载实例
private static Singleton03 singleton03;
//私有构造器,防止直接被new
private Singleton03() {
}
//提供静态共有方法获取实例,加入synchronized关键字,解决线程不安全问题
public static synchronized Singleton03 getInstance() {
if (singleton03 == null) {
singleton03 = new Singleton03();
}
return singleton03;
}
}
- 1) 解决了线程不安全问题
- 2) 效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行 同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例, 直接return就行了。方法进行同步效率太低
- 3) 结论:在实际开发中,不推荐使用这种方式
4.懒汉式(线程安全?,同步代码块),并不能做到真正的线程安全
(1) 代码实现
package com.singleton;
/**
* @author zhourui
* @date 2020/2/1 14:36
* @description 懒汉式(线程安全 ? , 同步代码块), 并不能做到真正的线程安全
*/
public class Singleton04 {
//静态类属性,用于装载实例
private static Singleton04 singleton04;
//私有构造器,防止直接被new
private Singleton04() {
}
//提供静态共有方法获取实例
public static Singleton04 getInstance() {
if (singleton04 == null) {
//将同步方法改为同步代码块,本意是相对前面同步方法效率太低做出改进,实际没用
synchronized (Singleton04.class) {
singleton04 = new Singleton04();
}
}
return singleton04;
}
}
(2) 优缺点说明
- 1) 这种方式,本意是想对第上一种实现方式的改进,因为前面同步方法效率太低, 改为同步产生实例化的的代码块
- 2) 但是这种同步并不能起到线程同步的作用。假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行, 另一个线程也通过了这个判断语句,这时便会产生多个实例
- 3) 结论:在实际开发中,不能使用这种方式
5. 双重检查,推荐使用这种单例设计模式
(1) 代码实现
package com.singleton;
/**
* @author zhourui
* @date 2020/2/1 14:36
* @description 懒汉式(线程安全 ? , 同步代码块), 并不能做到真正的线程安全
*/
public class Singleton05 {
//静态类属性,用于装载实例,加入volatile关键字防止指令重排
private static volatile Singleton05 singleton05;
//私有构造器,防止直接被new
private Singleton05() {
}
//提供静态共有方法获取实例
public static Singleton05 getInstance() {
if (singleton05 == null) {
//将同步方法改为同步代码块
synchronized (Singleton05.class) {
//继续加入非空判断
if (singleton05 == null) {
singleton05 = new Singleton05();
}
}
}
return singleton05;
}
}
上面的代码就是经典的double-checked locking 解决单例懒汉多线程的安全问题的示例,其实就是把锁的粒度减低,但是其实还是会有问题,比如AB线程,A先进入同步代码块,它创建新的Singleton05 对象。
但是,如果不加volatile关键字 , JVM不会保证对对象使用构造函数进行初始化和将内存地址赋给栈中的变量这两个操作的先后顺序。所以A在分配好内存后,可能直接赋值给栈中的变量而还没有初始化,这个时候B想进去第二个if判断,发现已经存在实例了,所以直接拿去用了这个还没初始化的对象,出现错误。
加上volatile关键字保持内存可见性和防止指令重排序,从而避免了上面问题的出现
- 内存可见性(MemoryVisibility):所有线程都能看到共享内存的最新状态;
- 指令重排:在基于排序关系的Happens-Before内存模型中,指令重排技术大大提高了程序执行效率,但同时也引入了一些问题,如上面的不同线程对相同变量的内存不可见
(2) 优缺点说明
- 1) Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两 次if (singleton == null)检查,这样就可以保证线程安全了。
- 2) 这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null), 直接return实例化对象,也避免的反复进行方法同步.
- 3) 线程安全;延迟加载;效率较高
- 4) 结论:在实际开发中,推荐使用这种单例设计模式
6.静态内部类 ( 最完美的方式 )
(1) 代码实现
package com.singleton;
/**
* @author zhourui
* @date 2020/2/1 16:23
* @description 静态内部类
*/
public class Singleton06 {
//私有构造器,防止直接被new
private Singleton06() {
}
//静态内部类提供获取实例的内部获取实例的方式,在Singleton06被装载时并不会立即实例化,而是在需要实例化
//时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton06的实例化。
private static class SingletonInstance {
private static final Singleton06 INSTANCE = new Singleton06();
}
//对外或区实例的方法
public static Singleton06 getInstance(){
return SingletonInstance.INSTANCE;
}
}
(2) 优缺点说明
- 1) 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
- 2) 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化 时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的 实例化。
- 3) 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
- 4) 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
- 5) 结论:推荐使用.
三. 单例模式的实际应用
1. jedis连接工厂的创建(双重检查方式)
在实际项目开发中会用到redis,而Jedis是Redis官方推荐的Java连接开发工具,但是Jedis对象不是线程安全的,在多线程下使用同一个Jedis对象会出现并发问题,为了避免每次使用Jedis对象时都需要重新创建,Jedis提供了JedisPool。JedisPool是线程安全的连接池 。因此项目中会创建并初始化一个JedisPool对象,通过这个JedisPool来安全的获取Jedis对象,而为了避免频繁的进行创建和销毁JedisPool连接池对象, 所以用到了单例模式
@Slf4j
public class JedisFactory {
//静态类属性,用于装载JedisPool实例,加入volatile关键字防止指令重排
private static volatile JedisPool pool;
//提供静态共有方法获取实例
public static Jedis getJedis() {
if (pool == null || pool.isClosed()) {
//同步代码块保证为同一个JedisPool
synchronized (JedisFactory.class){
//双重检测
if (pool == null || pool.isClosed()){
initPool();
}
}
}
return pool.getResource();
}
public static void initPool() {
//结合spring从配置文件读取
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(PropertyConfig.maxActive);
config.setMaxIdle(PropertyConfig.maxIdle);
config.setMaxWaitMillis(PropertyConfig.maxWait);
config.setTestOnBorrow(PropertyConfig.testOnBorrow);
config.setTestOnReturn(PropertyConfig.testOnReturn);
pool = new JedisPool(config, PropertyConfig.redisHost,
PropertyConfig.redisPort);
}
public static void returnJedis(Jedis jedis) {
pool.returnResource(jedis);
}
public static void returnBrokenJedis(Jedis jedis) {
pool.returnBrokenResource(jedis);
}
}
2.JDK中的java.lang.Runtime(饿汉式)
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
}