深入分析`Java I/O`的工作机制

不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以I/O操作的都是字节而不是字符。但是我们的程序中通常操作的数据都是字符形式的。

基于字节的I/O操作接口输入和输出分别是InputStreamOutputStream

写字符的I/O操作接口涉及的是write(char[] buf, int off, int len)

读字符的I/O操作是read(char[] buf, int off, int len)

字节与字符的转化接口

数据持久化或网络传输都是以字节进行的,所以必须要有字符到字节或者字节到字符的转化。

几种访问文件的方式

读取和写入文件I/O操作都调用操作系统提供的接口,因为磁盘设备是由操作系统管理的,应用程序要访问物理设备只能通过系统调用的方式来工作。

只要是系统调用就可能存在内核空间地址和用户空间地址切换的问题,这是操作系统为了保护系统本身的运行安全而将内核程序运行使用的内存空间和用户程序运行的内存空间隔离造成的。这样虽然保证了内核程序运行的安全性,但是也必然存在数据可能需要从内核空间向用户空间复制的问题

如果遇到非常耗时的操作,如磁盘I/O,数据从磁盘复制到内核空间,然后又从内核空间复制到用户空间,将会非常缓慢。这时操作系统为了加速I/O访问,在内核空间使用缓存机制,也就是将从磁盘读取的文件按照一定的组织方式进行缓存

标准访问文件方式

当应用程序调用read()接口时,操作系统检查内核的告诉缓存中有没有需要的数据。如果已经缓存了,那么就直接从缓存中返回;如果没有,从磁盘中读取,然后缓存在操作系统的缓存中。

当应用程序调用write()接口时,将数据从用户地址空间复制到内核地址空间的缓存中。这时,对用户程序来说,写操作就已经完成了,至于什么时候再写到磁盘中是有操作系统决定的,除非显式地调用sync同步命令

直接I/O方式

应用程序直接访问磁盘数据,而不经过操作系统内核数据缓冲区,这样做的目的就是减少一次从内核缓冲区到用户程序缓存的数据复制。

这种访问文件的方式通常是在对数据的缓存管理由应用程序实现的数据库管理程序中。(如数据库管理系统中,系统明确地知道应该缓存哪些数据,应该失效哪些数据,还可以对一些热点数据做预加载,提前将热点数据加载到内存,可以加速数据的访问效率;而操作系统并不知道哪些是热点数据,只是简单地缓存最近一次从磁盘读取的数据)

缺点:如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘加载。这种直接加载会非常缓慢

同步访问文件方式

数据的读取和写入都是同步操作的,它与标准访问文件方式不同的是,只有当数据被成功写到磁盘时才返回给应用程序成功标志

这种访问文件方式性能比较差,只有在一些对数据安全性要求比较高的场景中才会使用,而且通常这种操作方式的硬件都是定制的。

异步访问文件方式

当访问数据的线程发出请求之后,线程会接着去处理其他事情,而不是阻塞等待,当请求的数据返回后继续处理下面的操作。这种访问文件的方式可以明显地提高应用程序的效率,但是不会改变访问文件的效率。

内存映射方式

内存映射方式是指操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要访问内存中一段数据时,转换为访问文件的某一段数据。这种方式的目的同样是减少数据从内核空间缓存到用户空间缓存的数据复制操作,因为这两个空间的数据是共享的。

Java访问磁盘文件

数据在磁盘中的唯一最小描述就是文件,也就是说上层应用程序只能通过文件来操作磁盘上的数据,文件也是操作系统和磁盘驱动器交互的最小单元。

Java中通常的File并不代表一个真实存在的文件对象,当你指定一个路径描述符时,它就会返回一个代表这个路径的一个虚拟对象,这个可能是一个真实存在的文件或者是一个包含多个文件的目录。

如何从磁盘读取一段文本字符:

当传入一个文件路径时,将会根据这个路径创建一个File对象来标识这个文件,然后根据这个File对象创建真正读取文件的操作对象,这时将会真正创建一个关联真实存在的磁盘文件的文件描述符FileDescriptor,通过这个对象可以直接控制这个磁盘文件。

由于我们需要读取的是字符格式,所以需要StreamDecoder类将byte解码为char格式。

Java序列化

Java序列化就是将一个对象转化成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。需要持久化,对象必须继承java.io.Serializable接口。

反序列化则是相反的过程,将这个字节数组再重新构造成对象。

网络I/O工作机制

TCP状态转化

1、CLOSED:起始点,在超时或者连接关闭时进入此状态
2、LISTEN:Server端在等待连接时的状态,Server端为此要调用Scok

