25 Guarded Suspension设计模式和生产者消费者模式

1 引入

比如A在干一件事情,走不开不能中断,此时快递员来送快递,你走不开,只能等做完这件事,再去拿快递,让快递员先放到快递柜。

再比如tomcat中,当Tomcat处理请求到限制,又来请求,处理不了,就会把请求放到一个队列中等空闲再去处理

要点:

  • 有一个结果需要从一个线程传递到另一个线程,他们之间需要关联同一个GuardedObject
  • 如果有结果不断的来,可以讲结果放到一个队列中

2 简单的实现

2.1 第一版

定义一个GuardedObject,核心方法就是两个,获取结果,提交结果,在获取结果的时候,如果没有获取到,那就等待;提交结果的时候,为了防止另一个线程已经进入了等待,需要唤醒那些正在等待的线程

package study.wyy.concurrency.guared.demo1;

public class GuardObject<T> {

    private T result;

    /*****
     * 获取结果
     * @return
     */
    public T get(){
        synchronized (this){
            while (null == result){
                // 没有结果时候,就等待
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return result;
        }
    }

    /*****
     * 提交结果
     * @return
     */
    public void submit(T result){
        synchronized (this){
            this.result = result;
            // 唤醒等待结果的线程,来获取结果
            this.notifyAll();
        }
    }
}

测试

package study.wyy.concurrency.guared.demo1;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;
import java.util.concurrent.TimeUnit;

@Slf4j
public class Test {

    public static void main(String[] args) {
        GuardObject<String> guardObject = new GuardObject<>();
        Random random = new Random();
        // 一个线程在从db进行数据的导出,
        // 另一个线程负责将导出的数据进行写入文件,
        new Thread(()->{
            System.out.println("写入文件前置处理");
            // 模拟前置处理的耗时
            sleepSecond(random.nextInt(5));
            // 获取另一个线程从数据库中读取到的数据
            String res = guardObject.get();
            System.out.println("开始写入文件。。。");
            // 模拟写入数据的耗时
            sleepSecond(random.nextInt(5));
            System.out.println("写入文件结束。。。文件内容: " + res);
        },"t1").start();
        new Thread(()->{
            // 读取数据库数据
            System.out.println("读取数据库数据开始");
            // 模拟读取数据的耗时
            sleepSecond(random.nextInt(5));
            String res = "hello word";
            System.out.println("读取数据库数据结束");
            // 提交数据
            guardObject.submit(res);
        },"t2").start();


    }

