并行设计模式

有关多线程的设计模式,有关多线程的设计模式呢,单例,不变模式,Future模式,和生产者消费者模式

设计模式是软件工程当中的一个概念,它是指在软件设计当中,普遍存在的或者反复出现的一种通用解决方案,最早是由Eric Gamma,1990年从建筑学当中引入到计算机当中的,设计模式来源于建筑学,建筑算是一个比较高深的一门领域,建筑是把数学和艺术相融合的一门学科,其实我们看到我们周围的很多学科,很少有一门学科说,能够通过兼具科学的性质,并且兼具艺术的性质,艺术的东西,舞蹈唱歌,建筑是一个很奇特的领域,你的好看,还得坚固,在这个建筑里面呢,他有些小的模式让你去套,相对比较成熟优雅的设计,这个概念引入到软件工程当中也是类似的,有的时候我们为了解决某个问题,为了解决某个问题呢,我们就把常用的结构,常用的组件就给抽出来,然后把它整理出来之后呢,我们每一次遇到问题以后,我们就去组件当中拿数据,那这个模型,套到我们的应用当中去,这就不需要我们每次重新思考说,这个我要怎么做才能避免这个问题,还有设计模式当中已经帮你解决这个方案,Gof是四人帮,Gang of Four,可复用面向对象软件的基础这个书,收录了23种设计模式,这个书很早了,90年代了,都是由C++作为描述语言的,比较有名的就是观察者设计模式,策略模式,装饰者模式,模板方法,他抽取的都是组件,根据不同的用意,在什么场景下去使用的,有些模式是为了解决某些特定的问题,比如说享元,有些大的对象,可能需要反复的复用,他就为了解决这么一个性能问题,有些模式可能会更加广泛一些,比如像模板方法,模板方法在有一些书里面归类为源模式,基本的物质我们就称为源什么,就有模板方法的身影,可大可小的一个概念

我们从广义上来说,我们认为是一个范围比较小的,相对比较微观的一个组件,从广义上来讲呢,设计模式有架构模式,比如MVC模式,MVC模式其实并没有给出说,V和C必须要是什么样的结构,他没有给出一个具体实现的一个东西,是思想上的一个东西,分层的模式也是一样,我们在做设计的时候也是一样,可以把一个程序分为几层,每层之间是有依赖的,我们可以把中间某一层完全给抽掉,把它抽象出来,这些模式的范围比较大,更加强调的是一种思想,所以可以认为是一种架构上的一种模式,另外设计模式就是我们刚刚说的,狭义上的模式,提取了一些小组件,然后不停的去复用这些组件,更加微观层面上的呢,就是代码模式,这是一种低层次的,是和编码直接相关的,不同的语言有不同的写法,比如JAVA当中equals的实现,如果你要去重写equals的话,不同的人可能会有不同的写法,但是我们可能有一套大家都公认的,还算不错的写法在那边,我们有一套写法来规范这个函数,怎么书写,还有双重检查模式,双重检查模式的一种实现,那我就这么写就可以了,我不认双重检查在JAVA中是一种好的模式,我只是拿出来举一个例子,规定了代码要怎么写,最微观上的一种模式,那在我们这里说的模式呢,基本上是属于下面两类,要么就是一种可复用的组件,要么就是告诉你代码就是这么写就可以了,其他就不用想了

之所以把单例拿出来,单例是非常重要的,同时他也是和多线程相关的,单例的特点就是我整个系统只有一个,在有多线程的程序当中,你怎么样让这个单例,能够同时被多线程正常的去访问,而不至于说,有多个线程之后呢,你这个线程创建了一个,你那个线程又创建了另外一个,单例用于全局对象,只有一个实例,比如系统全局性的一些配置

单例一个最简单的实现呢,首先我们给他一个static的一个修饰,表示这个instance是和类相关的,然后我们在getInstance方法呢,也是一个static方法,当你去调用getInstance方法的时候,那你自然就可以拿到唯一的实例,因为static本身,他在初始化的时候,这个类第一次访问的时候,他就会被初始化,这个是由虚拟机来保证的,所以就可以确定这个类是惟一的,当你有多个线程访问的时候呢,这个类的初始化函数,它是线程安全的,但是这个模式有个小小的问题,何时产生实例呢,不太好控制,一般来说没有意外的话呢,getInstance的时候,但是他实际产生实例的时间呢,Single对象第一次方法的时候

对象当中有其他的字段,比如status,如果我们访问Singleton.STATUS呢,这个实例也会被创建,如果比较在意这个问题呢,就会有些问题,只是因为你很不小心的去访问这个字段,而导致这个单例被初始化出来

