前言
设计模式一直都是我们程序语言中较为重要的代码优化套路
为了减少一些冗余代码,提高代码可复用性、可维护性、可读性、稳健性以及安全性。
设计是一种语法规范,是一种为了解决某种特点场景下的某个问题,而提出来的一系列方案。
设计模式一共分为23种,其中分为三种大类型;而我们今天所有讲的是创造型中的单例模式
什么是单例模式呢?
单例模式就是一个类中只存在一个实例对象,java中存在了多个单例的案例
比如:
一:ervlet就是一个单例模式,我们每次访问的时候都是同一个servlet,只会在第一次访问的时候才会生成
二: application也是一个单例文件,整个web中只存在这一个对象可以访问web.xml内容
三:我们使用的连接池也是一个单例模式
四:以及框架中的bean也是一个单例模式
我们不难发现在java中多处使用到了单例模式,那它到底是有什么优点,才会让被多次使用到
单例模式的优点
一:整个类只存在一个对象,那么就代表单例模式减少了系统的消耗资源
二:由于只存在一个对象,所以它可以进行资源共享(比如application对象)
除了java中多处使用到单例模式之外,其实在我们的面试中呢,也会经常考到,基本都是要求手写一个单例模式出来,我相信大部分朋友都会写饿汉式或者懒汉式,但是在这两个模式现在已经烂大街的情况下,我们怎么样才能吸取面试官的眼光呢?下面我就来详细介绍一下单例模式的五种书写方法
单例模式注意点
一:整个类中只能创建一个对象,不能有其他因素去创建对象 ,所以构造方法需要私有化
二:当单例类中已经存在一个对象的时候,我们就不需要去创建对象,否则我们需要创建一个对象
单例模式的写法
一:饿汉式
//直接创建对象
private static SingletonType1 instance = new SingletonType1();
//私有化构造器
private SingletonType1(){
}
//提供公共方法访问
public static SingletonType1 getInstance(){
return instance;
}
简要来说饿汉式就是一个饿汉,看到食物一上来就吃,但是在好久没吃东西的情况下,能直接狼吞虎咽吗?这肯定是由一点缺陷的
我们可以看到饿汉式一上来就创建了一个对象,那么什么时候会被创建出对象呢?
这就是我们类加载的多种场景了,比如,当调用类中方法的时候,反射,序列化等等情况都会引发类加载
那么我们在开发就可能无意识的导致了单例类被加载,这就会在无意识中消耗了系统资源,虽然一个对象对系统资源不会占用很多内存,但是这也是不可取的,我们希望的·是当我们需要使用的时候才让他创建对象,
这样会不会比较好呢?
饿汉式的缺点:
一:在无意识触发类加载的时候,创建了对象。且消耗了系统资源
二:无法做到延时加载
二:懒汉式
//1.定义接受静态常量
private static SingletonType2 instanse = null;
// 2.私有化构造器
private SingletonType2(){}
// 3.定义公共访问方法并在方法内部判断对象是否为null
public static SingletonType2 getInstance() {
if(instanse == null){
instanse = new SingletonType2();
return instanse ;
}
return instanse;
}
懒汉式和饿汉式的区别在于懒汉式需要在调用getInstance方法的时候才会创建对象
优点:懒汉式做到的延时加载,且不会去无故浪费系统资源
缺点:我们可以看到instanse 是一个共享变量,所以在单线程的情况下这样写法是没有一点问题的;
但是在多线程的情况下毫无疑问会产生线程安全的问题
这是线程类中的run方法
class TestClass extends Thread{
@Override
public void run() {
SingletonType2 instance = SingletonType2.getInstance();
System.out.println(instance.hashCode());
}
}
我创建了200个线程对象 并发访问
for (int i = 0; i < 200; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
TestClass testClass = new TestClass();
testClass.start();
}
//在单例类中稍作了改变
if(instanse == null){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
instanse = new SingletonType2();
return instanse ;
}
得出的结果是
这样的话无疑就破坏了我们单例模式的初衷
三:懒汉式同步
代码不变,在单例类中加了同步方法之后发现确实防止了单例,但是加同步之后却降低了效率,当每一个线程访问的时候,都需要等待,那么这样显然是不可行的;我们需要的是当实例对象生成之后,线程就不需要再进行同步了
缺点:虽然解决了线程并发所带来的的安全问题,但是大大降低了效率
public static synchronized SingletonType2 getInstance() {
if(instanse == null){
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
instanse = new SingletonType2();
return instanse ;
}
return instanse;
}
四:双重锁检式
双重锁优化了第三种单例方式,只有在第一次并发访问的时候需要进行同步,之后都不需要在进行同步
lass SingletonType3 {
//1.定义接受静态常量
private static volatile SingletonType3 instanse = null;
// 2.私有化构造器
private SingletonType3() {
}
// 3.定义公共访问方法并在方法内部判断对象是否为null
public static SingletonType3 getInstance() {
//多线程情况下先进行第一步判断
//第二次访问的时候不需要进行同步
if (instanse == null) {
//在第一次访问时可能多个线程执行到此行,这时候需要进行同步等待
synchronized (SingletonType3.class) {
//只有第一个线程可以执行实例,其他线程都无法执行实例化
if (instanse == null) {
instanse = new SingletonType3();
return instanse;
}
}
}
return instanse;
}
}
优点:大大提高了效率
缺点:代码太过繁琐
五:静态内部类单例模式
class SingletonType4 {
static class SingleClass{
private static final SingletonType4 instance=new SingletonType4();
}
private SingletonType4(){}
public static SingletonType4 getInstance(){
return SingleClass.instance;
}
}
可以看到在外部类中存在了一个静态内部类,但是在加载外部类的时候内部类并不会被加载,除非在调用内部类中的静态方法
优点一:实现了延迟加载
优点二:不用担心线程安全问题,因为在类加载的时候是线程安全的
优点三:简化了代码的书写
注意: 虽然以上五种方式都可以实现单例模式,但是他们都有一个缺点
在特殊的场景中他们的单例都会被破坏掉
1.反射场景
//测试通过反射的对象
Class<?> aClass = Class.forName("cn.itcast.singleton.SingletonType1");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
Object o1 = declaredConstructor.newInstance(null);
System.out.println("反射对象"+o1);
//直接创建对象
private static SingletonType1 instance = new SingletonType1();
//私有化构造器
private SingletonType1(){
}
//提供公共方法访问
public static SingletonType1 getInstance(){
return instance;
}
在测试中发现生成的对象和反射后生成对象不一致,这就破坏了我们的单例、
解决办法:反射的时候,无非就是通过调用构造器来创建对象,那么我们可以再构造器中去写异常
判断了instance是否为空,如果不为空那就直接抛出异常,这样就解决了反射破坏单例的情况
private SingletonType1(){
//防止反射的破解单例
if (instance!=null){
throw new RuntimeException("对象已经实例化");
}
}
2.序列化场景
在我们使用反序列化的时候
//测试反序列化的对象
FileOutputStream fileOutputStream = new FileOutputStream("f://d.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(instance1);
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("f://d.txt"));
objectOutputStream.writeObject(instance1);
SingletonType1 o = (SingletonType1)objectInputStream.readObject();
System.out.println("反序列化对象"+o);
结果
可以发现创建出来的单例对象与反序列的单例对象不一致
解决办法:单例类实现序列化接口
并定义一个方法
此方法是当文件在被反序列化的时候,直接将单例对象返回不需要再创建新对象 解决了反序列化破坏单列的问题
public Object readResolve() throws ObjectStreamException {
return instance;
}
那这样呢就解决了反射和反序列化破坏单例的情况了,其实呢我们也可以不使用以上五种单例方式
六:枚举
public class Singleton5 {
@Test
public void test() throws InterruptedException {
SingletonType5 instance = SingletonType5.instance;
SingletonType5 instance1 = SingletonType5.instance;
System.out.println(instance==instance1);
}
}
/*
使用枚举也可以实现单例模式
好处:枚举是天生的线程安全而且只会在枚举加载的时候加载
注意:枚举不会被反射或者是反序列化破坏单例
*/
enum SingletonType5{
instance;
}
由于枚举类实现单例,所以我们可以借由枚举类来完成单例
其次枚举类不会被反射,因为在反射检查中特地检查了枚举类型
第三
序列化一个枚举类的对象,调用的是继承的Enum的valueOf 方法T result = enumType.enumConstantDirectory().get(name);根据name去找存入的对象,所以不会生成多个对象。
所以我们也可以使用枚举
以上呢就是单例模式的几种方式,觉得博主写的有问题或者想要交流的童鞋们,可以评论与博主进行交流…