- 作用:单例模式主要解决的是,防止一个全局使用的类频繁的创建和消费,从而提升提升整体的代码的性能。
- 特点:单例模式有一个特点就是不允许外部直接创建,因此在默认的构造函数上添加了私有属性
private
。
懒汉式(线程不安全)
/**
* 懒汉式
*/
public class Lazy {
private static Lazy instance;
/**
* 私有构造方法,防止被实例化
*/
private Lazy(){
}
public static Lazy getInstance(){
if(instance == null){
instance = new Lazy();
}
return instance;
}
}
优点:延迟加载,真正用的时候才实例化对象,提高了资源的利用率
缺点:存在并发访问的问题(可能进行了多次new操作)
懒汉式(线程安全)
/**
* 懒汉式
*/
public class Lazy {
private static Lazy instance;
private Lazy(){
}
public synchronized static Lazy getInstance(){
if(instance == null){
instance = new Lazy();
}
return instance;
}
}
拥有上面懒汉式的优点,同时也克服了其缺点,使用synchronized
关键字同步加锁,保证了线程安全,但所有的访问都需要加锁,造成了资源的浪费。
饿汉式(线程安全)
/**
* 饿汉式
*/
public class Hungry {
private static Hungry hungry = new Hungry();
/**
* 私有构造方法,防止被实例化
*/
private Hungry(){
}
public static Hungry getInstance(){
return hungry;
}
}
优点:static变量会在类装载时初始化,不存在并发访问问题,可以省略synchronized关键字
缺点:类初始化时就创建了对象,如果只是加载本类,而不是要调用 getInstance(),甚至永远没有调用,则会造成资源浪费
双重校验锁(线程安全)
/**
* 双重校验锁
*/
public class DoubleLock {
private static volatile DoubleLock instance;
private DoubleLock(){
}
public static DoubleLock getInstance(){
if(instance != null)
return instance;
synchronized (DoubleLock.class){
if(instance == null)
instance = new DoubleLock();
}
return instance;
}
}
- 双重锁的方式是方法级锁的优化,减少了部分获取实例的耗时。
- 同时这种方式也满足了懒加载。
为什么使用volatile
?
采⽤ volatile 关键字修饰也是很有必要的, singleton = new Singleton(); 这段代码其实是分为三步执⾏:
为 singleton 分配内存空间
初始化 singleton
将 singleton 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1-3-2。指令重排在单线程环境下不会出 现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和 3,此时 T2 调⽤ getInstance() 后发现 singleton 不为空,因此返回 singleton ,但此时 singleton 还未被初始化。 使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。
静态内部类(线程安全)
/**
* 静态内部类
*/
public class InnerClass {
private static class inner{
private static InnerClass instance = new InnerClass();
}
private InnerClass(){
}
public static InnerClass getInstance(){
return inner.instance;
}
}
- 既保证了线程安全又保证了懒加载,同时不会因为加锁的方式耗费性能。
- 这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载。
CAS(线程安全)
/**
* CAS
*/
public class CAS {
private static final AtomicReference<CAS> INSTANCE = new AtomicReference<>();
private CAS(){
}
public static CAS getInstance(){
for( ; ; ){
CAS instance = INSTANCE.get();
if(instance != null){
return instance;
}
INSTANCE.compareAndSet(null, new CAS());
return INSTANCE.get();
}
}
}
- java并发库提供了很多原子类来支持并发访问的数据安全性;
AtomicInteger
、AtomicBoolean
、AtomicLong
、AtomicReference
。 - AtomicReference 可以封装引用一个V实例,支持并发访问如上的单例方式就是使用了这样的一个特点。
- 使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。
- 当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。
枚举单例(线程安全)
/**
* 枚举单例
*/
public enum EnumSingle {
INSTANCE;
public void test(){
System.out.println("枚举单例");
}
public static void main(String[] args) {
EnumSingle.INSTANCE.test();
}
}
- 优点:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
- 缺点:无延迟加载
参考资料
重学 Java 设计模式:实战单例模式「7种单例模式案例,Effective Java 作者推荐枚举单例模式」 - bugstack虫洞栈