单例模式
单例模式是一种设计模式,
所谓设计模式,就像棋谱一样,针对了一些特定的场景,已经产生了对应的解决方案。
比如:
我们在学习JDBC的时候创建的一个数据源DataSource就应该是一个单例。
还有,在实际开发中还有一些负责加载数据到内存的类,也应该是单例,
因为这样的类一般有几个G,甚至十几个G,如果不是单例的话,每次运行都要占用大量的内存,
单例模式相当于从语法的角度,强制规定,某一个类只能够有一个实例。
单例模式的实现
单例模式的实现主要依托于static关键字~(static关键字修饰的成员,称之为静态成员),
在Java中,一个成员被static修饰后,他就变成了一个“类属性”,而非一个“实例属性”,我们在调用它的时候只能通过:类名.静态成员 的方式去调用,,而不是通过创建类的实例对象来调用。
单例模式的风格
饿汉模式
饿汉,饿了一次想吃三大碗
/**
* @Name: ThreadDemo1
* @Description: 饿汉模式
* @Author: panlai
* @Date: 2021/8/10 10:00
*/
public class ThreadDemo1 {
//Singleton类是一个单例类,只有一个实例
static class Singleton{
//创建一个成员,保存唯一的一个Singleton实例
private static Singleton instance = new Singleton();
//通过public方法来获取到这个实例
public static Singleton getInstance(){
return instance;
}
//再将类的构造方法设为私有的,以避免其他成员构建类的实例
private Singleton(){};
}
public static void main(String[] args) {
Singleton s = Singleton.getInstance();
}
}
懒汉模式
懒汉,懒得动,吃几碗就洗几个碗。
/**
* @Name: ThreadDemo2
* @Description: 懒汉模式
* @Author: panlai
* @Date: 2021/8/10 10:07
*/
public class ThreadDemo2 {
//懒汉模式的实现
//创建实例的时机是第一次使用getInstance方法的时候,就比饿汉模式更晚
static class Singleton {
private static Singleton instance = null;
public static Singleton getInstance(){
//只有当使用instance实例的时候才创建,并非一开始就创建好。
if (instance == null){
instance = new Singleton();
}
return instance;
}
private Singleton(){}
}
public static void main(String[] args) {
Singleton s = Singleton.getInstance();
}
}
一般认为懒汉模式更好(但也不是绝对,主要还是看使用的场景,只不过懒汉模式更高效一些)。
因为:
饿汉模式:(立刻洗碗)吃完饭吃了三碗饭,就洗三个碗,
懒汉模式:吃了三碗饭,放着不洗,下次吃饭时要吃几碗就洗几个碗。(节约成本)
Linux命令less命令就是采用了懒汉模式,
每次打开一个文件,只加载眼前用到的这一屏幕数据到内存中,所以打开速度就比较快,翻页时在重新加载,
但是如果采用记事本打开,就会把所有的数据都加载出来,然后才能被看到,效率就比较低。
单例模式线程安全问题
上面两种中哪种是线程安全的?哪种是线程不安全的?
答案是:懒汉模式存在线程安全问题。
上述代码中懒汉模式的代码执行流程大致分为以下四个步骤:
但是如果两个线程是以这样的方式执行,
我们会发现,线程1在new了实例之后,还没有进行save时,线程2就又创建了一个实例,这整个过程当中创建了两个实例,不符合单例模式思想。()
那么该如何保证线程安全呢?
加锁 synchronized
参考:如何保证线程安全?
如果把synchronized加到getInstance方法外边,这个时候相当于内部的所有操作都是串行的,
如果加到方法内部,就只针对,判断,new操作是串行的,提高了效率(串行不如并行效率高)。
/**
* @Name: ThreadDemo3
* @Description: 加锁解决懒汉线程不安全
* @Author: panlai
* @Date: 2021/8/10 10:33
*/
public class ThreadDemo3 {
//懒汉模式的实现
//创建实例的时机是第一次使用getInstance方法的时候,就比饿汉模式更晚
static class Singleton {
private static Singleton instance = null;
//完全可以吧synchronized关键字加到getinstance方法前,但是这样不太好,过于粗暴
public static Singleton getInstance(){
//如果把关键字写在内部,就减少了串行的代码,提高了效率
synchronized(Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
return instance;
}
private Singleton(){}
}
}
但是,这样做也无法逃脱一个不变的定律:
加锁注定与高效无缘。
但是上面的方法还有可以有优化的地方,因为只有实例没有被初始化,需要进行创建实例时需要进行加锁操作,所以如果按照上面的方法,就会造成不必要的加锁解锁操作,浪费时间,浪费资源。
所以只需要在加锁前在进行一个if条件判断如果实例已经被初始化了,就不需要进行第二个加锁判断操作了。
如下:
public static Singleton getInstance(){
//如果把关键字写在内部,就减少了串行的代码,提高了效率
if (instance == null){
synchronized(Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
第一次if判断是为了判断这个线程是否被初始化,如果已经被初始化,就不需要进行加锁操作了。如果没有被初始化,则才进行加锁,并且进行初始化操作。,提高了效率,节省了时间。
此外,在多线程调用该方法时,还需要使用volatile关键字,来摆正线程读到的值都是内存中最新的值。