单例模式的八种写法比较
一,前言
单例模式详细大家都已经非常熟悉了,在文章单例模式的八种写法比较中,对单例模式的概念以及使用场景都做了很不错的说明。请在阅读本文之前,阅读一下这篇文章,因为本文就是按照这篇文章中的八种单例模式进行探索的。
本文的目的是:结合文章中的八种单例模式的写法,使用实际的示例,来演示线程安全和效率
既然是实际的示例,那么就首先定义一个业务场景:购票。大家都知道在春运的时候,抢票是非常激烈的。有可能同一张票就同时又成百上千的人同时在抢。这就对代码逻辑的要求很高了,即不能把同一张票多次出售,也不能出现票号相同的票。
那么,接下来我们就使用单例模式,实现票号的生成。同时呢在这个过程中利用上述文章中的八种单例模式的写法,来实践这八种单例模式的线程安全性和比较八种单例模式的效率。
既然文章中第三种单例模式(懒汉式)是线程不安全的,那么我就从这个单例模式的实现开始探索一下线程安全。
因为不管是八种单例模式的实现方式的哪一种,票号的生成逻辑都是一样的,所以,在此正式开始之前,为了更方便的编写示例代码,先做一些准备工作:封装票号生成父类代码。
二,封装票号生成父类代码
package com.zcz.singleton; public class ticketnumberhandler { //记录下一个唯一的号码 private long nextuniquenumber = 1; /** * 返回生成的号码 * @return */ public long getticketnumber() { return nextuniquenumber++; } }
票号的生成逻辑很简单,就是一个递增的整数,每获取一次,就增加1。以后我们的每一种单例模式都继承这个父类,就不用每一次都编写这部分代码,做到了代码的重用。
接下来就是实现第三种单例模式,探索一下会不会引起线程安全问题。
三,实现第三种单例模式
package com.zcz.singleton; /** * 票号生成类——单利模式,即整个系统中只有唯一的一个实例 * @author zhangchengzi * */ public class ticketnumberhandler3 extends ticketnumberhandler{ //保存单例实例对象 private static ticketnumberhandler3 instance; //私有化构造方法 private ticketnumberhandler3() {}; /** * 懒汉式,在第一次获取单例对象的时候初始化对象 * @return */ public static ticketnumberhandler3 getinsatance() { if(instance == null) { try { //这里为什么要让当前线程睡眠1毫秒呢? //因为在正常的业务逻辑中,单利模式的类不可能这么简单,所以实例化时间会多一些 //让当前线程睡眠1毫秒 thread.sleep(1); } catch (interruptedexception e) { // todo auto-generated catch block e.printstacktrace(); } instance = new ticketnumberhandler3(); } return instance; } }
代码与上述文章的一模一样,那么接下来就开始编写测试代码。
四,编写测试代码
package com.zcz.singleton; import java.util.arraylist; import java.util.hashset; import java.util.list; import java.util.objects; import java.util.set; import java.util.vector; public class buyticket { public static void main(string[] args) { // 用户人数 int usernumber = 10000; // 保存用户线程 set<thread> threadset = new hashset(); // 用于存放ticketnumberhandler实例对象 list<ticketnumberhandler> hanlderlist = new vector(); // 保存生成的票号 list<long> ticketnumberlist = new vector(); // 定义购票线程,一个线程模拟一个用户 for(int i=0;i<usernumber;i++) { thread t = new thread() { public void run() { ticketnumberhandler handler = ticketnumberhandler3.getinsatance(); hanlderlist.add(handler); long ticketnumber = handler.getticketnumber(); ticketnumberlist.add(ticketnumber); }; }; threadset.add(t); } system.out.println("当前购票人数:"+threadset.size()+" 人"); //记录购票开始时间 long begintime = system.currenttimemillis(); for(thread t : threadset) { //开始购票 t.start(); } //记录购票结束时间 long enttime; while(true) { //除去mian线程之外的所有线程结果后在记录结束时间 if(thread.activecount() == 1) { enttime = system.currenttimemillis(); break; } } //开始统计 system.out.println("票号生成类实例对象数目:"+new hashset(hanlderlist).size()); system.out.println("共出票:"+ticketnumberlist.size()+"张"); system.out.println("实际出票:"+new hashset(ticketnumberlist).size()+"张"); system.out.println("出票用时:"+(enttime - begintime)+" 毫秒"); } }
结合着代码中的注释,相信这部分测试代码理解起来并不难,首先初始化10000个线程,相当于10000个用户同时购票,然后启动这10000个线程开始购票,结束后做统计。
这里对代码中的hanlderlist和ticketnumberlist进行一下说明:
1,这连个list的作用是什么?这两个list是用来做统计的。
hanlderlist用来存放单例对象,然后在最后统计的部分会转换为set,去除重复的对象,剩余的对象数量就是真正的单例对象数量。如果真的是但是模式的话,在最后的统计打印的时候,票号生成类实例对象数目,应该是1。
ticketnumberlist是用来存放票号的,同样的在最后的统计部分也会转换为set去重,如果真的有存在重复的票号,那么打印信息中的实际出票数量应该小于共出票数量
2,这两个list为什么使用vector而不是arraylist,因为arraylist是线程不安全的,如果使用arraylist,在最后的统计中arraylist 会出现null,这样我们的数据就不准确了。
那么,开始测试。
五,第三中单例模式的测试结果
右键 -> run as -> java application。打印结果:
当前购票人数:10000 人 票号生成类实例对象数目:19 共出票:10000张 实际出票:9751张 出票用时:1130 毫秒
可以看到:
票号生成类实例对象数目:19
说明不只是有一个单例对象产生,原因在上述的文章中也做了解释说明。同时“共出票“实际出票数量”小于“共出票”属性,说明产生了票号相同的票。
ok,线程不安全的第三种单例示例结果之后,还有7中可用的线程安全的实现方式,我们就从1-8的顺序逐一检测,并通过执行时间来检测效率高低。
六,测试第一种单例模式:使用静态属性,并初始化单例
1,单例代码
package com.zcz.singleton; public class ticketnumberhandler1 extends ticketnumberhandler{ // 饿汉式,在类加载的时候初始化对象 private static ticketnumberhandler1 instance = new ticketnumberhandler1(); //私有化构造方法 private ticketnumberhandler1() {}; /** * 获取单例实例 * @return */ public static ticketnumberhandler1 getinstance() { return instance; } }
2,修改测试类中使用的单例
thread t = new thread() { public void run() { // ticketnumberhandler handler = ticketnumberhandler3.getinsatance(); ticketnumberhandler handler = ticketnumberhandler1.getinstance(); long ticketnumber = handler.getticketnumber(); ticketnumberlist.add(ticketnumber); }; };
3,测试结果
当前购票人数:10000 人 票号生成类实例对象数目:1 共出票:10000张 实际出票:10000张 出票用时:1093 毫秒
跟上一次的打印结果相比对,票号生成类实例对象数目确实只有一个了,这说明第一种单例模式,在多线程下是可以正确使用的。
而且,实际出票数量和共出票数量相同,也是没有出现重复的票号的。但是真的是这样的吗?我么把用户数量调整到20000人,多执行几次代码试试看,你会发现偶尔会出现下面的打印结果:
当前购票人数:20000 人 票号生成类实例对象数目:1 共出票:20000张 实际出票:19996张 出票用时:5291 毫秒
票号生成类的实例对象一直是1,这没问题,因为单例模式在多线程环境下正确执行了。
但是实际出票数量小于了共出票数量,这说明出现了重复的票号,为什么呢?因为我们票号的生成方法,不是线程安全的
public long getticketnumber() { return nextuniquenumber++; }
代码中的nextuniquenumber++是不具备原子性的,虽然看起来只有一行代码,但是实际上执行了三个步骤:读取nextuniquenumber的值,将nextuniquenumber的值加一,将结果赋值给nextuniquenumber。
所以出现重复票号的原因在于:在赋值没有结束前,有多个线程读取了值。
怎么优化呢?最简单的就是使用同步锁。在getticketnumber上添加关键字synchronized。
public synchronized long getticketnumber() { return nextuniquenumber++; }
还有另外一个方法,就是使用线程安全的atomiclong
package com.zcz.singleton; import java.util.concurrent.atomic.atomiclong; public class ticketnumberhandler { private atomiclong nextuniquenumber = new atomiclong(); //记录下一个唯一的号码 // private long nextuniquenumber = 1; /** * 返回生成的号码 * @return */ public synchronized long getticketnumber() { // return nextuniquenumber++; return nextuniquenumber.incrementandget(); } }
ok,解决了这里的问题之后,我们将用户人数,重新调整到10000人,运行10次,统计平均执行时间:1154.3毫秒
七,测试第二种单例模式:使用静态代码块
1,单例代码
package com.zcz.singleton; public class ticketnumberhandler2 extends ticketnumberhandler { // 饿汉式 private static ticketnumberhandler2 instance; //使用静态代码块,初始化对象 static { instance = new ticketnumberhandler2(); } //私有化构造方法 private ticketnumberhandler2() {}; /** * 获取单例实例 * @return */ public static ticketnumberhandler2 getinstance() { return instance; } }
2,修改测试代码
thread t = new thread() { public void run() { // ticketnumberhandler handler = ticketnumberhandler3.getinsatance(); // ticketnumberhandler handler = ticketnumberhandler1.getinstance(); ticketnumberhandler handler = ticketnumberhandler2.getinstance(); hanlderlist.add(handler); long ticketnumber = handler.getticketnumber(); ticketnumberlist.add(ticketnumber); }; };
3,测试结果
当前购票人数:10000 人 票号生成类实例对象数目:1 共出票:10000张 实际出票:10000张 出票用时:1234 毫秒
单例模式成功,出票数量正确,运行10次平均执行时间:1237.1毫秒
八,测试第四种单例模式:使用方法同步锁(synchronized)
1,单例代码
package com.zcz.singleton; public class ticketnumberhandler4 extends ticketnumberhandler { //保存单例实例对象 private static ticketnumberhandler4 instance; //私有化构造方法 private ticketnumberhandler4() {}; /** * 懒汉式,在第一次获取单例对象的时候初始化对象 * @return */ public synchronized static ticketnumberhandler4 getinsatance() { if(instance == null) { try { //这里为什么要让当前线程睡眠1毫秒呢? //因为在正常的业务逻辑中,单利模式的类不可能这么简单,所以实例化时间会多一些 //让当前线程睡眠1毫秒 thread.sleep(1); } catch (interruptedexception e) { // todo auto-generated catch block e.printstacktrace(); } instance = new ticketnumberhandler4(); } return instance; } }
2,修改测试代码
thread t = new thread() { public void run() { // ticketnumberhandler handler = ticketnumberhandler3.getinsatance(); // ticketnumberhandler handler = ticketnumberhandler1.getinstance(); // ticketnumberhandler handler = ticketnumberhandler2.getinstance(); ticketnumberhandler handler = ticketnumberhandler4.getinsatance(); hanlderlist.add(handler); long ticketnumber = handler.getticketnumber(); ticketnumberlist.add(ticketnumber); }; };
3,测试结果
当前购票人数:10000 人 票号生成类实例对象数目:1 共出票:10000张 实际出票:10000张 出票用时:1079 毫秒
单例模式成功,出票数量正确,运行10次平均执行时间:1091.86毫秒
九,测试第五种单例模式:使用同步代码块
1,单例代码
package com.zcz.singleton; public class ticketnumberhandler5 extends ticketnumberhandler { //保存单例实例对象 private static ticketnumberhandler5 instance; //私有化构造方法 private ticketnumberhandler5() {}; /** * 懒汉式,在第一次获取单例对象的时候初始化对象 * @return */ public static ticketnumberhandler5 getinsatance() { if(instance == null) { synchronized (ticketnumberhandler5.class) { try { //这里为什么要让当前线程睡眠1毫秒呢? //因为在正常的业务逻辑中,单利模式的类不可能这么简单,所以实例化时间会多一些 //让当前线程睡眠1毫秒 thread.sleep(1); } catch (interruptedexception e) { // todo auto-generated catch block e.printstacktrace(); } instance = new ticketnumberhandler5(); } } return instance; } }
2,修改测试代码
thread t = new thread() { public void run() { // ticketnumberhandler handler = ticketnumberhandler3.getinsatance(); // ticketnumberhandler handler = ticketnumberhandler1.getinstance(); // ticketnumberhandler handler = ticketnumberhandler2.getinstance(); // ticketnumberhandler handler = ticketnumberhandler4.getinsatance(); ticketnumberhandler handler = ticketnumberhandler5.getinsatance(); hanlderlist.add(handler); long ticketnumber = handler.getticketnumber(); ticketnumberlist.add(ticketnumber); }; };
3,测试结果
当前购票人数:10000 人 票号生成类实例对象数目:1 共出票:10000张 实际出票:10000张 出票用时:1117 毫秒
单例模式成功,出票数量正确,运行10次平均执行时间:1204.1毫秒
十,测试第六种单例模式:双重检查
1,单例代码
package com.zcz.singleton; public class ticketnumberhandler6 extends ticketnumberhandler { //保存单例实例对象 private static ticketnumberhandler6 instance; //私有化构造方法 private ticketnumberhandler6() {}; /** * 懒汉式,在第一次获取单例对象的时候初始化对象 * @return */ public static ticketnumberhandler6 getinsatance() { //双重检查 if(instance == null) { synchronized (ticketnumberhandler5.class) { try { //这里为什么要让当前线程睡眠1毫秒呢? //因为在正常的业务逻辑中,单利模式的类不可能这么简单,所以实例化时间会多一些 //让当前线程睡眠1毫秒 thread.sleep(1); } catch (interruptedexception e) { // todo auto-generated catch block e.printstacktrace(); } if(instance == null) { instance = new ticketnumberhandler6(); } } } return instance; } }
2,修改测试代码
thread t = new thread() { public void run() { // ticketnumberhandler handler = ticketnumberhandler3.getinsatance(); // ticketnumberhandler handler = ticketnumberhandler1.getinstance(); // ticketnumberhandler handler = ticketnumberhandler2.getinstance(); // ticketnumberhandler handler = ticketnumberhandler4.getinsatance(); // ticketnumberhandler handler = ticketnumberhandler5.getinsatance(); ticketnumberhandler handler = ticketnumberhandler6.getinsatance(); hanlderlist.add(handler); long ticketnumber = handler.getticketnumber(); ticketnumberlist.add(ticketnumber); }; };
3,测试结果
当前购票人数:10000 人 票号生成类实例对象数目:1 共出票:10000张 实际出票:10000张 出票用时:1041 毫秒
单例模式成功,出票数量正确,运行10次平均执行时间:1117.1毫秒
十一,测试第七种单例模式:使用静态内部类
1,单例代码
package com.zcz.singleton; public class ticketnumberhandler7 extends ticketnumberhandler { //私有化构造器 public ticketnumberhandler7() {}; //静态内部类 private static class ticketnumberhandler7instance{ private static final ticketnumberhandler7 instance = new ticketnumberhandler7(); } public static ticketnumberhandler7 getinstance() { return ticketnumberhandler7instance.instance; } }
2,修改测试代码
thread t = new thread() { public void run() { // ticketnumberhandler handler = ticketnumberhandler3.getinsatance(); // ticketnumberhandler handler = ticketnumberhandler1.getinstance(); // ticketnumberhandler handler = ticketnumberhandler2.getinstance(); // ticketnumberhandler handler = ticketnumberhandler4.getinsatance(); // ticketnumberhandler handler = ticketnumberhandler5.getinsatance(); // ticketnumberhandler handler = ticketnumberhandler6.getinsatance(); ticketnumberhandler handler = ticketnumberhandler7.getinstance(); hanlderlist.add(handler); long ticketnumber = handler.getticketnumber(); ticketnumberlist.add(ticketnumber); }; };
3,测试结果
当前购票人数:10000 人 票号生成类实例对象数目:1 共出票:10000张 实际出票:10000张 出票用时:1250 毫秒
单例模式成功,出票数量正确,运行10次平均执行时间:1184.4毫秒
十二,测试第八种单例模式:使用枚举
1,单例代码
package com.zcz.singleton; import java.util.concurrent.atomic.atomiclong; public enum ticketnumberhandler8 { instance; private atomiclong nextuniquenumber = new atomiclong(); //记录下一个唯一的号码 // private long nextuniquenumber = 1; /** * 返回生成的号码 * @return */ public synchronized long getticketnumber() { // return nextuniquenumber++; return nextuniquenumber.incrementandget(); } }
2,修改测试代码
public static void main(string[] args) { // 用户人数 int usernumber = 10000; // 保存用户线程 set<thread> threadset = new hashset(); // 用于存放ticketnumberhandler实例对象 list<ticketnumberhandler8> hanlderlist = new vector(); // 保存生成的票号 list<long> ticketnumberlist = new vector(); // 定义购票线程,一个线程模拟一个用户 for(int i=0;i<usernumber;i++) { thread t = new thread() { public void run() { // ticketnumberhandler handler = ticketnumberhandler3.getinsatance(); // ticketnumberhandler handler = ticketnumberhandler1.getinstance(); // ticketnumberhandler handler = ticketnumberhandler2.getinstance(); // ticketnumberhandler handler = ticketnumberhandler4.getinsatance(); // ticketnumberhandler handler = ticketnumberhandler5.getinsatance(); // ticketnumberhandler handler = ticketnumberhandler6.getinsatance(); // ticketnumberhandler handler = ticketnumberhandler7.getinstance(); ticketnumberhandler8 handler = ticketnumberhandler8.instance; hanlderlist.add(handler); long ticketnumber = handler.getticketnumber(); ticketnumberlist.add(ticketnumber); }; }; threadset.add(t); } system.out.println("当前购票人数:"+threadset.size()+" 人"); //记录购票开始时间 long begintime = system.currenttimemillis(); for(thread t : threadset) { //开始购票 t.start(); } //记录购票结束时间 long enttime; while(true) { //除去mian线程之外的所有线程结果后再记录时间 if(thread.activecount() == 1) { enttime = system.currenttimemillis(); break; } } //开始统计 system.out.println("票号生成类实例对象数目:"+new hashset(hanlderlist).size()); system.out.println("共出票:"+ticketnumberlist.size()+"张"); system.out.println("实际出票:"+new hashset(ticketnumberlist).size()+"张"); system.out.println("出票用时:"+(enttime - begintime)+" 毫秒"); }
3,测试结果
当前购票人数:10000 人 票号生成类实例对象数目:1 共出票:10000张 实际出票:10000张 出票用时:1031 毫秒
单例模式成功,出票数量正确,运行10次平均执行时间:1108毫秒
十三,总结
线程安全就不再多说,除去第三种方式。其他的都可以。
效率总结表:
单例模式名称 | 平均十次执行时间(毫秒) |
第一种(使用静态属性,并初始化单例) | 1154.3 |
第二种(使用静态代码块) | 1237.1 |
第四种(使用方法同步锁) | 1091.86 |
第五种(使用同步代码块) | 1204.1 |
第六种(双重检查) | 1117.1 |
第七种(使用静态内部类) | 1184.4 |
第八种(使用枚举) | 1108 |
跟我预想的不同,没有想到的是,竟然是第四种方法的效率最高,很可能跟我测试数据的数量有关系(10000个用户)。效率的话就不多做评论了,大家有兴趣的话可以自己亲自试一下。别忘记告诉我测试的结果哦。
从代码行数来看,使用枚举是最代码最少的方法了。
ok,这篇文章到这里就结束了,虽然在效率上没有结论,但是,在线程安全方面是明确了的。
原创不易,转载请注明出处:https://www.cnblogs.com/zhangchengzi/p/9718507.html