影响网络传输的因素

将一份数据从一个地方正确地传输到另一个地方所需要的时间我们称为响应时间。影响这个响应时间的因素有很多。

  • 网络带宽
  • 传输距离
  • TCP拥塞控制

TCP传输是一个停-等-停-等协议,传输放和接受方的步调要一致,要达到这个步调一致就要通过拥塞控制来调节。TCP在传输时会设定一个窗口(BDP,Brandwidth Delay Product),这个窗口的大小是由带宽和RTTRound-Trip Time,数据在两端的来回时间,也就是响应时间)决定的。计算的公式是带宽(b/s) * RTT(s)。通过这个值可以得出理论上最优的TCP缓冲区的大小。

Java Socket的工作机制

Socket描述计算机之间完成相互通信的一种抽象功能。

打个比方,可以把Socket比作两个城市之间的交通工具,有了它,就可以在城市之间来回穿梭了、交通工具有多种,每种交通工具也有相应的交通规则。Socket也一样,也有多种。大部分情况我们使用的是基于TCP/IP的流套接字,它是一种稳定的通信协议。

主机A的应用程序要能和主机B的应用程序通信,必须通过Socket建立连接,而建立Socket连接必须由底层TCP/IP协议来建立TCP连接建立TCP连接需要底层IP协议来寻址网络中的主机。网络层使用的IP协议可以帮助我们根据IP地址来找到目标主机,但是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通信就要通过TCPUDP的地址,也就是端口号来指定了。

建立通信链路

当客户端要与服务端通信时,客户端首先要创建一个Socket实例,操作系统将为这个Socket实例分配一个没有被使用的本地端口号,并创建一个包含本地和远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个连接关闭。

在创建Socket实例的构造函数正确返回之前,将要进行TCP的三次握手协议,TCP握手协议完成后,Socket实例对象将创建完成,否则将抛出IOException错误。

数据传输

当连接已经建立成功,服务端和客户端都会拥有一个Socket实例,每个Socket实例都有一个InputStreamOutputStream,并通过这两个对象来交换数据。

当创建Socket对象时,操作系统将会为InputStreamOutputStream分别分配一定大小的缓存区,数据的写入和读取都是通过这个缓存区完成的。

写入端将数据写到OutputStream对应的SendQ队列中,当队列填满时,数据将被转移到另一端InputStreamRecvQ队列中,如果这时RecvQ已经满了,那么OutputStreamwrite方法将会阻塞知道RecvQ队列有足够的空间容纳SendQ发送的数据。

NIO的工作方式

BIO带来的挑战

BIO即阻塞IO,不管是磁盘IO还是网络IO,数据在写入OutputStream或者从InputStream读取时都有可能会阻塞,一旦有阻塞,线程将会失去CPU的使用权。

NIO的工作机制

  • 这里的Channel可以比作某种具体的交通工具,如汽车或高铁;
  • 而Selector可以比作一个车站的车辆运行调度系统,它将负责监控每辆车的当前运行状态,是已经出站,还是在路上的。也就是它可以轮训每个Channel的状态。
  • Buffer可以比作车上的座位。信息已经封装在了Socket里面,对你是透明的。
public void selector() throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    Selector selector = Selector.open();
    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.configureBlocking(false);
    ssc.socket().bind(new InetSocketAddress(8080));
    ssc.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
        Set selectedKeys = selector.selectedKeys();
        Iterator it = selectedKeys.iterator();
        while (it.hasNext()) {
            SelectionKey key = (SelectionKey) it.next();
            if ((key.readOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
                ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
                SocketChannel sc = ssChannel.accept();
                sc.configureBlocking(false);
                sc.register(selector, SelectionKey.OP_READ);
                it.remove();
            }
        }
    }
}
  • 调用Selector的静态工厂创建一个选择器
  • 创建一个服务端的Channel,绑定到一个Socket对象,并把这个通信信道注册到选择器上,把这个通信信道设置为非阻塞模式
  • 然后就可以调用SelectorselectedKeys方法来检查已经注册在这个选择器上的所有通信信道是否有需要的事件发生。
  • 如果有某个事件发生,将会返回所有的SelectionKey,通过这个对象Channel方法就可以取得这个通信信道对象,从而可以读取通信的数据
  • 而这里读取的数据是Buffer,这个Buffer是我们可以控制的缓冲器

  • Selector可以同时监听一组通信信道(Channel)上的IO状态,前提是这个Selector已经注册到这些通信信道中。
  • 选择器Selector可以调用select()方法检查已经注册的通信信道上IO是否已经准备好,如果没有一个信道IO状态有变化,那么select方法会阻塞等待或在超时后返回0。
  • 如果有多个信道有数据,那么将会把这些数据分配到对应的数据Buffer中。
  • 所以关键的地方是,有一个线程来处理所有连接的数据交互,每个连接的数据交互都不是阻塞方式,所以可以同时处理大量的连接请求。