我们可以使用同步的方法,我们在第一次getInstance的时候呢,我们再去生成这个类的实例,他这样做的一个好处呢,我只有在getInstance第一次访问的时候,我会把这个类的实例给生出来,除此之外呢,我如果有其他的字段在这个类当中呢,对他进行访问呢,我这个类的实例是不会被生成的,所以这是一种延迟加载机制,只有当我真的要的时候,我才会去创建,同时为了防止多线程进入getInstance,从而创建多个实例呢,我们这里用synchronized进行修饰,当你有一个线程进来的时候,其他线程是进不来的,因此当你一个线程进来,判断是否为null的时候,如果他为null,就表示其他线程确实没有进来,因为同时只有一个线程能够进来,当创建单例并且返回之后呢,后面的线程会发现呢,instance就被复制了,就不会创建LazySingleton,就直接返回了,这个是延迟加载的一个典型,他有一个问题呢,在你高并发的时候呢,大家都要去拿synchronized这个锁,那我们知道这个东西他可能呢,对性能产生一定的影响,这里只是简单的做了一个等于,然后return了,并没有太多耗时间的操作,所以对性能影响并不会特别的大,让你高频率访问的时候呢,或多或少会因为synchronized,产生一些影响的,因为这并不是一个高效的关键字,所以我们还有一种更加的方法使用单例

这个方法呢,首先它是延迟加载的,当你第一次访问getInstance的时候呢,他才会创建static Singleton对象的实例,如果这个对象有其他字段,你访问了其他字段呢,这个实例是不会被创建起来的,因为这个实例的初始化是定义在了,一个类的静态内部类当中,很显然静态内部类是不会被初始化的,所以还是可以起到一个延迟加载的作用,只有当你getInstance的时候,这个时候你才访问了静态内部类,这样做的好处很显然,我们把synchronized关键字给去掉了,这个相对要比较好一些,单例的构造必须是private,如果构造函数不是单例,那就得不到很好的控制,任何人都可以去newinstance出来,你不能保证他一定是一个单例,当构造函数是private的时候呢,别人就不能去new instance了

下面我们来讲一个不变模式,不变模式对多线程来讲是非常重要的,不变模式是一个什么样的状态呢,一个类他在创建之后,他的内部状态就不再发生改变了,不会随着这个类的生命周期而改变了,比如一个坐标,坐标x,y被创建之后,在这个对象实例的整个使用当中,x永远是1,y永远是1,不会发生x和y的变化,为什么说不变模式,对多线程是非常重要的呢,因为不变模式不会被修改,它是一个只读的对象,对于一个只读对象来说呢,它是不需要同步的,而我们多线程之间呢,可能会进行各种各样的同步,同步可能是非常耗时的,消耗资源的,提供非常好的性能,如果要在多线程不停的访问,我们不妨可以把它设置为不变的东西

怎么样保证这个类是没办法被修改的,首先我们的字段声明为final,这个是符合JAVABEAN的一些规范,get字段的时候使用get方法,这里没有set,因为你不需要set,因为Product这个类是不变的,他从创建之后,他就不会发生改变,Product另外一个特点呢,是全部声明为final,为什么要必须声明为final呢,因为final表示为常量,因为他只能进行一次赋值,一次赋值之后呢,不能进行二次赋值了,如果不是final,在他赋值之后呢,我是不是还有可能再进行修改,final相对来时说一个比较保守的做法,保证这个字段永远都不可能被修改的,我什么时候对他进行赋值呢,在这个对象被创建的时候,你对他赋值,他一旦被创建完之后,它所有的字段就不会再修改,在这个构造函数当中进行赋值,一旦赋值完了,因为是final的,同时我整个类也声明为final,你不可能去继承我这个类,这是为什么呢,这个不是非常强烈的,你这个类不使用final问题也不是很大,但是有了这个final呢,你可以 保证他没有子类,这意味着说,不可能会出现一个没有Product的子类,而那个子类它是可变的,而根据里氏代换原则呢,我子类替代我的父类,如果你子类是可变的,而你父类是不可变的,并不是你在所有的场合,你都能够替代她的父类,所以只有当我把它声明为final之后,我让他不可能有子类,我可以保证所有的product都是不可变的

