java单例模式详解

本文详细介绍了Java中的单例模式实现,包括懒汉式、饿汉式、双重检查锁(DCL)和静态内部类法。每种实现方式都考虑了线程安全和延迟加载,同时指出了它们的优缺点。枚举实现被推荐为最佳实践,因为它天然线程安全并防止反序列化创建新实例。此外,文章还提及了单例模式在序列化和反射攻击下的注意事项。
摘要由CSDN通过智能技术生成

概念

java中单例模式是一种常见的设计模式,单例模式分三种:懒汉式单例、饿汉式单例、登记式单例三
种。

特点

单例模式有以下特点:

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。
  3. 单例类必须给所有其他对象提供这一实例

单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。避免生成多个对
象保证只对这一个唯一对象进行操作,保证线程的安全和数据的安全.

1.饿汉式:

顾名思义,饿汉式就是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建。代码如
下:
下面看一个示例:

public class Singleton { 
    private static Singleton = new Singleton(); 
    private Singleton() {} 
    public static getSignleton(){ return singleton; }
}

这样做的好处是编写简单,但是无法做到延迟创建对象。但是我们很多时候都希望对象可以尽可能地
延迟加载,从而减小负载,所以就需要下面的懒汉式

2.懒汉式:

单线程写法
public class Singleton { private static Singleton = new Singleton(); private Singleton() {} public static getSignleton(){ return singleton; }}
这种写法是最简单的,由私有构造器和一个公有静态工厂方法构成,在工厂方法中对singleton进行null
判断,如果是null就new一个出来,最后返回singleton对象。这种方法可以实现延时加载,但是有一个
致命弱点:线程不安全。如果有两条线程同时调用getSingleton()方法,就有很大可能导致重复创建对
象。

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

3.考虑线程安全的写法:

这种写法考虑了线程安全,将对singleton的null判断以及new的部分使用synchronized进行加锁。同
时,对singleton对象使用volatile关键字进行限制,保证其对所有线程的可见性,并且禁止对其进行指
令重排序优化。如此即可从语义上保证这种单例模式写法是线程安全的。注意,这里说的是语义上,实
际使用中还是存在小坑的

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

4.兼顾线程安全和效率的写法:

虽然上面这种写法是可以正确运行的,但是其效率低下,还是无法实际应用。因为每次调用
getSingleton()方法,都必须在synchronized这里进行排队,而真正遇到需要new的情况是非常少的。
所以,就诞生了第三种写法

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

这种写法被称为“双重检查锁”,顾名思义,就是在getSingleton()方法中,进行两次null检查。看似多
此一举,但实际上却极大提升了并发度,进而提升了性能。为什么可以提高并发度呢?就像上文说的,
在单例中new的情况非常少,绝大多数都是可以并行的读操作。因此在加锁前多进行一次null检查就可
以减少绝大多数的加锁操作,执行效率提高的目的也就达到了

5.小坑:

那么,这种写法是不是绝对安全呢?前面说了,从语义角度来看,并没有什么问题。但是其实还是有
坑。说这个坑之前我们要先来看看 volatile 这个关键字。其实这个关键字有两层语义。第一层语义相信
大家都比较熟悉,就是可见性。可见性指的是在一个线程中对该变量的修改会马上由工作内存( Work
Memory )写回主内存( Main Memory ),所以会马上反应在其它线程的读取操作中。顺便一提,工
作内存和主内存可以近似理解为实际电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共
享的。 volatile 的第二层语义是禁止指令重排序优化。大家知道我们写的代码(尤其是多线程代码),
由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码
相同,却不保证实际指令的顺序与源代码相同。这在单线程看起来没什么问题,然而一旦引入多线程,
这种乱序就可能导致严重问题。 volatile 关键字就可以从语义上解决这个问题。
注意,前面反复提到 从语义上讲是没有问题的 ,但是很不幸,禁止指令重排优化这条语义直到 jdk1.5
以后才能正确工作。此前的 JDK 中即使将变量声明为 volatile 也无法完全避免重排序所导致的问题。所
以,在 jdk1.5 版本前,双重检查锁形式的单例模式是无法保证线程安全的。

5.静态内部类法:

那么,有没有一种延时加载,并且能保证线程安全的简单写法呢?我们可以把 Singleton 实例放到一个
静态内部类中,这样就避免了静态实例在 Singleton 类加载的时候就创建对象,并且由于静态内部类只
会被加载一次,所以这种写法也是线程安全的:
public class Singleton { 
    private static class Holder { 
        private static Singleton singleton = new Singleton(); 
    }
    private Singleton(){} 
    public static Singleton getSingleton(){ 
        return Holder.singleton; 
    }
}
但是,上面提到的所有实现方式都有两个共同的缺点:
  • 都需要额外的工作(SerializabletransientreadResolve())来实现序列化,否则每次反序列化一 个序列化的对象实例时都会创建一个新的实例。
  • 可能会有人使用反射强行调用我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在 创建第二个实例的时候抛异常)。

6.登记式:

 

登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个 Map (登记薄)中,对于已经登记
过的实例,则从 Map 直接返回,对于没有登记的,则先登记,然后返回。
public class Singleton { 
    private static Map<String, Singleton> map = new HashMap<String, Singleton> (); 
    static { 
        Singleton single = new Singleton(); 
        map.put(single.getClass().getName(), single); 
    }
    // 保护的默认构造子 protected Singleton() { }
    // 静态工厂方法,返还此类惟一的实例 
    public static Singleton getInstance(String name) { 
        if (name == null) { 
            name = Singleton.class.getName(); 
            System.out.println("name == null" + "--->name=" + name); 
        }
        if (map.get(name) == null) { 
            try {
                map.put(name, (Singleton) Class.forName(name).newInstance());
            } catch (InstantiationException e) { 
                e.printStackTrace(); 
            } catch (IllegalAccessException e) { 
                e.printStackTrace(); 
            } catch (ClassNotFoundException e) { 
                e.printStackTrace(); 
            } 
        }
        return map.get(name);
    }
}
它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的 static 方法块,它的单例在类被装
载的时候就被实例化了。

7.枚举写法:

当然,还有一种更加优雅的方法来实现单例模式,那就是枚举写法

public enum Singleton { 
    INSTANCE; 
    private String name; 
    public String getName(){ return name; }
    public void setName(String name){ this.name = name; }
}
使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化
的时候创建新的对象。因此推荐尽可能地使用枚举来实现单例。

最后,不管采取何种方案,请时刻牢记单例的三大要点:

  • 线程安全
  • 延迟加载
  • 序列化与反序列化安全

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值