设计模式-单例模式

前言

设计模式对于自我提升很有帮助,能够提升自己对于代码的掌控,这都是前辈经过无数验证总结出的经验,都是精华,应该认真学习。在一些开源框架中几乎随处可见,掌握好设计模式,对于我们阅读源码亦有莫大帮助。

从单例模式开始

那么多设计模式中,得益于Spring的影响,最熟悉的要属单例模式了,所以先从单例开始整起来。

定义

一个类只有一个实例,并且该类可以自行创建这个实例的一种模式。

优点

  • 减少内存资源
  • 保证数据内容一致性

缺点

  • 单例模式一般没有接口,扩展困难,如果要扩展需要修改原来的代码,违反开闭原则
  • 并发测试,单例不利于代码调试。

应用场景

  • 需要频繁创建的一些类,可以考虑使用单例
  • 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
  • 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
  • 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
  • 频繁访问数据库或文件的对象。
  • 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
  • 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。

如何手写一个单例

一个单例要注意三个要素
1.构造器私有化
2. 自行创建,用静态变量保存
3. 提供获取方法(静态方法)

饿汉式

代码

/**
 * 饿汉式
 * 优点:效率高,线程安全
 * 缺点:占用内存
 */
package com.yang.singleton;

public class Singleton01 {
    //自行创建,用静态变量保存
    private static Singleton01 instance = new Singleton01();
    //构造器私有化
    private Singleton01() {
    }
    //向外提供获取方法
    public static Singleton01 getInstance() {
        return instance;
    }
}

测试代码

package com.yang.singleton;

class Test {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": " + Singleton01.getInstance());
        }, "线程1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": " + Singleton01.getInstance());
        }, "线程2").start();
    }
}

打印结果

线程1: com.yang.singleton.Singleton01@6218fb2e
线程2: com.yang.singleton.Singleton01@6218fb2e

测试结果没问题,类加载的时候就已经初始化创建实例 ,所以不存在访问线程安全问题,但是有一个缺点,如果这个类没有被使用,就会一直占用内存资源,造成内存浪费, 可进一步优化

进一步优化

懒汉式

代码

/**
 * 懒汉式
 * 优点:调用时才创建实例
 * 缺点:线程不安全
 */
package com.yang.singleton;

public class Singleton02 {

    private static Singleton02 instance = null;

    private Singleton02() {
    }
	//修改成需要使用时,创建对象,节省内存空间
    public static Singleton02 getInstance() {
        if (instance == null) {
            instance = new Singleton02();
        }
        return instance;
    }
}

打印测试结果

线程1: com.yang.singleton.Singleton02@6dd2a02f
线程2: com.yang.singleton.Singleton02@4920b564

结果创建了两个对象。
思考后认识到,修改为调用方法的时候创建对象,但是这个时候如果两个线程同时调用getInstance()方法,此时对于两个线程来说instance == null都为true,两个线程都会创建一个新的对象,就会有线程安全问题。

知道问题所在,进步修改

/**
 * 懒汉式
 * 优点:线程安全
 * 缺点:性能差
 */
package com.yang.singleton;

public class Singleton02 {

    private static Singleton02 instance = null;

    private Singleton02() {
    }

    public static synchronized Singleton02 getInstance() {
        if (instance == null) {
            instance = new Singleton02();
        }
        return instance;
    }
}

打印测试结果

线程1: com.yang.singleton.Singleton02@6dd2a02f
线程2: com.yang.singleton.Singleton02@6dd2a02f

在getInstance()方法上加 synchronized关键字(为方法加锁),好像可以了。但是只有在第一次实例化的时候才有必要加锁保证单例,感觉没有必要每次调用getInstance()方法都让它排队等待,安全性保证了,但是影响了程序的性能。

将锁的粒度调整一下

DCL(Double Check Lock)

代码

/**
 * DCL(Double Check Lock)
 * 优点:线程安全,调用时创建实例
 * 缺点:复杂,可读性差
 */
package com.yang.singleton;
public class Singleton02 {
    private volatile static Singleton02 instance = null;
    private Singleton02() {
    }
    public static Singleton02 getInstance() {
        //判断是否需要阻塞
        if (instance == null) {
            //线程1,线程2。。。在这里等待。。
            synchronized (Singleton02.class) {
                //判断是否别的线程已经创建了实例
                if (instance == null) {
                    instance = new Singleton02();
                }
            }
        }
        return instance;
    }
}

打印测试结果

线程1: com.yang.singleton.Singleton02@6dd2a02f
线程2: com.yang.singleton.Singleton02@6dd2a02f

我们真正想要加锁的其实只是创建实例这一步,在多个线程进入方法时,先判断instance == null,如果为true将接下来的动作加锁,否则直接返回,极大的提高了效率,第二个instance == null判断是否其它线程已经创建了实例,有则直接返回。

  • 第一个 instance == null判断是否需要阻塞
  • 第二个 instance == null判断是否别的线程已经创建了实例

细心的人会发现不仅仅是加了两层判空,在变量上还加了volatile关键字

volatile作用:

  • 保证变量可见性
  • 禁止指令重排序

有锁就会带来性能问题

有没有不加锁的方法?

重新整理一下思路。
以饿汉式为始,后面衍生出来的一系列问题都是由于想要将创建实例的动作滞后,达到节省内存的目的,那么有没有别的方法可以让我们在调用的时候创建。

可以利用静态内部类

调用的时候加载

静态内部类

代码

