Java知识点杂谈

作者: 薛定谔的猫Plus | 来源:发表于2018-09-25 09:48 被阅读78次

    0 概述

    Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比较,通道就像是流,而且他们面向缓冲区的。所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
    通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。
    因为它们是双向的,所以通道可以比流更好地反映底层操作系统的真实情况。特别是在 UNIX 模型中,底层操作系统通道是双向的。

    通道类型:

    • FileChannel:从文件中读写数据。
    • DatagramChannel:能通过UDP读写网络中的数据。
    • SocketChannel:能通过TCP读写网络中的数据。
    • ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。

    缓冲区类型:

    • ByteBuffer
    • MappedByteBuffer
    • CharBuffer
    • DoubleBuffer
    • FloatBuffer
    • IntBuffer
    • LongBuffer
    • ShortBuffer

    常用方法:

    • allocate() - 分配一块缓冲区
    • put() - 向缓冲区写数据
    • get() - 向缓冲区读数据
    • filp() - 将缓冲区从写模式切换到读模式
    • clear() - 从读模式切换到写模式,不会清空数据,但后续写数据会覆盖原来的数据,即使有部分数据没有读,也会被遗忘;
    • compact() - 从读数据切换到写模式,数据不会被清空,会将所有未读的数据copy到缓冲区头部,后续写数据不会覆盖,而是在这些数据之后写数据
    • mark() - 对position做出标记,配合reset使用
    • reset() - 将position置为标记值

    缓冲区的一些属性:

    • capacity - 缓冲区大小,无论是读模式还是写模式,此属性值不会变;
    • position - 写数据时,position表示当前写的位置,每写一个数据,会向下移动一个数据单元,初始为0;最大为capacity - 1,切换到读模式时,position会被置为0,表示当前读的位置
    • limit - 写模式下,limit 相当于capacity 表示最多可以写多少数据,切换到读模式时,limit 等于原先的position,表示最多可以读多少数据。

    1 mmap机制

    mmap是一种内存映射文件的方法,即将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对应关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read、write等系统调用函数。相反,内核空间对这段区域的修改也直接反应用户空间,从而可以实现不同进程间的文件共享。

    2 内存映射文件的优缺点

    可能内存映射IO的主要优势是性能,内存映射文件比通过普通的IO来访问文件要快,这对于繁忙的电子交易系统来说非常重要。内存映射IO另外一个优势是能够加载普通方式无法访问的大文件,实验表明内存映射IO在大文件处理中表现得更好;但缺点是有增加页面错误(page fault)的可能,因为操作系统仅仅加载一部分文件到内存中,如果被请求的页面不在内存中那就会导致一个页面错误。大多数主流操作系统如Windows, Unix, Solaris和其他类Unix的操作系统都支持内存映射IO,在64位架构下,你几乎可以将任何文件映射到内存中并直接使用Java访问。另外一个优势是这些文件能够共享,在进程间提供共享内存,而且比普通的基于loopback接口的Socket要快10倍。

    3 nnmap示例

    public class MmapTest {
        private static int count = 10485760;
    
        public static void main(String[] args) throws Exception {
            RandomAccessFile memoryMapFile = new RandomAccessFile("fileTest.txt", "rw");
    
            MappedByteBuffer out = memoryMapFile.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, count);
    
            for (int i = 0; i < count; i++) {
                out.put((byte) 'A');
            }
            System.out.println("writing completed");
    
            for (int i = 0; i < 10; i++) {
                System.out.println((char) out.get(i));
            }
    
            System.out.println("reading completed");
    
            memoryMapFile.close();
        }
    }
    

    4 磁盘数据直接发网卡

    使用sendfile将磁盘数据直接拷贝到网卡发送缓冲区。原始实现方式如下,如一个典型的web服务器传送静态文件的过程如下:

    read(file, tmp_buf, len);
    write(socket, tmp_buf, len);
    

    首先调用read将文件从磁盘读取到tmp_buf,然后调用write将tmp_buf写入到socket,在这过程中会出现四次数据copy,过程如下图所示.

    zc1.png
    • 当调用read系统调用时,通过DMA(Direct Memory Access)将数据copy到内核模式;
    • 然后由CPU控制将内核模式数据copy到用户模式下的buffer中
    • read调用完成后,write调用首先将用户模式下 buffer中的数据copy到内核模式下的socket buffer中
    • 最后通过DMA copy将内核模式下的socket buffer中的数据copy到网卡设备中传送。

    从上面的过程可以看出,数据白白从内核模式到用户模式走了一 圈,浪费了两次copy,而这两次copy都是CPU copy,即占用CPU资源。

    在linux内核中引入了sendfile函数,可以完成在内核中的Zero-Copy,如下所示。

    zc2.png
    • DMA copy将磁盘数据copy到kernel buffer中
    • 向socket buffer中追加当前要发送的数据在kernel buffer中的位置和偏移量
    • DMA gather copy根据socket buffer中的位置和偏移量直接将kernel buffer中的数据copy到网卡上。
    在现代操作系统中,外设有数据到来时,基本上都采用中断方式通知CPU,操作系统响应中断,然后再从外设读取数据,这时,如果外设的数据比较频繁,那么是否每到一个数据都中断一次呢??这样 CPU 就非常频繁地被外调中断打断,操作系统在处理中断时要浪费一定时间,而且 CPU 读外部 IO 速度也很慢,这样的话,大量时间被用在了响应中断上,而去调度其它任务的时间减少,让人感觉系统响应速度不够,为了进一步减少CPU对I/O的干预, 提高CPU与I/O设备的并行度引入DMA控制方式。  
    
    DMA方式的特点为:
    - 数据传送的基本单位是数据块。
    - 数据是直接在设备─内存之间传送的。
    - 仅在传送一个或多个数据块的开始和结束时才需要CPU对I/O的干预, 传送工作本身是由DMA控制器完成的。
    

    经过上述过程,数据只经过了2次copy就从磁盘传送出去了。

    可能有人要纠结“不是说Zero-Copy么?怎么还有两次copy啊”,事实上这个Zero copy是针对内核来讲的,数据在内核模式下是Zero-copy的。
    

    在Java NIO中

    FileChannel.transferTo(long position, long count, WriteableByteChannel target)
    

    方法将当前通道中的数据传送到目标通道target中,在支持Zero-Copy的linux系统中,transferTo()的实现依赖于sendfile()调用。

    5 java的wait以及notify

    • wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。
    • 调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁),但是在wait时会释放该对象锁;
    • 调用notifyAll方法能够唤醒所有正在等待这个对象的monitor的线程;

    在java中,是没有类似于PV操作,进程互斥等相关的方法的。Java的进程同步时通过synchronized()来实现的,需要说明的是,Java的synchronized方法类似于操作系统概念中的互斥内存块,在Java中Object类对象中,都是带有一个内存锁的,在有线程获取该内存锁后,其他线程无法访问该内存,从而实现Java中简单的同步、互斥操作。明白这个原理,就能理解为什么synchronized(this)与synchronized(static XXX)的区别了,synchronized就是针对内存区块申请内存锁,this关键字代表类的一个对象,所以其内存锁是针对相同对象的互斥操作,而static成员属于类专有,其内存空间为该类所有成员共有,这就导致synchronized对static成员加锁,相当于对类加锁,也就是在该类的所有成员间实现互斥,在同一时间只有一个线程可以访问该类实例,如果需要在线程间相互唤醒就需要借助Object类的wait方法和notify方法。

    public class ConcurrentTest {
        public static void main(String[] args) {
            final byte a[] = {0};
            new Thread(new NumberPrint(1, a), "1").start();
            new Thread(new NumberPrint(2, a), "2").start();
        }
    }
    
    class NumberPrint implements Runnable {
        private int number;
        public byte res[];
        public static int count = 5;
    
        public NumberPrint(int number, byte[] a) {
            this.number = number;
            res = a;
        }
    
        public void run() {
            synchronized (res) {
                while (count-- > 0) {
                    try {
                        res.notify();
                        System.out.println("" + number);
                        res.wait();
                        System.out.println("------线程" + Thread.currentThread().getName() + "获得锁,wait()后的代码继续运行:" + number);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    wait()方法与notify()必须要与synchronized(resource)一起使用。也就是wait与notify针对已经获取了resource锁的线程进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。从功能上来说wait()线程在获取对象锁后,主动释放CPU控制权,主动释放对象锁,同时本线程休眠。直到有其他线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的释放操作。

    目前wait和notify方法均可以释放对象的锁,但是wait同时释放CPU控制权,即它后面的代码停止执行,线程进入阻塞状态,而notify方法不立刻释放CPU控制权,而是在相应的synchronized(){}语句块执行结束,再自动释放锁。
    

    释放锁后,JVM会等待resource的线程中选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制,而在同步块中的Thread.sleep()方法并不释放锁,仅释放CPU控制权。

    相关文章

      网友评论

      • Java耕耘者:你好!我们是Java耕耘者专注于程序员Java开发公众号“Java这点事”。我们很赞赏你的文章,希望能获得转载授权。授权后,你的文章将会在公众号“Java这点事”、发布。我们会注明来源和作者姓名。
        非常感谢~~~
        薛定谔的猫Plus:@Java耕耘者 好的~

      本文标题:Java知识点杂谈

      本文链接:https://www.haomeiwen.com/subject/geykoftx.html