java高并发实战(七)——并发设计模式

由于之前看的容易忘记,因此特记录下来,以便学习总结与更好理解,该系列博文也是第一次记录,所有有好多不完善之处请见谅与留言指出,如果有幸大家看到该博文,希望报以参考目的看浏览,如有错误之处,谢谢大家指出与留言。

一、什么是设计模式

1.在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛(Erich Gamma)等人在1990年代从建筑设计领域引入到计算机科学的。用于解决某个问题,避免重复想,反复的思考怎么避免问题,所以把某一类问题给设计好,以后可以拿去反复的复用。

2.“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”

尽管Alexander所指的是城市和建筑模式,但他的思想也同样适用于面向对象设计模式,只是在面向对象的解决方案里,我们用对象和接口代替了墙壁和门窗。两类模式的核心都在于提供了相关问题的解决方案。   

    一般而言,一个模式有四个基本要素:

    1. 模式名称(pattern name) 一个助记名,它用一两个词来描述模式的问题、解决方案和效果。

    2. 问题(problem) 描述了应该在何时使用模式。它解释了设计问题和问题存在的前因后果,它可能描述了特定的设计问题,如怎样用对象表示算法等。也可能描述了导致不灵活设计的类或对象结构。有时候,问题部分会包括使用模式必须满足的一系列先决条件。

    3. 解决方案(solution) 描述了设计的组成成分,它们之间的相互关系及各自的职责和协作方式。因为模式就像一个模板,可应用于多种不同场合,所以解决方案并不描述一个特定而具体的设计或实现,而是提供设计问题的抽象描述和怎样用一个具有一般意义的元素组合(类或对象组合)来解决这个问题。

    4. 效果(consequences) 描述了模式应用的效果及使用模式应权衡的问题。尽管我们描述设计决策时,并不总提到模式效果,但它们对于评价设计选择和理解使用模式的代价及好处具有重要意义。软件效果大多关注对时间和空间的衡量,它们也表述了语言和实现问题。因为复用是面向对象设计的要素之一,所以模式效果包括它对系统的灵活性、扩充性或可移植性的影响,显式地列出这些效果对理解和评价这些模式很有帮助。


3.《设计模式:可复用面向对象软件的基础》 收录 23种模式:这个是最早收录一本书,比较老:

    – 观察者模式

    – 策略模式

    – 装饰者模式

    – 享元模式

    – 模板方法

    – .....

4.架构模式(是从广义上的,更多强调的是一些思想)例如包括如下两个:

    – MVC

    – 分层

5.设计模式(是狭义上的)

    – 提炼系统中的组件

 6.代码模式(成例 Idiom)(微观上的)

    – 低层次,与编码直接相关

    – 如DCL(双重检查)

   如下代码:

class Person {
 String name;
 int birthYear;
 byte[] raw;

 public boolean equals(Object obj) {
 if (!obj instanceof Person)
 return false;

 Person other = (Person)obj;
 return name.equals(other.name)
 && birthYear == other.birthYear
 && Arrays.equals(raw, other.raw);
 }

 public int hashCode() { ... }
}

本博文主要讲解并发下的设计模式:包块单例模式、不变模式、Future模式、生产消费者模式。如下:

二、单例模式

1.单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。一般用于全局接口(比如用于全局信息配置)。他是非常重要的,也最广泛的。同时跟多线程有关,并发中怎么处理多线程去操作这个单利进行实例问题,下面会有代码实例。这里只是列举普遍的三种方式,详细可以查看该博客:https://blog.csdn.net/cselmu9/article/details/51366946

2.最简单实现方式(饿汉式:在程序启动或单件模式类被加载的时候,单件模式实例就已经被创建。)

1 public class Singleton {
2 private Singleton(){
3 System.out.println("Singleton is create");
4 }
5 private static Singleton instance = new Singleton();
6 public static Singleton getInstance() {
7 return instance;
8 }
9 }

存在缺陷是:何时产生实例 不好控制

public class Singleton {
public static int STATUS=1;
private Singleton(){
System.out.println("Singleton is create");
}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}

System.out.println(Singleton.STATUS);

