设计模式之单例设计模式

单例设计模式

5.1 基本介绍

单例模式的定义就是确保某一个类只有一个实例,并且提供一个全局访问点。属于设计模式三大类中的创建型模式

单例模式具有典型的三个特点:

  • 单例类只有一个实例对象
  • 该单例对象必须由单例类自行创建
  • 单例类对外提供一个访问该单例的全局访问点

5.2 单例设计模式的实现

举例:

我们可以做一个这样的尝试,在Windows的“任务栏”的右键弹出菜单上多次点击“启动任务管理器”,看能否打开多个任务管理器窗口?能找到咱两凑一对?

通常情况下,无论我们启动任务管理多少次,Windows系统始终只能弹出一个任务管理器窗口,也就是说在一个Windows系统中,任务管理器存在唯一性。

为什么要这样设计呢?我们可以从以下两个方面来分析:

其一,如果能弹出多个窗口,且这些窗口的内容完全一致,全部是重复对象,这势必会浪费系统资源,任务管理器需要获取系统运行时的诸多信息,这些信息的获取需要消耗一定的系统资源,包括CPU资源及内存资源等,浪费是可耻的,而且根本没有必要显示多个内容完全相同的窗口。

其二,如果弹出的多个窗口内容不一致,问题就更加严重了,这意味着在某一瞬间系统资源使用情况和进程、服务等信息存在多个状态,例如任务管理器窗口A显示“CPU使用率”为10%,窗口B显示“CPU使用率”为15%,到底哪个才是真实的呢?

5.2.1 饿汉式(立即加载)

实现步骤:

1、既然要保证类不能随便实例化,因此需要私有化构造器

private Singleton() {}

2、私有的静态的当前类的对象作为属性

private static final Singleton instance = new Singleton();

注意点:

  • private

    如果不私有化属性,使用public会存在什么问题呢?

    拿到的实例对象轻轻松松就给修改了,因此私有化属性,不让其进行修改。

  • static

    如果没有使用static修饰会发生什么问题呢?

    可以看出在未使用static修饰的话,出现了栈溢出错误。

    使用static的话可以保证你的实例对象只有一份,不会出现溢出。

  • final

    私有化属性还是存在问题,可以通过反射获取进行修改,因此使用final进行修饰,这下你通过反射对属性就无法进行修改了。

3、公有的静态的方法 返回当前唯一属性

// 当前类不能实例只能通过static进行方法获取
public static Singleton getInstance() {
        return instance;
}

完整代码:

public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1.hashCode());
        System.out.println(singleton2.hashCode());
        System.out.println(singleton1 == singleton2);
    }
}

// 饿汉式(静态常量的方式)
class Singleton {
    // 1 构造器私有化
    private Singleton() {
    }

    // 2 本类内部创建对象实例
    private static final Singleton instance = new Singleton();

    // 3、共有的静态的方法 返回当前唯一属性
    public static Singleton getInstance() {
        return instance;
    }
}

测试结果:
	460141958
    460141958
    true

优缺点说明:

  • 优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
  • 缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
  • 结论:这种单例模式可用,可能造成内存浪费

5.2.2 懒汉式(延迟加载)

1、方式一(线程不安全)

代码实现:

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // 提供一个静态的公有方法,当使用到该方法时,才去创建 instance
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优缺点说明:

  • 起到了 Lazy Loading 的效果,但是只能在单线程下使用。
  • 如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会 产生多个实例。所以在多线程环境下不可使用这种方式
  • 结论:在实际开发中, 不要使用这种方式。

2、方式二(线程安全)

代码实现:

// 懒汉式(线程安全,同步方法)
class Singleton2 {
    private static Singleton2 instance;

    private Singleton2() {
    }

    //提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
    // 添加synchronized锁 解决线程安全的问题
    public static synchronized Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

优缺点说明:

  • 解决了 线程安全问题。
  • 效率太低了,每个线程在想获得类的实例时候,执行 getInstance()方法都要进行同步。
  • 结论:在实际开发中, 不推荐使用这种方式。

5.2.3 双重检查

代码实现:

// 懒汉式(线程安全,同步方法)
class Singleton {
    // 添加volatile 保证属性 加载 赋值的过程中 不会被JVM指令重排序
    private static volatile Singleton instance;

    private Singleton() {}

