【设计模式】创建型模式其六: 单例模式

229 阅读5分钟

创建型模式其六:《单例模式》

什么是单例模式

大家看名字应该不知道是什么意思。但是大家在电脑上肯定使用过任务管理器, 它永远是创建一个实例,不管打开多少次都是打开同一个。

意思就是永远只会new一个实例在内存

设计思路

如何保证一个类只有一个实例并且这个实例易于被访问?
使用全局变量:可以确保对象随时可以被访问,但不能防止创建多个对象

正确方法: 让类自身负责创建和保存它的唯一实例,并保证不能通过其他方法创建实例,同时提供一个访问该实例的方法

定义

单例模式:确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

那我该怎么实现这个步骤呢? 既然要类自己创建,可以将自身实例作为属性,当要使用时就调用方法访问实例。

要满足的条件:

  • 某个类只能有一个实例
  • 必须自行创建这个实例
  • 必须自行向整个系统提供这个实例

既然要自己创建实例,肯定不能将构造方法公有化,因此将构造函数私有化

通过例子来加深理解

实例

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

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

客户端测试

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

上面的方法其实是懒汉式: 只有在使用的时候才会创建实例。

还有一种方法为饿汉式: 在加载类的同时创建实例。

饿汉式代码

public class LoadBalancer {
   //私有静态成员变量,直接创建
   private static LoadBalancer instance = new LoadBalancer();
   //服务器集合
   private List serverList = new ArrayList<>();

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

   //公有静态成员方法,永远返回唯一实例
   public static LoadBalancer getLoadBalancer() {
      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);
   }
}

分析: 饿汉式与懒汉式

  • 饿汉式单例类:无须考虑多个线程同时访问的问题;调用速度和反应时间优于懒汉式单例资源利用效率不及懒汉式单例系统加载时间可能会比较长
  • 懒汉式单例类:实现了延迟加载;必须处理好多个线程同时访问的问题;需通过双重检查锁定等机制进行控制,将导致系统性能受到一定影响

上面的懒汉式单例虽然能够完成基本功能,但是出现多线程的情况可能会创建多个实例,因此,需要对代码进行修改。

为了让线程之间不冲突,我们选择使用Serializable

synchronized

synchronized是Java中的关键字,用于实现线程同步。在Java中,多个线程可以同时访问同一个对象的方法或者代码块,如果多个线程同时修改该对象的状态,就可能导致数据不一致或者其他问题。为了避免这种情况,需要使用synchronized关键字来保证多个线程之间的同步。

synchronized关键字可以用于方法或者代码块中。使用synchronized修饰的方法或者代码块在同一时刻只能被一个线程访问,其他线程需要等待当前线程执行完毕后才能访问。这种方式可以保证多个线程之间的同步,避免出现数据不一致或者其他问题。

下面是修改之后的getLoadBalancer方法:

public synchronized static LoadBalancer getLoadBalancer() {
   if (instance == null) {
      instance = new LoadBalancer();
   }
   return instance;
}

解决了多线程的问题,但是这样效率未免有点低:明明已经有线程调用过getLoadBalancer方法,表示实例已经存在,应该是不会再创建实例了,但还是会将其上锁,等待方法调用完毕才能访问实例,有点浪费资源了。

因此,我们对锁进行进一步的改进:

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

   }
   return instance;
}

synchronized关键字放在代码块中而不是方法中,是为了控制同步的范围。如果synchronized关键字放在方法中,那么整个方法都会被同步,这意味着即使instance已经被初始化了,其他线程也需要等待该方法执行完毕才能访问instance。这种方式的效率比较低下,因为即使instance已经存在,其他线程也需要进入synchronized块来等待。

而将synchronized关键字放在代码块中,只会同步代码块中的内容,这使得其他线程可以在instance已经被初始化的情况下直接访问它,而不需要等待整个方法执行完毕。

synchronized (LoadBalancer.class) {
         instance = new LoadBalancer();
}

这种写法我也是第一次见,查阅资料得:

在 Java 中,每个对象都有一把内置锁(也称为监视器锁或管程锁),可以用于实现线程同步。当一个线程获取了该对象的锁之后,其他线程就不能再访问该对象的同步代码块,直到该线程释放了锁。

synchronized 关键字可以用于修饰方法、代码块等语法结构,并且可以传入一个对象作为锁。如果使用synchronized修饰的是一个方法,那么该方法所属的对象就是锁。如果使用synchronized修饰的是一个代码块,那么括号中指定的对象就是锁。

在这段代码中,使用synchronized关键字直接加类名 LoadBalancer.class 作为锁。这是因为在 Java 中,每个类都有一个唯一的 Class 对象,可以用于实现类级别的同步。因此,使用 LoadBalancer.class 作为锁,可以保证在整个类中只有一个线程可以进入同步块中的代码,从而保证了单例实例的唯一性。

总的来说,使用类级别的同步可以保证整个类中只有一个线程可以进入同步块中的代码,从而保证了单例实例的唯一性。但是由于类级别的同步会对整个类造成影响,因此在高并发场景下可能会影响系统的性能,需要谨慎使用。

我总结一波: synchronized关键字可以用于修饰方法,也可以直接修饰代码块,即我们现在改进的代码(后面就是一个代码块,括号里的参数为指定为哪个类进行),为啥修饰方法不需要指定哪个类,而修饰代码块却需要呢?,对于方法来说,因为每个对象都有一个内置锁,所以在使用synchronized修饰方法时,锁定的就是该方法所属对象的内置锁。因此,在方法声明中不需要显式地指定锁对象。而代码块确是一个独立于对象的函数,Java并不知道它属于哪个对象,所以需要显式的表示。

再次改进:

使用双重判断:

// 第一重判断可以让其在不满足条件的情况下立马返回

// 第二重判断,是因为多线程,当两个线程进入时,此时都为空,进行等待,不判断条件的话,可能会创建多个实例。

    //第一重判断 
    if (instance == null) {
        //锁定代码块
        synchronized (LazySingleton.class) {
            //第二重判断
            if (instance == null) {
                instance = new LazySingleton(); //创建单例实例
            }
        }

单例模式的优缺点

模式优点

  • 提供了对唯一实例的受控访问
  • 可以节约系统资源,提高系统的性能
  • 允许可变数目的实例(多例类)(比如我搞个列表来存放实例,当列表长度大于三,就不创建实例了)

模式缺点

  • 扩展困难(缺少抽象层)
  • 不符合开闭原则(为其添加功能必须修改源代码)
  • 单例类的职责过重
  • 由于自动垃圾回收机制,可能会导致共享的单例对象的状态丢失(当有可变变量时,会出现状态共享问题:在多线程环境下,如果单例对象中存储了可变状态,那么多个线程可能会同时修改这个状态,导致状态竞争和不一致性问题。 1.)

其实这个模式有点像简单工厂模式: 不过它只有唯一的产品而且是自身。

如果大家不懂多线程,我可以去学然后给你们讲,没有的话最后我肯定也会讲,时间先后问题。
下一篇: 【设计模式】结构型模式其一: 适配器模式 - 掘金 (juejin.cn)