单例模式可以使得一个类只有一个对象实例,能够减少频繁创建对象的时间和空间开销。
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
饿汉单例
在类初始化后即创建好,等待被调用,多线程下安全可靠,无副作用
public class Singleton{
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
懒汉模式(懒加载)
1.单线程下的懒汉单例
只有被用到的时候才会给单例创建对象。
public class Singleton {
private static Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if(singleton==null) {
return new Singleton();
}
return singleton;
}
}
问题:在多线程状态下,如果A,B同时执行到if条件判断 A判断singleton为空准备执行,然后让出了CPU时间片; B也判断singleton为空,接着执行,此时创建了一个实例对象;A获取了CPU时间片后接着执行,也创建了实例对象,这就导致多个单例对象的情况。
解决方案:加同步。
2同步保证线程安全的单例
public class Singleton2 {
private static Singleton2 singleton2;
private Singleton2() {
}
public static synchronized Singleton2 getInstance() {
if(singleton2==null) {
return new Singleton2();//1
}
return singleton2;
}
}
新问题:虽然解决了多线程并发的问题,但是却带来了效率问题: 我们的目的是只创建一个实例,即//1处代码只会执行一次,也正是这个地方才需要同步。但现在的写法是锁住了整个方法,同一时间只能有一个线程获得这个单例对象,效率低下。
解决方案:降低锁的粒度,只需要锁住1即可
同步代码块降低锁粒度
public class Singleton3 {
private static Singleton3 singleton3;
private Singleton3() {
}
public static Singleton3 getInstance() {
if(singleton3==null) {
synchronized (Singleton3.class) {
return new Singleton3();//1
}
}
return singleton3;
}
}
新问题:这样会带来与第一种一样的问题,即多个线程同时执行到条件判断语句时,会创建多个实例。 A,B同时判断为空,同时执行if,与第一种懒汉的区别是这里A获得锁先创建对象,B后获得锁后创建对象。
解决方案: 问题在于当一个线程创建一个实例之后,singleton就不再为空了,
但是后续的线程并没有做第二次非空检查。那么很明显,在同步代码块中应该再次做检查,也就是所谓的双重检测。
懒汉的双重检查
public class Singleton4 {
private static Singleton4 singleton4;
private Singleton4() {
}
public static Singleton4 getInstance() {
if(singleton4==null) {//一重检测
synchronized (Singleton4.class) {//加锁
if(singleton4==null) {//二重检测
return new Singleton4();//1
}
}
}
return singleton4;
}
}
单例在双重加载模型下在逻辑上已经是线程安全的了,但是依然可能出现问题,即空指针异常。
问题:一般出在单例的构造方法里执行语句过多。原因还是出在1上,由于jvm会自动对程序优化,会发生指令重排序的情况,即jvm的执行顺序可能与程序的顺序不相同。
无序写入可能会造成顺序的颠倒,即内存分配、返回对象引用、初始化的顺序,这种情况下对应到//1就是singleton已经不是null,而是指向了堆上的一个对象,这时这个对象还是个空壳子,啥都没有。但是此时B线程却看到堆的引用已经出来了,就直接拿来用,结果出现空指针异常。
解决方案:volatile可解决指令重排序问题。(为什么能解决推荐看深入jvm虚拟机,简单说下就是在字节码执行时有内存屏障,重排序中顺序后面的语句不能越过屏障排序)
懒汉最终板volatile+双重检测
public class Singleton5 {
private volatile static Singleton5 singleton5;
private Singleton5() {
}
public static Singleton5 getInstance() {
if(singleton5==null) {//一重检测
synchronized (Singleton5.class) {//加锁
if(singleton5==null) {//二重检测
return new Singleton5();
}
}
}
return singleton5;
}
}
网友评论