比如当访问Singleton.STATUS也会进行创建实例。但本意是在这个类在第一次初始化时才初始化实例。

改进一(懒汉式:当程序第一次访问单件模式实例时才进行创建)但下面这个是线程不安全的

01 public class LazySingleton {
02 private LazySingleton() {
03 System.out.println("LazySingleton is create");
04 }
05 private static LazySingleton instance = null;
06 public static  LazySingleton getInstance() {
07 if (instance == null)
08 instance = new LazySingleton();
09 return instance;
10 }
11 }
改进二、 锁同步(延时加载机制同时是线程安全的)但在高并发下,会对性能产生影响,并不是高效的实例,运行效率会很低。同步方法效率低
01 public class LazySingleton {
02 private LazySingleton() {
03 System.out.println("LazySingleton is create");
04 }
05 private static LazySingleton instance = null;
06 public static synchronized LazySingleton getInstance() {
07 if (instance == null)
08 instance = new LazySingleton();
09 return instance;
10 }
11 }

改进三:(使用静态内置类)这样就没有synchronized锁就不存在了,高并发下,不会再出现线程排队情况。

01 public class StaticSingleton {
02 private StaticSingleton(){
03 System.out.println("StaticSingleton is create");
04 }
05 private static class SingletonHolder {
06 private static StaticSingleton instance = new StaticSingleton();
07 }
08 public static StaticSingleton getInstance() {
09 return SingletonHolder.instance;
10 }
11 }

三、不变模式

他对多线程是非常重要的。原因是因为在多线程下他不会被修改,是只读类对象,只读对象不需要同步的,因为在多线程下类会经常被同步的,因为同步是非常耗时的,因此不变模式是非常不耗性能的。比如创建一个类后他的内部状态不会在发生变化。

1.一个类的内部状态创建后,在整个生命期间都不会发生变化时,就是不变类

2.不变模式不需要同步