Buffer的工作方式

Selector检测到通信信道IO有数据传输时,通过select()取得SocketChannel,将数据读取或写入Buffer缓冲区。

Buffer可以简单地理解为一组基本数据类型的元素列表,它通过几个变量来保存这个数据的当前位置状态,也就是有四个索引。

  • capacity:缓冲区数组的总长度
  • position:下一个要操作的数据元素的位置
  • limit:缓冲区数组中不可操作的下一个元素的位置,limit<=capacity
  • mark:用于记录当前position的前一个位置或者默认是0

我们通过ByteBuffer.allocate(11)方法创建一个11个byte的数组缓冲区,初始状态时,position为0,capactiylimit默认都是数组长度。

当我们写入5个字节时,位置变化如下:

这时,我们需要将缓冲区的5个字节数据写入Channel通信信道,所以我们调用byteBuffer.flip()方法

这时,底层操作系统就可以从缓冲区中正确读取这5个字节数据,并发送出去了。在下一次写数据之前,我们再调一下clear()方法,缓冲区的索引状态又回到初始位置。

当我们调用mark()时,它将记录当前position的前一个位置,当我们调用reset时,position将恢复mark记录下来的值。

通过Channel获取的IO数据首先要经过操作系统的Socket缓冲区再将数据复制到Buffer中,这个操作系统缓冲区就是底层的TCP协议关联的RecvQ或者SendQ队列。

从操作系统缓冲区到用户缓冲区复制数据比较耗性能,Buffer提供了另外一种直接操作操作系统缓冲区的方式,即ByteBuffer.allocateDirector(size),这个方法返回的DirectByteBuffer就是与底层存储空间关联的缓冲区,它通过Native代码操作非JVM堆的内存空间。

NIO的数据访问方式

FileChannel.transferXXX

FileChannel.transferXXX与传统的访问文件方式相比可以减少数据从内核到用户空间的复制,数据直接在内核空间中移动。

FileChannel.map

将文件按照一定大小映射为内存区域,当程序访问这个内存区域时将直接操作这个文件数据,这种方式省去了数据从内核空间向用户空间复制的损耗。这种方式适合对大文件的只读性操作,如大文件的MD5校验。

适配器模式装饰器模式的区别

  • 适配器模式的意义是要将一个接口转变成另外一个接口,它的目的是通过改变接口来达到重复使用的目的
  • 装饰器模式不是要改变被装饰对象的接口,而是要保持原有的接口,但是增强原有对象的功能,或者改变原有对象的处理方式而提升性能。
2016/3/25 posted in  深入分析Java Web技术内幕

深入Web请求过程

B/S => Browser / Server
C/S => Client / Server

如何发起一个请求

浏览器在建立Socket连接之前,必须根据地址栏里输入的URL的域名DNS解析出IP地址,再根据这个IP地址和默认80端口与远程服务器建立Socket连接,然后浏览器根据这个URL组装成一个get类型的HTTP请求头,通过outputStream.write()发送到目标服务器,服务器等待inputStream.read()返回数据,最后断开这个连接。

2016/3/25 posted in  深入分析Java Web技术内幕

Java 基础之`final`、`static`、`transient`

一、关于final

根据程序上下文环境,Java关键字fina这是无法改变的或者终态的含义,它可以修饰非抽象类、非抽象类成员方法和变量。你可能出于两种理解而需要阻止改变:设计或效率。

final类不能被继承,没有子类,final类中的方法默认是final的。

final方法不能被子类的方法覆盖,但可以被继承。

final成员变量表示常量,只能被赋值一次,赋值后值不再改变。

final不能用于修饰构造方法。

注意:父类的private成员方法是不能被子类方法覆盖的,因此private类型的方法默认是final类型的。

1、final

final类不能被继承,因此final类的成员方法没有机会被覆盖,默认都是final的。在设计类时候,如果这个类不需要有子类,类的实现细节不允许改变,并且确信这个类不会再被扩展,那么就设计为final

2、final方法

如果一个类不允许其子类覆盖某个方法,则可以把这个方法声明为final方法

使用final方法的原因有二:

