04-01-设计模式 单利模式

单利模式介绍

简介

说白了, 单利模式, 从字面上就能理解, 就是采取一定的方法保证在整个系统中, 对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)

举例

比如Hibernate的SessionFactory,它充当数据存储源的代理, 并负责创建Session对象, SessionFactory并不是轻量级的, 一般情况下, 一个项目通常只需要一个SessionFactory就够了,这时就会使用到单利模式

Hibernate可能现在基本用的不多了,但是Spring大家应该都用, 在Spring中, 一般声明的Bean, 如果没有特殊配置, 那么它就是单利的

单利模式实现的八种方式

  1. 饿汉式(静态常量)
  2. 饿汉式(静态代码块)
  3. 懒汉式(线程不安全)
  4. 懒汉式(线程安全, 同步方法)
  5. 懒汉式(线程安全, 同步代码块)
  6. 双重检查
  7. 静态内部类
  8. 枚举

饿汉式(静态常量)

步骤

  1. 私有化构造方法(防止 new)
  2. 类的内部创建对象
  3. 向外部暴露一个静态的公共方法, getInstance

代码

package com.dance.design.designmodel.simpleinterestmodel;

public class SimpleOne {

}

/**
 * 饿汉式(静态常量)
 */
class SingleOne{

    // 创建静态常量
    private static final SingleOne singleOne = new SingleOne();

    // 私有化构造
    private SingleOne(){
    }

    /**
     * 提供一个公共静态方法
     * @return 类
     */
    public static SingleOne getInstance() {
        return singleOne;
    }
}

我感觉这没啥好测试的, 我下面就直接写代码了, 但凡做过开发的,应该都看的懂

总结

  • 优点
    • 写法简单, 线程安全
  • 缺点
    • 不是懒加载, 如果不用的话, 就会造成内存浪费
  • 为什么线程安全?
    • 基于类加载实现的, 存放于元空间, 应为是static的 在类加载的时候就会创建
  • 为什么不用,会造成内存浪费?
    • 应为在类加载的时候就创建, 不是用的时候才创建, 所以不是懒加载, 应为是一上来就加载, 并且是放在元空间的, 并不会被垃圾回收器回收, 如果不用就一直 存在, 所以会造成内存浪费
  • 结论
    • 如果是一定会用到, 可以用, 但是还是建议使用懒加载的

饿汉式(静态代码块)

步骤

  1. 私有化构造
  2. 声明静态成员
  3. 静态代码块初始化
  4. 对外提供公共静态方法

代码

package com.dance.design.designmodel.simpleinterestmodel;

public class SimpleTwo {

}

/**
 * 饿汉式(静态代码块)
 */
class SingleTwo{

    // 创建静态常量
    private static final SingleTwo singletwo;

    static {
        singletwo = new SingleTwo();
    }

    // 私有化构造
    private SingleTwo(){
    }

    /**
     * 提供一个公共静态方法
     * @return 类
     */
    public static SingleTwo getInstance() {
        return singletwo;
    }
}

总结

其实和静态常量方式,一样, 就是从直接New改到了代码块中

懒汉式(线程不安全)

步骤

  1. 私有化构造
  2. 声明静态成员变量
  3. 提供对外公共方法
  4. 在公共方法中去创建对象

代码

package com.dance.design.designmodel.simpleinterestmodel;

public class SimpleThree {

}

/**
 * 懒汉式(线程不安全)
 */
class SingleThree{

    // 创建静态常量
    private static SingleThree singleThree;

    // 私有化构造
    private SingleThree(){
    }

    /**
     * 提供一个公共静态方法
     * @return 类
     */
    public static SingleThree getInstance() {
        if (null == singleThree) {
            singleThree = new SingleThree();
        }
        return singleThree;
    }
}

总结

  • 优点
    • 提供了懒加载
  • 缺点
    • 线程不安全
  • 为什么说提供了懒加载?
    • 应为并没有在类加载的时候就创建, 而是在第一次调用的时候才创建的
  • 为什么说线程不安全
    • 应为没有锁机制, 导致多个线程可能同时进入到if块的内部, 导致都创建了对象,导致多利的存在, 破坏了单利模式的存在
  • 总结
    • 不要用, 不为啥

