Java设计模式已是老生常谈,单例模式是Java设设计模式中,相对比较容易理解的一个模式。
先来看下,单例模式的特点:
1.单例类只能为其他对象提供唯一实例,且由自己创建;
2.单例模式分为懒汉模式(延迟初始化实例)和饿汉模式(类加载即初始化实例),具体还可细分。
我是上帝,唯我独尊!
一、懒汉模式来看一个简单的懒汉模式的例子:
private static God god;
private God() { //私有化构造方法,保证不被其他类创建实例
}
public static God getInstance() {
if(god == null) {
god = new God();
}
return god;
}
乍一看,这样实现没什么问题,但是在并发多线程环境下,可能就会出现多个God实例;并且即使构造方法已经私有化,但通过java反射机制,仍然可以实例化多个对象。这就违背了构造单例模式的初衷,这种实现方式是线程不安全的。
何为 线程安全(引自百度百科)?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
那么,我们如何改进呢?如下:
public static synchronized God getInstance() {
if(god == null) {
god = new God();
}
return god;
}
在getInstance方法上加synchronized关键字标示同步,这种方法确实可行。然而,每次获取God实例都需要同步,无形中降低了性能,提高了负担,其实我们只需要在第一次实例化的时候同步即可。
看一个改进的例子(避免了每次getInstance时都同步):
public static God getInstance() {
if(god == null) {
synchronized (God.class) {
if(god == null) {
god = new God();
}
}
}
return god;
}
这里添加了双重检验锁,并不是每次getInstance时同步,而是第一次实例化时同步。可能会疑问,既然synchronized已经做了同步处理,为何还要再次对god进行判空处理呢?
来看下,没有进行二次判空处理的代码:
public class God {
private static God god;
private God() {
System.out.println("init God");
}
public static God getInstance() {
System.out.println("step1"); //step1
if(god == null) {
System.out.println("step2"); //step2
synchronized (God.class) {
System.out.println("step3"); //step3
god = new God();
}
}
return god;
}
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
God.getInstance();
God.getInstance();
God.getInstance();
}
}).start();
God.getInstance();
God.getInstance();
God.getInstance();
}
}
在main方法中开启了一个新的线程,并调用多个getInstance方法,按理说,只会有一个方法会进入到step3,且只输出一次“init God”,但是结果却出乎意料:
step1
step1
step2
step3
step2
init God
step1
step1
step3
init God
step1
step1
为什么出现这种情况?假设A、B两个同时调用了getInstance方法,此时A、B同时进入了step2,接下来A抢占先机进入step3,由于做了同步锁,所以B不能再次进入step3,只能乖乖呆在原地。当A已经成功实例化god后跳出了synchronized,而此时B会再次进入step3,也就会重复创建god实例,不能实现单例模式的事实便不攻自破。
试想,如果做了二次判空,是不是B就永远无法进入到step3,并再次创建God实例了呢?事实却是如此。
所以进行二次判空是有其道理可言的。
懒汉模式还存在一种懒加载实现方式:
public class God {
static class GodImpl {
private static final God SINGLETON = new God();
}
private God() {
System.out.println("init God");
}
public static God getInstance() {
return GodImpl.SINGLETON;
}
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
God.getInstance();
God.getInstance();
God.getInstance();
}
}).start();
God.getInstance();
God.getInstance();
God.getInstance();
}
}
利用静态内部类和ClassLoarer单线程特性,不仅保证了性能,也实现了单例的目的。
以上提供了懒汉模式的几个例子,唯有双重检验锁、静态内部类以及“方法加synchronized标示”的实现方式保证了单例的创建、线程的安全,但是在性能上,“方法加synchronized标示”的实现方式稍显逊色。
二、饿汉模式饿汉模式,即一旦类加载时就实例化了单例,所以不存在多个实例的情况,依赖ClassLoader的特性,其无疑是线程安全的。
public class God {
private static God god = new God ();
private God () {
System.out.println("init God");
}
public static God getInstance() {
return god;
}
}
饿汉模式在初始化类的时候已经实例化了一个单例对象,所以会占用一定内存,但是第一次获取对象时,速度会非常快;而懒汉模式恰巧相反,无非是空间和时间的较量。
三、jdk中的单例模式package java.lang;
public class Runtime {
/**
* Holds the Singleton global instance of Runtime.
*/
private static final Runtime mRuntime = new Runtime();
public static Runtime getRuntime() {
return mRuntime;
}
......
}
本文由慕课网 ifynn原创,转载请注明!
热门评论
已拜读,单例模式我们一般常用的是哪种,饿汉模式,懒汉模式分别适用到什么具体场景。