又踩坑了。ThreadPoolExecutor?

问题是出现在 24 号的时候,当时有台 weblogic 实例出现阻塞,运维 dump 线程栈后重启了,有个同事进行分析。

该同事分析线程栈后认为问题出在一个被外部系统调用的接口,这个接口收到请求后会从数据库查询数据,然后把数据处理后发提交到线程池,再由线程池异步发送到 MQ 服务器,调用方监听 MQ 进行数据处理,接口代码大致如下:

继续阅读

踩坑 Druid 连接池

这周有个应用的一个实例出现了没有响应,庆幸运维那边在重启前做了线程和内存的 dump 。

线程 dump 文件打开一看,竟然4万多行。。后来发现同事用一个可视化工具来分析线程栈,我也把这个工具加入工具箱:IBM Thread and Monitor Dump Analyzer for Java

下图是这个工具的方法栈分析视图:

可以按线程名词、状态、方法栈的深度来进行排序。

下面说说这次踩的坑。

继续阅读

HashMap

本文源码基于 JDK 1.8.0_101 。

在 JDK 1.8 之前,HashMap 采用 槽数组 + 单链表 来实现;

在 JDK 1.8 开始采用 槽数组 + 单链表 + 红黑树 来实现。

1. 为什么引入红黑树?

解决哈希冲突时、链表过长导致访问效率低下的问题。

为什么是红黑树不是其他树:
二叉排序树在极端情况下会退化成线性结构。
平衡二叉树(AVL树)是严格平衡树,在增加或删除节点时,旋转次数比红黑树要多。红黑树的统计性能高于 AVL 树。

红黑树特性,RBT树上的每个节点,都要遵循下面的规则:
① 每个节点都是红色或者黑色;
② 根节点必须始终是黑色;
③ 没有两个相邻的红色节点;
④ 对每个结点,从该结点到其子孙节点的所有路径上包含相同数目的黑结点。

继续阅读

HashTable 有什么奇怪的知识?

本文涉及的源码基于 JDK 1.8.0_101 。

1. HashTable

  1. 采用数组 + 链表(表头插入)方式解决哈希冲突。
  2. 所有的 public 方法都用 synchronized 修饰以保证线程安全。
  3. 在构造时就初始化槽数组(默认大小为 11)。
  4. 键、值 都不能为 null
  5. 指定 key 的目标槽的定位逻辑:(key.hashCode() & 0x7FFFFFFF) % table.length,掩码+求模。
  6. 槽数组的最大尺寸为 MAX_ARRAY_SIZE: Integer.MAX_VALUE - 8(减 8 是因为一些 JVM 会在数组里保留一些 header words)。
  7. 扩容逻辑为 两倍 + 1。
  8. 阈值为: (int)(capacity * loadFactor),但不能超过 MAX_ARRAY_SIZE + 1

继续阅读

Spring-MVC 文件上传优化

Spring-MVC 文件上传优化

  1. 配置 CommonsMultipartResolver 时把 maxInMemorySize 配置为合适的大小,让小文件可以缓存在内存中,对磁盘只需一次写操作;

  2. 在 Controller 里调用 multipartFile.transferTo(file); 把文件保存到目标路径。该方法首先进行重命名,如果不成功则进行流拷贝,如果成功则可以省下一次读、写操作。对于 org.apache.commons.fileupload.disk.DiskFileItem 类,调用的方法是下面这个:

public void write(File file) throws Exception {
    if (isInMemory()) {
        FileOutputStream fout = null;
        try {
            fout = new FileOutputStream(file);
            fout.write(get());
        } finally {
            if (fout != null) {
                fout.close();
            }
        }
    } else {
        File outputFile = getStoreLocation();
        if (outputFile != null) {
            // Save the length of the file
            size = outputFile.length();
            /*
             * The uploaded file is being stored on disk
             * in a temporary location so move it to the
             * desired file.
             */
            if (!outputFile.renameTo(file)) {
                BufferedInputStream in = null;
                BufferedOutputStream out = null;
                try {
                    in = new BufferedInputStream(
                        new FileInputStream(outputFile));
                    out = new BufferedOutputStream(
                            new FileOutputStream(file));
                    IOUtils.copy(in, out);
                } finally {
                    if (in != null) {
                        try {
                            in.close();
                        } catch (IOException e) {
                            // ignore
                        }
                    }
                    if (out != null) {
                        try {
                            out.close();
                        } catch (IOException e) {
                            // ignore
                        }
                    }
                }
            }
        } else {
            /*
             * For whatever reason we cannot write the
             * file to disk.
             */
            throw new FileUploadException(
                "Cannot write uploaded file to disk!");
        }
    }
}

欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

java.util.Collections.singleton*

今天在抄 Motan 的代码时才发现 java.util.Collections 有三个以 singleton 开头的方法:

  • public static <T> List<T> singletonList(T o):返回一个内部类 SingletonList 的实例。

  • public static <T> Set<T> singleton(T o):返回一个内部类 SingletonSet 的实例。

  • public static <K,V> Map<K,V> singletonMap(K key, V value):返回一个内部类 SingletonMap 的实例。

这三个内部类都是非常高效的:
* SingletonListSingletonSet 都用一个属性来表示拥有的元素,而不是用数组、列表来表示;SingletonMap 分别用两个属性表示 key/value;内存使用上更高效;
* 在方法的实现上也更高效,减少了循环。比如 size 方法都是直接返回 1 ;List.contains 方法是把参数与属性元素直接对比。

真是一种追求性能极限的精神!我们要充分利用好这些特性。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

HttpURLConnection 自动 重复 提交 POST

一、问题

测试环境偶尔反馈有个服务 HTTP 请求重复提交。这个 HTTP 请求头里有个 UUID 作为唯一标识,会存入日志表里作为主键的,因为主键冲突,服务端是有打印异常信息的,但客户端完全正常,没有任何错误信息,业务逻辑也正常完成了。

通过查看客户端、服务端两边的日志,怀疑是 HttpURLConnection 自动进行了请求重试。在网上搜索一番,发现已经有人提了 bug JDK-6427251 ,按照这个的操作步骤是可以复现的,包括 JDK 8 的版本。

HttpURLConnection 采用 Sun 私有的一个 HTTP 协议实现类: HttpClient.java

关键是下面这段发送请求、解析响应头的方法:

  569       /** Parse the first line of the HTTP request.  It usually looks
  570           something like: "HTTP/1.0 <number> comment\r\n". */
  571   
  572       public boolean parseHTTP(MessageHeader responses, ProgressSource pi, HttpURLConnection httpuc)
  573       throws IOException {
  574           /* If "HTTP/*" is found in the beginning, return true.  Let
  575            * HttpURLConnection parse the mime header itself.
  576            *
  577            * If this isn't valid HTTP, then we don't try to parse a header
  578            * out of the beginning of the response into the responses,
  579            * and instead just queue up the output stream to it's very beginning.
  580            * This seems most reasonable, and is what the NN browser does.
  581            */
  582   
  583           try {
  584               serverInput = serverSocket.getInputStream();
  585               if (capture != null) {
  586                   serverInput = new HttpCaptureInputStream(serverInput, capture);
  587               }
  588               serverInput = new BufferedInputStream(serverInput);
  589               return (parseHTTPHeader(responses, pi, httpuc));
  590           } catch (SocketTimeoutException stex) {
  591               // We don't want to retry the request when the app. sets a timeout
  592               // but don't close the server if timeout while waiting for 100-continue
  593               if (ignoreContinue) {
  594                   closeServer();
  595               }
  596               throw stex;
  597           } catch (IOException e) {
  598               closeServer();
  599               cachedHttpClient = false;
  600               if (!failedOnce && requests != null) {
  601                   failedOnce = true;
  602                   if (httpuc.getRequestMethod().equals("POST") && (!retryPostProp || streaming)) {
  603                       // do not retry the request
  604                   }  else {
  605                       // try once more
  606                       openServer();
  607                       if (needsTunneling()) {
  608                           httpuc.doTunneling();
  609                       }
  610                       afterConnect();
  611                       writeRequests(requests, poster);
  612                       return parseHTTP(responses, pi, httpuc);
  613                   }
  614               }
  615               throw e;
  616           }
  617   
  618       }

