说到Java的23种设计模式 , 我们最熟悉的可能就是单例模式。其实Java有23种设计模式,都是前人的经验之谈,不得不说采用设计模式编写的代码,可维护性是真的强。
首先我们就先来介绍一下我们常说的单例模式吧?
单例
单例设计模式,顾名思义就是在运行过程中只产生一个对象实例。那我们如何实现呢?话不多说,直接上代码
单例的核心就是将构造器私有化,给客户端提供一个公有方法获取实例。 下面是我们的第一种写法,也是饿汉式单例
看到这个写法,我们觉得他有什么问题吗? 答案如下,我们这种写法很明显,只要类被加载的时候就会初始化这个对象,虽然是单例的,但是我们也许并用不到这个对象,就会造成一些性能的影响。
package com.starcpdk.test;
public class Singleton {
private Singleton(){};
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance(){
return INSTANCE;
}
}
我们还有一种意思和上边这个完全一样的写法 , 那就是用静态语句块给静态常量赋值的方式 , 其实和第一种写法是完全一样的
package com.starcpdk.test;
public class Singleton {
private Singleton(){};
private static final Singleton INSTANCE;
static {
INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return INSTANCE;
}
}
既然以上两种写法存在着只要类加载对象就会被实例化的问题,那么我们就换一个地方让对象实例化,我们在调用getInstance获取实例的时候进行判断,如果对象被实例化了就直接返回该对象,如果该对象没有被实例化,就实例化该对象后再给他返回该对象。这种方式称为懒加载。
package com.starcpdk.test;
public class Singleton {
private Singleton(){};
private static Singleton INSTANCE;
public static Singleton getInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
但是以上代码同样存在着问题,我们试想 , 假如有多个线程同时在创建这个对象。假设一个线程执到了if (INSTANCE == null)的代码,还未创建对象,另外一个线程此时也执行到了if(INSTANCE== null)的代码,由于上一个线程还没创建代码,该线程执行到判断语句时也同样是满足条件,也会创建一个对象。此时我们的实例就无法保证单例了。
我们来写一个测试方法 , 对这个并发的情况进行一个测试。
为了方便测试 , 我们模仿一下业务场景的耗时,获取实例时让线程睡眠一毫秒
package com.starcpdk.test;
public class Singleton {
private Singleton(){};
private static Singleton INSTANCE;
public static Singleton getInstance(){
if (INSTANCE == null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
INSTANCE = new Singleton();
}
return INSTANCE;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Singleton.getInstance().hashCode());
}).start();
}
}
}
为了解决上述问题,我们想到多线程的情况下并发问题,第一想打肯定就是加锁。于是就有了我们下面的代码。也相对比较简单,就是在上面的获取实例的方法上加一个synchronized关键字,给方法上锁。
package com.starcpdk.test;
public class Singleton {
private Singleton(){};
private static Singleton INSTANCE;
public static synchronized Singleton getInstance(){
if (INSTANCE == null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
我们会发现确实保证了单例。同时也实现了懒加载。 但是随之而来的又有新问题了 , 我们给整个方法上锁,那么所有要获取该实例时,锁被占用都只能排队等待释放锁后下一个线程才能获取锁,这样就导致了效率的大幅下降,一个多线程并行执行变成了串行执行,这样固然是不可以的。为了解决这个问题,我们又有了一种新思路,我们给方法上锁不行,那么我们就在哪儿用在哪儿上锁不就好了,于是我们将锁移到了方法内部
package com.starcpdk.test;
public class Singleton {
private Singleton(){};
private static Singleton INSTANCE;
public static Singleton getInstance(){
if (INSTANCE == null) {
synchronized (Singleton.class){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
我们分析上面的代码,我们发现锁加到方法里面的话,依然无法保证单例。假设我们的第一个线程执行到if判断,发现INSTANCE是空,于是我们获取锁,进入锁方法中,但是此时我们的第二个线程也执行到了if语句,此时我们的第一个线程还未执行搭配创建实例的语句,也就是说第二个线程到达的时候,INSTANCE还是空,那么我们第二个线程就会进入到if语句里面,此时就是等待第一个线程释放锁后,第二个线程获取锁,因为此时第二个线程已经进入if判断语句块里面了,只要第一个线程释放锁,第二个线程就会获取到锁继续往下执行,会再次创建一个对象。这样就无法保证单例了。
因为以上的问题,我们即想实现懒加载,又想能够保证单例,同时还想要效率高一点点,那么我们就又想到一种思路,上面这种方法无法保证单例无非就是因为if判断和加锁不是一个过程,会导致多线程的不一致。因此我们想到了一种双重判断,我们获取到锁之后让他再做一次判断,看此时对象是否被实例化了,这样就可以保证第二个线程获取到锁之后不会直接创建对象实例了。
我们将上面的代码改为如下代码
package com.starcpdk.test;
public class Singleton {
private Singleton(){};
private static Singleton INSTANCE;
public static Singleton getInstance(){
if (INSTANCE == null) {
synchronized (Singleton.class){
if (INSTANCE == null){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
如上的代码可以说是已经相对比较完美了。
下面还有两种比较特别的单例写法,可以说是更完美的写法吧 , 首先我们先来阐述第一种实现。 我们才用静态内部类的方式实现。
package com.starcpdk.test;
public class Singleton {
private Singleton(){};
static class InnerSingleton{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return InnerSingleton.INSTANCE;
}
}
这种方式我们利用Java的特性,由于静态属性只会被加载一次,因此决定了此种方式的单例可以保证,防止了多线程的情况下造成的多实例问题。由于内部类只在调用getInstance时才会被加载,也因此保证了该种方式的懒加载。
最后一种方式我们便介绍Java effective一书中的单例实现方式,此种方式是才用枚举方式返回单例。
package com.starcpdk.test;
public enum Singleton {
INSTANCE
}
这种方式是利用了Java枚举的特性,即可以实现单例,同时可以防止反序列化反射等带来的破坏单例的可能。
|