1.单例的介绍
1.1什么是单例?
顾名思义就是在整个运行时域,一个类只有一个实例对象
1.2为什么需要单例?
因为有的类型的实例对象的创建和销毁对资源并不大,比如string、int…有的类型,则较为庞大复杂,如果我们在使用到它的时候反复的创建与销毁并且这些对象是完全可以复用的话,则会造成一些不必要的性能浪费,所以当我们想控制实例数目,节省系统资源的时候会用到。
1.3使用场景
比如现在需要写一个访问数据库的Demo,而我每次创建数据库实例是一个耗资源的操作,并且数据库的实例是完全可以复用的,那么我们可以将这个对象设计为单例的,如此操作每次只需创建一次并重复使用该对象即可·。
2.单例的实现
2.1实现单例考虑的问题
a. 是否懒加载
b. 是否线程安全
c. 是否能反射破坏
2.2单例的实践步骤
1.构造私有化(P1-L1)后则不可被其他类通过new来构造这个对象实例,
需要对象的话只能使用getInstance()(P1-L3)方法来获取该类对象
在getInstance()方法中首先判断sMySingLeton是否被构造过(P1-L4),如果构造就直接使用,反之则当场构造
public class MySingLeton {
private MySingLeton() {
} //构造私有化 1
private static MySingLeton sMySingLeton = null; //2
public static MySingLeton getInstance() {//3
if (sMySingLeton == null) {//4
sMySingLeton = new MySingLeton();//5
}
return sMySingLeton;
}
}
2.我们可以看P1代码知道MySingleton对象是我们第一次调用才真正构建的而不是程序一启动好久等着调用的,这种滞后的构建方式即可叫做懒加载。为什么要懒加载呢,我们要考虑到当程序启动后,我们即便构建了该对象,但如果一直到程序结束对象也没有被调用过,那么就比较浪费了,只有真正使用了再去创建才较为合理(个人理解)
3.看完了懒加载再看线程是否安全问题,看上面的例子不难知道他并非是一个线程安全的单例,
因为在执行以下判断的时候,可能会有多个线程同时进入并实例化多次
if (sMySingLeton==null) {}
4.我们可以把代码改为如下的样子,在我们加入了synchronized的时就能保证在同一时刻只有一个线程能够进入这个方法,。
public class MySingLeton {
private MySingLeton() {
}
private static MySingLeton sMySingLeton = null;
public static synchronized MySingLeton getInstance() {
if (sMySingLeton == null) {
sMySingLeton = new MySingLeton();
}
return sMySingLeton;
}
}
5.以上代码虽然会解决线程安全的问题,但同时也引入了新的问题,我们只想在对象构建的时候同步线程,而如果我们每次在获取对象就都要进行同步操作,也是会对性能有很大的影响,有因小失大之嫌,所以这种写法不可取。通过观察我们会发现线程安全是出现在了我们构造方法的阶段,那么我们只要在编译期构建对象,在运行时调用也可不用考虑线程安全问题,由此对以上代码进行改造后得出以下代码。
public class MySingLeton {
private MySingLeton() {
}
private static MySingLeton sMySingLeton = new MySingLeton();
public static synchronized MySingLeton getInstance() {
return sMySingLeton;
}
}
6.以上代码虽然能解决线程安全问题,但我们也能一眼看出这种写法并不是懒加载,所以为了追求既能懒加载又能线程安全呢?让我们回到第六个步骤.仔细想想形成低效的原因是我们在getInstance()上加了 synchronized所以每个进入该方法的线程都首先会获得锁,那么之前也提到了我们只需要在构建对象的时候同步,而可以直接使用对象时候就没有必要同步了所以可以把代码改为如下所示:
public class MySingLeton {
private MySingLeton() {
}
private static MySingLeton sMySingLeton = null;
public static MySingLeton getInstance() {//1
if (sMySingLeton == null) {//2 b
synchronized (MySingLeton.class) {//3 a
sMySingLeton = new MySingLeton();//4
}
}
return sMySingLeton;
}
}
7.如上代码所示,在多个线程代码执行语句2的时候后虽然只有一个线程(a线程)能抢到锁去执行语句3但是可能会有其他线程(b线程)已经进入了if代码块在等待a线程执行完成,一旦a执行完成b就会立即获取锁然后就会进行对象创建,这样的话对象就会被创建多次,解决这个问题的办法就是在语句3和4之间再添加一个判空,将代码改成如下所示:
public class MySingLeton {
private MySingLeton() {
}
private static MySingLeton sMySingLeton = null;
public static MySingLeton getInstance() {//1
if (sMySingLeton == null) {//2 b
synchronized (MySingLeton.class) {//3 a
sMySingLeton = new MySingLeton();//4
}
}
return sMySingLeton;
}
}
8.如上代码所示我们再次假设A,B两个线程同时进入getInstance方法A线程首先获取了锁并进行了instance的构建,当他构建完成后会交还锁,与此同时B线程也会获取锁,在获得锁后他会进行一个判空(上图getInstance()方法L5)由于instance已经被A线程初始化了,所以该判断不可能等于null所以
B线程会直接退出并返回instance实例。这样就不会造成线程不安全的问题了,这种对对象进行两次判空的单例叫做双检索(Double-Checked Locking),看到如上代码就以为完了么?还未必呀,熟悉happens-before原则的程序员都知道该单例还有一个问题出在实例化对象的地方(上图L6)。
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
因为该行代码在指令层面并不是一个原子性操作,而是需要有如上图代码所示分解为三步操作:在真正执行的时候JVM虚拟机可能会为了效率会对指令进行重排,比如先执行L1再执行L3再执行L2,假设A线程执行到了L3的时候此时的instance还未进行初始化那么如果有一B线程执行到了如P5图所示的第L3行代码此时在B线程instance==null会返回false并直接跳过返回 instance对象,但是此时的A线程由于指令重排的缘故导致instance还并未初始化完成所以导致出现了,B线程的getInstance()获取到的还是一个并未初始化完成的对象。解决该问题的方式即是给instance加上volatile关键字即可解决在instance上指令重排的问题,如下图所示即为完整无Bug的写法。
public class MySingLeton {
private MySingLeton() {
}
private volatile static MySingLeton sMySingLeton = null;
public static MySingLeton getInstance() {
if (sMySingLeton == null) {
synchronized (MySingLeton.class) {
if (sMySingLeton == null) {
sMySingLeton = new MySingLeton();
}
}
}
return sMySingLeton;
}
}
9.回到2.1提出的问题实现单例需要考虑的三点关于懒加载与线程安全上文已经说得很清楚了,关于反射破坏这个问题是一种人为的操作只有故意这样操作才会出现,想要避免单例被反射破坏可以使用枚举来避免,而我们正常工作中也不用考虑到会不会被反射破坏,只需要考虑2.1.1与2.1.2的问题即可。