23种设计模式之单例设计模式(一)),Java程序员必读(从菜鸟到高手)架构师必备技能

书中自有黄金屋,书中自有颜如玉

单例设计模式(Singleton)

1.模式定义:

在程序运行期间保证一个类只存在一个实例,并且只提供一个全局访问点

2.场景:

重量级的大对象,不需要多个实例,比如:线程池,数据库连接池,Spring的Bean对象池等。

3.单例模式的经典实现

3.1懒汉模式:

懒汉模式是一种实例的延迟加载方案,只有在使用的时候,才会开始实例化对象:
接下来我们一起来看一下在单线程下的懒汉设计模式

//先创建一个main方法的主类,用来做结果测试
public class TestLazySingleton{
      public static void main(String[] args){
      //对懒加载LazySingleton进行测试
      LazySingleton instance =LazySingleton.getInstance();
      LazySingleton instance1 =LazySingleton.getInstance();
      System.out.println(instance==instance1);
      //控制台会打印结果为true
   }
}
/*在创建一个懒加载类*/
class LazySingleton{
//1.提供私有的静态属性instance
private static LazySingleton instance;
//2.提供一个私有的构造方法
private LazySingleton(){}
//3.在提供一个共有的静态全局访问点
public static LazySingleton getInstance(){
   //4.在调用方法的时候会被实例化,所以要判断是否已经被实例化
   if(instance==null){
      //5.为null则创建一个实例
      instance=new LazySingleton();
  }
   return instance;
}
}

我们再来看一下多线程情况下的懒汉模式的设计:
(为了便于书写,我使用了两个类)

//先创建一个main方法的主类,用来测试结果
public class LazySingletonTest1(){
    public static void main(String[] args){
      //测试多线程下的懒加载
      //线程一:
      new Thread(()->{
         LazySingleton instance =LazySingleton.getInstance();
         System.out.println(instance);
       }).start();
      //线程二:
      new Thread(()->{
        LazySingleton instance=LazySingleton.getInstance();
        System.out.println(instance);
     }).start(); 
     //从控制台打印结果我们会神奇的发现,出问题了,多线程下的懒加载出现了线程安全问题。
  }
}
//在创建一个懒加载类
class LazySingleton{
    //定义一个私有的静态属性
    private static LazySingleton instance;
    //定义一个私有的构造方法
    private lazySingleton(){}
    //定义一个懒加载的全局唯一访问点
    public LazySingleton getInstance(){
       //先做实例化判断
       if(instance==null){
          //为null则创建一个实例
          instance=new LazySingleton();
       }
       return instance;
   }
}

探究:从上述代码的运行结果,我们发现,在多线程下的懒加载设计模式,出现了线程安全的问题。
我们来分析一下,出现的线程安全的原因:当有两个或多个线程同时进入**getInstance()**方法之后(以两个线程A与B做讨论),A线程上未实例化进入分支结构,进行实例化,与此同时B线程上也未实例化,进入分支结构,也进行了实例化,这个时候就会出现实例化多次的情况,不匹配单例设计模式的需求,也就是我们说的线程安全问题。

出现问题之后,我们就尝试进行改进,最先最容易想到的就是对出现线程安全问题的方法加锁。代码如下、

//对出现线程安全的方法枷锁
public synchronized LazySingleton(){
      if(instance==null){
         instance=new lazySingleton();
     }
      return instance;
}

这样是不是就解决了,我们要请楚,一但在方法上上锁,意味着我们每一次调用访问点,实例化的时候,都要加载锁。这是特别浪费系统资源的,会极大的影响我们程序的性能,接下来,我们继续进行一下该进。缩小锁的范围,代码如下-:

//缩小锁范围
public LazySingleton getInstance(){
    if(instance==null){
    //在分支内部加锁。当实例存在就不会加载锁,极大的提升了程序性能
     synchronized(LazySingleton.class){
         if(instance==null){
             instance=new LazySingleton();
         }
     }   
   }
}