    private static void sleepSecond(int i)  {
        try {
            TimeUnit.SECONDS.sleep(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果

写入文件前置处理
读取数据库数据开始
读取数据库数据结束
开始写入文件。。。
写入文件结束。。。文件内容: +hello word

2.2 扩展:增加超时时间

比如在get等待获取另一个线程的结果的时候,不能一直等待,有一个超时时间。

public T get(){
    synchronized (this){
        while (null == result){
            // 不能在这一直等,需要一个超时时间
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return result;
    }
}

wait本身就有一个重载方法,支持超时时间的,但是不能这么简单的就把超时时间通过wait来简单的控制:

public T get(long seconds){
        synchronized (this){
            while (null == result){
                // 没有结果时候,就等待
                try {
                    this.wait(seconds);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return result;
        }
    }

测试:

package study.wyy.concurrency.guared.demo1;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

@Slf4j
public class Test2 {
    public static void main(String[] args) {
        GuardObject<String> guardObject = new GuardObject<>();
        new Thread(()->{
            try {
                log.info("开始获取结果");
                String res = guardObject.get(3);
                log.info("拿到结果: => " + res);
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        },"t1").start();
        new Thread(()->{
            log.info("开始准备结果");
            sleepSecond(5);
            guardObject.submit("哈哈哈哈哈");
        },"t2").start();
    }
    private static void sleepSecond(int i)  {
        try {
            TimeUnit.SECONDS.sleep(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:

20:58:03.374 [t1] INFO study.wyy.concurrency.guared.demo1.Test2 - 开始获取结果
20:58:03.377 [t2] INFO study.wyy.concurrency.guared.demo1.Test2 - 开始准备结果
20:58:08.382 [t1] INFO study.wyy.concurrency.guared.demo1.Test2 - 拿到结果: => 哈哈哈哈哈
  • 可以发现:
    • 3s的时候开始获取结果
    • 拿到结果的时候却是8s,已经超时了
  • 分析原因:
    • wait在等待3s后,写入文件的线程会继续get,发现还是null,又继续wait了,和之前的没有实质的区别
      在这里插入图片描述
      所以这里需要我们自己计算等待时间:
 public T get(long timeoutSeconds) throws TimeoutException {
        synchronized (this) {
            // 开始时间
            long beginTime = System.currentTimeMillis();
            // 经历的时间
            long passedTime = 0;
            while (null == result) {
            	// 我设定的单位是秒,乘以1000是为了单位转换
                if (passedTime >= timeoutSeconds * 1000 ) {
                    // 经历的时间已经大于我们的超时时间,就跳出循环
                    throw new TimeoutException();
                }
                try {
                    // 没有结果时候,就等待
                    // 注意的是这里的wait时间,超时时间减去已经等待的时间,防止虚假唤醒
                    // 比如,超时时间是3s
                    // 经历了1s,还没超时,被别人唤醒,但是get结果还是null,
                    // 这个时候,我们只能只需等待2s(超时时间-经历的时间)
                    this.wait(timeoutSeconds * 1000-passedTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 计算经历的时间
                passedTime = System.currentTimeMillis() - beginTime;
            }
            return result;
        }
    }

简化一下代码

public T get(long timeoutSeconds) throws TimeoutException {
      synchronized (this) {
          // 开始时间
          long beginTime = System.currentTimeMillis();
          // 经历的时间
          long passedTime = 0;
          long waitTime = timeoutSeconds * 1000 - passedTime;
          while (null == result) {
          		// passedTime大等于timeoutSeconds就是超时
          		// 根据waitTime = timeoutSeconds * 1000 - passedTime,也就是说
          		// waitTime <=0 也就是超时了
              if (waitTime <= 0) {
                  // 经历的时间已经大于我们的超时时间,就跳出循环
                  throw new TimeoutException();
              }
              try {
                  // 没有结果时候,就等待
                  // 这里等到
                  this.wait(waitTime);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              // 计算经历的时间
              passedTime = System.currentTimeMillis() - beginTime;
          }
          return result;
      }
  }

测试:

package study.wyy.concurrency.guared.demo1;

import lombok.extern.slf4j.Slf4j;

import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@Slf4j
public class Test2 {
    public static void main(String[] args) {
        GuardObject<String> guardObject = new GuardObject<>();
        new Thread(()->{
            try {
                log.info("开始获取结果");
                String res = guardObject.get(3);
                log.info("拿到结果: => " + res);
            } catch (TimeoutException e) {
                log.error("time out,",e);
            }
        },"t1").start();
        new Thread(()->{
            log.info("开始准备结果");
            sleepSecond(5);
            guardObject.submit("哈哈哈哈哈");
        },"t2").start();
    }
    private static void sleepSecond(int i)  {
        try {
            TimeUnit.SECONDS.sleep(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出:

21:22:33.522 [t1] INFO study.wyy.concurrency.guared.demo1.Test2 - 开始获取结果
21:22:33.522 [t2] INFO study.wyy.concurrency.guared.demo1.Test2 - 开始准备结果
21:22:36.538 [t1] ERROR study.wyy.concurrency.guared.demo1.Test2 - time out,
java.util.concurrent.TimeoutException: null
	at study.wyy.concurrency.guared.demo1.GuardObject.get(GuardObject.java:43)
	at study.wyy.concurrency.guared.demo1.Test2.lambda$main$0(Test2.java:23)
	at java.lang.Thread.run(Thread.java:748)

2.3 扩展二:增加队列

如果产生了过多结果,但是没有足够的消费者,就会丢失数据,就可以增加一个缓冲队列。

package study.wyy.concurrency.guared.demo1;

import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.IntStream;

/**
 * @author by wyaoyao
 * @Description
 * @Date 2021/1/20 8:22 下午
 */
@Slf4j
public class Test3 {
    public static void main(String[] args) {
        GuardObject<String> guardObject = new GuardObject<>();
        Random random = new Random();

        new Thread(()->{
            while (true){
                log.info("开始获取结果");
                String res = guardObject.get();
                 sleepSecond(2);
                log.info("拿到结果: => " + res);
            }
        },"t1").start();

        // 10个线程来生产数据
        IntStream.range(1,10).forEach(i->
            new Thread(()->{
                guardObject.submit("第"+ i + "个数据");
            },Integer.toString(i)).start()
        );
    }

    private static void sleepSecond(int i)  {
        try {
            TimeUnit.SECONDS.sleep(i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

21:23:07.775 [t1] INFO study.wyy.concurrency.guared.demo1.Test3 - 开始获取结果
21:23:09.785 [t1] INFO study.wyy.concurrency.guared.demo1.Test3 - 拿到结果: =>1个数据
21:23:09.785 [t1] INFO study.wyy.concurrency.guared.demo1.Test3 - 开始获取结果
21:23:11.789 [t1] INFO study.wyy.concurrency.guared.demo1.Test3 - 拿到结果: =>9个数据
21:23:11.789 [t1] INFO study.wyy.concurrency.guared.demo1.Test3 - 开始获取结果
21:23:13.794 [t1] INFO study.wyy.concurrency.guared.demo1.Test3 - 拿到结果: =>9个数据
21:23:13.795 [t1] INFO study.wyy.concurrency.guared.demo1.Test3 - 开始获取结果
21:23:15.799 [t1] INFO study.wyy.concurrency.guared.demo1.Test3 - 拿到结果: =>9个数据
21:23:15.799 [t1] INFO study.wyy.concurrency.guared.demo1.Test3 - 开始获取结果

显然丢失数据了。

  • 解决:加一个缓冲队列,当队列数据满了不能放数据。

2 案例二

  • tomcat中,当Tomcat处理请求到限制,又来请求,处理不了,就会把请求放到一个队列中等空闲再去处理
  • 定义一个请求实体:
package study.wyy.concurrency.guared;


/**
 *  @author: wyaoyao
 *  @Date: 2020/9/10 9:47 下午
 *  @Description: 请求
 */
public class Request {
    // 简单模拟请求参数中的属性
    private String value;

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public Request(String value) {
        this.value = value;
    }
}
  • 定义队列
package study.wyy.concurrency.guared;


import java.util.LinkedList;

/**
 *  @author: wyaoyao
 *  @Date: 2020/9/10 9:48 下午
 *  @Description: 请求队列
 */
public class RequestQueue {

    LinkedList<Request> queue = new LinkedList<>();

    /**
     *  @author: wyaoyao
     *  @Date: 2020/9/10 9:41 下午
     *  @Description: 服务端端通过该方法向队列中get请求
     */
    public Request getRequest(){
        synchronized (queue){
            while (queue.size()<=0){
                // 请求队列中没有请求,那就等着
                try {
                    queue.wait();
                } catch (InterruptedException e) {
                    // 当被唤醒的时候,需要break出去,进行工作,比如客户端请求来临的时候
                    return null;
                }
            }
            // 请求队列中有请求数据,那就返回出去
            return queue.removeFirst();
        }
    }

    /**
     *  @author: wyaoyao
     *  @Date: 2020/9/10 9:41 下午
     *  @Description: 客户端通过该方法向队列中提交请求
     *
     */
    public void putRequest(Request request){
        synchronized (queue){
           queue.addLast(request);
           // 唤醒其他线程 起来工作,在get的时候,可能没有请求数据,给wait了,一旦放入请求数据,自然就要唤醒
            queue.notifyAll();
        }
    }

}

  • client
package study.wyy.concurrency.guared;


import java.util.Random;

/**
 *  @author: wyaoyao
 *  @Date: 2020/9/10 9:47 下午
 *  @Description: 模拟客户端
 */
public class ClientThread extends Thread{
    private final RequestQueue queue;

    private final Random random;

    public ClientThread(RequestQueue queue) {
        this.queue = queue;
        this.random = new Random(System.currentTimeMillis());
    }

    @Override
    public void run() {
        for(int i =0;i<10;i++){
            // 模拟put 请求 10次
            Request login_request = new Request("login request");
            System.out.println("client -> request " + login_request.getValue());
            queue.putRequest(login_request);
            try {
                Thread.sleep(random.nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

  • server
package study.wyy.concurrency.guared;

import java.util.Objects;
import java.util.Random;

public class ServerThread extends Thread{

    private final RequestQueue queue;

    private final Random random;

    /****
     * 标记:是否关掉
     */
    private volatile Boolean closed = false;

    public ServerThread(RequestQueue queue) {
        this.queue = queue;
        this.random = new Random(System.currentTimeMillis());
    }

    @Override
    public void run() {
        while (!closed){
            Request request = queue.getRequest();
            if(Objects.isNull(request)){
                // 忽略掉这个为null,这里不能break,那就跳出循环,服务就停了
                continue;
            }
            System.out.println("server -> request " + request.getValue());
            try {
                // 模拟处理请求的耗时
                Thread.sleep(random.nextInt(1000));
            } catch (InterruptedException e) {
                //e.printStackTrace();
                // 这里收到中断信号(调用了closed),就return,关闭服务
                return;
            }
        }
    }


    /**
    * @Description 关闭服务
    * @Author  wyaoyao
    * @Date   2020/9/10 9:57 下午
    * @Param
    * @Return
    * @Exception
    */
    public void closed(){
        closed = true;
        // 这里要打断,因为在get请求的时候可能因为队列中没有请求而wait,是无法判断到这个标记的
        this.interrupt();
        System.out.println("server closed");
    }
}

  • 测试
package study.wyy.concurrency.guared;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        final RequestQueue requestQueue = new RequestQueue();
        ServerThread serverThread = new ServerThread(requestQueue);
        serverThread.start();
        new ClientThread(requestQueue).start();
        Thread.sleep(10000);
        serverThread.closed();
    }
}

client -> request login request
server -> request login request
client -> request login request
server -> request login request
client -> request login request
server -> request login request
client -> request login request
server -> request login request
client -> request login request
server -> request login request
client -> request login request
server -> request login request
client -> request login request
client -> request login request
server -> request login request
server -> request login request
client -> request login request
server -> request login request
client -> request login request
server -> request login request
server closed

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值