java23种设计模式------单例模式

参考资料:
https://bbs.csdn.net/topics/391840031
https://blog.csdn.net/maosijunzi/article/details/18315013
https://blog.csdn.net/andywuchuanlong/article/details/37651757

所谓单例模式, 简单来说就时整个应用中只保证一个实例的存在. 就像是java web中的application, 也就是提供;了一个全局变量, 用处相当广泛, 比如保存全局数据, 实现全局性的操作等,


1.最简单的实现
首先,能狗想到的最简单的实现是, 把类的构造函数写成private, 从而保证别的类不能实例化此类, 然后在类中提供一个静态的实例并能返回给使用者, 这样, 使用者就可以通过这个引用使用到这个类的实例了.

    public class SingletonClass {

        private static final SingletonClass instance = new SingletonClass();

        public static SingletonClass getInstance() {
            return instance;
        }

        private SingletonClass() {
        }
    }

如上: 外部调用者如果需要使用SingletonClass 实例, 只能通过getInstance()方法, 并且它的构造方法是private的, 这样就保证了只能有一个对象的存在


2.性能优化—lazy loaded

上面代码虽然简单, 但是有一个问题——-吴坤这个类是否被使用, 都会创建一个instance对象, 如果这个创建过程中很耗时呢? 比如需要连接一万次数据库(假设), 并且这个类还不一定会被使用, 那么这个创建过程就是无用的, 怎么办呢?

为了解决这个问题, 我们想到了新的解决方案:

public class SingletonClass { 

  private static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 

}

代码变化有两处—-首先, 把晋三测初始化为null, 知道第一次使用的时候通过判断是否为null来创建对象, 因为创建过程不在声明处 , 所以那个final修饰必须去掉

我们来想想一下这个过程, 要使用SingletonClass, 调用getInstance()方法, 第一次的时候发现instance是null, 然后就创建一个对象, 返回出去, 第二次再使用的时候, 因为instance是static的, 所以不再是null, 因此不会再创建对象, 直接将其返回.
这个过程称之为lazy loaded, 也就是迟加载—直到使用的时候才进行加载

注: 当一个方法或着变量需要初始化加载, 或者是经常被调用的 时候可以加上static.

同static修饰的方法可以用类名直接调用, 不用的一定要先实例化一个对象后才可以调用.

坏处: 初始化加载, 比较占内存, 所以不经常使用的方法, 不建议加此关键词.
static是在容器架子啊的时候就已经加载到内存中了, 所以static方法和变量不宜过度使用, 有选择的 使用

static和final一块使用表示什么:

static final用来修饰成员变量和成员方法, 可以理解为全局常量,

private的作用

用private修饰的类, 属性, 方法, 只能自己使用, 别的类是不能访问的, 也就是说对于别的类是隐藏不见的, private一般不修饰类, 但是可以修饰内部类

3.同步

上面的代码很清楚, 也很简单, 单线程下, 这段代码没什么问题, 那么如果是多线程呢,?我们 来分析一下

线程A希望可以使用 SingletonClass,调用getInstance()方法. 因为是第一次调用,A就发现instance是null, 于是它开始创建实例, 就在这个时候, cpu发生时间片切换, 线程B开始执行, 它要是用SingletonClass,调用getInstance()方法, 同样检测到instance是null—-注意, 这是在A检测完之后切换的, 也就A并没有来得及创建对象—-因此B开始创建, B创建完成后, 切换到A继续执行,因为它已经检测完了, 所以A不会再检测一遍, 它会直接创建对象, 这样线程A和B各自拥有一个SingletonClass对象—单例失败.

解决的方法很简单, 那就是加锁:

public class SingletonClass { 

  private static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 

}

是要getInstance加上同步锁, 一个线程必须等待另外一个线程创建完后才能后才能使用这个方法, 这样就保证了单例的唯一性.

4.性能

上面的代码又是很清楚很简单, 这段代码毫无疑问存在性能的问题—synchronized修饰的同步块可是要比一般的代码慢上好几倍的, 如果存在很多次getInstance()调用, 那么性能问题就不能不考虑了,

分析一下, 究竟是整个方法都必须加上锁, 还是其中一句加锁就够了? 我们为什么要加锁呢?分析一下出现lay loaded的那种情形的原因, 原因是检测null的操作和创建对象的操作分离了, 吐过这两个擦欧洲哦能够原子的进行, 那么单例就已经保证了

public class SingletonClass { 

  private static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 

}

首先去掉getInstance()的同步操作, 然后吧同步锁加载if语句上, 但是这样修改起不到任何作用, 因为每次调用getInstance()的时候必须要同步, 性能问题还是存在, 如果我们事先判断一下是不是为null再去判断呢?

