书中自有黄金屋,书中自有颜如玉
单例设计模式(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类加载机制的懒加载模式,保证了线程安全的问题。
以上就是关于单例设计模式的底层实现,又不懂的地方欢迎在评论区留言,一起讨论!