写点什么

深入灵魂的考验,每行注释都是灵魂的单例模式,源码 + 实例降临

用户头像
小Q
关注
发布于: 2020 年 12 月 08 日

不管是设计模式也好,别的模式也要,他都是为了解决问题而发明的有效的方法。除了我们已经熟悉的23种设计模式以外,还有MVVMCombinator等其它的东西,都已经是前辈们经过多年的摸爬滚打总结出来的,其有效性不容置疑。我这篇文章也不会用来证明设计模式是有用的,因为在我看来,这就跟1+1=2一样明显(在黑板上写下1+1=2)



而这,在现在这个追求高质量代码的时代,虽然显得有一些复杂,但是我个人还是“推崇”(看好了,我有引号的)这个东西,毕竟面试必问系列,你咋整



来看今天的内容吧,有代码,有实例,并且有一些内容我直接放在代码中通过注释进行讲解,会更好理解

文章首发公众号:Java架构师联盟

一、设计部分:单例的实现思想、代码及注意问题

package com.test.hibernate;

/*生成一个懒汉式单例的基础理解:
1.Singleton顾名思义就是只能创建一个实例对象。。所以不能拥有public的构造方法
2.既然构造方法是私有的,那么从外面不可能创建Singleton实例了。。只能从内部创建。。所以需要一个方法来创建此实例,也因此只能通过类名来创建对象。。此方法肯定必须是static的
3.静态的getInstance方法要返回一个Singleton实例。。就要一个Singleton类型的变量来存储。。声明一个Singleton类型的属性。。同样需要是static 的。。静态方法只能访问静态属性。。。
!!!前3步是单例的共性!后三步是懒汉式需要考虑的地方!
4.为了保证只生成一个实例,需要做判断是否为null
5.此时考虑线程问题,假设有两个线程。。thread1,thread2。。thread1运行到判断那个Singleton类型的变量是否为null,然后跳到了thread2。。也运行到判断之后。。。此时两线程都得到single为空。。。那么就会有两个实例了。。。解决办法。。同步
6.同步又要考虑效率,不能有太多的没用同步
* */


//思考总结:其实饿汉式没什么问题,问题就是出现在懒汉式上,一般就是牵扯到执行效率和线程安全2个角度上来思考
//个人感觉 如果是饿汉式就用天然的没毛病,如果想用懒汉式就用静态内部类方式吧
//存在问题:如何在2个jvm上保证单例还未解决:这个就牵扯到分布式锁,可以用zookeeper来实现
//还缺少一种懒汉式的枚举方式实现有待研究,听说这个方法也不错。

public class danli { //模拟一下静态代码块的使用方式,静态代码块在类加载的运行,先静态代码块》再构造代码块》再构造函数 ,只研究单例可以忽略

public static final String STR1;
static {
STR1 = new String("zzh");
}

}
//下面正式演示各种单例的实现:

class danli2{//单例饿汉式(非延时加载),提前加载,有利于速度和反应时间,天然的线程安全的。没毛病
private danli2(){};
private static final danli2 two = new danli2();//final可加可不加,final的目的就是最终的,只允许一次赋值,但不加是因为没法在本类外给他赋值了,因为构造方法是私有的没法创建这个类的对象了,而且这个成员变量也是私有的所以不能在外面调用到,但是可以在本类中的其他方法调用到,所以其实还是可以修改的,所以还是加上final吧
public static danli2 getSingleInstance(){
return two;
}
}