第一、把方法锁定,防止任何继承类修改它的意义和实现。

第二、高效。编译器在遇到调用final方法时候会转入内嵌机制,大大提高执行效率。

3、final变量(常量)

final修饰的成员变量表示常量,值一旦给定就无法改变

final修饰的变量有三种:静态变量、实例变量和局部变量,分别表示三种类型的常量。

另外,final变量定义的时候,可以先声明,而不给初值,这中变量也称为final空白,无论什么情况,编译器都确保空白final在使用之前必须被初始化。但是,final空白在final关键字final的使用上提供了更大的灵活性,为此,一个类中的final数据成员就可以实现依对象而有所不同,却有保持其恒定不变的特征。

4、final参数

当函数参数为final类型时,你可以读取使用该参数,但是无法改变该参数的值(网上最流行的说法)。

特例

public class Test {
    public static void main(String[] args)  {
        MyClass myClass = new MyClass();
        StringBuffer buffer = new StringBuffer("hello");
        myClass.changeValue(buffer);
        System.out.println(buffer.toString());
    }
}
 
class MyClass {
 
    void changeValue(final StringBuffer buffer) {
        buffer.append("world");
    }
}

====> helloworld

Once a final variable has been assigned, it always contains the same value. If a final variable holds a reference to an object, then the state of the object may be changed by operations on the object, but the variable will always refer to the same object.

如果一个被final关键字修饰的变量A指向一个对象B的引用,那么这个变量A的状态可能会随着B的改变而改变,但A一直都指向B

在上例中,变量Abuffer)指向一个对象BStringBuffer)的引用,对象BStringBuffer)的值改变了,但是他的内存地址没有改变。

final StringBuffer a = new StringBuffer("Hello");
a = new StringBuffer("World"); //this wont compile

二、关于static

static表示全局或者静态的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块,但是Java语言中没有全局变量的概念。

1、static变量

按照是否静态的对类成员变量进行分类可分两种:一种是被static修饰的变量,叫静态变量或类变量;另一种是没有被static修饰的变量,叫实例变量。两者的区别是:

对于静态变量在内存中只有一个拷贝(节省内存), JVM 只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问(方便),当然也可以通过对象来访问(但是这是不推荐的)。

对于实例变量,每创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响(灵活)。

public修饰的static成员变量和成员方法本质是全局变量和全局方法,当声明其他类的对象时,不生成static变量的副本,而是类的所有实例共享同一个static变量。

static变量前可以有private修饰,表示这个变量可以在类的静态代码块中,或者类的其他静态成员方法中使用,但是不能在其他类中通过类名来直接引用,这一点很重要。实际上你需要搞明白,private是访问权限限定,static表示不要实例化就可以使用,这样就容易理解多了。static前面加上其它访问权限关键字的效果也以此类推。

2、static方法

静态方法可以直接通过类名调用,任何的实例也都可以调用,因此静态方法中不能用thissuper关键字,不能直接访问所属类的实例变量和实例方法(就是不带static的成员变量和成员成员方法),只能访问所属类的静态成员变量和成员方法。因为实例成员与特定的对象关联!

因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract

3、static代码块

static代码块也叫静态代码块,是在类中独立于类成员的static语句块,可以有多个,位置可以随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,如果static代码块有多个,JVM将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次

4、staticfinal一块用表示什么

staticfinal用来修饰成员变量和成员方法,可简单理解为“全局常量”

对于变量,表示一旦给值就不可修改,并且通过类名可以访问。

对于方法,表示不可覆盖,并且可以通过类名直接访问。

特别要注意一个问题:

对于被staticfinal修饰过的实例常量,实例本身不能再改变了,但对于一些容器类型(比如,ArrayListHashMap)的实例变量,不可以改变容器变量本身,但可以修改容器中存放的对象,这一点在编程中用到很多。

5、静态内部类

这里简单介绍下,什么是静态内部类。

简单的说内部类前加static就是静态内部类了,上代码:

public class Outer { 
    static int x =1;
    static class Nest {
        void print(){
            System.out.println("Nest "+x);
        }
    }
    public static void main(String[] args){
        Outer.Nest nest = new Outer.Nest();
        nest.print();
    }
}

当内部类是static时,意味着:

  • 要创建静态内部类的对象,并不需要其外部类的对象;

  • 不能够从静态内部类的对象中访问外部类的非静态成员;

与普通的内部类的一个区别:在非静态内部类中不可以声明静态成员,只有将某个内部类修饰为静态类,然后才能够在这个类中定义静态的成员变量与成员方法。

