本文将给大家介绍java中设计模式——单例模式; 单例模式:保证在⼀个JVM中,该对象只有⼀个实例存在; 适⽤场景: 1、某些类创建⽐较频繁,对于⼀些⼤型的对象,这是⼀笔很⼤的
单例模式:保证在⼀个JVM中,该对象只有⼀个实例存在;
适⽤场景:
1、某些类创建⽐较频繁,对于⼀些⼤型的对象,这是⼀笔很⼤的系统开销。
2、省去了new操作符,降低了系统内存的使⽤频率,减轻GC压⼒。
3、有些类如交易所的核⼼交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(⽐如⼀个军队出现了多个司 令员同时指挥,肯定会乱成⼀团),所以只有使⽤单例模式,才能保证核⼼交易服务器独⽴控制整个流程。
代码:
1 public class Singleton{ 2 //持有私有静态实例,防止被引用,赋值为null,目的就是实现延迟加载 3 private static Singleton instance = null; 4 5 6 //私有构造方法,防止被实例化。 7 private Singleton(){ 8 } 9 10 //创建实例 11 public static Singleton getInstance() 12 { 13 if (instance=null){ 14 instance=new Singleton(); 15 16 } 17 return instance; 18 //如果该对象被⽤于序列化,可以保证对象在序列化前后保持⼀致 19 public Object readReaslove(){ 20 return instance; 21 } 22 23 } 24 }
分类:
1.饿汉式:类初始化时创建单例,线程安全,但占用内存空间大,适⽤于占内存⼩的场景,否则推荐使⽤懒汉式延迟加载;
public class Singleton{
private static Singleton instance = new Singleton();
//构造器私有, 外部就无法new这个对象,保证一个类中只有一个实例对象
private Singleton(){}
public static Singleton newInstance(){ return instance; } }
2.懒汉式:需要单例实例的时候再创建,需要考虑线程安全(性能不太好)
懒汉式单例如何保证线程安全呢? 通过 synchronized 关键字加锁保证线程安全, synchronized 可以添加在⽅法上⾯,也可以添加在代码块上⾯,这⾥演示添加在⽅法上⾯,存在的问题是 每⼀次调⽤ getInstance 获取实例时都需要加锁和释放锁,这样是⾮常影响性能的。1 public class Singleton{ 2 private static Singleton instance = null;//定义一个静态变量指向自己 3 private Singleton(){}//私有化构造方法 4 public static synchronized Singleton newInstance(){//加锁, 5 if (instance == null){ 6 instance = new Singleton(); 7 } 8 } 9 return instance; 10 11 }
3.双重检验锁:假如两个线程A、B,A执⾏了if (instance == null)语句,它会认为单例对象没有创建,此时线程切到B也执⾏了同样的语句,B也认为单例对象没有创建,然后两个线程依次执⾏同步代码块,并分别创建了⼀个单例对象。
这⾥的双重检查是指两次⾮空判断,锁指的是 synchronized 加锁,为什么要进⾏双重判断,其实很简单,第⼀重判断,如果实例已经存在,那么就不再需要进⾏同步操作,⽽是直接返回这个实例,如果没有创建,才会进⼊同步块,同步块的⽬的与之前相同,⽬的是为了防⽌有多个线程同时调⽤时,导致⽣成多个实例,有了同步块,每次只能有⼀个线程调⽤访问同步块内容,当第⼀个抢到锁的调⽤获取了实例之后,这个实例就会被创建,之后的所有调⽤都不会进⼊同步块,直接在第⼀重判断就返回了单例。 关于内部的第⼆重空判断的作⽤,当多个线程⼀起到达锁位置时,进⾏锁竞争,其中⼀个线程获取锁,如果是第⼀次进⼊则为 null,会进⾏单例对象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返回已创建的单例对象。其中最关键的⼀个点就是 volatile 关键字的使⽤,关于 volatile 的详细介绍可以直接搜索 volatile 关键字即可,有很多写的⾮常好的⽂章,这⾥不做详细介绍,简单说明⼀下,双重检查锁中使⽤ volatile 的两个重要特性:可⻅性、禁⽌指令重排序 这⾥为什么要使⽤ volatile ? 这是因为 new 关键字创建对象不是原⼦操作,创建⼀个对象会经历下⾯的步骤: 1. 在堆内存开辟内存空间 2. 调⽤构造⽅法,初始化对象 3. 引⽤变量指向堆内存空间 代码:1 public class Singleton{ 2 private static volatile Singleton instance = null; 3 private Singleton(){} 4 public static Singleton getInstance(){ 5 if (instance == null) 6 { synchronized (Singleton.class){ 7 if (instance == null){ 8 instance = new Singleton(); 9 10 } 11 } 12 } 13 return instance; 14 }
4.静态内部类:可以同时保证延迟加载和线程安全。静态内部类单例是如何实现懒加载的呢?(懒加载 :使⽤的时候再创建对象)⾸先,我们先了解下类的加载时机。
虚拟机规范要求有且只有 5 种情况必须⽴即对类进⾏初始化(加载、验证、准备需要在此之前开始): 1. 遇到 new 、 getstatic 、 putstatic 、 invokestatic 这 4 条字节码指令时。⽣成这 4 条指令最常⻅的 Java 代码场景是:使⽤ new 关键字实例化对象的时候、读取或设置⼀个类的静态字段(final 修饰除外,被final 修饰的静态字段是常量,已在编译期把结果放⼊常量池)的时候,以及调⽤⼀个类的静态⽅法的时候。 2. 使⽤ java.lang.reflect 包⽅法对类进⾏反射调⽤的时候。 3. 当初始化⼀个类的时候,如果发现其⽗类还没有进⾏过初始化,则需要先触发其⽗类的初始化。 4. 当虚拟机启动时,⽤户需要指定⼀个要执⾏的主类(包含 main()的那个类),虚拟机会先初始化这个主类。 5. 当使⽤ JDK 1.7 的动态语⾔⽀持时,如果⼀个java.lang.invoke.MethodHandle 实例最后的解析结果是REF_getStatic 、 REF_putStatic 、 REF_invokeStatic 的⽅法句柄,则需要先触发这个⽅法句柄所对应的类的初始化。 这 5 种情况被称为是类的主动引⽤,注意,这⾥《虚拟机规范》中使⽤的限定词是 "有且仅有",那么,除此之外的所有引⽤类都不会对类进⾏初始化,称为被动引⽤。静态内部类就属于被动引⽤的情况。当 getInstance()⽅法被调⽤时,InnerClass 才在 Singleton 的运⾏时常量池⾥,把符号引⽤替换为直接引⽤,这时静态对象 INSTANCE 也真正被创建,然后再被 getInstance()⽅法返回出去,这点同饿汉模式。那么 INSTANCE 在创建过程中⼜是如何保证线程安全的呢?在《深⼊理解 JAVA 虚拟机》中,有这么⼀句话:虚拟机会保证⼀个类的 <clinit>() ⽅法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化⼀个类,那么只会有⼀个线程去执⾏这个类的 <clinit>() ⽅法,其他线程都需要阻塞等待,直到活动线程执⾏<clinit>() ⽅法完毕。如果在⼀个类的 <clinit>() ⽅法中有耗时很⻓的操 作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执⾏ <clinit>() ⽅法后,其他线程唤醒之后不会再次进⼊ <clinit>() ⽅法。同⼀个加载器下,⼀个类型只会初始化⼀次。),在实际应⽤中,这种阻塞往往是很隐蔽的。 从上⾯的分析可以看出 INSTANCE 在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯⼀性,同时也延迟了单例的实例化。1 public class Singleton{ 2 private Singleton(){}//私有化构造方法 3 public static Singleton getInstance(){//对外提供获取实例的公共方法 4 return InnerClass.Instance;} 5 private static class InnerClass{//定义静态内部类 6 private final static Singleton Instance=new Singleton; 7 8 } 9 10 }
5.枚举:
使⽤枚举除了线程安全和防⽌反射调⽤构造器之外,还提供了⾃动序列化机制,防⽌反序列化的时候创建新的对象。1 public enum Singleton{ 2 instance; 3 public void whateverMethod(){} 4 }