懒汉式(线程安全, 同步方法)

步骤

  1. 私有化构造
  2. 声明静态成员
  3. 提供对外公共静态方法
  4. 在方法中创建对象
  5. 在方法上加内置锁

代码

package com.dance.design.designmodel.simpleinterestmodel;

public class SimpleFour {

}

/**
 * 懒汉式(线程安全, 同步方法)
 */
class SingleFour{

    // 创建静态常量
    private static SingleFour singleFour;

    // 私有化构造
    private SingleFour(){
    }

    /**
     * 提供一个公共静态方法
     * @return 类
     */
    public synchronized static SingleFour getInstance() {
        if (null == singleFour) {
            singleFour = new SingleFour();
        }
        return singleFour;
    }
}

总结

  • 优点
    • 懒加载
    • 线程安全
  • 缺点
    • 效率问题
  • 为什么线程安全?
    • 应为在方法上加上了内置锁, 并且方法是static的, 所以是类锁, 保证了所有的线程访问这个方法都必须排队, 所以保证了线程安全
  • 有什么效率问题?
    • 是应为所有的线程排队这个问题, 应为大量线程获取的时候,其实第一个线程就创建好了,其他的线程其实是不需要排队的, 所以存在效率问题
  • 总结
    • 不要用, 不为啥

懒汉式(线程安全, 同步代码块)

步骤

  1. 私有化构造
  2. 声明静态成员
  3. 对外提供公共静态方法
  4. 在方法内部创建对象
  5. 添加同步代码块

代码

package com.dance.design.designmodel.simpleinterestmodel;

public class SimpleFive {

}

/**
 * 懒汉式(线程安全, 同步代码块)
 */
class SingleFive {

    // 创建静态常量
    private static SingleFive singleFive;

    // 私有化构造
    private SingleFive() {
    }

    /**
     * 提供一个公共静态方法
     *
     * @return 类
     */
    public static SingleFive getInstance() {
        if (null == singleFive) {
            // 其实还是类锁
            synchronized (SingleFive.class) {
                singleFive = new SingleFive();
            }
        }
        return singleFive;
    }
}

总结

  • 我都不是很想总结这个单同步代码块的单利模式了, 在写的时候思考到了一些问题, 和大家聊聊问题吧
  • 大家看代码,这个同步代码块,其实还是类锁, 这个同步块如果加在if块的内部, 还是会导致线程不安全说的那个问题,就是多线程卡在if块里面, 如果是加载if块外面, 就会和同步方法一样,直接卡到开头, 所我感觉我起的这个名字很好"单利Five", 怪不得排在第五
  • 不要用,不为啥

双重检查

步骤

参考同步方法的步骤

将同步块放在if块的外面,然后在同步块的外面再包一层if块

代码

package com.dance.design.designmodel.simpleinterestmodel;

public class SimpleSix {

}

/**
 * 双重检查
 */
class SingleSix {

    // 创建静态常量
    private static volatile SingleSix singleSix;

    // 私有化构造
    private SingleSix() {
    }

    /**
     * 提供一个公共静态方法
     *
     * @return 类
     */
    public static SingleSix getInstance() {
        // 再包一层if块用于卡线程, 如果有了就可以直接获取, 解决排队问题
        if (null == singleSix) {
            // 其实还是类锁
            synchronized (SingleSix.class) {
                if (null == singleSix){
                    singleSix = new SingleSix();
                }
            }
        }
        return singleSix;
    }
}

