设计模式之单例模式

1:单例模式理论知识

1.1:特点

  • 构造函数私有化(private),防止其他类直接new对象。
  • 单例类只有一个实例,可以理解只在单例类中new了一个对象
  • 提供一个外部调用的公共方法,获取到唯一的实例

1.2:优点

  • 省去在其他类中new对象,降低了系统内存的使用频率,减轻GC压力
  • 避免了对资源的重复占用

2:单例模式之恶汉模式

public class singleModel{
	/**
	* 类和方法分为:实例变量 实例方法  类变量,类方法
	* 被static修饰的都是属于类的, 类所有的实例都共享, 不需要实例化,可以直接通过类名进行访问。
	* 但是当下使用private修饰, 只有本类共享
	/
	private static SingleModel singleModel=new SingleModel();
	
	/**
	* 构造函数私有化,防止外部创建实例
	/
	private singleModel(){
	}

	/**
	*	提供外部一个get方法,可以获取实例。
	/
	public static SingleModel getSingleModel(){
		return singleModel;
	}
}

提前一步把对象new出来了,外部第一次获取这个类对象的时候可以直接拿到实例,省去了创建类这一步的开销。

1.3: 单例模式之懒汉模式

public class SingleModel{
	private static SingleModel singleModel;
	private SingleModel(){
	
	}
	public static SingleModel getSingleModel(){
		if(singleModel==null){
			singleModel=new SingleModel();
		}
		return singleModel
	}
}

懒汉式大家可以理解为他懒,别人第一次调用的时候他发现自己的实例是空的,然后去初始化了,再赋值,后面的调用就和饿汉没区别了。

1.4懒汉和饿汉的应用场景

科普小知识: Java类加载过程及static详解, static会优先加载进内存区 ,链接详解https://www.cnblogs.com/cxiang/p/10082160.html

如果这个类是经常被调用方,在系统启动的时候使用饿汉模式提前加载, 这样可以直接给别人用, 节省了创建开销, 调用频繁不会导致内存的浪费。

如果这个类被调用不频繁, 就推荐使用懒汉,提前加载的类在内存中是有资源浪费的。

1.5懒汉的线程安全问题

	public static SingleModel getSingleModel(){
		if(singleModel==null){
			singleModel=new SingleModel();
		}
		return singleModel
	}

一个线程, 不会造成线程安全问题
当两个个线程访问时, 同时进入了if(singleModel==null)中的代码块, 这样就会造成创建了两次对象。

1.5.1解决办法之高low办法

public class SingleModel{
	private static SingleModel singleModel;
	private SingleModel(){
	
	}
	public static synchronized SingleModel getSingleModel(){
		if(singleModel==null){
			singleModel=new SingleModel();
		}
		return singleModel
	}
}

加synchronized 修饰静态方法,针对的是整个类,创建实例时先把类锁起来,再进行判断,严重降低了系统的处理速度。效率低,用时间换取线程安全,典型的时间换空间。

举例子场景:一张桌子就是一个类,类中有吃饭的方法,桌子上有四个空位子。

用static修饰的属于类,此处可以理解成桌子, 用synchronized修饰的静态方法,针对的是整个类。 每个对象只有一个锁(lock),现在外面来了四个人吃饭, 一个人拿到了开锁的synchronized。 另外三个人只能等着他吃完,才能上桌子。是不是效率极低???

埋下伏笔: synchronized就像一个磨磨唧唧的细节怪, 很有原则,吃完饭总要检查是否掉东西,不达到目标不释放锁。

1.5.2解决方法之中low方法

public class SingleModel{
	private static SingleModel singleModel;
	private SingleModel(){
	
	}
	public static SingleModel getSingleModel(){
		if(singleModel==null){
			synchronized(this){
				if(singleModel==null){
					return new SingleModel();
				}
			}
		}
		return singleModel
	}
}

将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。
科普一下知识点:https://www.cnblogs.com/cxiang/p/10082160.html
jvm的简述加载过程
1:加载–>将类的权限定名以二进制字节流入JVM中
2:验证–>验证是否规范, 主要是格式,语义,操作验证
3:准备–>目的是为静态变量分配空间,并赋值
4:解析
5:初始化
6:使用
7:卸载
以上是七个操作
但是问题来了: instance = new Singleton();不是一个原子操作。初始化对象和赋值,并不是同一个操作
错误场景复现:A, B两个线程
A 进入synchronized代码块,赋值,草率匆忙着急离开, 不知道是否初始化

B进入synchronized代码块, 发现singleModel已经被赋值了,不为null,返回给调用方,拿到的对象是没有初始化的。

事实上,上面发生的场景专业名词俗称”指令重排“, 它可以”抽象“为下面几条JVM指令:

memory = allocate();	//1:分配对象的内存空间
initInstance(memory);	//2:初始化对象
instance = memory;		//3:设置instance指向刚分配的内存地址

操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:

memory = allocate();	//1:分配对象的内存空间
instance = memory;		//3:设置instance指向刚分配的内存地址(此时对象还未初始化)
ctorInstance(memory);	//2:初始化对象

神奇的volatile关键字要上场了!!!!

1.5.3 解决方法之DCL方法

public class SingleModel{
	private volatile  static SingleModel singleModel;
	private SingleModel(){
	
	}
	public static SingleModel getSingleModel(){
		if(singleModel==null){
			synchronized(this){
				if(singleModel==null){
					return new SingleModel();
				}
			}
		}
		return singleModel
	}
}

volatile的作用: 常用于保持内存可见性和防止指令重排序。

内存可见性: 所有线程都能看到共享内存的最新状态。

防止指令重排序: volatile关键字通过提供“内存屏障”的方式来防止指令被重排序,我理解的是: jvm每走一步, 加一个屏障。

问题来了?既然volatile可以防止指令重排序, 那为什么synchronized不防止指令重排序? 解释如下

1.5.3.4 Happens-Before内存模型

假设是并发场景

public static void main(String[] args) {
	    synchronized(this){
		User user=new User();  //A,无论先复制还是初始化,随意,反正到了下一步都完成了
        user.toString().sout;  //B
		}
        
    }

程序顺序规则:如果程序中操作A在操作B之前,那么当前线程中操作A将在操作B之前执行。这就是从上到下的规则。

Happens-Before内存模型维护了几种Happens-Before规则,程序顺序规则最基本的规则。程序顺序规则的目标对象是一段程序代码中的两个操作A、B,其保证此处的指令重排不会破坏操作A、B在代码中的先后顺序,但与不同代码甚至不同线程中的顺序无关。

1.6单例模式之静态内部类

public class SingleModel{
	private SingleModel(){
		
	}
	/**
	* 静态内部类的加载不需要依附外部类,在使用时才加载。不过在加载静态内部类的过程中也会加载外部类
	/
	private static class SingleModelFactory{
		private static SingleModel singleModel=new SingleModel();
	}
	public static SingleModel getSingleModel(){
		return SingleModelFactory.singleModel;
	}
	/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 
 	任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。
	*/  
    public Object readResolve() {  
        return getSingleModel();  
    } 
}

使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕, 这样我们就不用担心上面的问题。
在这里插入图片描述

1.7枚举模式之单例模式

最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。

public class User {
    /**
     * 构造函数私有化
     */
    private User() {
    }

    public enum SingleModel {
        /**
         * 创建一个枚举对象,该对象天生为单例
         */
        INSTANCE;
        private User user;

        /**
         * 私有化枚举的构造函数
         */
        SingleModel() {
            user = new User();
        }

        public User getInstance() {
            return user;
        }
    }

    public static User getInstance() {
        return SingleModel.INSTANCE.getInstance();
    }

    public static void main(String[] args) {
        System.out.println(User.getInstance());
        System.out.println(User.getInstance());
        System.out.println(User.getInstance() == User.getInstance());
    }
}

结果: true

说实在的,严谨性肯定没有DCL好, 但是书上,网上都说枚举是最优的选择。 待我研究研究待补充

公众号搜索: 意姆斯Talk, 即可领取大量学习资料及实战经验
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值