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, 即可领取大量学习资料及实战经验