大数据程序员@IT·互联网

Java知识点杂谈

2018-09-25  本文已影响78人  薛定谔的猫Plus

0 概述

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

通道类型:

缓冲区类型:

常用方法:

缓冲区的一些属性:

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

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

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

zc2.png
在现代操作系统中,外设有数据到来时,基本上都采用中断方式通知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

在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控制权。

上一篇下一篇

猜你喜欢

热点阅读