最近打算再回头系统的看看Java 的设计模式,Java 有23种设计模式,就先从最简单的单例模式开始吧。
我们常见的单例模式无非就是所谓的 懒汉式和饿汉式, 除此之外呢,还有 双同步锁模式,静态内部类模式,以及枚举模式等。
一,常见写法
先看看常见的饿汉式和懒汉式吧
饿汉式:
/**
* 单例模式-饿汉式(线程安全,效率高,因为在类加载时对对象进行初始化,所以叫做饿汉式,饿的等不及)
*/
public class SingletonHungry {
private static SingletonHungry instance = new SingletonHungry();
private SingletonHungry(){
}
public static SingletonHungry getInstance() {
return instance;
}
}
/**
* 单例模式-懒汉模式(线程安全的,加载效率低,因为要加同步锁)
*/
public class SingletonLazy {
private static SingletonLazy instance;
private SingletonLazy(){
}
public static synchronized SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
饿汉式和懒汉式的主要区别是对象的初始化时间不同,所以它们的执行效率也是不同的,饿汉式对象的创建是在类的初始化的时候就创建了,而懒汉式是在我们调用的时候才进行初始化。而懒汉式由于有同步锁,所以它的执行效率相对比较低,后面我们会进行测试比较各个模式的执行效率。
而我们在代码中常用的单例模式是懒汉式,只不过多加了一重锁,所以也就是双重锁模式,看代码;
/**
* 单例模式-双重锁模式;
* 接下来我解释一下在并发时,双重校验锁法会有怎样的情景:
*
* STEP 1. 线程A访问getInstance()方法,因为单例还没有实例化,所以进入了锁定块。
*
* STEP 2. 线程B访问getInstance()方法,因为单例还没有实例化,得以访问接下来代码块,而接下来代码块已经被线程1锁定。
*
* STEP 3. 线程A进入下一判断,因为单例还没有实例化,所以进行单例实例化,成功实例化后退出代码块,解除锁定。
*
* STEP 4. 线程B进入接下来代码块,锁定线程,进入下一判断,因为已经实例化,退出代码块,解除锁定。
*
* STEP 5. 线程A初始化并获取到了单例实例并返回,线程B获取了在线程A中初始化的单例。
*
* 理论上双重校验锁法是线程安全的,并且,这种方法实现了lazyloading。
*/
public class SingletonDoubleLock {
private static SingletonDoubleLock instance;
private SingletonDoubleLock() {
}
public static synchronized SingletonDoubleLock getInstance() {
if (instance == null) {
synchronized (SingletonDoubleLock.class) {
if (instance == null) {
instance = new SingletonDoubleLock();
}
}
}
return instance;
}
}
为什么要再加一个锁呢,上面的注释已经写的很清楚了,大伙可以看看。
另外再写一下静态内部类模式和枚举模式吧,这两种及少用到
/**
* 单例模式-静态内部类模式
*/
public class SingletonStatic {
private static class SingletonStaticClass {
private static final SingletonStatic instance = new SingletonStatic();
}
private SingletonStatic() {
}
public static SingletonStatic getInstance() {
return SingletonStaticClass.instance;
}
}
**
* d单例模式-枚举单例
*/
public enum SingletonEnum {
INSTANCE; // 枚举类型本来就是一个单例
/**
* 添加自己的操作
* */
public void operate() {
}
}
二,测试是不是只生成一个对象
在这里我们写了一个SingletonClient类,用来测试我们的单例
ublic class SingletonClient {
public static void main(String[] args) {
SingletonHungry s1 = SingletonHungry.getInstance();
SingletonHungry s2 = SingletonHungry.getInstance();
SingletonLazy s3 = SingletonLazy.getInstance();
SingletonLazy s4 = SingletonLazy.getInstance();
SingletonDoubleLock s5 = SingletonDoubleLock.getInstance();
SingletonDoubleLock s6 = SingletonDoubleLock.getInstance();
SingletonStatic s7 = SingletonStatic.getInstance();
SingletonStatic s8= SingletonStatic.getInstance();
System.out.println(s1 + "===" + s2);
System.out.println(s3 + "===" + s4);
System.out.println(s5 + "===" + s6);
System.out.println(s7 + "===" + s8);
}
}
我们对上面的每个单例模式进行打印比较,看看是不是对象的值是同一个
com.hwang.designpattern.singleton.SingletonHungry@7e0b37bc===com.hwang.designpattern.singleton.SingletonHungry@7e0b37bc
com.hwang.designpattern.singleton.SingletonLazy@3b95a09c===com.hwang.designpattern.singleton.SingletonLazy@3b95a09c
com.hwang.designpattern.singleton.SingletonDoubleLock@6ae40994===com.hwang.designpattern.singleton.SingletonDoubleLock@6ae40994
com.hwang.designpattern.singleton.SingletonStatic@1a93a7ca===com.hwang.designpattern.singleton.SingletonStatic@1a93a7ca
这里是打印出的结果,可见他们是同一个对象,对象只重新创建了一次。
三,测试各个模式执行效率
int threadNum = 10; // 线程数
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
long startTime = System.currentTimeMillis();// 开始时间
for (int j = 0; j < threadNum; j++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
SingletonHungry.getInstance();
}
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await(); // main 线程阻塞,直到计数变为0,才会继续往下执行
long endTime = System.currentTimeMillis();
System.out.println("饿汉式耗时时间为:" + (endTime - startTime));
这里引用 CountDownLatch 这个类呢,是因为我们的计时是在主线程中的,而对象的获取执行是在子线程中,是异步的,所以加入了计时,当子线程执行完成时候,在执行main线程的endTime。
最后看看测试结果:
饿汉式耗时时间为:19
懒汉式耗时时间为:540
双重锁式耗时时间为:320
内部静态类式耗时时间为:82
可见饿汉式以及内部静态类由于没有同步锁,执行效率还是很高的。
写的很简单,有不正确的地方还望各位朋友指出,一起交流学习。
详细代码请移步github:https://github.com/Trac-MacGrady/DesignPattern/tree/master