下面我们来看不变模式的案例,其实不变模式在JAVA中也是比较普遍的,我们最常用的String他就是一个不变的对象,我们可以看到String对象呢,他一旦被创建之后,他不会再发生改变了,不管你是JDK6,7,8,String他都是不变的,这是一个最基本的,有些人可能会有疑惑,其实很多看起来像String的操作呢,他其实都是在后面生成一个String做的,而不是在老的String上做修改,这个就是String模式的特点,你如果对他做一个修改,你并不是把这个对象给改掉,而是生成一个新的对象,来替代原来老的对象,这个是不变模式的特点,除了String之外,我们很多跟基本类型相对应的类,布尔,Byte,他都是不变的,这里也是要强调一下,有时候我们会写这样的代码,我们声明了一个Integer i = 0,然后我们会做一个i++,这个时候看起来是i做了++操作,其实大家知道Integer是一个不变对象之后呢,其实这样的操作显然大家知道,i其实并没有改变,而内部i++是怎么做的呢,他必定是生成了一个新的Integer对象,然后把新的对象变成多少呢,变成1,然后把1替换到原来i的位置上去,也就把i给替换掉,i++之后,Integer的引用本身都会发生变化,这一切都封装到自动装箱和自动拆箱当中,如果把i++汇编之后就可以看得非常清楚,这里也要强调一些不便模式的思想,你只要对不变的对象做任何的操作呢,对象本身是一定不会发生改变的,如果你看他好像被改变了,那一定是他生成了一个新的对象实例,不是在原有的对象上做修修补补,如果他在原有对象上做修修补补,他就没有办法保证他在多线程上的安全性了

下面我们来看一下Future模式,是一个使用非常广泛的模式,现在我们来简单了解一下Future模式,Future模式是什么意思呢,他的核心是异步调用,客户端要向服务端调用一个程序,服务程序需要做某些事情,构造某一个数据,但这个数据构造很慢,要做很久,做完之后他把数据返回,返回之后再返回给客户端,这里模拟的是函数调用,然后这个时候客户端就做其他的事情了,可以看到这个函数调用呢,我们发起端的那个人,他可能要等一段时间,他要等,如果是Future模式它会变成什么样子呢,我客户端如果要去调用某一个数据,那我就给服务端发送一个请求,可能就是一个函数调用,但是这个时候不一样,花了好久才返回,这里立即返回,数据的构造是需要花很长时间的,你立即返回数据在哪里呢,其实你返回的是一个空的东西,什么都没有,这个数据并没有构造出来,只是返回给你说,我给你一张契约,这就好比说,我们在网上买东西,我们下了一个订单,我下了订单之后呢,货不可能马上就到,但是你凭着这张订单,你在未来就可以收到,你要的东西,万一他没有发给你,万一他发给你了你就把快递收下了,那就没什么事情了,但是万一你没有拿到货,那你凭着这张订单,可以质疑那个商家说,那我就凭着这个收据,我要求你把这个东西给我,这里也是这个意思,你这个操作是马上返回的,但是你货没有,我只给你一个空箱子,只是给你一个订单,在将来如果你要拿到真实的数据,那你可以在将来的某一个时间段,你凭着我给你的订单,你把这个货拿出去,也就是Future模式呢,Future指的就是未来,就像是订单一样的,虽然一张契约,两者签了一个协议,我承诺以后会给你这个东西,但是我现在拿不出来,那你先把承诺书带走,先做其他的事情,因为我现在没有,先把其他的活干掉,不用等着我这个东西,等到某个时间段需要这个东西的时候呢,凭着我这个单子,我这个契约书,你把我这个货拿走,你可以在这里做一些额外的事情,对于实际的数据构造者来讲呢,他在这里慢慢的做,然后客户端可以拿到,就凭着这个订单把数据拿到,这个思想就是Future模式的思想,你不需要立即去得到他

下面我们来看一下Future模式的一个简单的实现,这里Future模式的结构当中,我们有一个接口Data,Data就是我们要拿到的数据,FutureData相当于订单,他不是一个真实的数据,但是他和真实数据之间共享一个接口,因此我们在整个使用当中呢,我们可以使用接口来代替数据,我只要给你一个Data就可以了,你不用管我是realData还是FutureData,RealData可能很慢,所以我可能等不及,所以你拿到一个真实的实例呢,很有可能是一个FutureData,也就是订单本身,接着你在未来的某一个阶段,你可以通过FutureData拿RealData,FutureData当中呢,去包含RealData,看到这两个关系没有,FutureData会聚合一个realData,对于Future模式的实现核心就是这个,ClientThread我用一个线程装配起来,核心是这三个,真实数据是构造的很慢的,FutureData是很快的,因为他只是构造一个空壳而已,在未来某个时间段,这个RealData装配完之后呢,把它设置到FutureData里面去

