目录
一,什么是单例模式。
举个最简单的例子,一山容不得二虎,简单地说就是一座山不能同时生活两只老虎,以此引入到oop的世界也就是一个类只能有一个实例化的对象,再多就不行了。那么什么时候会有这种需求呢?比较常见的就是数据库的连接池,线程池,windows的控制面板等,为什么同时只能实例化一个对象呢?针对前两者,无论是数据库的连接池还是线程池创建的目的就是用于公共,循环使用的有限资源,你要是同时创建多个,没能复用,不白白地浪费了资源;而对于控制面板则更好理解,要是能同时创建多个控制面板的实例,当我对不同的实例进行不同的操作,那么设置肯定会出问题的,比如一个设置让我向东,一个设置让我向西,为了避免此类情况,只能有一个控制面板的设置有效。
二,实现方式。
在Java中创建类对象是使用new关键字,要想达到阻止客户端调用时随便new对象,不难想到只要将类的构造器设为私有的,就能达成目的;但是不能new了,那么该如何获取类的对象呢?此时可以通过暴露公共的接口,然后每次返回同一个类对象即可,代码如下:
public class Singleton {
private Singleton(){}
private static Singleton singleton;
public static Singleton getInstance(){
if(null == singleton){
singleton = new Singleton();
}
return singleton;
}
}
通过上面的方式,我们每次调用的时候首先会判断singleton对象是否存在,如果不存在则在内部new一个对象赋值给singleton,然后下次再调用时直接将其返回。然而看似运行很好的代码在单线程环境下没有问题,但是在多线程环境下呢?请看下面的例子:
public class SingletonThread extends Thread{
public SingletonThread(String name){
super(name);
}
@Override
public void run() {
Singleton singleton = Singleton.getInstance();
System.out.println(Thread.currentThread().getName() + ":" + singleton);
}
}
class Test{
public static void main(String[] args){
Thread[] threads = new Thread[10];
for(int i = 0; i < 10; i++){
threads[i] = new SingletonThread("线程" + i);
}
for(Thread t : threads){
t.start();
}
}
}
上面的代码很好理解,我们创建了10个线程,每个线程在运行时会打印出获取到的Singleton对象,那么会有怎么样的结果呢?
从上面的打印输出我们发现,每次获取到的singleton对象竟然并不都是一个,其实这是因为在调用getInstance方法时,若同时有多个线程进入,并且都通过了singleton == null的验证,那么就导致不同线程都new了一个singleton对象;解决方式主要有两个,一个是加锁,一个是使用普通的方式。
2.1 使用普通的方式改进
public class Singleton {
private Singleton(){}
private static Singleton singleton = new Singleton();
public static Singleton getInstance(){
return singleton;
}
}
上面的代码把初始化singleton对象的过程放到了定义它的地方,因为线程调用时singleton对象肯定已经存在,所以运行结果很明显10次打印都将是同一个singleton。此外需要注意的一点就是这种在定义时就初始化单例对象的方式被称为饥汉模式,而再上面的通过方法调用,当为null时再初始化的模式被称为懒汉模式。
虽然通过饥汉模式解决了多线程同时调用会出现new多个singleton的问题,但我们却丢掉了延迟初始化的好处,众所周知,定义为static类型的成员是属于类的,也就是说当类被加载的时候static变量就会被初始化,结果就是不管你用不用singleton对象,反正我都直接给你初始化了一个(这也就是为什么叫做饥汉模式,饿的不行了,不询问厨师,上来直接把食材给吃了;而懒汉则是当你需要了,调用的时候我再临时给你做饭,你不叫我,我反正懒得去做),有些同学肯定觉得这样又没什么,那是因为我们现在的代码很简单,试想如果创建单例对象的过程很复杂,需要不少的时间,我要使用100个线程,那么在刚启动时,即使没有使用该类对象的需求也会造成大量时间和资源浪费在了初始化上,所以延迟其初始化是很有必要的。下来就看看如何使用锁的方式来进行同步。
2.2 使用加锁的方式改进
2.2.1 直接加锁
java中用来同步最简单的方式就是使用synchronized,该关键字可以用来同步方法,代码块。放在此处的用法就是如下:
public class Singleton {
private Singleton(){}
private static volatile Singleton singleton;
public synchronized static Singleton getInstance(){
if(null == singleton)
singleton = new Singleton();
return singleton;
}
}
如上代码,只要在getInstance()方法前加上synchronized关键字,则每个线程在进入该方法时都需要获取该锁,因为该锁是“排他锁”, 所以可以保证该方法同时只会被一个线程访问,而其他线程没锁只能一直等待; volatile关键字可以保证该变量的可见性,意思就是当其被一个线程改变后,其他线程也能够第一时间获知,而不是从线程自己的缓存中取。
这样做很简单,但是带来的问题很大,试想如果同时有多个线程来访问该方法,则线程因为获取不到锁一直阻塞,而cpu在多个线程间切换也会带来很大的问题,造成的结果就是很慢;另一方面singleton对象只会在第一次访问时为空,初始化后就没必要再进行判断,所以给整个方法加上锁是不合理的,改进方式就是改变加锁的范围:
2.2.2 双重检查
public class Singleton {
private Singleton() {
}
private static volatile Singleton singleton;
public static Singleton getInstance() {
if (null == singleton) //第一次检查
synchronized (Singleton.class) {
if (null == singleton) { //第二次检查
singleton = new Singleton();
}
}
return singleton;
}
}
如上代码,通过改变加锁的范围,我们在singleton对象为null时进行加锁处理(不为null就不用加锁会直接返回),然后进入区块后,在只有singleton还为null时才进行初始化,这样做的好处是控制了加锁的粒度,不用同步时直接返回就行了,大大提高了效率。
三, 总结
单例模式常常用于管理公共的资源,它能确保全局的资源只有一份。如果你遇到了全局只能保留一个类实例的情况,那么就放心使用单例模式吧。需要注意的一点是使用静态常量也能保证所有类共享一个变量,但是却因此丢失了延迟初始化的优点。
来源:CSDN
作者:奔跑的代码君
链接:https://blog.csdn.net/qq_38353700/article/details/103950491