总结

  • 优点
    • 懒加载, 线程安全, 效率高
  • 缺点
    • emmm... 不知道怎么写了.
  • 为什么线程安全?
    • 应为采用了类锁, 线程去排队, 并且成员变量采用了volatile修饰
    • 为什么需要volatile修饰?
    • 因为这种双重检测机制在JDK1.5之前是有问题的,问题还是出在(//创建实例),由所谓的无序写入造成的。一般来讲,当初始化一个对象的时候,会经历
    • 内存分配 -> 初始化 -> 返回对象引用
    • 这种方式产生的对象是一个完整的对象,可以正常使用。但是JAVA的无序写入可能会造成顺序的颠倒,即
    • 内存分配 -> 返回对象引用 -> 初始化
    • 这种情况下对应到(//创建实例)就是singleton已经不是null,而是指向了堆上的一个对象,但是该对象却还没有完成初始化动作。当后续的线程发现singleton不是null而直接使用的时候,就会出现意料之外的问题。
    • 解决方案:
    • JDK1.5之后,可以使用volatile关键字修饰变量来解决无序写入产生的问题,因为volatile关键字的一个重要作用是禁止指令重排序,即保证不会出现内存分配、返回对象引用、初始化这样的顺序,从而使得双重检测真正发挥作用
  • 为什么效率高?
    • 应为在类锁的,外部和内部都有检查, 在创建一次之后,以后就不会走类锁了, 所以后续不会排队
  • 总结
    • 没错, 写不出来缺点, 就用这种吧

静态内部类

步骤

  1. 私有化构造
  2. 声明静态成员
  3. 声明静态内部类
  4. 内部类声明属性
  5. 提供对外公共静态方法

代码

package com.dance.design.designmodel.simpleinterestmodel;

public class SimpleSeven {

}

/**
 * 双重检查
 */
class SingleSeven {

    // 私有化构造
    private SingleSeven() {
    }

    private static final class SingleSevenHolder {
        // 创建静态常量
        static final SingleSeven singleSeven = new SingleSeven();
    }

    /**
     * 提供一个公共静态方法
     *
     * @return 类
     */
    public static SingleSeven getInstance() {
        // 再包一层if块用于卡线程, 如果有了就可以直接获取, 解决排队问题
        // 其实还是类锁
        return SingleSevenHolder.singleSeven;
    }
}

总结

  • 优点
    • 线程安全, 懒加载, 效率高
  • 缺点
    • emm.. 一样不知道
  • 为什么懒加载, 不是static的吗?
    • 应为类只有在第一次调用或者其他类依赖的时候才会进行类加载, 类加载, 这个内部类没有没其他类依赖, 并且是内部的所以在加载外部类的时候,也不会加载内部类, 只有第一次调用 getInstance方法时才会触发类加载
  • 为什么是线程安全的?
    • 类加载只会触发一次, 除非类卸载,
  • 为什么效率高?
    • 只触发一次类加载, 不需要判断(双重检查的判断都省略了), 直接可以返回, 不用排队,
  • 总结
    • 用就完了,不为啥

枚举

步骤

  1. 创建属性
  2. 创建方法

代码

package com.dance.design.designmodel.simpleinterestmodel;

public class SimpleEight {
    public static void main(String[] args) {
        SingleEight.INSTANCE.say();
    }
}

/**
 * 静态内部类
 */
enum SingleEight {
    INSTANCE;

    public void say(){
        System.out.println("say 单利模式");
    }
}

总结

  • 优点
    • 线程安全, 开发简单, 防止反序列化创建, 效率高
  • 缺点
    • 一样不清楚
  • 总结
    • 用就完了, 大佬推荐[Effective Java作者 Josh Bloch 提倡]

源码剖析

JDK源码中的单利模式

  1. JDK中 java.long.Runtime就是经典的单利模式(饿汉式)

代码

package java.lang;

import java.io.*;
import java.util.StringTokenizer;
import sun.reflect.CallerSensitive;
import sun.reflect.Reflection;

/**
 * 第一句话就表明了这是一个单利的类
 * Every Java application has a single instance of class
 * .......
 */

public class Runtime {
    // 声明私有的成员变量 一上来就new 所以是饿汉式(静态变量)
    private static Runtime currentRuntime = new Runtime();
    
    // 提供对外的公共静态方法
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    // 私有化构造方法
    /** Don't let anyone else instantiate this class */
    private Runtime() {}

}

单利模式注意事项和细节说明

  1. 单利保证了系统中只存在一个对象, 节省了系统资源, 对于一些需要频繁创建销毁的对象, 使用单利可以提高系统性能
  2. 当你想要获得一个单利类的时候,应该是调用公共静态方法获取,而不是通过new
  3. 使用场景
    1. 需要频繁的进行创建和销毁的对象
    2. 创建对象耗时过多或耗费资源过多(即: 重量级对象), 但又经常用的对象
    1. 工具类
    1. 频繁访问数据库或文件的对象
    1. 数据源
    2. Session工厂
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值