学习到多线程安全这块的知识时,就会接触到单例模式。单例模式指的是多个线程共享一个实例对象,要实现这样的操作,有 两种方式,俗称:饿汉模式和懒汉模式,接下来,让我们进一步学习
1.饿汉模式
饿汉模式通俗的来讲就是在类加载初期就为其分配一个空间供以后使用,无论是否会用到。
接下来,让我们通过代码学习:
public class Sington {
//饿汉式
private static final Sington SINGTON = new Sington();
private Sington(){}
public static Sington getInstance(){
return SINGTON;
}
}
这种方式是线程安全的,但会有一定的空间损耗,我们还有第二种懒汉模式:
2.懒汉模式
懒汉模式使用懒加载方式,一开始并不会分配空间,只有在使用时,才将对象创建出来。
一般情况下,我们会写出如下代码:
public class Sington {
private static Sington SINGTON2 = null;
private Sington(){}
public static Sington getSington2(){
if(SINGTON2 == null){
//多个线程可以同时进入这行代码进行判断,都判断为null
SINGTON2 = new Sington();
}
return SINGTON2;
}
}
这样写我们希望实现的效果就是如果需要使用该对象,再进行分配空间。如果当第一次使用时,那就new 否则就返回。这样做再单线程下是没有问题的,但如果多线程的情况下,就导致线程不安全试想一下,如果多个线程同时执行到第一句,此时它们都将执行new操作,这样显然违反了我们的意图。
这时候,我们就想着对其进行改进,在这里我们使用一个关键字:synchronization使用此关键字可以对对象进行加锁操作仅使持有当前对象锁的线程进行操作,实现同步互斥的功能。
由此,我们生成了第二版代码:
public class Sington {
private static Sington SINGTON3 = null;
private Sington(){}
public synchronized static Sington getSington3(){
if(SINGTON2 == null){
SINGTON2 = new Sington();
}
return SINGTON2;
}
}
当我们这样写出代码后,却发现还是有问题的,原因就是synchronization关键字是将对象锁住,而当刚开始所有线程执行到判断是否为空这一步时,均判断为空准备new时,此时其中一个线程开始执行new操作,将将对象加锁,其它线程处于阻塞状态。当此线程执行完毕将锁释放后,接下来的线程由于之前已经判断过为空,故还将再次执行new操作,也不符合我们的预期。
这样我们就想到了二次判断,写出第三版代码:
public class Sington {
private volatile static Sington SINGTON4 = null;
private Sington(){}
public static Sington getSington4(){
if(SINGTON2 == null) {
synchronized (Sington.class)
if(SINGTON2 == null) {
SINGTON2 = new Sington();
}
}
}
return SINGTON2;
}
}
此处,我们加入了两次判空,这样就算在其它线程已经判断一次后又有一个线程已经创建了对象,其它线程仍然会进行第二次判断,这时就不在多余创建对象了。
另外,我们将SINGTON4变量使用volatile关键字来避免指令重排序的问题,保护线程之间的可见性。
由此就生成我们初识多线程下的单例模式的两种方法。