Skip to main content

3 posts tagged with "log"

View All Tags

· 7 min read
jeesk

本文并不设置某个日志框架的配置,只是介绍其他日志框架和slf4j 的切换与桥接配合使用

当你的项目中有spring ,kafka, hbase , hadoop 等框架的时候, 日志依赖混乱,各种pom眼花缭乱, 看了下面的文字, 让你轻松掌握各种日志框架的混合使用和嫁接.

各种包介绍
  1. log4j

    • log4j : 实现和接口都在这个包
  2. log4j2

    • log4j-core : 核心包,日志实现

    • log4j-api : 日志api

  3. logback

    • logback-core : logback 核心包

    • logback-classic : 实现包,实现了slf4j-api

  4. commons-logging

    • commons-logging : jcl的原生全部内容

    • log4j-jcl : jcl到log4j2的桥梁 , 使用jcl的实现, 但是使用log4j2的接口

    • jcl-over-slf4j :jcl到slf4j的桥梁,使用slf4j 的api, 日志打印 实现使用jcl

日志框架包的分类

  1. api 包, 这种包只有接口, 比如slf4j-api, log4j-api ,每个日志框架都要自己api.

  2. 实现包, 比如slf4j-api 的实现有logback-classic, log4j-api的实现有log4j.

  3. 桥接器 日志统一管理使用slf4j, 但是实现用的jcl, 或者jul,或者log4j , 但是这些实现都不是slf4j的api的实现,只能提供桥接器,将slf4j 打印的日志, 交给具体的实现去操作, 这里的桥接器也可以叫做驱动.

  4. 切换器, 我需要将log4j 切换到Logback , 这个时候, 我可以使用slf4j提供的假包, 替换原有的实现类, 然后将日志重新交给slf4j管理. 比如, 项目使用的是Log4j, 那么直接使用 log4j-over-slf4j 替换log4j 即可. (其实在很多的文章中并没有切换器这个概念, 包括maven的分类中, 但是为了区分功能, 这里我还是区分出来了)

桥接器包

什么是桥接器, 就是某个日志的具体实现, 要想配合slf4j的api使用, 但是在使用slf4j 的api的打印, 无法找到对应的实现类, 这个时候需要第三方包桥接一下, 当slf4j 打印日志的时候,交给具体的日志实现类.

  1. slf4j-jdk14 :slf4j到jdk-logging的桥梁

  2. slf4j-log4j12 :slf4j到log4j1的桥梁

  3. log4j-slf4j-impl :slf4j到log4j2的桥梁

  4. logback-classic :slf4j到logback的桥梁

  5. slf4j-jcl :slf4j到commons-logging的桥梁

切换器包( 偷天换日, slf4j 提供了其他日志的包, 替换了原来的程序, 直接将日志交给了slf4j管理, 并且不需要更改代码. 但是jul 除外)

什么是切换器, 就是当一个模块使用了他自己的日志实现和api , 但是想将日志的输出交给slf4j 管理. 这个时候需要一个切换器将第三方模块的日志切换到slf4j.

  1. jul-to-slf4j :jdk-logging到slf4j的桥梁, 将jul 日志交给slf4j管理, 这个包不能和slf4j-jdk14 共存, 否则会导致无限循环.

  2. jcl-over-slf4j :commons-logging到slf4j的桥梁,commons-logging.jar替换 为 jcl-over-slf4j.jar, 将jcl的日志交给slf4j管理.

  3. slf4j-jcl.jar : 我们的一些用户在切换到 SLF4J API 后意识到,在某些情况下,JCL 的使用是强制性的,他们使用 SLF4J 可能是一个问题。对于这种不常见但很重要的情况,SLF4J 提供了 JCL 绑定,可在文件slf4j-jcl.jar 中找到。JCL 绑定会将通过 SLF4J API 进行的所有日志记录调用委托给 JCL。因此,如果由于某种原因现有应用程序必须 使用 JCL,那么您的应用程序部分仍然可以以对更大的应用程序环境透明的方式针对 SLF4J API 进行编码。您选择的 SLF4J API 对可以继续使用 JCL 的应用程序的其余部分是不可见的。 这种情况下. jcl-overslf4j.jar-不可与slf4j-jcl.jar 同时使用.

  4. log4j-over-slf4j :log4j1到slf4j的桥梁, 而只需将log4j.jar文件替换为 log4j-over-slf4j.jar.

  5. log4j-to-slf4j log4j2 到slf4j的桥梁, 将log4j2的日志移交给slf4j, 原文 原文.

