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 

  • 相关阅读:
    Codeforces Round #649 (Div. 2) D. Ehab's Last Corollary
    Educational Codeforces Round 89 (Rated for Div. 2) E. Two Arrays
    Educational Codeforces Round 89 (Rated for Div. 2) D. Two Divisors
    Codeforces Round #647 (Div. 2) E. Johnny and Grandmaster
    Codeforces Round #647 (Div. 2) F. Johnny and Megan's Necklace
    Codeforces Round #648 (Div. 2) G. Secure Password
    Codeforces Round #646 (Div. 2) F. Rotating Substrings
    C++STL常见用法
    各类学习慕课(不定期更新
    高阶等差数列
  • 原文地址:https://www.cnblogs.com/zhangchengzi/p/9718507.html
Copyright © 2011-2022 走看看