class danli3{ //单例懒汉式(延时加载),用的时候再去加载,有利于资源充分利用
private danli3(){};
private static danli3 three = null;
public static synchronized danli3 getSingleInstance(){//加上synchronized变得线程安全了,但是效率下降了,每次还需要检查同步等等
if(three == null){//保证只生成一个实例
three = new danli3();
}
return three;
}
}
/* 该类跟上面那个是一样的,上面是synchronized方法,下面这个是代码块。
class Singleton {

private Singleton() {}

private volatile static Singleton instance = null;

public static Singleton getInstance() {

synchronized (Singleton.class) {//利用synchronized代码块,每次需要先检查有没有同步锁,效率较低,为了解决这个问题又提出了加入双层检查,也就是在这个同步代码块的外面再加一层为null判断,来减少除第一次以外的同步检查,提高了效率
if (instance == null) {
instance = new Singleton();
}
}

return instance;
}
}
*/
双重检查加锁就是在同步代码块的外面一层再来一个== null的判断,解决除第一次以外所有的同步判断导致的效率下降问题
//但是这个双重检查加锁在多线程环境下存在系统崩溃的可能(一个线程初始化一半的对象,被第二个线程直接拿去用了,所以系统崩溃了)
/*原因如下
1、线程 1 进入 getInSingleton() 方法。
2、由于 uniqueInstance 为 null,线程 1 在 //1 处进入 synchronized 块。
3、线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非 null。
4、线程 1 被线程 2 预占。
5、线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将 uniqueInstance 引用返回给一个构造完整但部分初始化了的 Singleton 对象。
6、线程 2 被线程 1 预占。
7、线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。

*/
class Singleton {//双重检查加锁,线程相对安全了,避开了过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同获取锁了),效率比上面那个能提高一些
// volatile关键字确保当uniqueInstance变量被初始化成Singleton实例时,多个线程正确地处理uniqueInstance变量,这个关键字其实也解决了上面说的系统可能崩溃的问题,因为使用这个变量也需要一个线程一个线程的来使用了
private volatile static Singleton uniqueInstance;
private Singleton() {
}

public static Singleton getInSingleton() {
if (uniqueInstance == null) {// 检查实例,如是不存在就进行同步代码区
synchronized (Singleton.class) {//1 // 对其进行锁,防止两个线程同时进入同步代码区
if (uniqueInstance == null) {//2 // 双重检查,非常重要,如果两个同时访问的线程,当第一线程访问完同步代码区后,生成一个实例;当第二个已进入getInstance方法等待的线程进入同步代码区时,也会产生一个新的实例
uniqueInstance = new Singleton();//3
}
}
}
return uniqueInstance;
}
// ...Remainder omitted
}

//使用静态内部类是没问题的,而且效率也不会降低,而且还是懒加载
class Singleton2 {//jvm加载SingletonHolder的时候会初始化INSTANCE,所以既是lazy的又保证是单例的
private static class SingletonHolder {//静态内部类,只会被加载一次(在加载外部类的时候),所以线程安全,注意静态只能使用静态
static final Singleton2 INSTANCE = new Singleton2();
}

private Singleton2 (){}//静态构造方法

public static Singleton2 getInstance() {//对外提供单例的接口
return SingletonHolder.INSTANCE;
}
}


class ceshi{//只是简单测试了一下单例,都为true,可以忽略
public static void main(String[] args) {
System.out.println(danli.STR1 == danli.STR1);//true
System.out.println(danli2.getSingleInstance() == danli2.getSingleInstance());
System.out.println(danli3.getSingleInstance() == danli3.getSingleInstance());
System.out.println(Singleton.getInSingleton() == Singleton.getInSingleton());
System.out.println(Singleton2.getInstance() == Singleton2.getInstance());
}
}



二、应用部分:单例的适用场景

优点:

第一、能减少资源的使用,但有时需要通过线程同步来控制资源的并发访问;也避免对共享资源的多重占用



第二、控制实例产生的数量(允许可变数目的实例),由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。



第三、作为通信媒介使用,也就是数据共享,共享这一个对象一个实例(如线程池),它可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程之间实现通信,但注意多线程同步问题。



缺点:

1.不太适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态,所以就算保存了,需要加入同步机制来避免错误。2.由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。3.单例类的职责过重,在一定程度上违背了“单一职责原则”。4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。



使用注意事项:

1.使用时不能用反射模式创建单例,否则会实例化一个新的对象2.使用懒单例模式时注意线程安全问题3.饿单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式)



适合场景:

1、有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式;



2、创建对象时耗时过多或者耗资源过多,但又经常用到的对象;



3、频繁访问 IO 资源的对象,例如数据库连接池或访问本地文件;