下面我们来看一下具体的实现,首先我们data是一个接口,他能得到某一个数据,要返回的是一个String,FutureData里面会聚合一个RealData,这个是真实要返回给别人的,我整个的包一层,因为我立即要返回的是FutureData,当我去拿到getResult的时候呢,我要看一下我这个realData是不是已经ready了,如果没有ready呢,那我当前线程拿的线程要做等待,因为你不能继续往下走,因为我这个数据还没有准备好,直到什么时候你可以往下走呢,我真实数据被设置进来之后,装配进来之后,你才能往下走,所以当真实数据被装配进来之后呢,我isReady就会设置我为true,同时会通知,数据已经准备好了,你可以继续往下走了

对于RealData来讲,我们简单的做了一个模拟,我们RealData返回一个字符串,我们RealData构造的很慢,我们特意加了一些sleep方法,模拟一次很慢的构造,这个构造函数,构造的实例,是很慢的,根据data这个接口,getResult方法,把这个result给返回

那我们来看看这个client,这个client请求一个数据的时候,他直接构造的是FutureData,首先明确返回值是Data接口,你不能返回RealData,因为RealData是一下子返回不了的,他需要快速返回,只能返回Data,它会先构造一个FutureData,FutureData构造是很快的,然后在一个新的线程当中,去构造这个RealData,因为RealData构造是很慢的,那我必须把它异步执行,线程构造很慢没有关系,我可以慢慢做,但是我FutureData一旦构造完成之后呢,我马上就返回了,但是这个返回的FutureData,当其他线程拿到这个返回值之后,它里面的东西很有可能是空的,他对象实例是有了,他要的东西是没有的,但是这个时候没有关系,在未来某一个时间,如果真的需要result的话,需要future.getResult,把这个数据给拿出来,如果当时的数据已经构造完了,他这个result拿到数据,否则他就会等待,他才会把这个数据拿到,这就是Future模式的一个核心

我们可以做一些业务逻辑上的事情,我们不需要去拿result,如果request拿到data之后,我们马上就去拿getResult呢,这个地方一定会被阻塞,一定会被阻塞,真实数据还没有构造完,线程就陷入等待,知道你notifyAll被调用了,但是如果你在这个地方去做其他的事情,做完之后再去拿getResult,那你有可能马上就返回了,这个数据马上就被构造了,这里是怎么把一个同步调用阻塞调用,变成一个异步的调用

Future模式是很常用的,JDK当中也是把Future模式拿进来了,做了一个包装和实现,这里就是对Future模式的一个包装和实现了,首先他在这个地方有一个Future,Future和Runnable,Future可以去拿某一个数据,另外一个接口是RunnableFuture,重要的我们看FutureTask,FutureTask它是一个带有Future性质的Runnable,他可以被一个线程带起来,Future相当于一个订单而已,FutureTask可以把线程调起来,另外一个很重要的呢,Callable,Callalbe是什么呢,他其实和Runnable很接近,他也可以直接构造成一个线程,但是他的本质不同呢,Callable是有返回值的,也就这么点区别,Runnable返回值void,当你用Runnable,你线程做某一个动作,Callable做某一个线程,并且告诉这个线程说,做完这个动作之后呢,你必须给我一个返回值,是一个模板类,可以返回任意的类型,事实上你在使用Future模式的时候呢,你既可以使用Future,在JDK当中你既可以使用Future,也可以使用Callable,都是可以的,在使用Future的时候呢,本质上使用FutureTask,FutureTask他本身是一个Runnable,同时也是一个Future,它是带有Future功能的一个Runnable的接口,现在我们使用Callable来实现

刚才说的RealData,之前在我们自己的小小框架当中呢,是继承了Future,现在我让他实现Callable,Callable他有个方法call,他就这么一个方法,他就返回你要的String,在我这个RealData当中呢,当然这个返回是很慢的,模拟一个时间比较长的调用,当你使用RealData Callable接口,来构造一个线程的时候呢,这个线程的执行呢,可能是比较慢的,线程的返回String要花一些时间

Callable接口来实现Future模式,首先我们通过Callable接口,去生成一个FutureTask,FutureTask它是一个Runnable,同时他也是一个Future,它是带有Future功能的Runnable,他有Future功能是能带返回值,然后你把Runnable提交给线程池,线程就会返回,就会执行,执行之后你通过Future,因为他有Future功能,FutureTask他有Future的功能,通过Future的getResult方法,把这个数据给查出来,这一整套实现可以看到,相对来说还是比较好用的,使用了JDK当中的功能,线程池,FutureTask,我们要把RealData给实现一下,这个是和业务逻辑相关的东西,然后把整个给串起来,这里是模拟了一个其他业务逻辑的操作,然后把这个数据拿到就行了,如果你在future.get的时候数据还没有准备好,这个地方同样会陷入等待,等待这个数据准备好之后,你再去把这个数据给拿出来,这个就是另外一个更加简便的实现,这里我们并没有构造FutureTask,实际上也没有这个必要

