目录
什么是设计模式?
设计模式是面向对象设计中反复出现的问题的解决方案,通常描述了一组相互紧密作用的类与对象。大概就是比如针对软件开发中的一些常见的“场景问题”出了一些固定的套路来实现代码。
单例模式就是设计模式中的一种。
一、什么是单例模式
单例:就是指单个实例,只能创建一个对象/实例。
单例模式能保证某个类在程序种只存在唯一实例,而不会创建出多个实例。
单例模式具体的实现方式,下面主要介绍两种写法:“饿汉模式''和”懒汉模式“。
二、介绍”饿汉“和”懒汉“模式
(1).饿汉模式
举例说明:吃完饭之后,就立即去洗碗
(2).懒汉模式
举例说明:吃完之后,碗放槽里,先不洗,等到下一顿吃的时候,需要用了再去洗碗。
通常认为懒汉模式效率更高。
针对两种模式,举例:打开一个硬盘上的文件,读取文件内容,并且显示出来。
饿汉模式:把所有的内容读取到内存种,并显示出来。
如果内容过多,用户在使用时,可能会卡顿很久,内存也不一定够。
懒汉模式:只把文件都一小部分(比如看小说看文),把当前屏幕填充上,如果用户翻页了,在读取其他内容,不过不翻页,就省下了。懒汉模式可以快速打开内容,比饿汉模式效率更高,非必要不读入。
三、”饿汉“和”懒汉“模式代码实现
1.饿汉模式
//把这个类设置为单例的
class Singleton{
//创建唯一的实例
private static Singleton instance = new Singleton(); //被static修饰,表示该属性是类的属性
//获取到实例的方法 读操作
public static Singleton getInstance() {
return instance;
}
//在类内部把实例创建好,同时禁止外部new实例,这样就可以保证单例模式的特性
private Singleton() {
}
}
public class ThreadDemo17 {
public static void main(String[] args) {
//此时s1与s2是一个实例,是唯一的实例
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
//Singleton s3 = new Singleton();//如果再new一个实例,就会报错
}
}
2.懒汉模式
class SingletonLazy {
//创建唯一的实例
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
//在类内部把实例创建好,同时禁止外部new实例,这样就可以保证单例模式的特性
private SingletonLazy() {
}
public class ThreadDemo18 {
public static void main(String[] args) {
//此时s1与s2是一个实例,是唯一的实例
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);//true
}
}
四、单例模式 ——线程安全问题(针对饿汉模式和懒汉模式)
对于多线程安全,我们可以知道:
一个线程修改同一个变量 → 安全
一个线程读取同一个变量 → 安全
多个线程修改同一个变量 → 不安全
多个线程读取同一个变量 → 安全
1.饿汉模式 ——安全
首先饿汉模式,根据以下代码(与前面写的代码一致):
//获取到实例的方法 读操作
public static Singleton getInstance() {
return instance;
}
这里的return instance只是一个读操作,是安全的。
2.懒汉模式——不安全
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
首先,懒汉模式要去new对象,假如此时有多个线程,然后多个线程执行顺序紊乱,比如此时有两个线程,一个t1线程,一个t2线程,如下,假如执行顺序如下:
如上图这样,就可能导致产生了两个对象,这样就不是单例模式了,是一个很危险的操作,如果产生一个对象消耗的内存过大,这种操作可能就会导致内存负荷很大,很危险。
所以总结来说,出现上述问题的原因主要还是因为上面的操作不是原子的,所以想要解决这个问题就需要将上面的操作变成原子的。
五、解决线程安全的方案
1.加锁
(1)把if与new变成原子操作;
(2)双重if,减少不必要的加锁操作。
//保证判定和new是一个原子操作
//这个条件,判定是否要加锁,如果对象已经有了,就不必加锁了,此时本身就是线程安全
if (instance == null) {//判断是否要加锁
synchronized (SingletonLazy.class){
if (instance == null) {//判断是否要创建对象
instance = new SingletonLazy();
}
}
}
以上代码,一定要首先判定是否要加锁,因为不知道内存里面是否已经有instance对象了,如果不加这个判定,就会导致又创建出一个对象,就会导致不是单例模式,导致线程不安全。
2.new对象的指令重排序问题
instance = new SingletonLazy();
首先这个new对象操作,分为三个指令:
1.创建内存;
2.调用构造方法;
3.把内存地址付给引用。
关于这三个指令,2和3的执行顺序是随机的,有可能是下面这种情况:
1.创建内存;
3.把内存地址付给引用。
2.调用构造方法;
如果是以上的情况,在多线程的环境下,可能会出现问题。
假如t1以以上方式执行到了3,因为编译器优化指令重排序,系统调度给t2了,再去判定条件,发现条件不成立,非空 ,就直接返回了实例的引用,但是实际上,上一步操作其实还没有调用构造方法,后面就出现了问题。
所以我们需要使用volatile关键字:
volatile private static SingletonLazy instance = null;
所以前面懒汉模式改良后的代码如下:
package threading;
//懒汉模式:非必要,不创建
class SingletonLazy {
//加volatile主要是为了防止创建对象重排序,保证内存稳定性
//创建对象分为三步:
//1.申请内存空间
//2.调用构造方法
//3.把内存地址,赋给引用
//其中2,3项是打乱进行的,假如出现了一个极端情况,t1执行1,3操作以后
// ,系统调度t2,导致t1还没有调用构造方法,然后后导致判定条件不成立,非空,
// 直接返回实例的引用,导致t2直接调用的该实例
volatile private static SingletonLazy instance = null;
//只有调用了getInstance 才会去创建实例
public static SingletonLazy getInstance() {
//保证判定和new是一个原子操作
//这个条件,判定是否要加锁,如果对象已经有了,就不必加锁了,此时本身就是线程安全
if (instance == null) {//判断是否要加锁
synchronized (SingletonLazy.class){
if (instance == null) {//判断是否要创建对象
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {
}
}
public class ThreadDemo18 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}