这一次我们就完美解决了线程安全问题了---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------NONONO!!!现在的懒加载依旧存在问题,下面我们来思考一个问题,在这之前就不得不涉及一点点底层字节码的知识了。我们现在看一张图:
在这里插入图片描述
上面这张图是我们在实例化对象时的一个底层的字节码文件:通过字节码文件我们可以发现,
实例化的过程是:
a.先在堆内存中开辟一块空间
b.然后在然后初始化该实例
c.然后在栈内存种开辟空间,为引用赋值。
而在字节码文件的运行中,编译器或是CPU都可能会干扰初始化与赋值过程的执行顺序,可能会出现先赋值后实例化的现象,那么这个时候问题就出现了,
假设现在A,B线程同时进入了第一层分支结构,A线程先获得锁,进入第二层分支结构完成实例化过程,结果在途中先被赋值,那么这个时候,远在锁之外的B线程就无法通过第二层分子结构,就会直接 return instance; 而这个时候的对象是没有被初始化的,就有可能出现NullpointerException异常;
**解决办法:**在Java中有一个语义词:volatile(标记被修饰的对象在实例化过程中,既定的实例化流程不被更改)
代码如下-:

//在要实例化的对象上加入语义词 volatile
private volatile static LazySingleton instance;

总结:懒汉设计模式就是 只有一个实例化的对象,全局共享,
注意:懒汉设计模式在多线程中容易出现线程安全问题,解决方案就是 在分支结构内部加锁,然后使用语义次volatile 加在实例化的对象之上。

3.2饿汉模式

相对开始的懒汉模式而言。饿汉模式是超级简单的,接下来我们一起来看一看到底是如何的简单
饿汉模式:实例的创建会随类的加载一起创建,
接下来我们废话不多说直接上代码:

//定义一个main方法主类,测试结果
public class HungrySingletonTest{
  public static void main(String[] args){
    HungrySingleton instance =HungrySingleton.getInstance();
     HungrySingleton instance1 =HungrySingleton.getInstance();
     System.out.println(instance==instance1);
     //控制台打印结果为true
 }
}
//定义饿汉模式的具体类
class HungrySingleton{
    //1.定义一个静态的属性,
    private static HungrySingleton instance=new hungrySingleton();
    //2.定义一个私有的构造方法
    private HungrySingleton(){}
    //定义一个全局访问点
    public HungrySingleton getInstance(){
       return instance;
   }
}

以上就是饿汉模式的全部代码。是不是超级简单呢!而且饿汉模式下是不存在线程安全问题的,饿汉模式的线程安全问题是基于JVM的类加载机制解决的,
我们来研究一下类加载机制的一些步骤:
a. 将类的编译后的二进制文件加载到内存中,生成相应的class数据类型结构
b. 连接: 验证(字节码文件是否符合JVM的规范) ,准备(为静态属性赋默认值),解析(检查属性上书否有关键词,例如final)
c. 初始化(为静态属性赋值)
那么什么时候会触发类加载呢?
只有在类被真正调用的时候,才会触发类加载机制,而且只加载一次,所以饿汉模式随类加载只实例化一次,也就不存在线程安全问题了。
总结:超级简单的饿汉模式,随类加载进行实例化,基于JVM虚拟机类加载机制保证了线程安全。是单例模式的一个经典实现。

3.3 静态内部类实现的单例模式

话不多说我们先上代码,在解释:

//main方法
public class innerSingletonTest{
   public static void main(Stringp[] args){
     //测试方法如上的懒加载或是饿汉模式
  }
}
//定义具体的单例具体类
class InnerSingleton{
   //定义静态内部类
   private static class InnerHolder{
     private static InnerSingleton instance=new InnerSingleton();
  }
  private InnerSingleton(){}
  public static InnerSingleton getInstance(){
    return  InnerHolder.instance; 
  } 
}

经过懒加载与饿汉加载的研究,我们其实可以很容易的看出来,静态内部类加载其实就是一种懒加载,只有在调用getInstance()方法的时候才会实例化对象,同时又利用JVM的类加载机制,随类加载,保证了线程安全的问题
总结:静态内部类的形式就是一种基于JVM类加载机制的懒加载模式,保证了线程安全的问题。

以上就是关于单例设计模式的底层实现,又不懂的地方欢迎在评论区留言,一起讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值