Java单例模式

设计模式是开发中很重要,单例模式是常用的设计模式

本文用示例介绍Java的单例模式的写法。有如下六种写法:懒汉式,饿汉式,静态内部类,双重校验锁,枚举,非synchronized的加锁。

简介

单例模式需要考虑如下几点

  • 线程安全性

  • 懒加载

  • 性能

要保证多线程环境下也是单例的,使用的时候才会创建对象,而不是一开始就创建对象,有的实现方式需要每次加锁,这样性能很差,有的实现方式不需要每次都加锁,性能很高。 

本文所述的单例模式都是线程安全的。线程不安全的单例模式,不是合格的单例模式。在下边的单例模式中,我比较喜欢静态内部类。如果涉及到反序列化创建对象我会使用枚举的方式。我永远不会使用饿汉式,如果有其他特殊的需求,我可能会使用双重校验锁。

懒汉

懒汉模式就是,它很懒,直到用到的时候才会去创建对象,而不是一开始就创建对象。

  • 支持多线程

  • 支持懒加载

  • 性能很低(因为是加锁同步)


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

饿汉

饿汉模式就是,它很饿,想快点创建对象,此模式在类加载时就立即创建对象。

  • 支持多线程(这种方式基于classloder机制保证初始化instance时只有一个线程)

  • 不完全支持懒加载(instance在类装载时就实例化,大多数都是调用getInstance方法。但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果)

  • 性能很高(因为使用时不需要加锁同步)

写法一


public class Singleton {
    private static Singleton instance = new Singleton();
 
    private Singleton() {
    }
 
    public static Singleton getInstance() {
        return instance;
    }
}

写法二

表面上看起来差别挺大,其实跟法1差不多,都是在类初始化即实例化instance。


public class Singleton {
    private static Singleton instance = null;
 
    static {
        instance = new Singleton();
    }
 
    private Singleton() {
    }
 
    public static Singleton getInstance() {
        return instance;
    }
}

静态内部类

在类内部有一个静态内部类,它持有外部类的实例。

  • 支持多线程(同样基于classloder机制保证初始化instance时只有一个线程)

  • 支持懒加载

  • Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显式装载SingletonHolder类,从而实例化instance)

  • 它跟懒汉式不同的是(很细微的差别):懒汉式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果)

  • 一方面:如果实例化instance很耗资源,我想让他延迟加载;另一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这时实例化instance是不合适的。这时,这种方式相比第二种方式(饿汉)就更合理。

  • 性能很高(因为使用时不需要加锁同步)


public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
 
    private Singleton() {
    }
 
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

双重校验锁

此模式用了两次if判断。

  • 是懒汉模式的升级版
  • 在JDK1.5之后,双重检查锁定才能够正常达到单例效果
  • Java 5 以前的 JMM (Java 内存模型)存在缺陷,即使将变量声明成 volatile 也不能完全避免重排序

public class Singleton {
    private static volatile Singleton singleton;
 
    private Singleton() {
    }
 
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

为什么加volatile关键字?

volatile作用:保证有序性、可见性。

有序性

Singleton singleton = new Singleton() 这句话可以分为三步:

1. 为 Singleton 分配内存空间(加载、链接);
2. 初始化 singleton;
3. 将 singleton 指向分配的内存空间。

但是由于JVM具有指令重排的特性,执行顺序有可能变成 1-3-2。指令重排在单线程下不会出现问题,但是在多线程下会导致一个线程获得一个未初始化的实例。例如:线程T1执行了1和3,此时T2调用 getInstance() 后发现 singleton 不为空,因此返回 singleton, 但是此时的 singleton 还没有被初始化。所以,使用 volatile 会禁止JVM指令重排,从而保证在多线程下也能正常执行。

可见性

把变量声明为 volatile,就指示 JVM,修改的值立即被更新到主存。

普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

为什么有两次校验?

第一次校验:也就是第一个if(singleton == null)

这个是为了代码提高代码执行效率。由于单例模式只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance方法就不必要进入同步代码块,不用竞争锁。直接返回前面创建的实例即可。

第二次校验:也就是第二个if(singleton == null)

这个校验是防止二次创建实例。假如有一种情况,当singleton还未被创建时,线程t1调用getInstance方法,由于第一次判断singleton ==null,此时线程t1准备继续执行,但是由于资源被线程t2抢占了,此时t2页调用getInstance方法,同样的,由于singleton并没有实例化,t2同样可以通过第一个if,然后继续往下执行,同步代码块,第二个if也通过,然后t2线程创建了一个实例singleton。此时t2线程完成任务,资源又回到t1线程,t1此时也进入同步代码块,如果没有这个第二个if,那么,t1就也会创建一个singleton实例,那么,就会出现创建多个实例的情况,但是加上第二个if,就可以完全避免这个多线程导致多次创建实例的问题。

所以说:两次校验都必不可少。

枚举

Effective Java作者Josh Bloch 提倡的方式。

  • 支持多线程(可保证只有一个实例。这与枚举类的实现有关)

  • 支持防止反序列化和防止反射破坏单例


public enum Singleton {
    INSTANCE;
}

package org.example.a;
 
enum MyEnum{
    FIRST("第一个"),
    SECOND("第二个");
 
    private String desc;
    private MyEnum(String desc) {
        this.desc = desc;
    }
 
    public String getDesc() {
        return desc;
    }
 
    private String lastName;
 
    public String getLastName() {
        return lastName;
    }
 
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
}
 
public class Demo {
    public static void main(String[] args) {
        MyEnum.FIRST.setLastName("Tony");
        System.out.println(MyEnum.FIRST.getLastName());
        System.out.println(MyEnum.FIRST.getDesc());
        MyEnum myEnum = MyEnum.FIRST;
        MyEnum myEnum1 = MyEnum.FIRST;
        System.out.println(myEnum == myEnum1);
    }
}

Tony
第一个
true

非synchronized (不常用)

CAS


package org.example.a;
 
import java.util.concurrent.atomic.AtomicReference;
 
public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = 
            new AtomicReference<Singleton>();
 
    private Singleton() {
    }
 
    public static Singleton getInstance() {
        for (; ; ) {
            Singleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }
 
            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

 ThreadLocal


package org.example.a;
 
public class Singleton {
    private static final ThreadLocal<Singleton> singleton =
            new ThreadLocal<Singleton>() {
                @Override
                protected Singleton initialValue() {
                    return new Singleton();
                }
            };
 
    public static Singleton getInstance() {
        return singleton.get();
    }
 
    private Singleton() {
    }
}

 

  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值