日志方案:

直接使用api和实现
  1. 直接使用java common log 模块: use-jcl-log

  2. 直接使用log4j 的api和实现 模块: use-log4j

  3. 直接使用log4j2 的api和实现 模块: use-log4j2-impl-and-log4j2-api

  4. 直接使用jdk-logging 实现和api 模块: use-jdk-logging

  5. 直接使用slf4j-api和slf4j-simple简单的日志实现 模块: slf4j-sample-impl

使用slf4j自带
  1. 使用slf4j的api,没有任何实现 模块: nologImpl

  2. 使用slf4j的api, 并且同时使用slf4f-nop,和slf4j-sample 实现 模块: multilogImpl

使用第三方实现 和slf4j 的api
  1. 使用log4j 的实现,结合slf4j 的api 模块: user-log4jImpl-and-slf4japi

  2. 使用jdk的实现,结合slf4j 的api 模块: use-jdk-logging-imple-and-sl4j-api

  3. 使用 jcl 日志实现,结合slf4j的api 模块: use-jcl-imple-and-slf4j-api

  4. 使用logback的日志实现,结合slf4j的api 模块: use-logback-imple-and-slf4j-api

  5. 使用log4j2 的日志实现,结合slf4j 的api 模块: use-log4j2-imple-and-slf4j-api

日志切换器,引用的模块使用了自己的日志实现和api, 将日志输出切换到slf4j 上面 (项目中常用,常见依赖处理)
  1. jcl 切换到 slf4j , 最终由logback 打印 模块: remove-common-logging-use-slf4j

  2. log4j 切换到 slf4j ,最终由logback 打印 模块: remove-log4j-use-slf4j-and-use-logback-impl

  3. jdk logging 切换到 slf4j,最终由logback 打印 模块: remove-jdk-loggin-use-slf4j-and-logback-impl

由于作者水平有限, 出现的错误请多多包含. 烦请指点一二.

本文简书地址: https://www.jianshu.com/p/7d12a8c25a38 全文例子如下: https://gitee.com/imomoda/log4-study

· 9 min read
jeesk
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5,
10,
20,
TimeUnit.MINUTES,
new SynchronousQueue<Runnable>(),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread();
}
},
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

}
});

