目录
1.单例模式
1.1概念
1.2单例模式的实现方式
怎么去设计一个单例
饿汉模式
懒汉模式
懒汉模式-多线程版
解决懒汉模式多线程不安全问题-synchronized
解决懒汉模式多线程不安全问题-volatile
1.单例模式
1.1概念
单例是一种设计模式。
啥是设计模式 ?设计模式好比象棋中的 " 棋谱 ". 红方当头炮 , 黑方马来跳 . 针对红方的一些走法 , 黑方应招的时候有一些固定的套路 . 按照套路来走局势就不会吃亏 .软件开发中也有很多常见的 " 问题场景 ". 针对这些问题场景 , 大佬们总结出了一些固定的套路 . 按照这个套路来实现代码 , 也不会吃亏 .
单例:在全局范围内只有一个实例对象。
单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建多个实例。
例如:JDBC中的DateSourse实例就只需要一个。
1.2单例模式的实现方式
怎么去设计一个单例
1.口头约定
对外提供一个方法,要求大家使用这个对象的时候,通过这个方法来获取。(不太靠谱,一般不采用)
2.使用变成语言本身的特性来处理
首先分析一下在Java中哪些对象是全局唯一的:
①类对象 .class
②用static修饰的变量(static修饰的变量是类的成员变量,所有实例对象,访问的都是同一个成员变量)
通过类对象和static配合可以实现单例的目的。
示例(实现单例模式):
用static修饰变量,变量就是自己,并且赋初始值。再提供一个对外获取实例对象的方法。
private static Singleton instance=new Singleton();
public Singleton getInstance() {
return instance;
}
验证单例是否正确
public static void main(String[] args) {
//验证单例是否正确
Singleton singleton01=new Singleton();
Singleton singleton02=new Singleton();
Singleton singleton03=new Singleton();
//分别打印
System.out.println(singleton01.getInstance());
System.out.println(singleton02.getInstance());
System.out.println(singleton03.getInstance());
}
结果:
既然是单例,那么通过new的方法去获取对象是有歧义的,不能让外部去new这个对象。
将构造方法私有化,通过静态方法调用,获取单例:
public class Singleton {
private static Singleton instance=new Singleton();
//构造方法私有化
private Singleton(){};
public static Singleton getInstance() {
return instance;
}
}
用类名去调用:
Singleton singleton01=Singleton.getInstance();
Singleton singleton02=Singleton.getInstance();
Singleton singleton03=Singleton.getInstance();
System.out.println(singleton01);
System.out.println(singleton02);
System.out.println(singleton03);
结果依旧正确:
饿汉模式
类一加载就完成初始化的方式称为饿汉模式。
特点:书写简单,不容易出错。
上面那个例子就是饿汉模式:
public class Singleton {
private static Singleton instance=new Singleton();
//构造方法私有化
private Singleton(){};
public static Singleton getInstance() {
return instance;
}
}
懒汉模式
彼岸程序启动的时候浪费过多的系统资源,当程序使用这个对象时再对他进行初始化。即类加载的时候不创建实例,第一次使用的时候才创建实例的方式称为懒汉模式。
示例:
public class SingletonLazy {
//定义一个类成员变量
private static SingletonLazy instance=null;
public static SingletonLazy getInstance(){
if(instance==null){
instance=new SingletonLazy();
}
return instance;
}
}
运行一下,结果如下:
public static void main(String[] args) {
SingletonLazy singleton01=SingletonLazy.getInstance();
SingletonLazy singleton02=SingletonLazy.getInstance();
SingletonLazy singleton03=SingletonLazy.getInstance();
System.out.println(singleton01);
System.out.println(singleton02);
System.out.println(singleton03);
}
懒汉模式-多线程版
懒汉模式的实现其实是线程不安全的。
例如(在多线程环境下获取单例对象):
public static void main(String[] args) {
//创建多个线程,并获取单例对象
for (int i = 0; i < 10; i++) {
Thread thread=new Thread(()->{
//获取单例对象,并打印
SingletonLazy instance=SingletonLazy.getInstance();
System.out.println(instance);
});
//启动线程
thread.start();
}
}
结果:
发现获取到了不同的对象,并不符合我们的预期,也就是说出现了线程不安全的现象。
那具体是什么原因导致的线程不安全呢?
t1先LOAD,然后t2执行完所有指令后,t1再继续执行。如上图所示,进行了两次初始化。所以,再多线程环境下,多个线程又可能创建多个实例。
解决懒汉模式多线程不安全问题-synchronized
①可以给getInstance方法加锁。
②将synchronized的范围指定为整个初始化过程,与在方法中加锁是等价的
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
得到的结果目前看来是符合预期的。
但是有一个非常严重的问题:
每一次调用getInstance()方法的时候,都需要进行锁竞争,再进行判断,而锁竞争是非常耗费系统资源的。其实,synchronized代码块再整个程序运行过程中,只需要执行一次就够了。
用户态:Java层面,在JVM中执行的代码
内核态:执行的是CPU指令
也就是说加了synchronized参与锁竞争之后就从应用层面进入到了系统层面。
为了避免过度锁竞争,这里可以加入一层判断
public static SingletonDCL getInstance(){
if(instance==null) {
synchronized (SingletonDCL.class) {
if (instance == null) {
instance = new SingletonDCL();
}
}
}
return instance;
}
双重检查,避免了过度耗费系统资源。
执行一下,结果正确:
public static void main(String[] args) {
// 创建多个线程,并获取单例对象
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
// 获取单例对象,并打印
SingletonDCL instance = SingletonDCL.getInstance();
System.out.println(instance);
});
// 启动线程
thread.start();
}
}
解决懒汉模式多线程不安全问题-volatile
synchronized关键字解决了原子性,内存可见性问题。
volatile关键字解决有序性问题。
初始化过程,并不是一条指令。在整个初始化过程中,经历如下阶段:
1.在内存中开辟一片空间
2.初始化对象的属性(数据)
3.把内存中的地址,赋给instance变量
程序在正在执行的过程中就是按1,2,3这个顺序执行的。1与3是强相关的执行过程,2是一个单独的过程,所以编译或者CPU就有可能进行指令重排序,使程序执行过程变1,3,2.。如果出现这种顺序,那么其它线程就又肯拿到一个创建了一半的对象,导致了线程不安全。
这个时候就需要用volatile修饰变量,禁止指令重排序。