咳咳,想系统的整理一下知识想了好久了,毕竟工作了快半年了,业务代码感觉已经写得差不多了,明显感觉到又到了再夯实一遍基础的时候了,毕竟基础打得好后面才能得心应手,事半功倍。所以就从设计模式这里开始看吧。
设计模式感觉在写代码的时候也是挺重要的,确实有些时候就是不知道该如何设计自己的代码,所以这次就从这里入手啦。话虽如此我也不打算全写,就挑着常用的来写吧,感觉全都写了还是有点多的。。。
啰嗦了一大片,现在就从最常用的单例模式开始写吧。
单例模式是什么
简单来说单例模式就是一种类的写法,它保证了你所写的类最多只会被实例化一个对象。
使用场景
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
1.需要频繁实例化然后销毁的对象。
2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
3.有状态的工具类对象。
4.频繁访问数据库或文件的对象。
以下都是单例模式的经典使用场景:
1.资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
2.控制资源的情况下,方便资源之间的互相通信。如线程池等。
实现方法
首先说一下所有写法的共通之处,也是单例模式要想保证唯一对象并正常使用的必要写法:
1.构造器私有
2.自己创建自己的实例化对象
3.提供给外界获得此对象的方法
通过构造器私有,我们的类将不能够被外界实例化,同时外界只能通过本类提供的方法来获取由本类已经创建好的对象。
首先是最常见的两种方法,懒汉式和饿汉式:
首先是饿汉式:
/** 饿汉式*/
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
public void sayHello(){
System.out.println("hello world !");
}
}
接下来是懒汉式:
/** 懒汉式*/
public class Singleton{
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getInstance() {
if (singleton==null){
singleton=new Singleton();
}
return singleton;
}
}
这两个我们对比来看,主要区别就在于new Singleton()这一步执行的时机,饿汉式在初始化系统变量的时候就进行了new这一步动作,而当需要的时候直接返回已有的对象;反过来看懒汉式则并没有在初始化系统变量的时候创建对象,而是在调用的时候进行判断,如果还没有对象,就new一个出来,如果有就直接返回去,因此懒汉式总是在第一次调用的时候创建对象,以后一直返回这个对象。
这两种方法各有各的侧重点,饿汉式由于是在加载类的时候进行的对象创建,但是该对象可能在接下来的一段时间内并没有使用,所以会造成资源的浪费。而懒汉式在使用的时候进行创建,也就保证了不会白白浪费掉,但是创建对象毕竟会花费时间,所以又回到了计算机的一个永恒不变的话题,是时间换空间,还是空间换时间?
除了对于空间和时间的倾向性不同,还有一个问题需要我们考虑,那就是线程安全的问题。
对于饿汉式来说,由于虚拟机在加载类的时候会自动保证只有一个线程来处理,所以饿汉式是线程安全的;但是对于懒汉式来说,有可能出现在调用时,当第一个线程判断为空,进入了创建对象的步骤,这时候线程切换,第二个线程也运行到这里,由于上一个线程还没能创建出来对象,所以第二个线程也判断为空,进入了创建线程的操作,这样便会导致我们的单例不再”单例“。
既然知道了症结在哪里,那我们就对症下药就好了,我们知道通过synchronized关键字可以进行线程的同步,所以我们在可能产生问题的部分,也就是为空判断上加上线程同步:
/**
* 懒汉式(加线程同步)
*/
public class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
}
从而保证了懒汉式的单一实例。
但是问题又来了,众所周知的线程同步会导致效率低下,那么有没有办法提高一下效率呢?通过研究之前的代码我们发现,所有想要使用该类的对象的地方都要通过getInstance()方法,而每次执行该方法都会直接进入synchronized片段,所以我们可以在该片段之外再加上一层判断,判断该类的实例是否已经初始化成功
/**
* 懒汉式(双重加锁线程同步)
*/
public class Singleton {
private static Singleton singleton = null;
private Singleton() {}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
return singleton;
}
}
这样在懒加载的时候我们终于能够保证线程同步了,真的不容易。。。。
好了,上面介绍完麻烦的方法,我们来介绍一个非常巧妙的方法
/** 内部类方式*/
public class Singleton {
private Singleton(){}
private static class SingletonHolder {
private static final Singleton instance=new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
public void sayHello(){
System.out.println("hello world !");
}
}
我们可以看到在我们要实现单例的类内部实现了一个内部类,然后通过内部类来持有我们的实例对象。这种方法的巧妙之处就在于将饿汉式和懒汉式的优点都结合起来了,由于我们的内部类是在调用的时候才进行加载的,所以拥有懒汉式节省资源的好处,同时由于我们的实例对象是在加载内部类的过程中实例化的,又会有虚拟机级别的线程安全保证,可以说是一举两得了。
除了这种方法以外,由于Java在jdk1.5之后添加了枚举类型,可以说枚举类型的特性和单例模式有异曲同工之妙了,我们可以通过构建一个只有一个实例的枚举类型来实现单例模式:
/** 内部类方式*/
public enum Singleton {
INSTANCE;
public void sayHello(){
System.out.println("hello world !");
}
}
只需要在枚举的时候只枚举一个实例,单例模式就自动完成了!简单到爆炸QWQ