单例模式

单例模式 Singleton
很多时候,我们对于某个类,是不需要有很多实例存在的。打个比方:1个教室里面有很多学生要喝水,但是只有1台饮水机,没有必要给每个学生都安排1台饮水机。
这个时候,我们就需要使用单例模式

一.饿汉式(最常用)

public class Singleton {
    //1.final修饰的常量一般字母大写
    //2.自己内部new出1个对象后 给构造方法加private让其他人无法new
    private static final Singleton INSTANCE = new Singleton();
    private Singleton(){};

    //3.private修饰的 需要有1个返回方法
    public static Singleton getInstance(){return INSTANCE;};


    public static void main(String[] args) {
        Singleton m1 = Singleton.getInstance();
        Singleton m2 = Singleton.getInstance();
        //验证 是不是 只会new出1个对象 如果地址相等 那么一定是同1个
        System.out.println(m1 == m2);

    }
}

饿汉式的优点:
1.类加载到内存后,就实例化一个单例,而且JVM线程安全(因为JVM保证每个class类 只会load到内存一次 static变量是在类load后马上进行初始化的 所以它也只会初始化一次 多线程访问也没有关系)
2.简单方便

private static final Singleton INSTANCE;
//可以把 static修饰符 变成static代码块
static {
  INSTANCE = new Singleton();
}
...其他不变

二.懒汉式
懒汉式 :临到用时方恨少 用到时才创建

public class Singleton2 {
//注意我们这个对象不能 定义为final 因为定义了final 就是常量要进行static初始化(要么用static修饰 要么用static静态代码块)
    private  static Singleton2 INSTANCE;
    private Singleton2(){

    }

    public static Singleton2 getInstance() {
        //懒汉式 :临到用时方恨少 用到时才创建
        if(INSTANCE == null ){
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        //测试一下 是否获取的是同一对象
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Singleton2.getInstance().hashCode());
            }).start();
        }
    }
}

缺点:多线程同时访问时 线程不安全

    public static Singleton2 getInstance() {
        if(INSTANCE == null ){
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
    }

假如第1个线程 调用了getInstance() 但是还没有new Singleton2()
此时另外一个线程来了 它也会判断if 有没有INSTANCE 它此时是null的 那么第二个线程就会new一个Singletion2() 而第一个线程也继续执行 那么这个INSTANCE在两个线程中 已经不是同一个实例了

我们可以做个实验 在INSTACE = new Singleton2()之前 让线程sleep一会儿

if(INSTANCE == null ){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e){
                e.printStackTrace();
            }
            INSTANCE = new Singleton2();
        }
        return INSTANCE;
hashcode不同 所以new了不同的对象出来

三.加锁版
为了解决懒汉式线程不安全的问题,我们可以在调用getInstace()前,给方法加上锁

public class Singleton3 {
    private static Singleton3 INSTANCE;
    private Singleton3(){
    }

    public static synchronized Singleton3 getInstance(){
        if(INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Singleton3.getInstance().hashCode());
            }).start();
        }
    }
}

缺点:
1.内存中的对象 大得多比原来的
2.用的时候要看 有没有加锁,有没有申请到这把锁 效率降低

四.1个错误的单例模式
想减少锁住的范围,提高效率,但是却让线程不安全

public class Singleton4 {
    private static Singleton4 INSTANCE;
    private Singleton4(){
        
    }
    
    public  static Singleton4 getInstance() {
        if(INSTANCE == null ){
            synchronized (Singleton4.class) {
                INSTANCE = new Singleton4();
            }
            
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
              new Thread(()->{
                  System.out.println(Singleton4.getInstance().hashCode());
              }).start();  
        }
    }
}

这种方法不能做到只new出来1个实例:因为当第1个线程 执行到 synchronized之前时,第2个线程 往下执行完了,申请到了锁,并且new出来对象Singleton4;然后第2个线程释放锁,第1个进程进入synchronized继续执行,它又new出来了1个Singleton4对象
本质原因:就是因为 它对象的判空if(INSTANCE == null) 没有和创建对象INSTANCE = new Singleton4(); 同时操作 只要有1个时间间隙 就会出现 不同的对象 就会线程不安全 必须在INSTANCE==null的前提条件 去new Singleton4才可以

五.双重判断/检查法
我们在new Singleton前 我们再判空1次 就行了

public class Singleton5 {
    private static Singleton5 INSTANCE;
    private Singleton5(){

    }
    public static Singleton5 getInstance(){
        if(INSTANCE == null){
            synchronized (Singleton5.class){
                //双重检查
                if(INSTANCE == null ){
                    INSTANCE = new Singleton5();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Singleton4.getInstance().hashCode());
            }).start();
        }
    }
}

为什么要判断两次?第一次可以不可以去掉?

if(INSTANCE == null){
            synchronized (Singleton5.class){

其实有必要 第一次的判断 因为这样会省很多事 不然很多情况都要被上锁 有很多情况 一旦INSTANCE被初始化后 进来看到不为空后 它就不会继续执行下去了 提高效率

六.静态内部类方法

public class Singleton6 {
    //给构造方法加上private是单例模式的标配了
    private Singleton6(){
    }

    //加载外部类时不会加载内部类,这样可以实现懒加载
    private static class Singleton6Holder {
        //只有它的内部类在内部 才可以访问 外部类访问不了 
        private final static Singleton6 INSTANCE = new Singleton6();
    }

    public static Singleton6 getInstance() {
        return Singleton6Holder.INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Singleton6.getInstance().hashCode());
            }).start();
        }
    }
}

加载外部类时不会加载内部类,这样可以实现懒加载,只有当我们调用 getInstance()的时候 才会被加载,它是线程安全的,因为JVM只加载class一次
这比上面几种都好

七.枚举类(Java创始人的写法)

public enum Singleton7 {
    INSTANCE;

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(Singleton7.INSTANCE.hashCode());
            }).start();
        }
    }
}

这样不仅可以解决线程不同步 还可以反序列化(因为java 反射可以通过class文件 把整个class load到内存 new出1个实例 前面的写法 他都可以找到你的class文件 来new 1个INSTANCE出来 通过 反序列化的方式 而枚举单例 不会被反序列化 它只是一个抽象类,因为枚举类 没有构造方法 就算你拿到他的class文件 也没有办法 构造他的对象,返回的只是1个值 返回的是单例创建的1个对象)
缺点:本来你是一个resource manger的类 但是你定义为枚举 有点别扭

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容