栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

java设计模式

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

java设计模式

一、设计模式 1、单例模式

单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

优点:

  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
  • 避免对资源的多重占用(比如写文件操作)。

**缺点:**没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

1.1、饿汉式
  • 优点:实现简单,不存在多线程问题,直接声明一个私有对象,然后对外提供一个获取对象的方法。
  • 缺点:class 类在被加载的时候创建Singleton实例,如果对象创建后一直没有使用,则会浪费很大的内存空间,此方法不适合创建大对象。
public class Singleton
{
    private static Singleton instance = new Singleton();
    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}
1.2、懒汉式(线程不安全)
  • 优点:节省内存空间,在使用的时候才会创建;
  • 缺点:在多线程下,可能会创建多个实例
public class Singleton
{

    private static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {

        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

}
1.3.1、指令重排

指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.

编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

数据依赖关系的例子:

//写一个变量之后,再读这个位置。
//代码1 与代码2 这两行代码不会发生指令重排,因为他们具有依赖性。
int a,b;
a = 1;//1
b = a;//2
//写一个变量之后,再写这个变量。
//代码1 与代码2 这两行代码不会发生指令重排,因为他们具有依赖性。
int a,b;
a = 1;//1
a = 2;//2
//读一个变量之后,再写这个变量。
//代码1 与代码2 这两行代码不会发生指令重排,因为他们具有依赖性。
a = b;//1
b = 1;//2
//代码1、代码2可能发生指令重排,他们不具备依赖性。
//但是代码3 一定会排在 1、2的后面,因为3和 1、2具备依赖性。
int a,b,c;
a = 1;//1
b = 2;//2
c = a + b;//3

多线程环境下指令重排可能引发的问题:

class OrderExample {
    int a = 0;
    //默认false,无数据,不能读取
    boolean flag = false; 
    //写数据的方法,假如有一个线程A不停的写数据
    public void writer() {
        // 以下两句执行顺序可能会在指令重排等场景下发生变化
        a = 1;//写入数据
        flag = true;//true,标识着已经有数据可以读取了
    }
	//读取数据的方法,假如有一个线程B不停的读取数据
    public void reader() {
        //先判断是否有数据可以读取
        //如果线程A写数据时,writer方法中的代码发生了指令重排,先执行 flag=true
        //那么,假如此时线程B刚好 if(flag)判断,就会进入if内部,而此时线程A还没有对变量a进行设值,此时线程B拿到的是错误的值进行运算
        if (flag) {
            //进行相关运算等
            int i = a + 1;
            //TODO ...更多操作
        }
    }
}

如何解决多线程环境下指令重排引发的问题?解决方案就是大名鼎鼎的volatile关键字

1.3.2、volatile关键字

特性

  • volatile能禁止指令重排
  • volatile关键能保证变量的”可见性“
1.3.2.1、volatile的禁止指令重排

重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果

使用volatile关键字修饰共享变量便可以禁止这种重排序。若用volatile修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序,volatile禁止指令重排序也有一些规则:

  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  • 在进行指令优化时,不能将对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
1.3.2.2、volatile的“可见性”

说可见性之前,我们先来了解一下java虚拟机中线程的内存模型

  • 每一个线程运行时都有一个线程栈
  • 线程栈保存了线程运行时候变量值信息。
  • 当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值
  • 在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。

下面一幅图描述这写交互!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ciim7DB-1655812904816)(.imgs线程内存模型.jpg)]

  • 在多线程环境下,若多个线程同时在操作主内存的count变量,就可能会导致每个线程中操作的count的值不是最新的
  • 如果给count变量修饰了 volatile关键字,那么就能保证此变量的“可见性”
    • 当写一个volatile变量时,JMM(java内存模型)会把该线程本地内存中的变量强制刷新到主内存中去;
    • 这个写会操作会导致其他线程中的volatile变量缓存无效。
class OrderExample {
    int a = 0;
    //使用volatile修饰,既能保证此变量在多线程环境下的可见性,还能够进制指令重排
    volatile boolean flag = false; 
    
    //写数据的方法,假如有一个线程A不停的写数据
    public void writer() {
        a = 1;
        //禁止指令重排,此变量之前的操作必须是完成后才会轮到它
        flag = true;
    }
    
	//读取数据的方法,假如有一个线程B不停的读取数据
    public void reader() {
        //保证了变量的可见性,其他线程修改了flag变量,当前线程也能即时读取到
        if (flag) {
            int i = a + 1;
            //TODO ...更多操作
        }
    }
}
1.3、懒汉式(线程安全)
  • 优点:支持多线程,且以懒汉式的方式加载,不浪费内存空间。
  • 缺点:将 synchronized 块加在方法上,会影响并发量,每次调用getInstance()方法都会线程同步,效率十分低下。最重要的是,当创建好实例对象之后,就不必继续进行同步了。
public class Singleton
{
    private static Singleton instance = null;
    private Singleton() {}