按照上面参数顺序讲解

  1. $\color{#FF0000}{corePoolSize 核心线程池数量 }$
  • 创建线程池的时候没有线程, 当提交任务的时候会陆续创建线程, 当corePoolSize 满的时候, 会将任务放到队列中去, 队列满了, 那么会继续创建 corePoolIsze 到maxPoolSize之间的线程 .
  • 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭
  • prestartAllCoreThread 或者prestartAllCoreThread 初始化核心线程
  1. $\color{#FF0000}{maxPoolSize最大线程池数量}$
  • 当线程数量 = maxPoolSize , 且任务队列已经满了, 线程池会拒绝任务抛出一场
  • 我们的项目 这个参数是 Runtime.getRuntime().availableProcessors() * 4 +1 , 根据压力测试获得最好的最大核心数量
  1. $\color{#FF0000}{keepAliveTime 线程空闲时间}$
  • 空闲的线程能够保持空闲的时间, 超过这个时间, 这一部分线程将被回收.
  • 核心线程到最大线程数量的差异, 如果两个值相等, 那么这个参数毫无意义.
  1. $\color{#FF0000}{BlockingQueue 阻塞队列}$
  • 当核心线程数达到最大时,新任务会放在队列中排队等待执行
  • 常见的队列
    1. ArrayBlockingQueue: 有界队列,基于数组结构,按照队列FIFO原则对元素排序;
    2. LinkedBlockingQueue:无界队列,基于链表结构,按照队列FIFO原则对元素排序,Executors.newFixedThreadPool()使用了这个队列
    3. SynchronousQueue 同步队列, 该队列不存储元素,每个插入操作必须等待另一个线程调用移除操作,否则插入操作会一直被阻塞,Executors.newCachedThreadPool()使用了这个队列;
    4. PriorityBlockingQueue:优先级队列,具有优先级的无限阻塞队列。

6 . $\color{#FF0000}{ThreadFactory 线程工厂}$

  • 自定义线程的名字,daemon,优先级等
  1. $\color{#FF0000}{rejectedExecutionHandler 拒绝策略}$
  • 这里的拒绝可以采取你自定义的办法, 比如使用second 线程池处理 r , 或者丢弃r, 或者打印出日志, 取决于你的业务.
  • 线程池执行拒绝的两种情况
    1. 当线程数量达到maxPoolSize , 队列已经满了, 会拒绝新任务
    2. 当线程池调用shutdown 的时候, 会等待线程池的任务执行完毕, 再shutdown, 这是一种优雅的方式. 调用shutdown和shutdown关闭之前, 这段时间会拒绝新任务. shutdownNow, 会立刻关闭, 并且停止执行任务. 和shutdown 有很大区别.
  • 几种拒绝策略
    1. AbortPolicy 丢弃任务, 抛运行异常(如果不设置, 默认就是这一个策略)
    2. CallerRunsPolicy 执行任务
    3. DiscardPolicy 忽视,什么都不会发生
    4. DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务

线程执行的一些流程

  1. 如果当前线程池中的线程数目 小于 < corePoolSize 。则对于每个新任务,都会创建一个线程去执行这个任务(即使core线程中也有空闲线程, 也会新创建线程会执行)。
  2. 如果当前线程池中的线程数目 大于等于 >= corePoolSize 。 对于每个新任务,会将其添加到任务队列中。若添加成功,则任务由空闲的线程主动将其从队列中移除后执行。若添加失败(任务队列已满),则尝试创建新的线程执行。
  3. 若当前线程池中的线程数目到达 maximumPoolSize ,则对于新任务采取拒绝策略。
  4. 如果线程池中的数量大于 corePoolSize 时,如果某个线程空闲时间超过 keepAliveTime ,线程会被终止,直到线程池中的线程数目不超过 corePoolSize 。
  5. 如果为核心线程池中的线程设置存活时间,则当核心线程池中的线程空闲时间超过 keepAliveTime ,则该线程也会被终止
  6. 如果核心线程数已经达到, 如果没有队列没有满的话, 是不会创建新的线程. 有时候取决你使用什么队列. 比如使用 ArrayBlockingQueue(10), 当核心线程已经创建完成, 只有当队列满了之后才会继续创建新的线程. 如果你使用的是SynchronousQueue, 内部只能一个的队列,那么只有队列直接创建core线程到maxpoolSize 之间的线程

线程池性能优化建议

  1. 建议使用 prestartAllCoreThread 或者prestartAllCoreThread 初始化核心线程
  2. 考虑 allowCoreThreadTimeOut 允许核心线程能够回收节约机器的使用.
  3. 拒绝策略可以将任务, 提交给 second 线程池处理.
  4. 自定义ThreadFactory ,标识线程, 方便排查线程的使用情况

关于线程异常是否处理的问题:

  1. execute : 如果不手动捕获一场, 线程池只能重新创建新的异常来填补空白,重新创建线程这是有代价的
  2. submit: 因为能够调用 future.get(). 所以有异常也会捕获, 不会造成线程终止.
// 证明execute 的异常
@Test
public void test1() throws InterruptedException {
Thread.setDefaultUncaughtExceptionHandler((Thread t, Throwable e) -> {
log.warn("Exception in thread {}", t, e);
});
String prefix = "test";
ExecutorService threadPool = Executors.newFixedThreadPool(1, new ThreadUtil.ThreadFactoryImpl(prefix));
IntStream.rangeClosed(1, 10).forEach(i -> threadPool.execute(() -> {
if (i == 5) {
throw new RuntimeException("error");
}
log.info("I'm done : {}", i);
System.out.println(Thread.currentThread().getName() + " I'm done : " + i);
if (i < 5) {
Assert.assertEquals(prefix + "1", Thread.currentThread().getName());
} else {
Assert.assertEquals(prefix + "2", Thread.currentThread().getName());
}

}));
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
// 本来是通过test 1 线程执行的, 后面出现异常 确是test2 执行的, 说明x线程已经终止2, 并且重新创建线程
}
// 线程名称没有变, 说明已经帮你捕获异常.
String prefix = "test";
ExecutorService threadPool = Executors.newFixedThreadPool(1, new ThreadUtil.ThreadFactoryImpl(prefix));
List<Future> futures = new ArrayList<>();
IntStream.rangeClosed(1, 10).forEach(i -> futures.add(threadPool.submit(() -> {
if (i == 5) {
throw new RuntimeException("error");
}
log.info("I'm done : {}", i);
// if (i < 5) Assert.assertEquals(prefix + "1", Thread.currentThread().getName());
// else Assert.assertEquals(prefix + "2", Thread.currentThread().getName());
})));

for (Future future : futures) {
try {
future.get();
} catch (ExecutionException e) {
log.warn("future ExecutionException", e);
}
}
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);

面试问题

  1. 说说线程池的核心参数有哪些
  2. 说说你们的corePoolSize 的数量是如何设置,超时时间如何设置
  3. 你们使用的是什么队列, 为什么使用这个队列.
  4. 你们项目是如何优化自己的线程池参数的.
  5. 当线程池还没达到 corePoolSize 的时候, 线程池里面有空闲线程, 这个时候来了一个新的任务, 线程池是创建新的线程还是使用空闲线程 ?

路过点赞, 月入10w

· 2 min read
jeesk

log4j-slf4j-impl: log4j的日志转到slf4j上

LoggerFactory.getLogger () 方法 寻找对应的logger

org.slf4j.LoggerFactory#findPossibleStaticLoggerBinderPathSet 寻找staticLoggerBinder

paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH); 通过路径寻找所有的binder, 这里的STATIC_LOGGER_BINDER_PATH = org/slf4j/impl/StaticLoggerBinder.class, 每个slf4j的具体实现类都有一个StaticLoggerBinder.class

org.slf4j.LoggerFactory#reportMultipleBindingAmbiguity 判断是否有多个binder, 如果有将会打印日志: SLF4J: Class path contains multiple SLF4J bindings. SLF4J: Found binding in [jar:file:/Z:/repository/org/slf4j/slf4j-simple/1.7.28/slf4j-simple-1.7.28.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: Found binding in [jar:file:/Z:/repository/org/slf4j/slf4j-nop/1.7.28/slf4j-nop-1.7.28.jar!/org/slf4j/impl/StaticLoggerBinder.class] SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.., 这个日志是我们常见的, 因为有多个日志实现

org/slf4j/impl/StaticLoggerBinder.class, 每个slf4j的日志实现都有这个类 找到所有绑定的logger

调用 StaticLoggerBinder.getSingleton(); 这里如果有多个class, 应该是按顺序调用第一个class的getSingleton(), 调用 reportActualBinding 打印绑定的是哪一个日志实现 SLF4J: Actual binding is of type [org.slf4j.impl.SimpleLoggerFactory] getILoggerFactory() 获得到刚才StaticLoggerBinder的日志工厂 SimpleLoggerFactory.getLogger public Logger getLogger(String name) { Logger simpleLogger = (Logger)this.loggerMap.get(name); if (simpleLogger != null) { return simpleLogger; } else { Logger newInstance = new SimpleLogger(name); Logger oldInstance = (Logger)this.loggerMap.putIfAbsent(name, newInstance); return (Logger)(oldInstance == null ? newInstance : oldInstance); } } 指定的logger 作缓存, 然后调用info() 方法打印日志.