    //提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
    //同时保证了效率, 推荐使用
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile:用大白话来讲就好比排队取号问题,你的位置已经固定,不允许穿插。

什么是双重检查加锁机制:

并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。

这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

双重检查加锁机制的实现会使用一个关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

注意在Java1.4及以前版本中,很多JVM对于volatile关键字的实现有问题,会导致双重检查加锁的失败,因此双重检查加锁的机制只能用在Java5及以上的版本。

这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是在第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。
提示:
由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就
是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用

优缺点说明:

  • Double-Check 概念是多线程开发中常使用到的,如代码中所示,我们进行了两次 if (singleton == null)检查,这样就可以保证线程安全了。
  • 实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接 return 实例化对象,也避免的反复进行方法同步。
  • 线程安全,延迟加载,效率较高
  • 结论:在实际开发中, 推荐使用这种单例设计模式(根据情况来选用)

5.2.4 静态内部类

代码实现:

//饿汉式(静态内部类)
class Singleton {
        private Singleton() {}
        // Singleton进行类装载的时候并不会创建,从而懒加载
        private static class SingletonInstance {
            private static final Singleton INSTANCE = new Singleton();
        }
        // 调用时装载SingletonInstance,JVM装载时是线程安全的
        public static Singleton getInstance() {
            return SingletonInstance.INSTANCE;
        }
}

优缺点说明:

  • 这种方式采用了类装载的机制来保证初始化实例时只有一个线程
  • 静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化。
  • 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
  • 优点: 避免了线程不安全,利用 静态内部类特点实现延迟加载,效率高
  • 结论: 推荐使用

5.2.5 枚举

代码实现:

//使用枚举,可以实现单例, 推荐
public enum Singleton {
// 定义一个枚举元素,它就代表Singleton的一个实例
    INSTANCE; 
    // 单例可以有自己的方法
    public void singletonOperation() {
        // 功能处理
    }
}

优缺点说明:

  • 这借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
  • 结论: 推荐使用

5.2.6 单例模式在 JDK 应用的源码分析

我们 JDK 中,java.lang.Runtime 就是经典的单例模式(饿汉式)

5.2.7 单例模式的扩展

思考单例模式:

单例模式是为了控制在运行期间,某些类的实例数目只能有一个。

可能有人就会思考,能不能控制实例数目为2个,3个,或者是任意多个呢?

目的都是一样的,节约资源啊,有些时候单个实例不能满足实际的需要,会忙不过来,根据测算,3个实例刚刚好。

也就是说,现在要控制实例数目为3个,怎么办呢?

// 简单演示如何拓展单例模式,控制实例数目为3个
public class OneExtend {
    // 定义一个缺省的key值的前缀
    private final static String DEFAULT_PREKEY = "Cache";

    // 缓存实例的容器
    private static Map<String, OneExtend> map =
            new HashMap<String, OneExtend>();

    // 用来记录当前正在使用第几个实例,到了控制的最大数目,就返回从1开始

    private static int num = 1;

    // 定义控制实例的最大数目
    private final static int NUM_MAX = 3;

    // 构造器私有化
    private OneExtend() {
    }

    public static OneExtend getInstance() {
        String key = DEFAULT_PREKEY + num;
        // 缓存的体现,通过控制缓存的数据多少来控制实例数目
        OneExtend oneExtend = map.get(key);
        if (oneExtend == null) {
            oneExtend = new OneExtend();
            map.put(key, oneExtend);
        }
        // 把当前实例序号加一
        num++;
        if (num > NUM_MAX) {
            // 如果实例的序号达到最大值,从1开始
            num = 1;
        }
        return oneExtend;
    }

    public static void main(String[] args) {
        OneExtend t1 = getInstance();
        OneExtend t2 = getInstance();
        OneExtend t3 = getInstance();
        OneExtend t4 = getInstance();
        OneExtend t5 = getInstance();
        OneExtend t6 = getInstance();
        System.out.println("t1 = " + t1.hashCode());
        System.out.println("t2 = " + t2.hashCode());
        System.out.println("t3 = " + t3.hashCode());
        System.out.println("t4 = " + t4.hashCode());
        System.out.println("t5 = " + t5.hashCode());
        System.out.println("t6 = " + t6.hashCode());
    }
}

测试结果:
    t1 = 460141958
    t2 = 1163157884
    t3 = 1956725890
    t4 = 460141958
    t5 = 1163157884
    t6 = 1956725890

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值