了解单例的各种实现方式
官方定义:单例是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
补充:设计单例时,需要隐藏其所有的构造方法。并且单例是属于创建型模式。(后续会补充一篇文章专门描述创建型、行为型。)
在开发过程,我们会经常使用到一些单例的类,比如
ServletContext、ServletConfig、ApplicationContext、DBPool等等,
这些类都确保了在任何情况下都绝对只有一个实例。
饿汉式单例是在类首次加载的时候就会创建单例了
// # v1
public class Singleton
{
private static final Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){return singleton ;}
}
// # v2
public class Singleton
{
//类加载顺序
//先静态后动态
//先上,后下
//先属性后方法
private static final Singleton singleton;
//与构造方法没什么区别,只能装逼
static {
singleton= new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){return singleton;}
}
懒汉式是在被外部类调用时才创建实例
- 优点:节省了内存。
- 缺点:线程不安全,原因是并发情况下,在第一个线程判断完,进去初始化的时候,第二个线程进来了,因为第一个线程还没开始初始化,所有instance还是空的,所以也会进去初始化,那么就会存在多个对象了。(破坏单例了)
- 解决方法:在方法上加上关键字synchronized,当出现多个线程并发的时候,需要第一个线程返回值后,第二个线程才能进行。即线程阻塞了Monitor状态。
- synchronized关键字控制资源不被同时占用,保证资源只能被一个线程占用。线程安全了
- 随之问题又出现了,加了synchronized关键字,带来了新的缺点,性能较差。
public class Singleton
{
private static Singleton instance;
private Singleton(){}
// 关键字synchronized
public static synchronized Singleton getInstance(){
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 优点:线程安全、性能提高。只有在第一次为空的时候才会阻塞,后续都不阻塞。
- 变量必须加上volatile关键字,保证代码的执行顺序。原因是在多线程环境下,代码执行会存在一定的随机性,会不断的去抢CPU的时间片的。
- 补充:在执行的时候,首先会分配对象的内存地址,还会分配变量的内存地址,在赋值的时候,需要将变量的指针指向对象的内存地址。这个过程会存在创建两个内存地址,那么就会存在先后顺序问题,会出现给变量instance分配地址、后在给对象再地址,那么就会存在线程混乱的问题,所以必须要加上volatile关键字,保证代码的执行顺序
- 缺点:代码可读性差,难度大。不够优雅。
public class LazyDuobleCheckSingleton
{
private static volatile LazyDuobleCheckSingleton instance;
private LazyDuobleCheckSingleton(){}
public static LazyDuobleCheckSingleton getInstance(){
//检查是否需要阻塞
if (instance == null) {
synchronized (LazyDuobleCheckSingleton.class){
//检查是否要重新创建实例
if (instance == null){
instance = new LazyDuobleCheckSingleton();
}
}
}
return instance;
}
}
- 执行原理:假设类名为LazyStaticInnerClassSingleton,java编译后的class文件是 LazyStaticInnerClassSingleton.class。内部类是 LazyStaticInnerClassSingleton$LazyHolder.class,当程序调用LazyHolder.INSTANCE的时候,才会去调用加载内部类LazyHolder
- 优点:写法优雅、很好的利用的Java本身的语法特点,性能高、避免内存浪费。
- 缺点:能够被反射破坏。
- 解决方法在父类上加上LazyHolder.INSTANCE != null的时候就抛出异常,防止反射破坏,但是加了判断后又带来了新的问题,代码又变成不过优雅了,可读性差。
public class LazyStaticInnerClassSingleton {
private LazyStaticInnerClassSingleton(){
//解决反射破坏单例的问题,但是代码随之又变成了不优雅了
if (LazyHolder.INSTANCE != null){
throw new RuntimeException("不允许非法访问");
}
}
private static LazyStaticInnerClassSingleton getInstance(){
return LazyHolder.INSTANCE;
}
//静态内部类只有在调用的时候,才会进行初始化。所以是懒汉式
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
}
}
将每一个实例都缓存到统一的容器中,使用唯一标识获取实例。
- enum类相当于继承 Enum抽象类.
- 优点:代码优雅、线程安全、性能较高。还可以防止反射破坏,(为什么可以防止,大家可以私聊我,我专门为你解答)。
- 缺点:会存在资源浪费,单个对象情况下还好,如果存在成千上万个对象。那么全部使用枚举也会造成浪费资源,以及反序列化破坏单例的问题
public enum EnumSingleton
{
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){return INSTANCE;}
}
- 优点:性能较好、避免内存浪费
- 缺点:线程安全问题,以及反序列化破坏单例的问题,反序列化破坏单例的解决方法:在类中实现readResolve方法,改方法名必须为readResolve。
- 原理:原因是反序列化的时候,会调用ObjectInputStream类,而且在ObjectInputStream类中,做了判断,如果类里面存在readResolve方法,那么就会调用该方法,直接将方法内的返回值进行返回。INSTANCE
private Object readResolve(){return INSTANCE;}
public class ContainerSingleton
{
private ContainerSingleton(){}
private static Map ioc = new ConcurrentHashMap();
public static Object getInstance(String className){
if (ioc.containsKey(className)){
try {
Object instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e) {
e.printStackTrace();
}
}
return ioc.get(className);
}
}
保证线程内部的全局唯一,且天生线程安全。
- ThreadLocal 比较特殊,在同一线程内,获得的对象是相同的。
- 对于不同的线程来说,ThreadLocal获得的对象都是不一样的。
1. 在内存中只有一个实例、减少内存开销 2. 可以避免对资源的多重占用 3. 设置全局访问点,严格控制访问。
1. 没有接口,扩展困难 2. 如果要扩展单例对象,只有修改代码,没有其他途径。
1. 私有化构造器
2. 保证线程安全
3. 延迟加载
4. 防止序列化和反序列化破坏单例
5. 防御反射攻击单例