zoukankan      html  css  js  c++  java
  • 设计模式笔记(一):单例模式

      单例模式可以说是设计模式中最简单的设计模式之一了。顾名思义,单例模式指的是一个类只提供一个固定的单个实例,大家共用该实例。

      单例模式代码实现步骤:

      1、私有化类的构造方法

      2、提供私有静态实例变量

      3、提供公共静态方法使其返回私有实例变量

      基础的实现代码如下:

     1 public class Singleton {
     2 
     3     //私有化静态实例变量
     4     private static Singleton instance;
     5 
     6     //私有化构造方法,保证其他类无法创建该类的新对象
     7     private Singleton() {
     8     }
     9 
    10     public static Singleton getInstance() {
    11         //判断实例是否存在,若不存在则创建一个新对象并返回
    12         if (instance == null) {
    13             //创建对象时,打印一下
    14             System.out.println("创建了一个实例");
    15             instance = new Singleton();
    16         }
    17         return instance;
    18     }
    19     
    20 }

      上述代码乍一看貌似没有什么问题,但是当处于多线程情况下,问题就会暴露出来。接下来编写main方法进行测试:

     1 public class Singleton {
     2 
     3     //私有化静态实例变量
     4     private static Singleton instance;
     5 
     6     //私有化构造方法,保证其他类无法创建该类的新对象
     7     private Singleton() {
     8     }
     9 
    10     public static Singleton getInstance() {
    11         //判断实例是否存在,若不存在则创建一个新对象并返回
    12         if (instance == null) {
    13             //创建对象时,打印一下
    14             System.out.println("创建了一个实例");
    15             instance = new Singleton();
    16         }
    17         return instance;
    18     }
    19 
    20     public static void main(String[] args) {
    21         //开启10个线程,调用获取实例方法
    22         for (int i = 0; i < 10; i++) {
    23             new Thread(() -> {
    24                 Singleton.getInstance();
    25             }).start();
    26         }
    27     }
    28 }

      程序运行结果如下(每次程序运行结果都可能不同):

       结果很明显,在多线程情况下,使用上述代码创建单例实例时,会造成创建多个实例的问题。原因就在于上述代码并没有加锁,导致代码第12行,对instance判断为空时,可能存在多个线程同时执行到这一步,各个线程都认为实例没有被创建,于是又各自执行了初始化创建实例的代码(代码第15行)。

      问题的原因找到了,那么就好解决了,以下提供几种解决方法。

    解决方法一:使用synchronized关键字修饰getInstance方法

      使用synchronized关键字修饰getInstance方法,将其变为同步方法,即意味着该方法在某个线程首次进入该方法执行完成之后,其他线程才能进入该方法,而当其他线程再次进入该方法时,instance实例已经被首次进入该方法的线程初始化好了,于是就避免了产生重复创建对象的问题。示例代码如下:

     1 public class Singleton {
     2 
     3     //私有化静态实例变量
     4     private static Singleton instance;
     5 
     6     //私有化构造方法,保证其他类无法创建该类的新对象
     7     private Singleton() {
     8     }
     9 
    10     //添加了synchronized修饰该方法,使其变成同步方法
    11     public static synchronized Singleton getInstance() {
    12         //判断实例是否存在,若不存在则创建一个新对象并返回
    13         if (instance == null) {
    14             //创建对象时,打印一下
    15             System.out.println("创建了一个实例");
    16             instance = new Singleton();
    17         }
    18         return instance;
    19     }
    20 
    21     public static void main(String[] args) {
    22         //开启10个线程,调用获取实例方法
    23         for (int i = 0; i < 10; i++) {
    24             new Thread(() -> {
    25                 Singleton.getInstance();
    26             }).start();
    27         }
    28     }
    29 }

      上述代码仅仅在第11行定义方法处添加了synchronized关键字修饰该方法,程序运行结果如下:

      结果表明多线程情况下,该代码实现了真正的单例变量的创建。

      注:实际情况下,只有第一次执行该方法时,才需要同步,后续的其他线程调用该方法时,此时的同步是没有必要的。另外synchronized作为重量级锁,在同步方法时会带来一定的性能影响。

    解决方法二:使用“双重检查加锁”方法

      使用双重检查,首先判断实例是否已经被创建,若没有被创建则开始对对象加锁实现同步。这样的话,只会在首次创建实例时进行同步。

     1 public class Singleton {
     2 
     3     //私有化静态实例变量
     4     private static volatile Singleton instance;
     5 
     6     //私有化构造方法,保证其他类无法创建该类的新对象
     7     private Singleton() {
     8     }
     9 
    10     public static Singleton getInstance() {
    11         //判断实例是否存在,若不存在则创建一个新对象并返回
    12         if (instance == null) {
    13             //对Singleton.class对象加锁
    14             synchronized (Singleton.class) {
    15                 if (instance == null) {
    16                     //创建对象时,打印一下
    17                     System.out.println("创建了一个实例");
    18                     instance = new Singleton();
    19                 }
    20             }
    21         }
    22         return instance;
    23     }
    24 
    25     public static void main(String[] args) {
    26         //开启10个线程,调用获取实例方法
    27         for (int i = 0; i < 10; i++) {
    28             new Thread(() -> {
    29                 Singleton.getInstance();
    30             }).start();
    31         }
    32     }
    33 }

      请注意上述代码第4行,变量声明时使用了volatile修饰,这是为了保证多线程之间,某个对该变量的修改永远会对其他线程可见,这里涉及到happens-before原则还有重排序的知识,就不展开讨论了,大家有兴趣的可以自行百度看一下。

      注:在1.4以及更早版本的java中,很多JVM对于volatile的实现会导致双重检查加锁的失效,所以如果想使用双重检查加锁,请保证java版本至少在1.5及以上!

    解决方法三:基于类初始化的解决方案

      JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案,这个方案被称之为Initialization On Demand Holder idiom)。代码示例如下:

     1 public class Singleton {
     2 
     3     //私有化构造方法,保证其他类无法创建该类的新对象
     4     private Singleton() {
     5     }
     6 
     7     //私有静态内部类
     8     private static class SingletonHolder {
     9         //直接新建一个实例对象
    10         private static final Singleton INSTANCE = new Singleton();
    11     }
    12 
    13     public static Singleton getInstance() {
    14         return SingletonHolder.INSTANCE;
    15     }
    16 
    17     public static void main(String[] args) {
    18         //开启10个线程,调用获取实例方法
    19         for (int i = 0; i < 10; i++) {
    20             new Thread(() -> {
    21                 Singleton.getInstance();
    22             }).start();
    23         }
    24     }
    25 }

      可以简单理解为,类对象在初始化时也会存在一个锁,该锁可以保证对类对象静态变量的写入对后续其他线程可见。

      个人推荐使用解决方法二或者解决方法三。

      参考资料:

      《java并发编程的艺术》

      《HeadFirst设计模式》

  • 相关阅读:
    面向对象优势
    二维码
    数据库分页
    DBUtil连接数据库
    sqliteDOC创建数据库
    ajax的回调函数
    多线程
    JSTL优点
    WebSocket 搭建简单聊天网站
    全等和不全等
  • 原文地址:https://www.cnblogs.com/yjry-th/p/13578122.html
Copyright © 2011-2022 走看看