4、单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。



具体应用场景举例:

外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。

内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件,在我们日常使用的在Windows中也有不少单例模式设计的组件,象常用的文件管理器。由于Windows操作系统是一个典型的多进程多线程系统,那么在创建或者删除某个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象。采用单例模式设计的文件管理器就可以完美的解决这个问题,所有的文件操作都必须通过唯一的实例进行,这样就不会产生混乱的现象。

Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~

windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

网站的计数器,一般也是采用单例模式实现,否则难以同步。

应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。

Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。

数据库连接池的设计一般采用单例模式,数据库连接是一种数据库资源。软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的。当然,使用数据库连接池还有很多其它的好处,可以屏蔽不同数据数据库之间的差异,实现系统对数据库的低度耦合,也可以被多个系统同时使用,具有高可复用性,还能方便对数据库连接的管理等等。数据库连接池属于重量级资源,一个应用中只需要保留一份即可,既节省了资源又方便管理。所以数据库连接池采用单例模式进行设计会是一个非常好的选择。

多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。

spring的bean(scope)默认是single,当然也可以当然也可以设置为prototype,比如struts2的action就必须是prototype,因为请求不同,一个请求对应一个action对象。

我们知道单例会发生线程安全问题,那么spring是怎么来解决的呢?

问题:当Bean对象对应的类存在可变的成员变量并且其中存在改变这个变量的线程时,多线程操作该Bean对象时会出现线程安全。原因:当多线程中存在线程改变了bean对象的可变成员变量时,其他线程无法访问该bean对象的初始状态,从而造成数据错乱解决方式:1.在Bean对象中尽量避免定义可变的成员变量;2.在bean对象中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal中



2个具体场景案例

1、网站在线人数统计;



其实就是全局计数器,也就是说所有用户在相同的时刻获取到的在线人数数量都是一致的。要实现这个需求,计数器就要全局唯一,也就正好可以用单例模式来实现。当然这里不包括分布式场景,因为计数是存在内存中的,并且还要保证线程安全。下面代码是一个简单的计数器实现。



public class Counter {
private static class CounterHolder{
private static final Counter counter = new Counter();
}
private Counter(){
System.out.println("init...");
}
public static final Counter getInstance(){
return CounterHolder.counter;
}
private AtomicLong online = new AtomicLong();
public long getOnline(){
return online.get();
}
public long add(){
return online.incrementAndGet();
}
.......
}



1、配置文件访问类;



项目中经常需要一些环境相关的配置文件,比如短信通知相关的、邮件相关的。比如 properties 文件,这里就以读取一个properties 文件配置为例,如果你使用的 Spring ,可以用 @PropertySource 注解实现,默认就是单例模式。如果不用单例的话,每次都要 new 对象,每次都要重新读一遍配置文件,很影响性能,如果用单例模式,则只需要读取一遍就好了。以下是文件访问单例类简单实现:



public class SingleProperty {
private static Properties prop;
private static class SinglePropertyHolder{
private static final SingleProperty singleProperty = new SingleProperty();
}
/**
* config.properties 内容是 test.name=kite
*/
private SingleProperty(){
System.out.println("构造函数执行");
prop = new Properties();
InputStream stream = SingleProperty.class.getClassLoader()
.getResourceAsStream("config.properties");
try {
prop.load(new InputStreamReader(stream, "utf-8"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static SingleProperty getInstance(){
return SinglePropertyHolder.singleProperty;
}
public String getName(){
return prop.get("test.name").toString();
}
public static void main(String[] args){
SingleProperty singleProperty = SingleProperty.getInstance();
System.out.println(singleProperty.getName());
}
}





发布于: 2020 年 12 月 08 日阅读数: 54
用户头像

小Q

关注

还未添加个人签名 2020.06.30 加入

小Q 公众号:Java架构师联盟 作者多年从事一线互联网Java开发的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容。如果能为您提供帮助,请给予支持(关注、点赞、分享)!

评论

发布
暂无评论
深入灵魂的考验,每行注释都是灵魂的单例模式,源码+实例降临