Algorithm
这周做了一道 腾讯精选练习 50 题 中的中等题 LRU 缓存机制,容易想到的一个方案是:用一个 HashMap 存储数据 key 和 value,再用一个 LinkedList 放数据的 key,访问过的 key 移动到链表的头部(删除然后重新插入),满了就删除链表尾部的元素,但是这种方案由于移动到链表头部需要线性搜索,时间复杂度是 O(n)。其实有一种简单的时间复杂度是 O(1) 的解法:HashMap 仍然存储数据的 key,但是其 value 存储的是 LinkedList 节点的引用,LinkedList 的节点中存储数据的 key 和 value,这样移动到头部的操作就可以在 O(1) 时间完成了,我模仿 Java 的 LinkedList 实现了一个自己的 MyLinkedList,题解如下:
class LRUCache { |
Review
因为想要研究下 Java 的 FileInputStream 的机制,这周读的是《The Linux Programming Interface》这本书的第 4 章《File I/O: The Universal I/O Model》,主要介绍了 open、read、write、close 这几个文件操作相关的系统调用。之所示称为 Universal I/O Model 是因为这些 API 适用于所有文件类型,例如管道、Socket 等,而不仅仅局限于磁盘上的文件。这本书的作者 Michael Kerrisk(1961-)是 Linux man-pages 的维护者,对 Linux 系统调用十分熟悉,这本书也写得非常好,被誉为 Linux 版的 APUE(Advanced Programming in the UNIX Environment),建议每个从事计算机后端相关工作的工程师都来读读。
Tip
Shell 中 for 循环的几种常见写法:
|
Share
InputStream 这个抽象类是 Java 同步 IO 中的核心,读取操作都是基于 InputStream 的某个实现类做的,下图列出了常用的实现类,最近研究了一下 FileInputStream 这个实现类的机制,写篇文章总结一下。
FileInputStream
FileInputStream 用来读取文件,下面是一个按字节读取文件并打印的例子,其中 BufferedInputStream 是带缓冲的 Decorator,注意缓冲区的默认大小是 8192 字节:
public class FileInputStreamTest { |
接下来看一下 FileInputStream 的源码,它的 read 方法在内部直接调用了 native 方法 read0:
public class FileInputStream extends InputStream { |
Native Method
Java 通过 native 方法可以调用 C 或 C++ 的库函数,比较常见的用途是调用 glibc(The GNU C Library)中对系统调用的封装,就像上面说到的 read0 方法,最终会调用到 glibc 中的 read 函数,在追溯调用链之前先来看看 native 方法的机制:
NativeTest.java
// 生成字节码:javac NativeTest.java |
NativeTest.h
/* DO NOT EDIT THIS FILE - it is machine generated */ |
NativeTest.c
// 设置 JAVA_HOME:JAVA_HOME=$(/usr/libexec/java_home) |
一切准备就绪之后,执行 Java NativeTest
如果看到输出 42,说明 Java 代码通过 native 方法正常调用到了 C 代码。
OpenJDK 8
从上面 native 方法的实验可以看到,Java 代码中只有 native 方法的签名,read0 的情况也正是这样,真正的实现在 C 代码中,可以查看 OpenJDK 8 的源文件 FileInputStream.c 一探究竟,其内部直接调用了 readSingle 函数,而 readSingle 的实现在 io_util.c 中:
// FileInputStream.c |
// io_util.c |
readSingle 函数中的核心部分使用了 IO_Read 这个宏,在 io_util_md.h 可以看到宏定义 #define IO_Read handleRead
,handleRead 函数实现在 io_util_md.c 中,调用的是 glibc 中的 read 函数:
// io_util_md.c |
GNU C Library
glibc 中的 read 函数定义在 unistd.h 头文件中,最后写一个直接使用这个 read 函数读取文件并输出的例子来结束这次 FileInputStream 的探索:
// unistd.h |
|
FileInputStream 还有两个重载的 read 方法,内部调用的都是 native 方法 readBytes,注意到 readBytes 在 io_util.c 的实现中优先使用一块栈内存 char stackBuf[8192];
保存读取的结果,如果一次性读取的字节数超过 8192 就需要使用 malloc 分配堆内存,面临更多的开销。上面提到过 BufferedInputStream 缓冲区的默认大小是 8192 字节,这个数值的设置应该和 stackBuf 的默认大小是相关的。