设计模式-创建型-单例模式

1. 为什么需要单例模式

windows系统的任务管理器,只能有一个,唯一性的原因:

  1. 如果能弹出多个窗口,且这些窗口的内容完全一致,全都是重复对象,那势必会浪费资源,尤其是任务管理器会需要进入内核态调取各种状态信息,会对性能造成一定的影响。
  2. 而且多个窗口之间需要保持一致性,绝对的同步,相互之间的同步也是资源的浪费。

现实中的例子,就是为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个实例创建成功以后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一的实例。

1.1 处理资源的访问冲突

下述代码自定义了一个往文件当中打印日志的logger类:

public class Logger {
  private FileWriter writer;

  public Logger() {
    File file = new File("/Users/leilei/log.txt");
    writer = new FileWriter(file, true); //true表示追加写入
  }

  public void log(String message) {
    writer.write(mesasge);
  }
}

// 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());
  }
}

这段代码的问题在于每个类在实现的过程中都创建了一个新的Logger对象,如果我们同时创建了两个controller,然后执行的话,会同时写入同一个文件当中,这会有可能导致日志信息互相覆盖的情况。

想要解决这个问题,我们需要加上类级别的锁,让所有的对象都能够共享一把锁:

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);
    }
  }
}

我们也可以使用单例模式,使得程序当中只允许创建一个Logger对象,所有的线程共享这一个Logger对象,共享一个FileWriter对象(本身有对象级别的线程安全的保障)

public class Logger {
  private FileWriter writer;
  private static final Logger instance = new Logger();

  private Logger() {
    File file = new File("/Users/leilei/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());
  }
}

1.2 表示全局唯一类

对于只应该在系统当中保存一份的数据,比较适合设计为单例类。

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. 单例模式概述

  • 单例的定义

    • 一个类只允许创建唯一一个对象,那这个类就是一个单例类
  • 对象的唯一性指

    • 进程内只允许创建一个对象
    • 进程之间是不唯一的

2.1 模拟任务管理类

class TaskManager
{
     public TaskManager() {...} //初始化窗口
     public void displayProcesses()  {……} //显示进程
     public void  displayServices() {……} //显示服务
}

对其进行重构,为了使其是单一实例的,那我们需要禁止类的外部直接使用new来创建对象 —–> 将其构造函数的可见性变为private

public TaskManager() {...}

在类内部创建对象,保存这个唯一实例

private static TaskManager tm = null;

public static TaskManager getInstance() {
    if (tm == null) {
        tm = new TaskManager();
    }
    return tm;
}

getInstance()定义成一个静态方法,这样可以直接通过类名来使用

2.2 定义

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。

  1. 只有一个实例
  2. 必须自行创建这个实例
  3. 必须自行向整个系统提供这个实例

s1.gif

2.3 负载均衡器的设计与实现

Sunny软件公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的唯一性是该软件成功的关键。

使用单例模式来设计该负载均衡器:

s2.gif

import java.util.*;

//负载均衡器LoadBalancer:单例类,真实环境下该类将非常复杂,包括大量初始化的工作和业务方法,考虑到代码的可读性和易理解性,只列出部分与模式相关的核心代码
class LoadBalancer {
    //私有静态成员变量,存储唯一实例
    private static LoadBalancer instance = null;
    //服务器集合
    private List serverList = null;

    //私有构造函数
    private LoadBalancer() {
        serverList = new ArrayList();
    }

    //公有静态成员方法,返回唯一实例
    public static LoadBalancer getLoadBalancer() {
        if (instance == null) {
            instance = new LoadBalancer();
        }
        return instance;
    }

    //增加服务器
    public void addServer(String server) {
        serverList.add(server);
    }

    //删除服务器
    public void removeServer(String server) {
        serverList.remove(server);
    }

    //使用Random类随机获取服务器
    public String getServer() {
        Random random = new Random();
        int i = random.nextInt(serverList.size());
        return (String)serverList.get(i);
    }
}

3. 饿汉式单例模式和懒汉式单例模式

3.1 饿汉式单例模式

s3.gif

class EagerSingleton { 
    private static final EagerSingleton instance = new EagerSingleton(); 
    private EagerSingleton() { } 

    public static EagerSingleton getInstance() {
        return instance; 
    }   
}

在类加载的时候,静态变量instance就会被初始化,此时类的私有构造函数会被调用,然后单例类的唯一实例会在这个时候被创建出来。

恶汉模式的好处是没有延迟加载,这样子是在需要用到它的时候才来执行这个耗时长的初始化过程,可以避免在程序运行的时候才初始化导致的新跟那个问题。

3.2 懒汉式单例模式

s4.gif

在第一个调用getInstance()方法的时候进行实例化。又叫做延迟加载技术——在需要的时候再加载实例,为了避免多个线程同时调用getInstance()方法,我们需要使用synchronized关键字

class LazySingleton { 
    private static LazySingleton instance = null; 

    private LazySingleton() { } 

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

getInstance()方法带锁,并发度很低,如果频繁调用,需要频繁开关锁的话,效率是很低的。

3.3 兼顾效率和安全性的方式(饱汉+饿汉)

class LazySingleton { 
    private static LazySingleton instance; 

    private LazySingleton() { } 

    public static LazySingleton getInstance() { 
        if (instance == null) {
            synchronized(LazySingleton.class) {
                instance = new LazySingleton(); 
            }
        }
        return instance; 
    }
}

3.4 使用静态内部类

利用Java的静态内部类,因为静态内部类只有在被调用的时候,才会被加载。而静态内部类的唯一性,线程安全型都由JVM来保证。

public class LazySingleton {
private static class LazySingletonHolder{
private static final LazySingleton instance = new LazySingleton();
}

public static LazySingleton getInstance() {
    return LazySingletonHolder.instance;
}

}

4. 单例的问题

  • 违背了基于接口而非实现的设计原则,如果我们想要更改的话,是需要到每个类的位置去做更改的
  • 单例会隐藏类之间的依赖关系
    • 一般来说我们通过构造函数,参数传递来声明类之间的依赖关系
    • 单例不需要显示创建,不需要依赖参数传递,在函数中直接调用
    • 对代码的扩展性不友好

      5. 优缺点分析

5.1 优点

  1. 提供了对唯一实例的访问控制
  2. 因为内存中只存在一个对象,因此可以节约系统资源。尤其是对于一些需要频繁创建和销毁的对象,单例模式可以很大程度上提高系统性能

5.2 缺点

  1. 扩展困难
  2. 职责相对比较重。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
  3. 一些语言的垃圾自动回收技术,如果实例化的对象在一段时间内没有被使用,系统会认为它是垃圾,会自动销毁并回收资源。

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 stone2paul@gmail.com

文章标题:设计模式-创建型-单例模式

文章字数:2.3k

本文作者:Leilei Chen

发布时间:2020-06-12, 06:00:48

最后更新:2020-06-13, 13:00:36

原始链接:https://www.llchen60.com/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E5%88%9B%E5%BB%BA%E5%9E%8B-%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