    public synchronized static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
1.4、懒汉式-双重校验锁(线程安全,推荐)
  • 优点:支持多线程,并发量高,且以懒汉式加载,不浪费内存空间。
  • 缺点:非要说缺点的话,就是实现比较麻烦。
public class Singleton
{
    private static Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}

双重校验锁即实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,是否真的就万无一失了呢?

java中 Object obj = new Object() 实际上并不是原子性的操作,它可以被分解为如下3行伪代码:

//定义引用类型
Object obj = null;
//1:分配对象的内存空间
memory = allocate();
//2:初始化对象
ctorInstance(memory);  
//3:设置obj指向刚分配的内存地址
obj = memory;

上面三行伪代码中的2和3之间,可能会被重排序,即编译器或处理器为提高性能改变代码执行顺序

在单线程程序下,重排序不会对最终结果产生影响,但是并发的情况下,可能会导致某些线程访问到未初始化的变量。

解决方案就是大名鼎鼎的volatile关键字

public class Singleton
{
    //volatile修饰此变量,禁止指令重排 
    private static volatile Singleton instance = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}
1.5、Holder单例模式

特点:既实现懒加载,性能好,线程安全

public class HolderSingleton {
 
    // 外部类的私有构造方法
    private HolderSingleton() {
    }
 
    // 外部类静态方法,通过静态内部类的静态成员变量获取
    public static HolderSingleton getInstance() {
        //java类加载机制,使用此类静态变量时会加载此类,并对其进行初始化
        return Holder.instance;
    }
 
    // 静态内部类内部加载的时候创建外部类,只加载一次,所以只创建一个
    private static class Holder {
        private static HolderSingleton instance = new HolderSingleton();
    }
}

注:之后我们的service、dao层等实现类全部使用单例模式。

2、代理模式

网上概念太正规了,我这里就简单点说吧

有一件事情原本是你自己应该要做的,但是由于某些原因,你自己做起来很麻烦,这时候就可以叫别人来帮你(代理你)做。

如:结婚,你可以花点钱,叫婚庆公司代理你置办酒席,司仪,车队等琐事,你只要专心等待婚庆公司叫你洞房就成。

代理模式的组成:

  • 抽象角色:通过接口或抽象类声明真实角色实现的业务方法。(如:结婚,这本身是件很抽象的事情,很多人都可以结婚的)
  • 代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加额外的操作。(如:婚庆公司,他可以帮你做一些额外的琐事)
  • 真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用(如:结婚的真正的人)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FUziPFSN-1655812904818)(.imgs代理模式-1.png)]

2.1、静态代理

优点:可以做到在不修改目标对象的功能前提下,对目标功能扩展.

缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类,类太多.同时,一旦接口增加方法,目标对象与代理对象都要维护.

  • 抽象角色—约会接口
public interface AppointmentService {
    
    public void play1();

    
    public void play2();

}
  • 真实角色 — 约会的人(我)
public class MyServiceImpl implements AppointmentService {
    public void play1() {
        System.out.println("==== 今天和女朋友看了一场电影");
    }

    public void play2() {
        System.out.println("==== 今天和女朋友去做了过山车");
    }
}
  • 代理角色 — 可以在约会之前或结束额外处理一些我并不关心的琐事

public class StaticProxy implements AppointmentService {
    //真实的被代理的对象(我)
    private AppointmentService target =  null;

    public StaticProxy(AppointmentService target){
        this.target = target;
    }

    public void play1() {
        

        System.out.println("*** 女朋友起床了");
        System.out.println("*** 女朋友洗漱、化妆");
        //调用真实对象(我) 周一约会的方法
        this.target.play1();
        System.out.println("*** 女朋友回家卸妆");
        System.out.println("*** 女朋友有睡觉了");

    }

    public void play2() {
        System.out.println("*** 女朋友起床了");
        System.out.println("*** 女朋友洗漱、化妆");
        //调用真实对象(我) 周二约会的方法
        this.target.play2();
        System.out.println("*** 女朋友回家卸妆");
        System.out.println("*** 女朋友有睡觉了");
    }
}
  • 最后测试使用
public class Test {
    public static void main(String[] args) {
        //真实的约会对象---我
        MyServiceImpl my = new MyServiceImpl();
        //约会的代理对象 --- 代理的是我
        StaticProxy proxy = new StaticProxy(my);
        System.out.println("------周一");
        //开始约会 --  周一玩啥呢?
        proxy.play1();
        System.out.println("------周二");
        //开始约会 --  周二玩啥呢?
        proxy.play2();
    }
}

打印结果如下

------周一
*** 女朋友起床了
*** 女朋友洗漱、化妆
==== 今天和女朋友看了一场电影
*** 女朋友回家卸妆
*** 女朋友有睡觉了

------周二
*** 女朋友起床了
*** 女朋友洗漱、化妆
==== 今天和女朋友去做了过山车
*** 女朋友回家卸妆
*** 女朋友有睡觉了

从上面的例子中我们可以很明显的看到静态代理的缺点,一旦我们增加新的约会方法,那么代理类中也要增加其实现。万一我们的业务系统中需要代理的类很多,需要代理的方法也很多,我们将要去维护大量的代理类。

