一:前言

  写这个程序主要是用来理解生产者消费者模型,以及通过这个Demo来理解Redis的单线程取原子任务是怎么实现的和巩固一下并发相关的知识;这个虽然是个Demo,但是只要稍加改下Appender部分也是可以用于项目中的,假设项目里确实不需要log4j/logback之类的日志组件的时候;

二:实现方式

1.利用LinkedList作为MQ(还可以用jdk自带的LinkedBlockingQueue,不过这个Demo主要是为了更好的理解原理因此写的比较底层);

2.利用一个Daemon线程作为消费者从MQ里实时获取日志对象/日志记录,并将它提交给线程池,由线程池再遍历所有的appender并调用它们的通知方法,这个地方还可以根据场景进行效率优化,如将循环遍历appender改为将每个appender都再此提交到线程池实现异步通知观察者;

3.为生产者提供log方法作为生产日志记录的接口,无论是生产日志对象还是消费日志对象在操作队列时都需要对队列加锁,因为个人用的是非并发包里的;

4.消费者在获取之前会先判断MQ里是否有数据,有则获取并提交给线程池处理,否则wait;

5.生产者生产了日志对象后通过notify通知消费者去取,因为只有一个消费者,而生产者是不会wait的因此只需要notify而不用notifyAll

6.。。剩下的就看代码来说明吧;

三:代码

1.MyLogger类的实现

package me.silentdoer.mqlogger.log;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import static me.silentdoer.mqlogger.log.MyLogger.LogLevel.DEBUG;
import static me.silentdoer.mqlogger.log.MyLogger.LogLevel.ERROR; /**
* @author silentdoer
* @version 1.0
* @description 这里只是做一个简单的logger实现,不提供Appender之类的功能,主要是用来学习生产者和消费者及MQ的实现原理
* @date 4/26/18 6:07 PM
*/
public class MyLogger{
private LogLevel loggerLevel = DEBUG;
private String charset = "UTF-8"; // 暂且没用,但是当需要序列化时是可能用到的;
// TODO 也可以直接用LinkedQueue,然后手动通过ReentrantLock来实现并发时的数据安全(synchronized也可)
//private BlockingQueue<LogRecord> queue = new LinkedBlockingQueue<LogRecord>(); // 可以理解为支持并发的LinkedList
// TODO 想了一下既然是要学习原理干脆就实现的更底层一点
private final Queue<LogRecord> records = new LinkedList<LogRecord>();
// TODO 用于记录生产了多少条日志,可供外部获取
private AtomicLong produceCount = new AtomicLong(0);
// TODO 用于记录消费了多少条日志
private AtomicLong consumeCount = new AtomicLong(0);
// TODO 日志记录的Consumer
private Thread consumer = new LogDaemon(); public MyLogger(){
consumer.setDaemon(true);
consumer.start();
} /**
* 对外提供的接口,即log方法就是生产者用于生产日志数据的接口
* @param msg
* @param level
*/
public void log(String msg, LogLevel level){
Date curr = generateCurrDate();
log(new LogRecord(level, msg, curr));
} /**
* 对外提供的接口,即log方法就是生产者用于生产日志数据的接口
* @param msg
*/
public void log(String msg){
Date curr = generateCurrDate();
log(new LogRecord(this.loggerLevel, msg, curr));
} /**
* 给生产者(即调用log的方法都可以理解为生产者在生产日志对象)提供用于生产日志记录的接口
* @param record
*/
public void log(LogRecord record){
// ReentrantLock可以替代synchronized,不过当前场景下synchronized已经足够
synchronized (this.records){ // TODO 如果用的是LinkedBlockingQueue是不需要这个的
this.records.offer(record);
this.produceCount.incrementAndGet();
this.records.notify(); // TODO 只有一个线程会records.wait(),因此notify()足够
}
} // TODO 类似Redis的那个单线程,用于读取命令对象,而这里则是用于读取LogRecord并通过appender将数据写到相应位置
private class LogDaemon extends Thread{
private volatile boolean valid = true;
// 充当appenders的角色
private List<Writer> appenders = null;
private ExecutorService threadPool = new ThreadPoolExecutor(1, 3
, 180000, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024)); @Override
public void run() {
while(this.valid){
// TODO 根据最少知道原则,在这里不要去想整体里是否存在打断此线程的地方,你就认为此线程是可能被外界打断的即可,因此需要做一定处理
try {
synchronized (MyLogger.this.records) {
if (MyLogger.this.records.size() <= 0) {
MyLogger.this.records.wait();
}
final LogRecord firstRecord = MyLogger.this.records.poll();
MyLogger.this.consumeCount.incrementAndGet();
//threadPool.submit()
threadPool.execute(() -> MyLogger.this.notifyAppender(this.appenders, firstRecord));
}
}catch (InterruptedException ex){
this.valid = false;
ex.printStackTrace();
}catch (Throwable t){
t.printStackTrace();
}
}
}
} private void notifyAppender(final List<Writer> appenders, final LogRecord record) {
if(appenders == null){
PrintWriter writer = new PrintWriter(record.level == ERROR ? System.err : System.out);
writer.append(record.toString());
writer.flush();
}else{
// TODO 这种是同步的方式,如果是异步的方式可以将每个appender的执行都由一个Runnable对象包装,然后submit给线程池(或者中间加个中间件)
for(Writer writer : appenders){
try {
writer.append(record.toString());
}catch (IOException ex){
ex.printStackTrace();
}
}
}
} /**
* 用于产生当前时间的模块,防止因为并发而导致LogRecord的timestamp根实际情况不符
*/
private Lock currDateLock = new ReentrantLock(); // 直接用synchronized亦可
private Date generateCurrDate(){
currDateLock.lock();
Date result = new Date();
currDateLock.unlock();
return result;
} // 生产者生产的数据对象
public static class LogRecord{
private LogLevel level;
private String msg;
private Date timestamp;
private static final SimpleDateFormat DEFAULT_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS");
private SimpleDateFormat dateFormat = DEFAULT_DATE_FORMAT; /*public LogRecord(){
this(INFO, "");
}*/ public LogRecord(LogLevel level, String msg){
this(level, msg, new Date()); // 还是最好由外界设置timestamp,否则高并发下会比较不准
} // TODO 最好用这个,不然高并发下timestamp容易出现顺序不准确的情况。
public LogRecord(LogLevel level, String msg, Date timestamp){
this.level = level;
this.msg = msg;
this.timestamp = timestamp;
} @Override
public String toString(){
return String.format("[Level:%s, Datetime:%s] : %s\n", level, dateFormat.format(timestamp), msg);
} public LogLevel getLevel() {
return level;
} public String getMsg() {
return msg;
} public void setDateFormat(SimpleDateFormat dateFormat) {
this.dateFormat = dateFormat;
} public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
} public enum LogLevel{ // TODO 内部enum默认就是static
INFO,
DEBUG,
ERROR
} public LogLevel getLoggerLevel() {
return loggerLevel;
} public void setLoggerLevel(LogLevel loggerLevel) {
this.loggerLevel = loggerLevel;
} public String getCharset() {
return charset;
} public void setCharset(String charset) {
this.charset = charset;
} public AtomicLong getProduceCount() {
return produceCount;
} public AtomicLong getConsumeCount() {
return consumeCount;
}
}

