单例模式
概念
单例模式(Singleton Pattern)的定义为:确保一个类在任何情况下只有一个实例,而且自行实例化并向整个系统提供这个实例。
单例模式是创建型模式。
单例模式分为
-
饿汉式单例
-
懒汉式单例
知识铺垫:Java枚举
关键字
enum
可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。public enum t { SPRING,SUMMER; }
饿汉式单例
概念
饿汉式单例模式就是在类加载的时候就立即初始化,并且创建单例对象。不管你有没有用到,都先建好了再说。它绝对线程安全,在线程还没出现以前就实例化了,不可能存在访问安全问题。
优缺点
优点:线程安全,没有加任何锁、执行效率比较高。
缺点:类加载的时候就初始化,不管后期用不用都占着空间,浪费了内存。
简单的饿汉式写法(代码)
public class Hungry {
//类初始化的时候 就进行实例化
private static final Hungry hungry= new Hungry();
//构造函数
privat Hungry() {
}
//getInstance方法返回实例
private static Hungry getInstance(){
return hungry;
}
}
饿汉式单例适合用在单例类比较少的情况下,在实际项目中,有可能会存在很多的单例类,如果我们都使用饿汉式单例的话,对内存的浪费会很大,所以,我们要学习更优的写法。(懒汉式)
懒汉式单例
概念
懒汉式,顾名思义就是实例在用到的时候才去创建,“比较懒”,用的时候才去检查有没有实例,如果有则直接返回,没有则新建。
要点
-
定义为静态成员变量
-
私有化构造方法
-
公共静态方法获取单例对象,内部判空
简单实现
public class Lazy {
private static Lazy lazy;
// 让构造函数为private,这样该类就不会被实例化
private Lazy() {
System.out.println(Thread.currentThread()+"");
}
private static Lazy getInstance(){
//懒汉式 如果实例不存在 则创建
if (lazy == null){
lazy = new Lazy();
}
return lazy;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
Lazy.getInstance();
}).start();
}
}
上述代码在单线程下能够完美运行,但是在多线程下存在安全隐患。
多线程下的结果
Thread[Thread-0,5,main] Thread[Thread-3,5,main] Thread[Thread-1,5,main] Thread[Thread-2,5,main]
情况1:
每次线程执行getInstance方法,得到的结果是我们想要的
情况2:
此种情形下,该种写法的单例模式会出现多线程安全问题,得到两个完全不同的对象
情况3:
该种情形下,虽然表面上最终得到的对象是同一个,但是在底层上其实是生成了2个对象,只不过是后者覆盖了前者,不符合单例模式绝对只有一个实例的要求。
多线程下的单例模式的懒汉式写法
synchronized
加锁
public class LazyUpgrade {
private static LazyUpgrade instance;
// 让构造函数为private,这样该类就不会被实例化
private LazyUpgrade() {
System.out.println(Thread.currentThread()+"");
}
private static synchronized LazyUpgrade getInstance(){
//懒汉式 如果实例不存在 则创建
if (instance == null){
instance = new LazyUpgrade();
}
return instance;
}
但是用synchronized加锁时,在线程数量较多的情况下,会导致大批线程阻塞,从而导致程序性能大幅下降
双重检查锁
public class LazyUpgrade {
private volatile static LazyUpgrade instance;//volatile为了避免指令重排
// 让构造函数为private,这样该类就不会被实例化
private LazyUpgrade() {
System.out.println(Thread.currentThread()+"");
}
private static LazyUpgrade getInstance(){
// 第一重检查是为了确认instance是否已经被实例化,
// 如果是,直接返回实例化对象
// 否则进入同步代码块进行创建,避免每次都排队进入同步代码块影响效率;
if (instance == null){
//线程会被阻塞在这里
synchronized (LazyUpgrade.class){
if (instance == null){
// 第二重检查是真正与实例的创建相关,如果instance未被实例化,则在此过程中被实例化
instance = new LazyUpgrade();//不是一个原子性操作;
}
}
}
return instance;
}
当第一个线程调用 getInstance()方法时,第二个线程也可以调用。当第一个线程执行到 synchronized 时会上锁,第二个线程就会变成 MONITOR 状态,出现阻塞。此时,阻塞并不是基于整 个 LazySimpleSingleton 类的阻塞,而是在 getInstance()方法内部的阻塞,只要逻辑不太复杂,对于 调用者而言感知不到。
但是,用到 synchronized 关键字总归要上锁,对程序性能还是存在一定影响的。难道没有更好的方案吗?显示是有的,我们可以从类初始化的角度来考虑,采用静态内部类的方式。
静态内部类实现懒汉式单例
静态内部类模式中实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的方法/属性被调用时才会被加载,并初始化静态属性,静态属性由于被static修饰,保证只能被初始化一次,并且严格保证实例化顺序。
静态内部类模式是一种优秀的单例模式。在没有任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间浪费。
public class LazyInner {
//如果没有使用,则内部类不会加载;
private LazyInner() {
System.out.println(Thread.currentThread()+"");
}
//定义一个静态内部类, 在内部类中来创建实例;
//默认不会加载
public static class InnerClass{
private static final LazyInner INSYANCE = new LazyInner();
}
//对外访问方法 static 为了使单例的空间共享,保证这个方法不会被重写和重载
public static LazyInner getInstance(){
return InnerClass.INSYANCE;
}
-----
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyInner.getInstance();
}).start();
}
}
}
反射破坏单例
如果我们通过反射来调用其构造方法,在调用 getInstance() 方法得到 new 出来的实例,应该会存在两个不同的实例。
那么我们继续升级:
一旦检测到对象已经被实例化,但是构造方法仍然被调用时直接抛出异常。(也就是说如果构造方法一旦出现多次创建,则直接抛出异常:)
public class LazyInner {
private LazyInner() {
if (InnerClass.INSYANCE != null){
throw new RuntimeException("实例被重复创建");
}
System.out.println(Thread.currentThread()+"");
}
//对外访问方法
public static LazyInner getInstance(){
return InnerClass.INSYANCE;
}
//定义一个静态内部类
//未被使用的话是不会加载的;
public static class InnerClass{
private static final LazyInner INSYANCE = new LazyInner();
}
序列化破坏单例
一个单例对象创建好后,有时候我们需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象 并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化 的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。
public class SeriableSingle implements Serializable {
private static final SeriableSingle INSTANCE= new SeriableSingle;
private SeriableSingle() {
}
private static SeriableSingle getInstance(){
return INSTANCE;
}
private Object readResolve(){
return INSTANCE;
}
}
虽然增加 readResolve()方法返回实例解决了单例模式被破坏的 问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。
注册式单例模式
注册式单例模式又叫登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识 获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。
枚举式单例模式
public enum EnumSingle {
INSTANCE;//枚举类中放一个参数;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingle getInstance(){
return INSTANCE;
}
}
容器式单例
容器式单例模式适用于实例非常多的情况,便于管理。但它是非线程安全的。