6、静态导包

所谓静态导入包:import static com. ... . ClassName.*这样写可以导入相关类里面的所有静态方法,或者也可以直接静态导入具体到静态方法名。

这样写的好处是,引用静态方法不用在前面加上类名.

需要注意两点:

  • 提防含糊不清的命名static成员。例如,如果你对Integer类和Long类执行了静态导入,引用MAX_VALUE将导致一个编译器错误,因为IntegerLong都有一个MAX_VALUE常量,并且Java不会知道你在引用哪个MAX_VALUE

  • 方法名的命名尽量明确,让看代码的人看到名称就知道这个方法是干嘛用的,不然静态导入会让代码变的难读。

附:

对象的初始化顺序:

  • 首先执行父类静态的内容,父类静态的内容执行完毕后,
  • 接着去执行子类的静态的内容,当子类的静态内容执行完毕之后,
  • 再去看父类有没有非静态代码块,如果有就执行父类的非静态代码块,
  • 父类的非静态代码块执行完毕,接着执行父类的构造方法
  • 父类的构造方法执行完毕之后,它接着去看子类有没有非静态代码块,如果有就执行子类的非静态代码块。
  • 子类的非静态代码块执行完毕再去执行子类的构造方法

总之一句话,静态代码块内容先执行,接着执行父类非静态代码块和构造方法,然后执行子类非静态代码块和构造方法

**注意: 子类的构造方法,不管这个构造方法带不带参数,默认的它都会先去寻找父类的不带参数的构造方法**。如果父类没有不带参数的构造方法,那么子类必须用super关键子来调用父类带参数的构造方法,否则编译不能通过。

三、transient

java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

  • 一旦变量被transient修饰,变量将不再是对象持久化的一部分,该变量内容在序列化后无法获得访问。

  • transient关键字只能修饰变量,而不能修饰方法和类。注意,本地变量是不能被transient关键字修饰的。变量如果是用户自定义类变量,则该类需要实现Serializable接口。

  • transient关键字修饰的变量不再能被序列化,一个静态变量不管是否被transient修饰,均不能被序列化。

2016/3/25 posted in  Java

高性能MySQL - 创建高性能的索引

索引基础

索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为MySQL只能高效地使用索引的最左前缀列。创建一个包含两个列的索引,和创建两个只包含一列的索引是大不相同的。

索引的类型

B-Tree索引

B-Tree通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同。

2016/3/25 posted in  数据库

搜索引擎关键字智能提示的一种实现

解决方案

关键字收集

当用户输入一个前缀时,碰到提示的候选词很多的时候,如何取舍,哪些展示在前面,哪些展示在后面?

用户在使用搜索引擎查找商家时,会输入大量的关键字,每一次输入就是对关键字的一次投票,那么关键字被输入的次数越多,它对应的查询就比较热门,所以需要查询的关键字记录下来,并且统计出每个关键字的频率,方便提示结果按照频率排序。

搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来。

拼音缩写提取

chongqing, zhongqing -> cq, zq

索引与前缀查询

方案一:Trie树 + TopK算法

Trie树即字典树,又称单词查找树或键树,是一种树形结构,一种哈希树的变种。

它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。

从上图可知,当用户输入前缀i的时候,搜索框可能会展示以i为前缀的ininnint等关键词,再当用户输入前缀a的时候,搜索框里面可能会提示以a为前缀的ate等关键词。如此,实现搜索引擎智能提示suggestion的第一个步骤便清晰了

TopK算法用于解决统计热词的问题。解决TopK问题主要有两种策略:HashMap统计+排序(堆排序)。

HashMap统计:先对这批海量数据预处理。具体方法是:维护一个KeyQuery字串,Value为该Query出现次数的HashMap

该方案存在的问题是:

  • 需要维护拼音、缩写两棵Trie树。

方案二:Solr自带Suggest智能提示

该方案存在的问题是:

  • 返回的结果是基于索引中字段的词频进行排序,不是用户搜索关键字的频率,因此不能将一些热门关键字排在前面。
  • 拼音提示,多音字,缩写还是要另外加索引字段。

方案三 Solrcloud建立单独的collection,利用Solr前缀查询实现

专门为关键字建立一个索引collection,利用Solr前缀查询实现。Solr中的copyField能很好解决我们同时索引多个字段(汉字、pinyin, abbre)的需求,且fieldmultiValued属性设置为true时能解决同一个关键字的多音字组合问题。

Reference

http://tech.meituan.com/pinyin-suggest.html

2016/3/25 posted in  others