定义: 一种异步编程模式,允许任务的提交(对异步方法的调用)和任务的执行(异步方法的真正执行)分离
Proxy: 负责对外暴露异步方法接口
asyncService: 该异步方法负责创建与该方法相应的MethodRequest参与者实例,并将其提交给Scheduler参与者实例。该方法的返回值是一个Future参与者实例,客户端代码可以通过它获取异步方法对应的任务的执行结果
MethodRequest: 负责将客户端代码对Proxy实例的异步方法的调用封装成一个对象,该对象保留异步方法的名称及客户端代码传递的参数等上下文参数。它使Proxy的异步方法的调用和执行分离成为可能
call: 根据其所属MethodRequest实例所包含的上下文信息调用Servant实例的相应方法
ActivationQueue: 缓冲区,用于临时存储由Proxy的异步方法被调用时所创建的MethodRequest实例
enqueue: 将MethodRequest实例放入缓冲区
dequeue: 从缓冲区中取出一个MethodRequest实例
Scheduler: 负责将Proxy的异步方法所创建的MethodRequest实例存入其维护的缓冲区中,并根据一定的调度策略,对其维护的缓冲区中的MethodRequest实例进行执行。其调度策略可以根据实际需要来定,如FIFO,LIFO和根据MethodRequest中包含的信息所定的优先级等
enqueue: 接受一个MethodRequest实例,并将其存入缓冲区
dispatch: 反复地从缓冲区中取出MethodRequest实例进行执行
Servant: 负责Proxy所暴露的异步方法的具体实现
doSomething: 执行Proxy所暴露的异步方法对应的任务
Future: 负责存储和获取主动对象模式异步方法的执行结果
get: 获取异步方法对应的任务的执行结果
set: 设置异步方法对应的任务的执行结果
有时候请求发送过来会因为网络问题或者数据库问题等问题出现失败的情况,为了避免错失重要的请求,这时候可以将请求做持久化处理,缓存到磁盘,等问题解决后再继续处理。由于缓存到磁盘涉及到文件IO,操作缓慢,不希望因此导致延时的增加,此时可以使用异步处理的方式。下列代码MMSDeliveryServlet#doPost()方法检测到有数据库发送异常,调用store()方法将请求缓存起来。
package com.bruce.activeObject;
import java.io.Serializable;
import java.util.Arrays;
/**
* @Author: Bruce
* @Date: 2019/6/4 21:33
* @Version 1.0
*/
public class Attachment implements Serializable {
private static final long serialVersionUID = -3242951327009946609L;
private String contentType;
private byte[] content = new byte[0];
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
@Override
public String toString() {
return "Attachment {" +
"contentType='" + contentType + '\'' +
", content=" + Arrays.toString(content) +
'}';
}
}
package com.bruce.activeObject;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* @Author: Bruce
* @Date: 2019/6/4 21:31
* @Version 1.0
*/
public class Recipient implements Serializable {
private static final long serialVersionUID = 2208629750371068868L;
private Set<String> to = new HashSet<String>();
public void addTo(String msisdn) {
to.add(msisdn);
}
public Set<String> getToList() {
return (Set<String>) Collections.unmodifiableCollection(to);
}
}
package com.bruce.activeObject;
import java.io.Closeable;
/**
* @Author: Bruce
* @Date: 2019/6/4 21:44
* @Version 1.0
*/
public interface RequestPersistence extends Closeable {
void store(MMSDeliverRequest request);
}
package com.bruce.activeObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* @Author: Bruce
* @Date: 2019/6/4 21:43
* @Version 1.0
*/
public class AsyncRequestPersistence implements RequestPersistence {
private static final long ONE_MINUTE_IN_SECONDS = 60;
final static Logger logger = LoggerFactory.getLogger(AsyncRequestPersistence.class);
final AtomicLong taskTimeConsumedPerInterval = new AtomicLong(0);
final AtomicLong requestSubmittedPerInterval = new AtomicLong(0);
private final DiskbasedRequestPersistence delegate =
new DiskbasedRequestPersistence();
private final ThreadPoolExecutor scheduler;
private static class InstanceHolder {
final static RequestPersistence INSTANCE = new AsyncRequestPersistence();
}
private AsyncRequestPersistence() {
scheduler = new ThreadPoolExecutor(1, 3, 60 * ONE_MINUTE_IN_SECONDS, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(200), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t;
t = new Thread(r, "AsyncRequestPersistence");
return t;
}
});
scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(6);
TimerTask task = new TimerTask() {
@Override
public void run() {
if (logger.isInfoEnabled()) {
logger.info("task count: " + requestSubmittedPerInterval
+ ", Queue size: " + scheduler.getQueue().size()
+ ", taskTimeConsumedPerInterval: "
+ taskTimeConsumedPerInterval.get() + " ms");
}
taskTimeConsumedPerInterval.set(0);
requestSubmittedPerInterval.set(0);
}
};
scheduledExecutorService.scheduleAtFixedRate(task, 0,ONE_MINUTE_IN_SECONDS * 1000, TimeUnit.SECONDS);
}
public static RequestPersistence getInstance() {
return InstanceHolder.INSTANCE;
}
@Override
public void store(final MMSDeliverRequest request) {
Callable<Boolean> methodRequest = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
long start = System.currentTimeMillis();
try {
delegate.store(request);
} finally {
taskTimeConsumedPerInterval.addAndGet(System.currentTimeMillis() - start);
}
return Boolean.TRUE;
}
};
scheduler.submit(methodRequest);
requestSubmittedPerInterval.incrementAndGet();
}
@Override
public void close() throws IOException {
scheduler.shutdown();
}
}
package com.bruce.activeObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.rmi.runtime.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author: Bruce
* @Date: 2019/6/4 21:47
* @Version 1.0
*/
public class DiskbasedRequestPersistence implements RequestPersistence{
final SectionBasedDiskStorage storage = new SectionBasedDiskStorage();
private Logger LOG = LoggerFactory.getLogger(DiskbasedRequestPersistence.class);
@Override
public void store(MMSDeliverRequest request) {
String[] fileNameParts = storage.apply4Filename(request);
File file = new File(fileNameParts[0]);
try (ObjectOutputStream objOutput = new ObjectOutputStream(new FileOutputStream(file));) {
objOutput.writeObject(request);
} catch (IOException e) {
storage.decrementSectionFileCount(fileNameParts[1]);
LOG.error("Failed to store request");
}
}
@Override
public void close() throws IOException {
}
class SectionBasedDiskStorage {
private Deque<String> sectionNames = new LinkedList<String>();
private Map<String/*Storage Directory*/, AtomicInteger/*Counter*/> sectionFileCountMap =
new HashMap<>();
private int maxFilesPerSection = 2000;
private int maxSectionCount = 100;
private String storageBaseDir = "C:\\Users";
private final Object sectionLock = new Object();
public SectionBasedDiskStorage() {
File dir = new File(storageBaseDir);
if (!dir.exists()) {
dir.mkdirs();
}
}
public String[] apply4Filename(MMSDeliverRequest request) {
String sectionName;
int iFileCount;
String oldestSectionName = null;
String[] fileName = new String[2];
synchronized (sectionLock) {
sectionName = getSectionName();
AtomicInteger fileCount;
fileCount = sectionFileCountMap.get(sectionName);
iFileCount = fileCount.get();
if (iFileCount >= maxFilesPerSection) {
if (sectionNames.size() >= maxSectionCount) {
oldestSectionName = sectionNames.removeFirst();
}
sectionName = makeNewSectionDir();
fileCount = sectionFileCountMap.get(sectionName);
}
iFileCount = fileCount.addAndGet(1);
}
fileName[0] = storageBaseDir + "/" + sectionName + "/"
+ new DecimalFormat("0000").format(iFileCount) + "-"
+ request.getTimeStamp().getTime() / 1000 + "-"
+ request.getExpiry() + ".rq";
fileName[1] = sectionName;
if (null != oldestSectionName) {
this.removeSection(oldestSectionName);
}
return fileName;
}
public void decrementSectionFileCount(String sectionName) {
AtomicInteger fileCount = sectionFileCountMap.get(sectionName);
if (null != fileCount) {
fileCount.decrementAndGet();
}
}
private boolean removeSection(String sectionName) {
boolean result = true;
File dir = new File(storageBaseDir + "/" + sectionName);
for (File file : dir.listFiles()) {
result = result && file.delete();
}
result = result && dir.delete();
return result;
}
private String getSectionName() {
String sectionName;
if (sectionNames.isEmpty()) {
sectionName = this.makeNewSectionDir();
} else {
sectionName = sectionNames.getLast();
}
return sectionName;
}
private String makeNewSectionDir() {
String sectionName;
SimpleDateFormat sdf = new SimpleDateFormat("MMddMM HHmmss");
sectionName = sdf.format(new Date());
File dir = new File(storageBaseDir + "/" + sectionName);
if (dir.mkdir()) {
sectionNames.addLast(sectionName);
sectionFileCountMap.put(sectionName, new AtomicInteger(0));
} else {
throw new RuntimeException("Cannot create section dir " + sectionName);
}
return sectionName;
}
}
}
package com.bruce.activeObject;
import java.io.Serializable;
import java.util.Date;
/**
* @Author: Bruce
* @Date: 2019/6/4 21:29
* @Version 1.0
*/
public class MMSDeliverRequest implements Serializable {
private String transactionID;
private String messageType = "Delivery.req";
private String senderAddress;
private Recipient recipient = new Recipient();
private String subject;
private Attachment attachment = new Attachment();
private long expiry;
private Date timeStamp;
public MMSDeliverRequest() {
}
public String getTransactionID() {
return transactionID;
}
public void setTransactionID(String transactionID) {
this.transactionID = transactionID;
}
public String getMessageType() {
return messageType;
}
public void setMessageType(String messageType) {
this.messageType = messageType;
}
public String getSenderAddress() {
return senderAddress;
}
public void setSenderAddress(String senderAddress) {
this.senderAddress = senderAddress;
}
public Recipient getRecipient() {
return recipient;
}
public void setRecipient(Recipient recipient) {
this.recipient = recipient;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public Attachment getAttachment() {
return attachment;
}
public void setAttachment(Attachment attachment) {
this.attachment = attachment;
}
public long getExpiry() {
return expiry;
}
public void setExpiry(long expiry) {
this.expiry = expiry;
}
public Date getTimeStamp() {
return timeStamp;
}
public void setTimeStamp(Date timeStamp) {
this.timeStamp = timeStamp;
}
@Override
public String toString() {
return "MMSDeliverRequest {" +
"transactionID='" + transactionID + '\'' +
", messageType='" + messageType + '\'' +
", senderAddress='" + senderAddress + '\'' +
", recipient=" + recipient +
", subject='" + subject + '\'' +
", attachment=" + attachment +
", timeStamp=" + timeStamp +
'}';
}
}
package com.bruce.activeObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
/**
* @Author: Bruce
* @Date: 2019/6/4 21:22
* @Version 1.0
*/
public class MMSDeliveryServlet extends HttpServlet {
private Logger LOG = LoggerFactory.getLogger(MMSDeliverRequest.class);
private static final long serialVersionUID = -5177695993579658630L;
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
MMSDeliverRequest mmsDeliveryRequest = this.parseRequest(req.getInputStream());
Recipient shortNumberRecipient = mmsDeliveryRequest.getRecipient();
Recipient originalNumberRecipient = null;
try {
originalNumberRecipient = convertShortNumber(shortNumberRecipient);
} catch (SQLException e) {
AsyncRequestPersistence.getInstance().store(mmsDeliveryRequest);
resp.setStatus(202);
}
LOG.info(String.valueOf(originalNumberRecipient));
}
private MMSDeliverRequest parseRequest(InputStream reqInputStream) {
MMSDeliverRequest mmsDeliverRequest = new MMSDeliverRequest();
return mmsDeliverRequest;
}
private Recipient convertShortNumber(Recipient shortNumberRecipient) throws SQLException {
Recipient recipient = null;
return recipient;
}
}
package com.bruce.activeObject;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author: Bruce
* @Date: 2019/6/5 2:12
* @Version 1.0
*/
public class CaseRunner {
public static void main(String[] args) {
int numRequestThreads = Runtime.getRuntime().availableProcessors();
int targetTPS = 50;
float duration = 12;
final RequestSender reqSender = new RequestSender(
(int) duration * targetTPS
);
}
static class RequestSender implements Runnable {
final AtomicInteger totalCount = new AtomicInteger();
private final RequestPersistence persistence;
private final Attachment attachment;
public RequestSender(int n) {
totalCount.set(n);
persistence = AsyncRequestPersistence.getInstance();
attachment = new Attachment();
try {
URL url = CaseRunner.class.getClassLoader().getResource("C:\\Users\\Bruce\\Pictures\\Camera Roll\\0.jpg");
attachment.setContentType("image/jpeg");
attachment.setContent(Files.readAllBytes(Paths.get(url.toURI())));
} catch (URISyntaxException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
int remainingCount;
while ((remainingCount = totalCount.getAndDecrement()) > 0) {
MMSDeliverRequest request = new MMSDeliverRequest();
request.setTransactionID(String.valueOf(remainingCount));
request.setSenderAddress("1234566987");
request.setTimeStamp(new Date());
request.setExpiry((System.currentTimeMillis() + 3600000) / 1000);
request.setSubject("Hi");
request.getRecipient().addTo("776");
request.setAttachment(attachment);
persistence.store(request);
}
}
public void shutdown() {
try {
persistence.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
主动对象模式的好处是将方法的调用和执行分离,实现了异步编程,有利于提高并发性,进而提高系统的吞吐率
同时,将任务的提交和任务的执行策略分离,降低系统耦合性。
因此,我们可以在任务的执行策略方面做很多拓展,如
1. 采用何种顺序执行任务,如FIFO,LIFO,或者基于任务的优先级
2. 多少个任务可以并发执行
3. 多少个任务可以被排队等待执行
4. 如果有任务由于系统过载被拒绝,此时哪个任务该被舍弃,应用程序如何被通知到
5. 任务执行前和执行后需要哪些操作
因为此模式会用到可控的无界队列或者有界队列,因此有必要对缓冲区进行监控
如果缓冲区饱和,此时可以使用JDK提供的策略实现来应对(ThreadPoolExecutor类中包含四种处理策略),即
1. ThreadPoolExecutor.AbortPolicy
2. ThreadPoolExecutor.DiscardPolicy
3. ThreadPoolExecutor.DiscardOldestPolicy
4. ThreadPoolExecutor.CallerRunsPolicy
由于使用到线程池,此时也有必要考虑清理空闲的线程以节约资源,因此,在初始化ThreadPoolExecutor实例时指定存活时间,这样当空闲的线程超过这个时间仍然没有任务要处理,则将它清理掉
参考资料