2.测试用例1

package me.silentdoer.mqlogger;

import me.silentdoer.mqlogger.log.MyLogger;

import java.util.Scanner;

/**
* @author silentdoer
* @version 1.0
* @description the description
* @date 4/26/18 10:13 PM
*/
public class Entrance {
private static MyLogger logger = new MyLogger(); public static void main(String[] args){
//logger.setLoggerLevel(MyLogger.LogLevel.ERROR);
Scanner scanner = new Scanner(System.in);
String line;
while(!(line = scanner.nextLine()).equals("exit")){
if(line.equals(""))
continue;
logger.log(line);
System.out.println(String.format("共生产了%s条日志。", logger.getConsumeCount()));
try {
Thread.sleep(500);
}catch (InterruptedException ex){ }
System.out.println(String.format("共消费了%s条日志。", logger.getProduceCount()));
}
}
}

3.测试用例2

package me.silentdoer.mqlogger;

import me.silentdoer.mqlogger.log.MyLogger;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; /**
* @author silentdoer
* @version 1.0
* @description the description
* @date 4/26/18 10:32 PM
*/
public class Entrance2 {
private static MyLogger logger = new MyLogger(); public static void main(String[] args){
logger.setLoggerLevel(MyLogger.LogLevel.ERROR);
ExecutorService threadPool = Executors.newCachedThreadPool();
for(int i=0;i<10;i++){
final int index = i + 1;
threadPool.execute(() -> {
logger.log(String.format("生产的第%s条记录。", index));
System.out.println(String.format("共生产了%s条记录。", index));
});
try {
Thread.sleep(100);
}catch (InterruptedException ex){ }
}
try {
Thread.sleep(3000);
System.out.println(String.format("共%s条记录被消费。", logger.getConsumeCount()));
}catch (InterruptedException ex){ }
//threadPool.shutdown();
//threadPool.shutdownNow();
}
}

四:补充

  如果想实现像BlockingQueue一样能够控制MQ的元素个数范围,则可以通过ReentrantLock的Confition来实现,即通过lock创建两个Condition对象,一个用来描述是否MQ中元素达到上限的情况,一个用于描述MQ中元素降到下限的情况;

无论是达到上限或降到下限都会通过相应的condition对象来阻塞对应的生产者或消费者的生产/消费过程从而实现MQ元素个数的可控性;