如何解决静态代理中的缺点呢?答案是可以使用动态代理方式

2.2、动态代理
  • 代理对象,不需要实现接口
  • 代理对象的生成,是利用JDK的API(java.lang.reflect.Proxy),动态的在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)
  • 动态代理也叫做:JDK代理,接口代理

jdk反射实现动态代理核心接口:

InvocationHandler(方法调用处理器)。实现此接口后,在调用指定java类方法时会进入此处理器。然后进行额外处理

jdk创建代理对象核心方法:

Proxy.newProxyInstance(类加载器对象,被拦截的接口class,被拦截时处理器实现对象)
  • 我们来看看动态代理如何做
  • 沿用上面的例子,将静态代理类改为动态代理类
    • 注意实现方法处理器的接口

public class DynamicProxy implements InvocationHandler {
    //目标对象--真实的代理对象
    private AppointmentService target = null;

    public DynamicProxy(AppointmentService target){
        this.target = target;
    }
    //获取代理对象
    public AppointmentService getProxyInstance(){
        //获取class信息
        Class clz = this.target.getClass();
        
        AppointmentService proxy =
                (AppointmentService)Proxy.newProxyInstance(
                clz.getClassLoader(),
                clz.getInterfaces(),
                this
        );
        //返回代理对象
        return proxy;
    }

    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("*** 女朋友起床了");
        System.out.println("*** 女朋友洗漱、化妆");
        //调用真实对象(我) 约会的方法
        Object ret = method.invoke(this.target,args);
        System.out.println("*** 女朋友回家卸妆");
        System.out.println("*** 女朋友有睡觉了");
        //返回
        return ret;
    }
}

  • 使用
public class Test {
    public static void main(String[] args) {
        //真实的约会对象---我
        MyServiceImpl my = new MyServiceImpl();
        //约会的代理帮助类/处理器实现类
        DynamicProxy proxyHandler = new DynamicProxy(my);
        //利用jdk反射,获取代理对象--- 代理的是我
        AppointmentService proxy = proxyHandler.getProxyInstance();
        System.out.println("------周一");
        //开始约会 --  周一玩啥呢?
        proxy.play1();
        System.out.println("------周二");
        //开始约会 --  周二玩啥呢?
        proxy.play2();
    }
}
2.3、CGLib代理
  • 上面的静态代理和动态代理模式都是要求目标对象是实现了接口
  • 如果我的目标对象没有实现任何接口,如何来做代理模式?

正常情况下是做不了的,但是有大佬开发了一款CGLib(一个强大的、高性能的代码生成库 )

CGLib代理,也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能的扩展(它可以在运行期扩展java类与实现java接口).它广泛的被许多AOP的框架使用,例如Spring AOP和synaop,为他们提供方法的interception(拦截)

  • 继续沿用上面的例子,注意,MyServiceImpl 类中没有实现任何接口了
    • 注意项目中引入cglib-2.2.2.jar、asm-3.3.1.jar 这两个jar
public class MyServiceImpl {
    public void play1() {
        System.out.println("==== 今天和女朋友看了一场电影");
    }

    public void play2() {
        System.out.println("==== 今天和女朋友去做了过山车");
    }
}
  • CGLib代理
import javafx.beans.property.adapter.ReadOnlyJavaBeanBooleanProperty;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGLibProxy {
    //CGLib代理只需要提供代理目标的 class即可
    private Class targetClz = null;

    public CGLibProxy(Class targetClz){
        this.targetClz = targetClz;
    }

    
    public Object getCGlibProxy(){
        //对java类的扩展器/增强器
        Enhancer enhancer = new Enhancer();
        //针对哪个class进行增强
        enhancer.setSuperclass(targetClz);
        //设置增强/扩展 的处理
        enhancer.setCallback(
            //匿名类,此类实现了 MethodInterceptor 接口
            new MethodInterceptor() {
                
                public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy)
                        throws Throwable {
                    System.out.println("*** 女朋友起床了");
                    System.out.println("*** 女朋友洗漱、化妆");
                    //调用真实对象(我) 约会的方法
                    
                    Object ret = methodProxy.invokeSuper(o,args);
                    System.out.println("*** 女朋友回家卸妆");
                    System.out.println("*** 女朋友有睡觉了");
                    //返回
                    return ret;
                }
            }
        );
        //利用增强器 创建targetClz的代理对象,并返回
        return enhancer.create();
        
    }
}
  • 使用
public class Test {
    public static void main(String[] args) {
        //利用cglib创建一个 MyServiceImpl 的代理对象
        MyServiceImpl proxy =
                (MyServiceImpl)new CGLibProxy(MyServiceImpl.class).getCGlibProxy();
        System.out.println("------周一");
        //开始约会 --  周一玩啥呢?
        proxy.play1();
        System.out.println("------周二");
        //开始约会 --  周二玩啥呢?
        proxy.play2();
    }
}
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/986665.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号