这段时间工作太忙。抽时间来整理一下设计模式,对这块不了解的同学可以看看。
今天先说设计模式中的单例模式吧。这个在我们平常android开发当中经常会使用。
一、单例的概念
单例是一种对象的创建模式,它用于创建对象的具体实例。它可以确保系统中一个类只产生一个实例。
java中实现的单例是在java虚拟机当中的。装载类classLoad 是java虚拟机的。一个虚拟机在通过自己的class类装载实现单例类的时候,就会创建一个类的实例,在java中这样实现有几个好处
(1)对于频繁使用的对象 可以减少创建对象所花费的时间。尤其是对重量级的对象来说。
(2)同时大量的new对象会造成内存抖动,使用单例可以减少new对象,对系统内存的使用也会降低,这样会降低GC的压力。避免OOM的发生。
单例模式其实就是把创建实例的权限收回来。给外部提供能访问这个实例的方法。
二、单例的六种写法
分别是:饿汉、懒汉 、懒汉线程安全、双重校验锁、静态内部类、枚举。
饿汉(空间换时间)
public class EagerSingleton {
private static EagerSingleton instance = new EagerSingleton();
/**
* 私有构造方法
*/
private EagerSingleton() {
}
/**
* 静态工厂方法
*
* @return
*/
public static EagerSingleton getInstance() {
return instance;
}
}
通过上面代码可以看出饿汉的典型的空间换时间,在类加载的时候直接就创建了对象。在创建实例的过程中是比较着急。所以叫饿汉。它有一个缺点是无法对我们的instance变量做延时加载,有时候我们的单例创建的过程会很慢。(变量初始化内容过多。)所以对它进行优化 优化的方式就是 ---》懒汉。
饿汉模式的特点:1.私有的构造方法,2.私有的静态的实例。3.静态的方法返回我们的instance变量。
懒汉(时间换空间)
懒汉有两种方式 一种是线程安全 另一种是线程不安全。
懒汉线程不安全
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
在多线程并发条件下,无法保证实例是唯一的。
懒汉线程安全(加锁)
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
懒汉线程安全使用 synchronized 同步 处理多线程。
懒汉单例模式因为它懒。所以在类装载的时候没有去创建对象。而在使用对象的时候才会创建对象,如果一直没有人使用就不会创建对象,节省了空间。是典型的时间换空间。由于饿汉实现线程安全 ,降低访问速度每次都要判断,就出现了双重校验锁。
双重校验锁
public class Singleton {
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
// 再次检验实例是否存在
instance = new Singleton();
}
}
}
return instance;
}
}
双重校验锁 顾名思义做了两次校验。即使线程安全 又对性能没有太大的影响但是也有一个缺点JVM即时编译器中存在指令重排列优化。
并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进行下面的同步块。这是第一重检查,进入同步块后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。为什么要进行两次检查空的操作,因为可能会有多个线程的实例进入同步块外的检查。所以同步线程完成,我们还需要进行一次判空操作。
仔细观察我们的instance被volatile 修饰,这是为什么呢?
instance = new Singleton(); 这个操作不是原子操作,(什么是原子操作:
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。)当我们执行这句话时,1.JVM会给instance分配内存,2.它会调用Singleton 的构造方法初始化变量,3.会将我们的instance 指向分配的内存。但是JVM存在指令重排序的优化,所以上面的三步不会按照我们想要执行的步骤来执行。这时候就会造成我们的线程不安全。
volatile 这个关键词 译为可见性,保证线程在本地不会存有instance的副本,而每次都会到内存中去读取。使用这个关键字可以禁止JVM的重排序优化。想要更加详细的了解这个关键字可以参考这个篇文章
https://www.cnblogs.com/dolphin0520/p/3920373.html
静态内部类(更优 推荐使用 没有性能缺点)
public class StaticSingleton {
private StaticSingleton() {
}
public static StaticSingleton getIntence() {
return StaticHolder.instance;
}
/**
* 类级静态内部类,相当于类的静态变量,
* 只有在使用时才会去装载,从而实现类延时加载。
*/
private static class StaticHolder {
// 静态初始化器 由JVM保证线程安全
private static final StaticSingleton instance = new StaticSingleton();
}
}
静态内部类 ,利用类中静态变量的唯一性。只有在我们调用getInstance时,才会初始化instance,保证了instance的唯一性。即简单又实现了懒汉式的延时加载,同时也是线程安全(final实现线程安全)。
优点:1.JVM虚拟机机制 保证线程安全(提供了static/final关键字)。2.它不需要synchronized保证线程安全,因为synchrnized保证线程安全需要花费时间,影响性能。它只能允许一个线程去读取数据,而静态内部类就不同,它可以同时读取实例。3.类级静态内部类只有在使用对象时才会创建对象实例。实现了延时加载的效果。
什么情况下JVM会帮我们保证线程同步?
1.由静态初始化器初始化数据时。
2.访问final字段时
3.在创建线程之前创建对象。
4.线程可以看见它将要处理的对象。
枚举
public enum Enum {
/**
* 定义一个枚举元素,它就代表Enum的一个实例
*/
INSTANCE;
/**
* 执行自己的操作
*/
public void getInstance() {
// 功能处理
}
}
单元素的枚举 是实现单例的最佳写法,但是如果写入别的实例就一定要保证线程安全。
优点:写法简单/线程安全
总结:
1.饿汉模式无法对instance实例进行延时加载。(优化 --》懒汉)
2.懒汉模式在多线程并发的情况下无法保证实例的唯一性。(优化 --》懒汉线程安全)
3.懒汉线程安全虽然保证了线程安全,但是synchronized导致性能缺陷,(一次只能一个线程读取,优化 --》双重校验锁DCL)
4.DCL :JVM指令重排序,导致有时候实例不会唯一。(优化 --》使用volatile关键字)
5.静态内部类/枚举:延时加载/线程安全/性能优势 推荐使用
参考博客
1. http://www.cnblogs.com/java-my-life/archive/2012/03/31/2425631.html
2.https://www.cnblogs.com/dolphin0520/p/3920373.html