利用生产者消费者模型和MQ模型写一个自己的日志系统-并发设计里一定会用到的手段的更多相关文章

  1. 复杂领域的Cynefin模型和Stacey模型

    最近好奇“复杂系统”,收集了点资料,本文关于Cynefin模型和Stacey模型.图文转自互联网后稍做修改. Cynefin模型提供一个从因果关系复杂情度来分析当前情况而作决定的框架,提出有五个领域: ...

  2. 文本信息检索——布尔模型和TF-IDF模型

    文本信息检索--布尔模型和TF-IDF模型 1. 布尔模型 ​ 如要检索"布尔检索"或"概率检索"但不包括"向量检索"方面的文档,其相应的查 ...

  3. 贫血模型和DDD模型

    贫血模型和DDD模型 1.贫血模型 1.1 概念 常见的mvc三层架构 简单.没有行为 2.领域驱动设计 2.1 概念(2004年提出的) Domain Driven Design 简称 DDD DD ...

  4. 并发编程:Actors 模型和 CSP 模型

    https://mp.weixin.qq.com/s/emB99CtEVXS4p6tRjJ2xww 并发编程:Actors 模型和 CSP 模型 ImportNew 2017-04-27    

  5. 三分钟掌控Actor模型和CSP模型

    回顾一下前文<三分钟掌握共享内存模型和 Actor模型> Actor vs CSP模型 传统多线程的的共享内存(ShareMemory)模型使用lock,condition等同步原语来强行 ...

  6. Python之小测试:用正则表达式写一个小爬虫用于保存贴吧里的所有图片

    很简单的两步: 1.获取网页源代码 2.利用正则表达式提取出图片地址 3.下载 #!/usr/bin/python #coding=utf8 import re # 正则表达式 import urll ...

  7. 使用PHP文件锁写一个多个请求同时并发写入一个文件,要求不脏读、数据不丢失

    使用PHP文件锁写一个多个请求同时并发写入一个文件,要求不脏读.数据不丢失. //并发文件操作 function filehandle($filename,$data){ $start = 0; $e ...

  8. java——利用生产者消费者模式思想实现简易版handler机制

    参考教程:http://www.sohu.com/a/237792762_659256 首先说一下这里面涉及到的线程: 1.mainLooper: 这个线程可以理解为消费者线程,里面运行了一个死循环, ...

  9. Inception模型和Residual模型卷积操作的keras实现

    Inception模型和Residual残差模型是卷积神经网络中对卷积升级的两个操作. 一.  Inception模型(by google) 这个模型的trick是将大卷积核变成小卷积核,将多个卷积核 ...

随机推荐

  1. 05_Linux目录文件操作命令2_我的Linux之路

    这一节我们继续来学习Linux中对文件和目录的操作命令 mkdir 创建目录 mkdir (选项)(参数) 在Linux端可以使用mkdir来创建目录,如果你没有加其他的路径名,那么默认是在当前目录下 ...

  2. [机器学习实战]K-近邻算法

    1. K-近邻算法概述(k-Nearest Neighbor,KNN) K-近邻算法采用测量不同的特征值之间的距离方法进行分类.该方法的思路是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近 ...

  3. 简述angular自定义过滤器在页面和控制器中的使用

    首先设置自定义过滤器. 定义模块名:angular ? 1 2 3 4 5 6 .module('myApp') .filter('filterName',function(){ return fun ...

  4. Centos MySQL数据库迁移详细步骤

    其实迁移数据库,一般用sql文件就行,把A服务器数据库的表结构和数据等等导出,然后导入到B服务器数据库, 但是这次数据文件过大,大约有40个G,使用命令行导入,效果不是很好,经常在执行过程中报错.卡死 ...

  5. Java 接口基础详解

    目录 Java接口示例 实现一个接口 接口实例 实现多个接口 方法签名重叠 接口变量 接口方法 接口默认方法 接口与继承 继承与默认方法 接口与多态性 在Java中,接口是一个抽象类型,有点类似于类, ...

  6. [LeetCode] Employee Importance 员工重要度

    You are given a data structure of employee information, which includes the employee's unique id, his ...

  7. zabbix利用orabbix监控oracle

    Orabbix 是一个用来监控 Oracle 数据库实例的 Zabbix 插件.(插件安装在zabbix-server端) 下载地址:http://www.smartmarmot.com/produc ...

  8. spring源码阅读(2)核心类介绍

    (1).BeanFactory作为一个主接口不继承任何接口,暂且称为一级接口. (2).有3个子接口继承了它,进行功能上的增强.这3个子接口称为二级接口. (3).ConfigurableBeanFa ...

  9. 机器学习基石:10 Logistic Regression

    线性分类中的是非题------>概率题, 设置概率阈值后,大于等于该值的为O,小于改值的为X.------>逻辑回归. O为1,X为0: 逻辑回归假设: 逻辑函数/S型函数:光滑,单调, ...

  10. mv&cp

    mv [选项] [源] [目标] 当目标不存在时,重命名源为目标 当目标存在时,若目标为目录文件,将源移动到目标文件里: 若目标为非目录文件,将源重命名为目标,并强制覆盖目标.   mv -b 1 2 ...