/**
 * 静态内部类
 * 优点:线程安全,调用时创建实例
 */
package com.yang.singleton;

public class Singleton03 {

    private Singleton03() {
    }
    public static Singleton03 getInstance() {
        return InnerSingleton.INSTANCE;
    }
    private static class InnerSingleton {
        private static final Singleton03 INSTANCE = new Singleton03();
    }
}

打印测试结果

线程2: com.yang.singleton.User@4920b564
线程1: com.yang.singleton.User@4920b564

至此,单例可以告一段落

下面补充几种网上搜集来的单例写法

枚举

/**
 * 枚举
 * 优点:线程安全,防止反序列化
 */
package com.yang.singleton;

public class User {
    //私有化构造函数
    private User() {
    }
    //定义一个静态枚举类
    static enum SingletonEnum {
        //创建一个枚举对象,该对象天生为单例
        INSTANCE;
        private User user;
        //私有化枚举的构造函数
        private SingletonEnum() {
            user = new User();
        }
        public User getInstnce() {
            return user;
        }
    }
    //对外暴露一个获取User对象的静态方法
    public static User getInstance() {
        return SingletonEnum.INSTANCE.getInstnce();
    }
}

容器式单例

代码

/**
 * 容器式单例
 * 实现思路:用一个Map保存对象,当需要使用的时候去map里拿,如果有,则直接取出来,没有则利用返回创建一个,放到map里,然后再返回出去。(相当于一个简单的Spring管理容器)
 */
package com.yang.singleton;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ContainerSingleton {

    private ContainerSingleton() {
    }

    private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getInstance(String className) {
        Object instance = null;
        if (!ioc.containsKey(className)) {
            try {
                instance = Class.forName(className).newInstance();
                ioc.put(className, instance);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return instance;
        } else {
            return ioc.get(className);
        }
    }
}

测试用例

package com.yang.singleton;

class Test {
    public static void main(String[] args) {
        Object instance1 = ContainerSingleton.getInstance("com.yang.singleton.TestPojo");
        Object instance2 = ContainerSingleton.getInstance("com.yang.singleton.TestPojo");
        System.out.println("instance1: " + instance1);
        System.out.println("instance2: " + instance2);

    }
}

打印测试结果

instance1: com.yang.singleton.TestPojo@61bbe9ba
instance2: com.yang.singleton.TestPojo@61bbe9ba

但是上述示例存在线程安全问题

看看在多线程环境下测试结果

package com.yang.singleton;

class Test {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": " + ContainerSingleton.getInstance("com.yang.singleton.TestPojo"));
        }, "线程1").start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + ": " + ContainerSingleton.getInstance("com.yang.singleton.TestPojo"));
        }, "线程2").start();
    }
}

打印测试结果

线程1: com.yang.singleton.TestPojo@6dd2a02f
线程2: com.yang.singleton.TestPojo@4920b564

简单修改一下

package com.yang.singleton;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ContainerSingleton {

    private ContainerSingleton() {
    }

    private volatile static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();

    public static Object getInstance(String className) {
        Object instance = null;
        //如果容器里没有,判断是否需要加锁
        if (!ioc.containsKey(className)) {
            synchronized (ContainerSingleton.class) {
                if (!ioc.containsKey(className)) {
                    try {
                        instance = Class.forName(className).newInstance();
                        ioc.put(className, instance);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    return instance;
                } else {
                    return ioc.get(className);
                }
            }
        } else {
            return ioc.get(className);
        }
    }
}

打印测试结果

线程1: com.yang.singleton.TestPojo@43d83830
线程2: com.yang.singleton.TestPojo@43d83830

CAS单例

/**
 * CAS单例
 * 思路:用AtomicReference包装保证原子性,然后利用无锁机制,循环去判断是否有现成的对象,有就返回,没有就创建,但后替INSTANCE
 */
package com.yang.singleton;

import java.util.concurrent.atomic.AtomicReference;

public class CASSingleton {
    private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<CASSingleton>();

    private CASSingleton() {
    }

    public static CASSingleton getInstance() {
        for (; ; ) {
            CASSingleton singleton = INSTANCE.get();
            if (singleton != null) { 
                return singleton;
            }
            singleton = new CASSingleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

扩展 volatile 作用

这不是本篇文章的重点

简单解释一下,想要详细了解可自行查阅相关文档。

可见性:就如字面意思,当一个线程改变了变量的值,其它持有该变量的线程能够立即感知到,避免了脏读的现象。

指令重排:Java代码最终会被转化为字节码,JVM通过解释字节码将其翻译成一条条对应的机器指令。

创建对象的过程大致分为以下几步:

  1. 申请分配内存空间
  2. 初始化对象
  3. 设置对象(instance )指向刚刚分配的内存地址
    正常执行顺序应该是1–>2–>3,但是当2初始化对象的时间过长时,编译器会交换2和3的顺序,导致执行最终执行的顺序为1–>3–>2,当执行到3的时候另外的线程判断instance == null不成立(此时instance 不为空,已经指向内存地址),直接返回没有初始化的instance,造成NPE
    加上volatile可以禁止23交换顺序,按顺序执行,避免NPE,最终得到了一个线程安全,省内存的单例。

如果这篇文章对您有帮助,非常欢迎您点赞支持!这不仅是对我工作的认可,也能鼓励我继续创作更多有价值的内容。感谢您的支持和反馈,希望未来还能为您提供更多有益的信息和灵感。

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值