因为Executor支持使用Callable来构造,在这种情况之下呢,Callable支持返回值,这个返回值使得我们submit方法呢,他可以返回一个Future对象,因此这样写就可以拿到Future,然后在未来某一个时间段呢,你可以通过future.get拿到东西,这里把Callable传进去,这两种方法都是可以的,我们只是希望在线程调用当中拿到一个返回值,这个场景也是非常常用的,我们并不是做完事情就好了,希望拿到一个东西,他其实可以把一个同步的调用,变成异步调用,同步调用太慢了,这就是一个很好的方法,而且使用起来也是比较容易的

生产者消费者模式,它是一个非常经典的多线程模式,我两个线程之间如何去共享数据,我线程A要给线程B数据,意味着我线程A,要知道线程B的存在,那同样的道理,我线程A要去线程B拿数据,我线程B要知道线程A的存在,但是他符合我们的松散耦合的原则,这个系统要尽可能的保持松散,你多线程之间彼此不要知道彼此的存在的,你知道的东西越少越好,你不要知道这个知道那个,就相当于耦合性太大,生产者可能有好多个,有各种各样的生产者,消费者也有可能有好多个,所以这也不是一件很现实的事情,所以我们就要想一个办法说,我能不能给一个公共的区间,公共的区域,所有人都往这个区间写数据,所有人都往这里去拿数据,这样所有的生产者,产生数据,产生请求的,处理这些数据,处理这些请求的,彼此之间不需要知道彼此的存在,软件工程的想法呢,如果说一个模块,他对外知道的应该是越少越好,最好是一无所知,如果他对外一无所知,我外界的程序不管你怎么改,对我都是没有影响的,就比较符合我们说的开闭原则,一个模块的修改应该是保持闭合,那么在这种情况之下呢,可能有各种类型的生产者,他们之间也不需要认识,往公共缓冲区里面放数据,缓冲区我们可以使用BlockQueue阻塞队列的方式来实现,所有的消费者也是一样,他也不需要知道生产者的存在,他也往缓冲区拿数据,就可以了

这个就是生产者和消费者的草图,在这里各种各样的生产者,各种各样的消费者,彼此之间都是不相关的,不知道他们之间是有存在的,生产者只往里面放,放内容放东西,所有生产者只知道缓冲区,消费者也只知道缓冲区,他一根筋的往外拿,生产者消费模式就建立起来了,最适合使用生产者消费者模式的呢,其实就是BlockingQueue,他不是一个高性能的实现,如果需要的是一个高性能的生产者消费者模式,它是不适合的,我自己去实现一个内存缓存区,ConcurrentLinkedQueue,它是一个高并发的实现,高并发的队列,进行包装来实现生产者消费者,如果你使用BlockingQueue,只是一个容易合理的实现,但是他不是一个性能很好的解决方案,但是在一般场景下也就够用了

生产者提交任务,消费者提取任务,内存缓冲区是生产者消费者的一个核心,任务是缓冲区里面放的数据结构,缓冲区它是一个队列


消费者包含了一个BlockingQueue,消费者要把BlockingQueue放到自己里面来,生产者也是一样,生产者也把BlockingQueue放到队列里面来,BlockingQueue里放的是什么东西呢,是PCData,这里放PCData数据结构,里面是数据,生产者产生一个数据请求,BlockingQueue里面去,消费者拿到这个数据请求,这里就是生产者消费者的一个实现

生产者会往队列里放数据,这个Queue就是BlockingQueue,BlockingQueue有很多种,有基于数组的,有基于链表的,BlockingQueue他本身是一个接口,我们只要构造一个实例就行了,offer会往这里放一个对象,把数据构造出来,然后消费者就能提取数据,take方法提取数据,提取完之后进行计算,while循环不停的往里放数据,不停的取数据,BlockingQueue的好处是什么呢,如果Queue当中为空,消费者没有放进来,消费者比生产者快很多,这个数据为空,这个消费者就会在take阻塞,这是一个Blocking的一个Queue,阻塞队列,这个时候消费者会挂起,做一个等待,并不会拼命的往下读,这个是BlockingQueue的特点,也是因为这样,称不上是一个高并发的方式,真正高并发是不会做等待,就是拼命的检查有没有数据,如果你这个队列真的有很多数据的话,真的有大量的阻塞进来的话,你就应该把这个循环结束掉

 


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部