public final class Product {
//确保无子类
private final String no;
//私有属性,不会被其他对象获取
private final String name;
//final保证属性不会被2次赋值
private final double price;
public Product(String no, String name, double price) { //在创建对象时,必须指定数据
super();
//因为创建之后,无法进行修改
this.no = no;
this.name = name;
this.price = price;
}
public String getNo() {
return no;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
在这个对象在创建的时候被赋值,所以之后不会再更改。因此用final去修饰常量,类也声明final,就是他不会再被继承,他是最终类。
当然这个类也不是必须用final修饰的。

下面举例一些不变对象模式:

比如Sting,一创建它,就不会对他进行修改,但String却有可以对字符串操作方法,因为他会在后面生成一个新的String对象去替代,去对他进行修改,不是对老的对象进行修改。例如跟String一样的一些基本类型的包装类对象都是不变对象。

 java.lang.String
 java.lang.Boolean
 java.lang.Byte
 java.lang.Character
 java.lang.Double
 java.lang.Float
 java.lang.Integer
 java.lang.Long
 java.lang.Short

不变模式的特点也是只要对不变的对象进行任何的操作,这个对象本身一定不会发生改变的,如果发现他看起来好像是被改变过了,那一定是他生成了一个新的对象实例,而不是在原有的对象上进行修修补补,如果在原对象进行修修补补,就不能保证他在多线程下的安全性。

不变模式的优点

  1. 不修改一个不变对象,可以避免由此引起的不必要的程序错误,所以更加容易维护。
  2. 一个不可变对象是线程安全的,这样就可以省掉处理同步化的开销,可以自由的被共享。

缺点

一旦需要修改状态,就只能创建一个新的同类对象,在需要频繁修改不变对象的环境里,会有大量的对象作为中间结果创建,造成资源浪费。

例如:

一个字符串对象创建后它的值不能改变。
String str1="hello";//创建一个对象hello,不会变;
System.out.println(str1);
str1+=" world!";//两个字符串对象粘粘,系统其实创建了一个新的对象,把Str1的指向改了,指向新的对象;hello就                     //变成了垃圾;
System.out.println(str1);
//如果一直这样创建会影响系统的效率;要频繁的改变字符串对象的值就用StringBuffer来描述;
StringBuffer sb=new StringBuffer("[");
sb.append("hehe");

sb.append("]");//append();不会制造垃圾,真正在改sb的值;

System.out.println(sb);

这里举例只是说明模式的缺点,但String不变模式主要优点是安全和节省资源。如果存在频繁操作使用StringBuffer就可解决这个缺点。有关String不变模式详细讲解https://www.cnblogs.com/jachin01/p/7932920.html

四、Future模式

核心思想是异步调用

上面这个图是模拟函数调用。下面对比用future模式的对比效果图。


比如快递:网上订了一个货之后,他会立即返回下好订单给你,你可以去做其他事,不用再一直等这个货物到达,以后有时间,货物到了,你就可以拿这个订单,去取货。(作为货物的构造者,在这个期间可以慢慢去造获取,货物接收者可以去做其他事情)。也就是一段耗时的程序,你不需要立即去得到他。


如上图:接口Data他是要拿到的数据;futureData对象不是一个真是的数据,例如上面提到他就是一个订单;但他跟真实数据之间共享一个接口,因此这个过程用我们可以使用这个接口Data来代替数据,被调用者也就是客户端我们把这个Data给他就可以了,不用管我们给他的事真实的还是future数据。在未来某个阶段通过futureData去拿这个真实数据realData,这个fututrData可以包含这个RealData,如图,他可以聚合一个ReaLData,如果RealData获取完,就会摄入,那么FutureData就会知道,就能获取。

下面进行具体的实现:

首先定义一个Data接口:

public interface Data {

    public String getResult ();

}

其次是FutureData,他会聚合一个RealData,他是真实返回的数据.FutureData是立即返回的对象

public class FutureData implements Data {
 protected RealData realdata = null; //FutureData是RealData的包装
 protected boolean isReady = false;
 public synchronized void setRealData(RealData realdata) {
 if (isReady) {
 return;
 }
 this.realdata = realdata;
 isReady = true;
 notifyAll(); //RealData已经被注入,通知getResult()
 }
 public synchronized String getResult() { //会等待RealData构造完成
 while (!isReady) {
 try {
 wait(); //一直等待,知道RealData被注入
 } catch (InterruptedException e) {
 }
 }
 return realdata.result; //由RealData实现
 }
}

在真实数据组装时,他一直被阻塞,当真实数据被摄入,组装完时才会执行notifyAll(),释放wait()

public class RealData implements Data {
 protected final String result;
 public RealData(String para) {
 //RealData的构造可能很慢,需要用户等待很久,这里使用sleep模拟
 StringBuffer sb=new StringBuffer();
 for (int i = 0; i < 10; i++) {
 sb.append(para);
 try {
//这里使用sleep,代替一个很慢的操作过程
 Thread.sleep(100);
 } catch (InterruptedException e) {
 }
 }
 result =sb.toString();
 }
 public String getResult() {
 return result;
 }
}
创建客户端:Client
public class Client {
 public Data request(final String queryStr) {
 final FutureData future = new FutureData();
 new Thread() {
 public void run() {// RealData的构建很慢,
//所以在单独的线程中进行
 RealData realdata = new RealData(queryStr);
 future.setRealData(realdata);
 }
 }.start();
 return future; // FutureData会被立即返回
 }
}
下面是主函数:
public static void main(String[] args) {
 Client client = new Client();
 //这里会立即返回,因为得到的是FutureData而不是RealData
 Data data = client.request("name");
 System.out.println("请求完毕");
 try {
 //这里可以用一个sleep代替了对其他业务逻辑的处理
 //在处理这些业务逻辑的过程中,RealData被创建,从而充分利用了等待时间
 Thread.sleep(2000);
 } catch (InterruptedException e) {
 }
 //使用真实的数据
 System.out.println("数据 = " + data.getResult());
}

JDK对Future模式的支持


FutureTask他是一个带有future性质的Runnable,他可以把这个线程调起来。Future就相当于订单,Callable与Runnable功能一样,本质区别是Callable是带有任意返回值的。因此在JDK中既可以使用Future也可以使用Callable,使用Future就是使用FutureTask,他本质也就是Runnable,FutureTask可以认为是带有Future的Runnable.

举例Callable:

01 public class RealData implements Callable<String> {
02 private String para;
03 public RealData(String para){
04 this.para=para;
05 }
06 @Override
07 public String call() throws Exception {
08
09 StringBuffer sb=new StringBuffer();
10 for (int i = 0; i < 10; i++) {
11 sb.append(para);
12 try {
13 Thread.sleep(100);
14 } catch (InterruptedException e) {
15 }
16 }
17 return sb.toString();
18 }
19 }
01 public class FutureMain {
02 public static void main(String[] args) throws InterruptedException, ExecutionException {
03 //构造FutureTask
04 FutureTask<String> future = new FutureTask<String>(new RealData("a"));
05 ExecutorService executor = Executors.newFixedThreadPool(1);
06 //执行FutureTask,相当于上例中的 client.request("a") 发送请求
07 //在这里开启线程进行RealData的call()执行
08 executor.submit(future);
09
10 System.out.println("请求完毕");
11 try {
12 //这里依然可以做额外的数据操作,这里使用sleep代替其他业务逻辑的处理
13 Thread.sleep(2000);
14 } catch (InterruptedException e) {
15 }
16 //相当于data.getResult (),取得call()方法的返回值
17 //如果此时call()方法没有执行完成,则依然会等待
18 System.out.println("数据 = " + future.get());
19 }
20 }

JDK实现方式相对于很简单的实现方式,比FutureTask也会很简单。而已可以拿到返回值与可以把同步变成异步的方式,拿到另一个线程里执行。


public class FutureMain2 {
 public static void main(String[] args) throws InterruptedException, ExecutionException {

 ExecutorService executor = Executors.newFixedThreadPool(1);
 //执行FutureTask,相当于上例中的 client.request("a") 发送请求
 //在这里开启线程进行RealData的call()执行
 Future<String> future=executor.submit(new RealData("a"));
 System.out.println("请求完毕");
 try {
 //这里依然可以做额外的数据操作,这里使用sleep代替其他业务逻辑的处理
 Thread.sleep(2000);
 } catch (InterruptedException e) {
 }
 //相当于data.getResult (),取得call()方法的返回值
 //如果此时call()方法没有执行完成,则依然会等待
 System.out.println("数据 = " + future.get());
 }
}

五、生产者消费者

生产者-消费者模式是一个经典的多线程设计模式。它为多线程间的协作提供了良好的解决方案。在生产者-消费者模式中,通常由两类线程,即若干个生产者线程和若干个消费者线程。生产者线程负责提交用户请求,消费者线程则负责具体处理生产者提交的任务。生产者和消费者之间则通过共享内存缓冲区进行通信。

是非常经典的多线程模式,线程之间彼此之间最好不要知道彼此存在,因此提供一个公共的区域,因此消息的生产与消费彼此不知道对方存在,不受互相影响,不需要彼此认识。



内存缓冲区其实可能就是一些队列,他是生产者消费者的核心。

实现生产者消费者最适合的是BlockingQueue,但对于高并发并不是最好的,他的性能会很低,可以说BlockingQueue他是最方便,很合理实现的方式,并不是很好的高性能的实现方案,但对于一般的场景下也够用了。


生产者与消费者都把BlockingQueue,拿到自己里面,BlockingQueue里面是一个PCData.


while (isRunning) {
Thread.sleep(r.nextInt(SLEEPTIME));
data = new PCData(count.incrementAndGet());
//构造任务数据
System.out.println(data+" is put into queue");
if (!queue.offer(data, 2, TimeUnit.SECONDS)) {
//提交数据到缓冲区中
System.err.println("failed to put data:" + data);
}
}
while(true){
PCData data = queue.take();
//提取任务
if (null != data) {
int re = data.getData() * data.getData(); //计算平方
System.out.println(MessageFormat.format("{0}*{1}={2}",
data.getData(),
data.getData(), re));
Thread.sleep(r.nextInt(SLEEPTIME));
}
}
queue.task();会阻塞,这也是BlockingQueue和好处,没有数据他会阻塞,不会一直的往下执行,同时也是他在高并发下劣势,高并发下就需要不停的执行,疯狂的执行,已达到高并发吞吐量,但坏处也有,会吃掉好多的cpu.

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平凡之路无尽路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值