ublic class SingletonClass { 

  private static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 

}

还有问题吗? 首先判断instance是不是null?如果是null, 就加锁初始化, 如果不为null, 直接返回instance,

到此为止, 一切很完美, 我们用一种很聪明的方式事先了单例模式.
这就是doble-checked locking设计事先单例模式

5.

下面介绍编译原理, 所谓编译, 就是把源代码”翻译成”目标代码–大多是指机器代码的过程. 针对java, 它的目标不是改变本地机器代码, 而是虚拟机代码,编译原理里面又一个很重要的内容就是编译器优化. 所谓编译器优化是指, 在不改变原来语义的情况下, 通过调整语句顺序, 来让程序运行的更苦熬, 这个过程就是reorder.

腰肢到, jvm只是一个标准, 并不是实现, jvm并没有规定有关编译器优化的内容, 也就是说, jvm实现可以自由的进行编译器优化.

下面来想一下,创建一个变量需要哪些步骤呢?一个是申请一块内存,调用构造方法进行初始化操作,另一个是分配一个指针指向这块内存。这两个操作谁在前谁在后呢?JVM规范并没有规定。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。

下面我们来考虑这么一种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,因此B检测到instance不为null,于是直接把instance返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将instance构造完成之前就是用了这个实例,程序就会出现错误了!

于是,我们想到了下面的代码:

public class SingletonClass { 

  private static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 

}

我们在第一个同步块里面创建一个临时变量,然后使用这个临时变量进行对象的创建,并且在最后把instance指针临时变量的内存空间。写出这种代码基于以下思想,即synchronized会起到一个代码屏蔽的作用,同步块里面的代码和外部的代码没有联系。因此,在外部的同步块里面对临时变量sc进行操作并不影响instance,所以外部类在instance=sc;之前检测instance的时候,结果instance依然是null。

不过,这种想法完全是错误的!同步块的释放保证在此之前——也就是同步块里面——的操作必须完成,但是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行。因此,编译器完全可以把instance=sc;这句移到内部同步块里面执行。这样,程序又是错误的了!

6.终极解决方案

说了这么多,难道单例没有办法在Java中实现吗?其实不然!

在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义——在JDK1.5之前,volatile是个关键字,但是并没有明确的规定其用途——被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,只要我们简单的把instance加上volatile关键字就可以了。

public class SingletonClass { 

  private volatile static SingletonClass instance = null; 

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

  private SingletonClass() { 

  } 

}

然而,这只是JDK1.5之后的Java的解决方案,那之前版本呢?其实,还有另外的一种解决方案,并不会受到Java版本的影响:

public class SingletonClass { 

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

  public static SingletonClass getInstance() { 
    return SingletonClassInstance.instance; 
  } 

  private SingletonClass() { 

  } 

}

在这一版本的返利模式实现代码中, 我们使用了ejava的静态内部类, 这一技术是被jvm明确说明的了. 因此不存在任何二义性,.在这段代码中, 因为SingletonClass没有static的属性, 因此不会被初始化, 知道抵用getInstacnce()的时候, 会首先加载SingletonClassInstance类, 这个类有一个static的SingletonClass实例, 因此需要调用SingletonClass的构造方法, 然后getInstance()将这个内部类的instance返回给使用者, 由于这个instance是static的, 因此并不会构造很多次

因为SingletonClassInstance是私有静态内部类, 所以不会被其他类知道, 同样static语义也要求不会 有多个实例的存在, 并且,JSL规范定义, 类的构造必须是原子性, 是非并发的, 因此不需要加同步快, 因此由于这个构造是并发的, 所以getInstance()也不需要加同步,

至此,我们完整的了解了单例模式在Java语言中的时候,提出了两种解决方案。个人偏向于第二种,并且Effiective Java也推荐的这种方式

注:volatile

volatile 让变量每次在使用的时候, 都从主存中取, 而不是从各个线程的工作内存取,
volatile 具有synchronized光尖子的可见性, 但是没有synchronized光尖子的”并发正确性”,也就是说不保证线程执行的有序性,
也就是说volatile变量对于每次使用,线程都能得到当前volatile变量的最新之, 但是volatile变量并不保证并发的正确性

被volatile修饰符修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,又强迫线程将变化了的值写回共享内存,这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。这样当多个线程同时与某个对象交互时,就必要要注意到让线程及时的得到共享成员的变化。

注意:在两个或者更多的线程访问的成员变量上使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。

1、原子性
(1)原子是构成物质的基本单位(当然电子等暂且不论),所以原子的意思代表着——“不可分”;

(2)原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。

  1. 可见性
    可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就这这个操作同样存在线程安全问题。

猜你喜欢

转载自blog.csdn.net/weixin_39297312/article/details/80462168