当前位置: 首页 > 后端技术 > Java

单例模式,关键字级详解

时间:2023-04-02 00:43:35 Java

大家好,我是课代表。关注我的公众号:Java课代表,获取更多java实用干货。0.前言如果问一个写了几年代码的程序员用过哪些设计模式,我敢打赌90%以上的答案都会包含【单例模式】。有的面试官甚至会直接问:说说你用过哪些设计模式,单例就更不用说了。你看连面试官都听腻了,受欢迎程度可见一斑。然而,看似简单的单例模式却蕴含着大量的Java基础。在日常的开发过程中,类代表看到了很多不规范甚至有问题的单例实现。所以整理这篇文章,总结一下单例模式的最佳实践。1、懒加载(lazyman)所谓懒加载就是直到第一次被调用才加载。它的实现需要考虑并发问题和指令重排。代码如下:publicclassSingleton{privatevolatilestaticSingletoninstance;//①privateSingleton(){//②}publicstaticSingletongetInstance(){if(instance==null){//③synchronized(Singleton.class){if(instance==null){//④instance=newSingleton();//⑤}}}返回实例;}}这段代码极其简洁,没有一个字符是多余的,我们逐行阅读:首先注意①处的volatile关键字,它有两个特点:一是保证这个变量对所有线程可见。也就是说,当一个线程修改这个变量的值时,新的值可以立即被其他线程知道。二是禁止指令重排序优化。这里解释一下指令重排序优化:代码⑤处的instance=newSingleton()不是原子的,大致分为以下三步:分配内存,调用构造函数,初始化为实例,让实例指向到分配的内存空间。在保证结果正确的前提下进行指令重排序优化。即上述3个步骤可能的顺序是1->2->3或者1->3->2。如果顺序为1->3->2,当3执行完毕,2还未执行时,另一个线程执行到代码③,发现实例不为null,直接返回未初始化的实例并使用,它会出错。所以使用volatile是为了保证线程间的可见性,防止指令重排。其次,在代码②处将构造函数声明为private的目的是为了防止使用newSingleton()等代码生成新的实例。最后,客户端调用Singleton.getInstance()时,首先检查是否已经实例化(代码③),如果没有实例化则同步代码块,然后再次检查是否已经实例化(代码④),然后执行代码⑤。这两个检查的意义在于防止在synchronized同步过程中实例化其他线程。这就是著名的双重检查锁(Doublechecklock)实现单例,也就是懒加载。TIPS:网上还有一个版本直接锁定了getInstance()方法。如此大规模的方法级锁会导致并发性降低。实际上,在第一次调用生成实例后,后续获取实例根本不需要并发控制。这个例子的双重检查锁定版本避免了这个并发问题。2.预加载(饿汉)对应的是懒加载。预加载是在类加载的时候初始化的,自然是线程安全的。代码如下:publicclassSingleton{privatestaticfinalSingletoninstance=newSingleton();//①privateSingleton(){}publicstaticSingletongetInstance(){returninstance;}}请注意,①处的类变量是最终的。这里使用final更大的意义在于提供语法约束。毕竟你是单例,只有这一个实例,不可能指向另一个。该实例有一个final约束,如果后面有人不小心写修改了它指向的代码,就会报语法错误。这就像@Override注解。你可以保证方法名和参数写对了,所以不写注解也没关系,但是有了注解的约束,编译器会帮你检查一下,防止别人乱改。3.静态内部类这种方法和预加载的原理是一样的。它利用JVM类加载的特性来实现天然的线程安全。不同的是静态内部类实现了懒加载。publicclassSingleton{privatestaticclassSingletonHolder{privatestaticSingletoninstance=newSingleton();}privateSingleton(){}publicstaticSingletongetInstance(){returnSingletonHolder.instance;}}SingletonHolder是一个静态内部类,当外部类Singleton被加载时,不会创建任何实例。只有在调用Singleton.getInstance()时,才会创建Singleton实例。这一切都是JVM自然而然地完成的,所以它既保证了线程安全,又实现了懒加载。4.枚举没错,枚举可以实现单例,这种方式是《Effective Java中文版》第二版中推荐的实现方式。代码极其简单:publicenumSingleton{/***singletoninstance*/INSTANCE;publicvoiddoSomeThing(){System.out.println("完成");}}使用时直接Singleton.INSTANCE.doSomeThing();即可以。这里主要使用了枚举的以下两个特性:枚举的构造函数始终是私有的,因此不需要像前面的方法那样显式定义私有构造函数。枚举类中的每一个值都是一个实例(只有INSTANCE这个实例)另外,枚举还有一些额外的好处:它免费提供了序列化机制,也可以防止通过多次反序列化产生多个实例。鉴于此,单例的最佳实践是用枚举来实现。5.总结其实单例的写法并不仅限于本文提到的四种。您可能还会看到许多其他变体,它们或多或少有缺陷。比如懒加载方式应用synchronizedto整个方法也可以实现,但是频繁的加锁和释放锁会造成性能瓶颈,完全去掉锁会造成并发问题。所以,只要对文中列举的四种单例方法理解透彻,举一反三,看到别人写的单例,一眼就能辨别是非。本文列举的4种单例模式,除了枚举外,都使用了static关键字。《Java 虚拟机规范》规定了类必须立即“初始化”的几种情况,其中涉及static的场景如下:ReadOr设置一个类型的静态字段时(除final修饰的static字段,其结果为在编译时被放入常量池)。当调用一个类型的静态方法时。延迟加载、预加载和静态内部类利用了这两个特性。忘记了static关键字的同学可以参考我的另一篇文章:《一题搞定static关键字》最后再次强调,如果开发中需要写单例,建议按照JoshuaBloch在第二版中的建议《一题搞定static关键字》:singleton元素的枚举类型已经成为Singleton最好的实现方式参考资料:《Effective Java中文版》JoshuaBloch2ndeditionP152,《深入理解 Java 虚拟机》ZhouZhiming3rdedition,P444-P448,P2643,单实例SINGLETON的简单解释设计模式【给Periodic原创推荐】解决static关键字,使用SpringValidation优雅校验参数。下载的附件名称总是乱码?是时候阅读RFC文档了!码字不易,欢迎点赞关注分享。