首页 > 其他分享 >单例模式

单例模式

时间:2024-04-10 16:56:00浏览次数:21  
标签:Singleton private instance 模式 单例 new Logger public

1、为什么使用单例(能解决什么问题)

(1)处理资源访问冲突

自定义实现了一个往文件中打印日志的 Logger 类。具体的代码实现如下所示:

public class Logger {
  private FileWriter writer;
  
  public Logger() {
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  
  public void log(String message) {
    writer.write(message);
  }
}
​
// Logger类的应用示例:
public class UserController {
  private Logger logger = new Logger();
  
  public void login(String username, String password) {
    // ...省略业务逻辑代码...
    logger.log(username + " logined!");
  }
}
​
public class OrderController {
  private Logger logger = new Logger();
  
  public void create(OrderVo order) {
    // ...省略业务逻辑代码...
    logger.log("Created an order: " + order.toString());
  }
}

在 UserController 和 OrderController 中,我们分别创建两个 Logger 对象。在 Web 容器的 Servlet 多线程环境下,如果两个 Servlet 线程同时分别执行 login() 和 create() 两个函数,并且同时写日志到 log.txt 文件中,那就有可能存在日志信息互相覆盖的情况。

解决方案:

(1)加类级别的锁,因为创建了两个 Logger 对象,所以加对象级别的锁没用。(额外说下:FileWriter本身就是线程安全的)

public class Logger {
  private FileWriter writer;
​
  public Logger() {
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  
  public void log(String message) {
    synchronized(Logger.class) { // 类级别的锁
      writer.write(mesasge);
    }
  }
}

(2)分布式级别的锁,可以使用Redis,不过实现一个安全可靠、高性能的分布式锁也不是件容易的事。

(3)并发队列(比如 Java 中的 BlockingQueue)也可以解决这个问题:多个线程同时往并发队列里写日志,一个单独的线程负责将并发队列中的数据,写入到日志文件。这种方式实现起来也稍微有点复杂。

(4)单例模式

public class Logger {
  private FileWriter writer;
  private static final Logger instance = new Logger();
​
  private Logger() {
    File file = new File("/Users/wangzheng/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }
  
  public static Logger getInstance() {
    return instance;
  }
  
  public void log(String message) {
    writer.write(mesasge);
  }
}
​
// Logger类的应用示例:
public class UserController {
  public void login(String username, String password) {
    // ...省略业务逻辑代码...
    Logger.getInstance().log(username + " logined!");
  }
}
​
public class OrderController {  
  public void create(OrderVo order) {
    // ...省略业务逻辑代码...
    Logger.getInstance().log("Created a order: " + order.toString());
  }
}

(2)表示全局唯一类

从业务概念上,如果有些数据在系统中只应保存一份,那就比较适合设计为单例类。比如,配置信息类。在系统中,只有一个配置文件,当配置文件被加载到内存之后,以对象的形式存在,也理所应当只有一份。再比如,唯一递增 ID 号码生成器,如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,应该将 ID 生成器类设计为单例。

import java.util.concurrent.atomic.AtomicLong;
public class IdGenerator {
  // AtomicLong是一个Java并发库中提供的一个原子变量类型,
  // 它将一些线程不安全需要加锁的复合操作封装为了线程安全的原子操作,
  // 比如下面会用到的incrementAndGet().
  private AtomicLong id = new AtomicLong(0);
  private static final IdGenerator instance = new IdGenerator();
  private IdGenerator() {}
  public static IdGenerator getInstance() {
    return instance;
  }
  public long getId() { 
    return id.incrementAndGet();
  }
}
​
// IdGenerator使用举例
long id = IdGenerator.getInstance().getId();

2、单例的实现方式

要实现一个单例,我们需要关注的点无外乎下面几个:

构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;

——考虑对象创建时的线程安全问题;

——考虑是否支持延迟加载;

——考虑 getInstance() 性能是否高(是否加锁)。

(1)饿汉式

通过定义静态的成员变量,以保证单例对象可以在类初始化的过程中被实例化。这其实是利用了ClassLoader的线程安全机制。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。

public class Singleton {
    
    private static Singleton instance = new Singleton();
  
    private Singleton(){}
    
    public static Singleton getInstance(){
        return instance;
    }
}

有人觉得这种实现方式不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过,我个人并不认同这样的观点。如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 Java 中的 PermGen Space OOM),可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。

(2)懒汉式

懒汉式相对于饿汉式的优势是支持延迟加载,劣势就是加了一把大锁(synchronized),性能较低,所以这种方式在现实中基本不会用。

public class Singleton {
​
    private static Singleton instance;
​
    private Singleton(){}
​
    public static synchronized Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

(3)双重检测

既支持延迟加载,又解决了懒汉式性能低的问题。

public class Singleton {
​
    private volatile static Singleton instance;
​
    private Singleton(){}
​
    public static Singleton getInstance(){
        if (instance == null){ //出于性能考虑
            synchronized (Singleton.class) { //类级别的锁
                if (instance == null){  //出于安全考虑
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

——为什么需要双重检测

同步块外层检查:这个检查是从性能当面考虑的,如果每次检查都加同步锁,显然性能是很低的,所以加这个检查保证只在第一次实例化时加锁。

同步块内层检查:这个检查是从安全方面考虑的,例如SingletonClass有一个属性int count = 3,当线程A和B获取对象时同时进入了外层检查,然后线程A拿到了Synchronized锁,实例化了对象并进行了累加操作,此时count=4,然后线程B在线程A释放锁之后获取到了锁权限,但是不管不顾的又进行了一次实例化,此时的singleton被重新实例化,count=3,这就会出问题了。

——为什么要加 volatile 修饰

CPU 指令重排序可能导致在 Singleton 类的对象被关键字 new 创建并赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。这样,另一个线程就使用了一个没有完整初始化的 Singleton 类的对象。

(4)静态内部类

这种方式比双重检测更简单,利用静态内部类的机制来实现延迟加载。

public class Singleton {
​
    private Singleton(){}
​
    private static class SingletonHolder{
        private static final Singleton instance = new Singleton();
    }
​
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

关于静态内部类: 《effective java》里面说静态内部类只是刚好写在了另一个类里面,实际上和外部类没什么附属关系,所以二者是独立加载的。

(5)枚举

JVM会保证枚举对象的唯一性,因此每个枚举类型和定义的枚举变量在JVM中都是唯一的。

public enum IdGenerator {
  INSTANCE;
  private AtomicLong id = new AtomicLong(0);
 
  public long getId() { 
    return id.incrementAndGet();
  }
}

(6)CAS方式

上面的方式都直接或间接使用了 synchronized,CAS则完全没有用到。

public class Singleton {
​
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<>();
​
    private Singleton(){}
​
    public static Singleton getInstance(){
        for (;;){
            Singleton singleton = INSTANCE.get();
            if (singleton != null){
                return singleton;
            }
            singleton = new Singleton();
            if(INSTANCE.compareAndSet(null,singleton)){
                return singleton;
            }
        }
    }
}

用CAS的优点在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。

CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。另外,代码中,如果N个线程同时执行到 singleton = new Singleton();的时候,会有大量对象被创建,可能导致内存溢出。

3、单例模式存在的问题

——单例对 OOP 特性的支持不友好

——单例对代码的扩展性不友好

——单例不支持有参数的构造函数

解决方式:

为了保证全局唯一,除了使用单例,还可以用静态方法来实现。不过,静态方法这种实现思路,并不能解决上面提到的问题。如果要完全解决这些问题,要从根上,寻找其他方式来实现全局唯一类。比如,通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,由程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。

4、单例的作用域

(1)一般意义上的单例都是指进程作用域

比如我们编写的代码通过编译、链接,组织在一起形成可执行文件(如Windows中的exe文件),当运行可执行文件时,操作系统会启动一个进程,将这个执行文件从磁盘加载到自己的进程地址空间(可以理解操作系统为进程分配的内存存储区,用来存储代码和数据)。接着,进程就一条一条地执行可执行文件中包含的代码。

上面我们说的单例都是在进程中唯一的,因为进行间是不共享地址空间的。

(2)线程唯一的单例

实现线程间唯一,通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。可以使用 ThreadLocal。

public class Singleton {
​
    private static final ThreadLocal<Singleton> INSTANCE = new ThreadLocal<Singleton>(){
        @Override
        protected Singleton initialValue() {
            return new Singleton();
        }
    };
​
    private Singleton(){}
​
    public static Singleton getInstance(){
        return INSTANCE.get();
    }
}

(3)集群唯一的单例

集群相当于多个进程构成的一个集合,“集群唯一”就相当于是进程内唯一、进程间也唯一。也就是说,不同的进程间共享同一个对象,不能创建同一个类的多个对象。

实现集群内单例的核心思路,其实跟利用分布式锁控制访问共享资源是一个道理,只是将创建/销毁单例对象的过程用分布式锁加以控制,保证每次只有一个节点能做创建/销毁的操作。

public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);
  private static IdGenerator instance;
  private static SharedObjectStorage storage = FileSharedObjectStorage(/*入参省略,比如文件地址*/);
  private static DistributedLock lock = new DistributedLock();
  
  private IdGenerator() {}
​
  public synchronized static IdGenerator getInstance() 
    if (instance == null) {
      lock.lock();
      instance = storage.load(IdGenerator.class);
    }
    return instance;
  }
  
  public synchroinzed void freeInstance() {
    storage.save(this, IdGeneator.class);
    instance = null; //释放对象
    lock.unlock();
  }
  
  public long getId() { 
    return id.incrementAndGet();
  }
}
​
// IdGenerator使用举例
IdGenerator idGeneator = IdGenerator.getInstance();
long id = idGenerator.getId();
idGenerator.freeInstance();

5、多例模式

单例指的是一个类只能创建一个对象,而多例则是指一个类型只能创建一个对象。

public class Logger {
  private static final ConcurrentHashMap<String, Logger> instances
          = new ConcurrentHashMap<>();
​
  private Logger() {}
​
  public static Logger getInstance(String loggerName) {
    instances.putIfAbsent(loggerName, new Logger());
    return instances.get(loggerName);
  }
​
  public void log() {
    //...
  }
}
​
//l1==l2, l1!=l3
Logger l1 = Logger.getInstance("User.class");
Logger l2 = Logger.getInstance("User.class");
Logger l3 = Logger.getInstance("Order.class");

这种多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。

 

标签:Singleton,private,instance,模式,单例,new,Logger,public
From: https://www.cnblogs.com/jing-yi/p/18126389

相关文章

  • 基于C语言的面向对象设计模式(持续更新)
    前言首先这篇文章只是初步的尝试,不涉及过于高深的编程技巧;同时需要表明的是,面向对象只是一种思想,不局限于什么样的编程语言,不可否认的是基于面向对象特性而设计的语言确实要比面向过程式的语言更加容易进行抽象和统筹,可以说面向对象的设计模式可以很大程度上摆脱过程的实例,但要论......
  • 设计模式概述
    学习设计模式的目的(1)应对面试设计模式是程序员的基本功,因此是面试中常考察的知识点。(2)写出高质量的代码学好数据结构与算法目的是写出高效的代码,学好设计模式则是为写出高质量的代码。(3)提高复杂代码的设计和开发能力掌握好设计模式才能在开发复杂系统时写出易扩展、易用......
  • 03-JAVA设计模式-代理模式详解
    代理模式什么是代理模式Java代理模式是一种常用的设计模式,主要用于在不修改现有类代码的情况下,为该类添加一些新的功能或行为。代理模式涉及到一个代理类和一个被代理类(也称为目标对象)。代理类负责控制对目标对象的访问,并可以在访问前后添加一些额外的操作。核心作用:通......
  • Hive - [02] Local模式的安装部署
     1、将hive的包解压到/opt/module目录下2、在conf/hive-env.sh中配置hadoop的路径3、依次启动Zookeeper、HDFS相关服务zkCluster.sh、jpsall.sh均为自行配置的shell脚本jpsall.sh:foripin`cat/etc/hosts|grepctos|awk'{print$2}'`;doecho----------$ip----......
  • .NET 设计模式—装饰器模式(Decorator Pattern)
    简介装饰者模式(DecoratorPattern)是一种结构型设计模式,它允许你在不改变对象接口的前提下,动态地将新行为附加到对象上。这种模式是通过创建一个包装(或装饰)对象,将要被装饰的对象包裹起来,从而实现对原有对象功能的增强和扩展。角色Component(组件):定义了一个抽象接口,可以是抽象......
  • 高可用之战:Redis Sentinal(哨兵模式)
    ★Redis24篇集合1背景在我们的《Redis高可用之战:主从架构》篇章中,介绍了Redis的主从架构模式,可以有效的提升Redis服务的可用性,减少甚至避免Redis服务发生完全宕机的可能。它主要包含如下能力:1.故障隔离和恢复:无论主节点或者从节点宕机,其他节点依然可以保证服务的正常运行,并......
  • 创建型模式--单例模式
    创建型模式--单例模式简介:单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象(例如数据库连接池)。单例模式......
  • Node.js毕业设计基于翻转课堂教学模式的小学英语微课互动平台(Express+附源码)
    本系统(程序+源码)带文档lw万字以上  文末可获取本课题的源码和程序系统程序文件列表系统的选题背景和意义选题背景:在当前的教育领域,随着信息技术的飞速发展和互联网的普及,传统的教学模式正在逐渐被新型的、更为灵活和互动性强的在线教学模式所取代。翻转课堂作为一种新型......
  • UEFI模式下安装Windows系统,您可以使用 Microsoft 提供的 Windows 安装媒体(如 USB 安装
    UEFI模式下安装Windows系统,您可以使用Microsoft提供的Windows安装媒体(如USB安装盘或光盘)。下面是一个简单的批处理脚本示例,用于在UEFI模式下安装Windows系统:CopyCode@echooffclsechoStartingWindowsinstallationinUEFImode...::设置安装媒体的路径,假设为D:......
  • Redis Sentinel 哨兵模式 故障转移失败 -failover-abort-no-good-slave master mymast
    根据网上的解决方案:1.我核对了sentinel.config和redis.configbind绑定的端口。2.三台redismasterauth都设置了密码3.sentinel.config的sentinelmonitormymaster和sentinelauth-passmymaster也没有错。但在我测试主从复制的时候,发现主从主机无法相连,我在网上找的解决......