在第 600 – 614 行的代码里:

  • failedOnce 默认是 false,表示是否已经失败过一次了。这也就限制了最多发送 2 次请求。
  • httpuc 是请求相关的信息。
  • retryPostProp 默认是 true,可以通过命令行参数(-Dsun.net.http.retryPost=false)来指定值。
  • streaming:默认 falsetrue if we are in streaming mode (fixed length or chunked) 。

通过 Linux 的命令 socat tcp4-listen:8080,fork,reuseaddr system:"sleep 1"\!\!stdout 建立一个只接收请求、不返回响应的 HTTP 服务器。
对于 POST 请求,第一次请求发送出去后解析响应会碰到流提前结束,这是个 SocketException: Unexpected end of file from serverparseHTTP 捕获后发现满足上面的条件就会进行重试。服务端就会收到第二个请求。

继续阅读

Javassist 字节码操作库

研究 Javassist 的起因是维护的项目是从外部采购的一个系统,有几个核心的类在构造函数里从数据库加载一些配置信息,而这些配置信息基本是不会改变的,应当缓存起来。这些类没有源码,class 文件还是混淆过的。

想来想去,觉得用字节码操作工具来改写是比较合适的,把改写后生成的 class 文件替换原来的,这样使用这些类的地方也不用做任何修改。

1. Javassist 简介

Javassist 是一个字节码操作库,通过它,可以在运行时改写类:添加新的字段、方法和构造函数,改变类、父类和接口的方法。

Javassist 定义了 CtField, CtMethod, CtConstructor, CtClass 来表示 字段、方法、构造函数、类。

继续阅读

Thread, Runnable, Callable

最近观察同事面试,发现他对问题的理解本身就有不对,这样即使面试者完全掌握了那个问题,也可能给不了他想要的回答。

他的问题是:Java 里有哪些创建线程的方式?他期望的答案是 Thread, Runnable, Callable, Future 这些。

这个理解是错误的,他也是网上看的。

一、线程与任务

线程可以理解为 可以执行程序指令的机器,而任务则是要执行的一段指令

在 Java 里用 java.lang.Thread 类来表示线程。用 java.lang.Runnable 接口表示任务。

任务创建出来后是需要放到线程上去执行的,所谓线程驱动任务执行

创建、启动线程

创建线程一般是创建一个 Thread 类的实例,可以在创建时指定任务,也可以覆写 void run() 方法来实现任务的逻辑。

Thread 类实例化后只是在 Java 堆里创建了一个线程对象,并没有跟操作系统的线程关联起来,还不能真正执行任务。需要调用 native void start() 方法(这个方法在不同的 JDK 版本里有所不同)。所以如果是继承了 Thread 类,是不能覆写 start 方法的,否则是没法启动线程的。

start 方法调用后,线程会自动执行 run 方法, run 方法执行完成后线程就会销毁。

二、Callable, Future

java.util.concurrent.Callable 表示一个带返回值的任务,是一种特殊点任务。

java.util.concurrent.Future 表示一个可在未来获得调用结果的存根。


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

发布组件到 Maven 中央仓库

折腾了几天,终于把一个小组件MyBatis-batch 发布了中央仓库,做个笔记记录下。

  1. 注册 sonatype JIRA 帐号并配置 settings.xml
    我是先在 sonatype 上发布,然后由 sonatype 自动同步到中央仓库的。首先要在 sonatype 注册一个 JIRA 帐号,

$M2_HOME/conf/settings.xmlservers 标签下添加如下配置:

<server>
    <id>sonatype-nexus</id>
    <username>sonatype 登录名</username>
    <password>sonatype 密码</password>
</server>
  1. 在 github 添加 ssh key
    Maven 构建的时候,会自动操作 github,比如创建 tag 。
    用 ssh-keygen 生成一对秘钥,在 https://github.com/settings/keys 页面可以添加 SSH key。

继续阅读