zoukankan      html  css  js  c++  java
  • JAVA设计模式-单例模式(Singleton)线程安全与效率

    一,前言

      单例模式详细大家都已经非常熟悉了,在文章单例模式的八种写法比较中,对单例模式的概念以及使用场景都做了很不错的说明。请在阅读本文之前,阅读一下这篇文章,因为本文就是按照这篇文章中的八种单例模式进行探索的。

      本文的目的是:结合文章中的八种单例模式的写法,使用实际的示例,来演示线程安全和效率

      既然是实际的示例,那么就首先定义一个业务场景:购票。大家都知道在春运的时候,抢票是非常激烈的。有可能同一张票就同时又成百上千的人同时在抢。这就对代码逻辑的要求很高了,即不能把同一张票多次出售,也不能出现票号相同的票。

      那么,接下来我们就使用单例模式,实现票号的生成。同时呢在这个过程中利用上述文章中的八种单例模式的写法,来实践这八种单例模式的线程安全性和比较八种单例模式的效率。

      既然文章中第三种单例模式(懒汉式)是线程不安全的,那么我就从这个单例模式的实现开始探索一下线程安全。

      因为不管是八种单例模式的实现方式的哪一种,票号的生成逻辑都是一样的,所以,在此正式开始之前,为了更方便的编写示例代码,先做一些准备工作:封装票号生成父类代码。

    二,封装票号生成父类代码

    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,这篇文章到这里就结束了,虽然在效率上没有结论,但是,在线程安全方面是明确了的。

    相关java设计模式的文章:

      JAVA设计模式-动态代理(Proxy)示例及说明

      JAVA设计模式-动态代理(Proxy)源码分析

      JAVA设计模式-单例模式(Singleton)线程安全与效率


     原创不易,转载请注明出处:https://www.cnblogs.com/zhangchengzi/p/9718507.html 

  • 相关阅读:
    INode满的处理方法
    分布式转码集群思路
    FreeBSD Set a Default Route / Gateway
    ssh遇到port 22:No route to host问题的解决方法
    debian 开启SSH
    virsh 查看信息
    virsh console配置
    virsh console hangs at the escape character “^]”
    virt-install命令---详解
    kmv 学习笔记 工具
  • 原文地址:https://www.cnblogs.com/zhangchengzi/p/9718507.html
Copyright © 2011-2022 走看看