Home 成长之路 Java

0 93

某周一,我正愉快的编写测试用例。既然是在Springboot框架下,那肯定是要调用其他类的

信心满满的使用@Autowried注解进行注入,启动测试类,这个时候报了注入类的空指针


开始排查…

一开始感觉是注入类没有打注解,看了之后发现是有的,那类应该是正常注入Springboot容器了,但是测试类没拿到。百度了一下,都不是对应的问题所在。

突然灵光一现,感觉会不会是注解用错了…

import org.junit.Test;

import org.testng.annotations.Test;
使用第一个注解时,无法注入,会报空指针;使用第二个注解就OK了,而且很明显看到第二个注解会启动容器


详细分析…

(待补充)

 

 

0 96
/**
 * 获取CVS文件
 * @return 待刷入的数据
 */
private List<DistrictInfo> readByCSV(){
    log.info("获取CVS文件内容");
    ArrayList<DistrictInfo> list = new ArrayList<>();
    try (Reader reader = Files.newBufferedReader(Paths.get(""), Charset.forName("GBK"))) {
        Iterable<CSVRecord> records = CSVFormat.DEFAULT.parse(reader);
        for (CSVRecord record : records) {
            DistrictInfo districtInfo = new DistrictInfo();
            districtInfo.setRegionId(Integer.valueOf(record.get(0)));
            districtInfo.setRegionName(record.get(1));
            districtInfo.setpRegionId(Integer.valueOf(record.get(2)));
            districtInfo.setClasses(Integer.valueOf(record.get(3)));
            districtInfo.setStatus(Integer.valueOf(record.get(4)));
            districtInfo.setOperator(record.get(5));
            districtInfo.setMemo(record.get(6));
            districtInfo.setVersion(Integer.valueOf(record.get(7)));
            list.add(districtInfo);
        }
    } catch (IOException ex) {
        ex.printStackTrace();
    }
    return list;
}

/** 获取CVS文件 
 *  
 */
public void writeToCSV(List <QueryBusinessFieldListResp> list){
    final String FILE_NAME = "businessIDPointFromZT.csv";
    final String[] FILE_HEADER = {"BusinessID"};
    // 这里显式地配置一下CSV文件的Header,然后设置跳过Header(要不然读的时候会把头也当成一条记录)
    CSVFormat format = CSVFormat.DEFAULT.withHeader(FILE_HEADER).withSkipHeaderRecord();
    // 这是写入CSV的代码
    try(Writer out = new FileWriter(FILE_NAME);
        CSVPrinter printer = new CSVPrinter(out, format)) {
        for (QueryBusinessFieldListResp resp : list) {
            if (resp != null) {
                for (QueryBusinessFieldResp queryBusinessFieldResp : resp.getBusinessFieldRespList()) {
                    List<String> records = new ArrayList<>();
                    records.add(queryBusinessFieldResp.getBusinessId());
                    records.add(queryBusinessFieldResp.getValue().toString());
                    printer.printRecord(records);
                }
            }
        }
        } catch (IOException ioException) {
        ioException.printStackTrace();
    }
}

原来 8 张图,就可以搞懂「零拷贝」了 – 知乎 (zhihu.com)

深入剖析Linux IO原理和几种零拷贝机制的实现 – 知乎 (zhihu.com)

前情提要

在学习Netty时,了解到其底层模型是NIO,同时还用到了零拷贝技术,所以特此学习零拷贝。本文复制链接如上,注为我所做


一、DMA技术

磁盘可以说是计算机系统最慢的硬件之一,读写速度相差内存 10 倍以上,所以针对优化磁盘的技术非常的多,比如零拷贝、直接 I/O、异步 I/O 等等,这些优化的目的就是为了提高系统的吞吐量,另外操作系统内核中的磁盘高速缓存区,可以有效的减少磁盘的访问次数。

注:磁盘读写非常慢,因此出现了零拷贝、直接I/O、异步I/O、DMA、缓存等优化技术

这次,我们就以「文件传输」作为切入点,来分析 I/O 工作方式,以及如何优化传输文件的性能。

为什么要有 DMA 技术?

在没有 DMA 技术前,I/O 的过程是这样的:

  • CPU 发出对应的指令给磁盘控制器,然后返回;
  • 磁盘控制器收到指令后,于是就开始准备数据,会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个中断
  • CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器,然后再把寄存器里的数据写入到内存,而在数据传输的期间 CPU 是无法执行其他任务的。

为了方便你理解,我画了一副图:

可以看到,整个数据的传输过程,都要需要 CPU 亲自参与搬运数据的过程,而且这个过程,CPU 是不能做其他事情的。

简单的搬运几个字符数据那没问题,但是如果我们用千兆网卡或者硬盘传输大量数据的时候,都用 CPU 来搬运的话,肯定忙不过来。

注:没有DMA技术时,CPU需要进行发送指令、响应IO中断、从磁盘控制器的缓冲区读取数据到CPU寄存器,再将数据写到用户缓冲区

计算机科学家们发现了事情的严重性后,于是就发明了 DMA 技术,也就是直接内存访问(Direct Memory Access 技术。

什么是 DMA 技术?简单理解就是,在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务

那使用 DMA 控制器进行数据传输的过程究竟是什么样的呢?下面我们来具体看看。

具体过程:

  • 用户进程调用 read 方法,向操作系统发出 I/O 请求,请求读取数据到自己的内存缓冲区中,进程进入阻塞状态;
  • 操作系统收到请求后,进一步将 I/O 请求发送 DMA,然后让 CPU 执行其他任务;
  • DMA 进一步将 I/O 请求发送给磁盘;
  • 磁盘收到 DMA 的 I/O 请求,把数据从磁盘读取到磁盘控制器的缓冲区中,当磁盘控制器的缓冲区被读满后,向 DMA 发起中断信号,告知自己缓冲区已满;
  • DMA 收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不占用 CPU,CPU 可以执行其他任务
  • 当 DMA 读取了足够多的数据,就会发送中断信号给 CPU;
  • CPU 收到 DMA 的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回;

可以看到, 整个数据传输的过程,CPU 不再参与数据搬运的工作,而是全程由 DMA 完成,但是 CPU 在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要 CPU 来告诉 DMA 控制器。

早期 DMA 只存在在主板上,如今由于 I/O 设备越来越多,数据传输的需求也不尽相同,所以每个 I/O 设备里面都有自己的 DMA 控制器。

注:用户向CPU发起IO请求—>CPU向DMA发送IO请求—>DMA从磁盘中读取数据到磁盘控制器缓冲区,再写入到内核缓存区—>DMA发送中断信号给CPU—>CPU将数据从内核拷贝到用户空间,系统调用返回。
注:有了DMA,CPU只用发送IO请求,响应中断,把内核缓冲区的数据写入到用户缓冲区


二、传统的文件传输

如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。

传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。

代码通常如下,一般会需要两个系统调用:

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

代码很简单,虽然就两行代码,但是这里面发生了不少的事情。

首先,期间共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。

上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。

其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的,下面说一下这个过程:

  • 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。
  • 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。
  • 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。
  • 第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。

我们回过头看这个文件传输的过程,我们只是搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能。

这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。

所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数

注:传统的文件传输发送了4次用户态和内核态的上下文切换(两次系统调用,一次read,一次write),还发生了4次数据拷贝(两次DMA拷贝,两次CPU拷贝)


三、如何优化文件传输的性能?

先来看看,如何减少「用户态与内核态的上下文切换」的次数呢?

读取磁盘数据的时候,之所以要发生上下文切换,这是因为用户空间没有权限操作磁盘或网卡,内核的权限最高,这些操作设备的过程都需要交由操作系统内核来完成,所以一般要通过内核去完成某些任务的时候,就需要使用操作系统提供的系统调用函数。

而一次系统调用必然会发生 2 次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行。

所以,要想减少上下文切换到次数,就要减少系统调用的次数

再来看看,如何减少「数据拷贝」的次数?

在前面我们知道了,传统的文件传输方式会历经 4 次数据拷贝,而且这里面,「从内核的读缓冲区拷贝到用户的缓冲区里,再从用户的缓冲区里拷贝到 socket 的缓冲区里」,这个过程是没有必要的。

因为文件传输的应用场景中,在用户空间我们并不会对数据「再加工」,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的

注:减少数据拷贝的次数。可以直接将数据从内核态拷贝到网卡缓冲区。

 


 

四、零拷贝(减少上下文切换和数据拷贝)

零拷贝技术实现的方式通常有 2 种:

  • mmap + write
  • sendfile

注:实际上可以有很多种方式,只要能减少上下文切换和数据拷贝的次数即可

下面就谈一谈,它们是如何减少「上下文切换」和「数据拷贝」的次数。

mmap + write

在前面我们知道,read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用 mmap() 替换 read() 系统调用函数。

buf = mmap(file, len);
write(sockfd, buf, len);

mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。

注:直接内存映射。Linux提供的mmap系统调用, 它可以将一段用户空间内存映射到内核空间, 当映射成功后, 用户对这段内存区域的修改可以直接反映到内核空间;同样地, 内核空间对这段区域的修改也直接反映用户空间。正因为有这样的映射关系, 就不需要在用户态(User-space)与内核态(Kernel-space) 之间拷贝数据, 提高了数据传输的效率,这就是内存直接映射技术

具体过程如下:

  • 应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区;
  • 应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据;
  • 最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。

我们可以得知,通过使用 mmap() 来代替 read(), 可以减少一次数据拷贝的过程。

但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。

注:直接内存映射mmap()减少一次CPU拷贝

sendfile

在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile(),函数形式如下:

#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。

首先,它可以替代前面的 read()write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。

其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。如下图:

但是这还不是真正的零拷贝技术,如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。

你可以在你的 Linux 系统通过下面这个命令,查看网卡是否支持 scatter-gather 特性:

$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on

于是,从 Linux 内核 2.4 版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile() 系统调用的过程发生了点变化,具体过程如下:

  • 第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
  • 第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;

所以,这个过程之中,只进行了 2 次数据拷贝,如下图:

这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。

零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。

所以,总体来看,零拷贝技术可以把文件传输的性能提高至少一倍以上


使用零拷贝技术的项目

Kafka、Nginx等


PageCache 有什么作用?(磁盘高速缓存)

回顾前面说道文件传输过程,其中第一步都是先需要先把磁盘文件数据拷贝「内核缓冲区」里,这个「内核缓冲区」实际上是磁盘高速缓存(PageCache

由于零拷贝使用了 PageCache 技术,可以使得零拷贝进一步提升了性能,我们接下来看看 PageCache 是如何做到这一点的。

读写磁盘相比读写内存的速度慢太多了,所以我们应该想办法把「读写磁盘」替换成「读写内存」。于是,我们会通过 DMA 把磁盘里的数据搬运到内存里,这样就可以用读内存替换读磁盘。

但是,内存空间远比磁盘要小,内存注定只能拷贝磁盘里的一小部分数据。

那问题来了,选择哪些磁盘数据拷贝到内存呢?

我们都知道程序运行的时候,具有「局部性」,所以通常,刚被访问的数据在短时间内再次被访问的概率很高,于是我们可以用 PageCache 来缓存最近被访问的数据,当空间不足时淘汰最久未被访问的缓存。

所以,读磁盘数据的时候,优先在 PageCache 找,如果数据存在则可以直接返回;如果没有,则从磁盘中读取,然后缓存 PageCache 中。

还有一点,读取磁盘数据的时候,需要找到数据所在的位置,但是对于机械磁盘来说,就是通过磁头旋转到数据所在的扇区,再开始「顺序」读取数据,但是旋转磁头这个物理动作是非常耗时的,为了降低它的影响,PageCache 使用了「预读功能」

比如,假设 read 方法每次只会读 32 KB 的字节,虽然 read 刚开始只会读 0 ~ 32 KB 的字节,但内核会把其后面的 32~64 KB 也读取到 PageCache,这样后面读取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出 PageCache 前,进程读取到它了,收益就非常大。

所以,PageCache 的优点主要是两个:

  • 缓存最近被访问的数据;
  • 预读功能;

这两个做法,将大大提高读写磁盘的性能。

但是,在传输大文件(GB 级别的文件)的时候,PageCache 会不起作用,那就白白浪费 DMA 多做的一次数据拷贝,造成性能的降低,即使使用了 PageCache 的零拷贝也会损失性能

这是因为如果你有很多 GB 级别文件需要传输,每当用户访问这些大文件的时候,内核就会把它们载入 PageCache 中,于是 PageCache 空间很快被这些大文件占满。

另外,由于文件太大,可能某些部分的文件数据被再次访问的概率比较低,这样就会带来 2 个问题:

  • PageCache 由于长时间被大文件占据,其他「热点」的小文件可能就无法充分使用到 PageCache,于是这样磁盘读写的性能就会下降了;
  • PageCache 中的大文件数据,由于没有享受到缓存带来的好处,但却耗费 DMA 多拷贝到 PageCache 一次;

所以,针对大文件的传输,不应该使用 PageCache,也就是说不应该使用零拷贝技术,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,这样在高并发的环境下,会带来严重的性能问题。


大文件传输用什么方式实现?

那针对大文件的传输,我们应该使用什么方式呢?

我们先来看看最初的例子,当调用 read 方法读取文件时,进程实际上会阻塞在 read 方法调用,因为要等待磁盘数据的返回,如下图:

具体过程:

  • 当调用 read 方法时,会阻塞着,此时内核会向磁盘发起 I/O 请求,磁盘收到请求后,便会寻址,当磁盘数据准备好后,就会向内核发起 I/O 中断,告知内核磁盘数据已经准备好;
  • 内核收到 I/O 中断后,就将数据从磁盘控制器缓冲区拷贝到 PageCache 里;
  • 最后,内核再把 PageCache 中的数据拷贝到用户缓冲区,于是 read 调用就正常返回了。

对于阻塞的问题,可以用异步 I/O 来解决,它工作方式如下图:

它把读操作分为两部分:

  • 前半部分,内核向磁盘发起读请求,但是可以不等待数据就位就可以返回,于是进程此时可以处理其他任务;
  • 后半部分,当内核将磁盘中的数据拷贝到进程缓冲区后,进程将接收到内核的通知,再去处理数据;

而且,我们可以发现,异步 I/O 并没有涉及到 PageCache,所以使用异步 I/O 就意味着要绕开 PageCache。

绕开 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 则叫缓存 I/O。通常,对于磁盘,异步 I/O 只支持直接 I/O。

前面也提到,大文件的传输不应该使用 PageCache,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache。

于是,在高并发的场景下,针对大文件的传输的方式,应该使用「异步 I/O + 直接 I/O」来替代零拷贝技术

直接 I/O 应用场景常见的两种:

  • 应用程序已经实现了磁盘数据的缓存,那么可以不需要 PageCache 再次缓存,减少额外的性能损耗。在 MySQL 数据库中,可以通过参数设置开启直接 I/O,默认是不开启;
  • 传输大文件的时候,由于大文件难以命中 PageCache 缓存,而且会占满 PageCache 导致「热点」文件无法充分利用缓存,从而增大了性能开销,因此,这时应该使用直接 I/O。

另外,由于直接 I/O 绕过了 PageCache,就无法享受内核的这两点的优化:

  • 内核的 I/O 调度算法会缓存尽可能多的 I/O 请求在 PageCache 中,最后「合并」成一个更大的 I/O 请求再发给磁盘,这样做是为了减少磁盘的寻址操作;
  • 内核也会「预读」后续的 I/O 请求放在 PageCache 中,一样是为了减少对磁盘的操作;

于是,传输大文件的时候,使用「异步 I/O + 直接 I/O」了,就可以无阻塞地读取文件了。

所以,传输文件的时候,我们要根据文件的大小来使用不同的方式:

  • 传输大文件的时候,使用「异步 I/O + 直接 I/O」;
  • 传输小文件的时候,则使用「零拷贝技术」;

在 nginx 中,我们可以用如下配置,来根据文件的大小来使用不同的方式:

location /video/ { 
    sendfile on; 
    aio on; 
    directio 1024m; 
}

当文件大小大于 directio 值后,使用「异步 I/O + 直接 I/O」,否则使用「零拷贝技术」。


总结

早期 I/O 操作,内存与磁盘的数据传输的工作都是由 CPU 完成的,而此时 CPU 不能执行其他任务,会特别浪费 CPU 资源。

于是,为了解决这一问题,DMA 技术就出现了,每个 I/O 设备都有自己的 DMA 控制器,通过这个 DMA 控制器,CPU 只需要告诉 DMA 控制器,我们要传输什么数据,从哪里来,到哪里去,就可以放心离开了。后续的实际数据传输工作,都会由 DMA 控制器来完成,CPU 不需要参与数据传输的工作。

传统 IO 的工作方式,从硬盘读取数据,然后再通过网卡向外发送,我们需要进行 4 上下文切换,和 4 次数据拷贝,其中 2 次数据拷贝发生在内存里的缓冲区和对应的硬件设备之间,这个是由 DMA 完成,另外 2 次则发生在内核态和用户态之间,这个数据搬移工作是由 CPU 完成的。

为了提高文件传输的性能,于是就出现了零拷贝技术,它通过一次系统调用(sendfile 方法)合并了磁盘读取与网络发送两个操作,降低了上下文切换次数。另外,拷贝数据都是发生在内核中的,天然就降低了数据拷贝的次数。

Kafka 和 Nginx 都有实现零拷贝技术,这将大大提高文件传输的性能。

零拷贝技术是基于 PageCache 的,PageCache 会缓存最近访问的数据,提升了访问缓存数据的性能,同时,为了解决机械硬盘寻址慢的问题,它还协助 I/O 调度算法实现了 IO 合并与预读,这也是顺序读比随机读性能好的原因。这些优势,进一步提升了零拷贝的性能。

需要注意的是,零拷贝技术是不允许进程对文件内容作进一步的加工的,比如压缩数据再发送。

另外,当传输大文件时,不能使用零拷贝,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,并且大文件的缓存命中率不高,这时就需要使用「异步 IO + 直接 IO 」的方式。

在 Nginx 里,可以通过配置,设定一个文件大小阈值,针对大文件使用异步 IO 和直接 IO,而对小文件使用零拷贝。

注:零拷贝不会经过用户空间

零拷贝是从磁盘到内核缓冲区再到网卡
大文件传输室友异步IO+直接IO(未使用PageCache)

 

0 94

常见的对象属性拷贝方式解析

一、常见的对象属拷贝方式

  1. Apache BeanUtils

  2. Apache ProrertyUtils

  3. Springframework BeanUtils

  4. Cglib BeanCopier

二、性能对比

private static void testCglibBeanCopier(OriginObject origin, int len) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        System.out.println();
        System.out.println("================cglib BeanCopier执行" + len + "次================");
        DestinationObject destination3 = new DestinationObject();

        for (int i = 0; i < len; i++) {
            BeanCopier copier = BeanCopier.create(OriginObject.class, DestinationObject.class, false);
            copier.copy(origin, destination3, null);
        }
        stopwatch.stop();

        System.out.println("testCglibBeanCopier 耗时: " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

    private static void testApacheBeanUtils(OriginObject origin, int len)
            throws IllegalAccessException, InvocationTargetException {
        Stopwatch stopwatch = Stopwatch.createStarted();
        System.out.println();
        System.out.println("================apache BeanUtils执行" + len + "次================");
        DestinationObject destination2 = new DestinationObject();
        for (int i = 0; i < len; i++) {
            BeanUtils.copyProperties(destination2, origin);
        }

        stopwatch.stop();

        System.out.println("testApacheBeanUtils 耗时: " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

    private static void testSpringFramework(OriginObject origin, int len) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        System.out.println("================springframework执行" + len + "次================");
        DestinationObject destination = new DestinationObject();

        for (int i = 0; i < len; i++) {
            org.springframework.beans.BeanUtils.copyProperties(origin, destination);
        }

        stopwatch.stop();

        System.out.println("testSpringFramework 耗时: " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

    private static void testApacheBeanUtilsPropertyUtils(OriginObject origin, int len)
            throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
        Stopwatch stopwatch = Stopwatch.createStarted();
        System.out.println();
        System.out.println("================apache BeanUtils PropertyUtils执行" + len + "次================");
        DestinationObject destination2 = new DestinationObject();
        for (int i = 0; i < len; i++) {
            PropertyUtils.copyProperties(destination2, origin);
        }

        stopwatch.stop();

        System.out.println("testApacheBeanUtilsPropertyUtils 耗时: " + stopwatch.elapsed(TimeUnit.MILLISECONDS));
    }

分别执行1000、10000、100000、1000000次耗时数(毫秒):

工具名称 执行1000次耗时 10000次 100000次 1000000次
Apache BeanUtils 390ms 854ms 1763ms 8408ms
Apache PropertyUtils 26ms 221ms 352ms 2663ms
spring BeanUtils 39ms 315ms 373ms 949ms
Cglib BeanCopier 64ms 144ms 171ms 309ms

性能对比结论

  1. Apache BeanUtils的性能最差,不建议使用。

  2. Apache PropertyUtils100000次以内性能还能接受,到百万级别性能就比较差了,可酌情考虑。

  3. spring BeanUtils和BeanCopier性能较好,如果对性能有特别要求,可使用BeanCopier,不然spring BeanUtils也是可取的。

  4. Cglib 的 BeanCopier 的拷贝速度是最快的,即使是百万次的拷贝也只需要 10 毫秒! 相比而言,最差的是 Commons 包的 BeanUtils.copyProperties 方法,100 次拷贝测试与表现最好的 Cglib 相差400 倍之多。百万次拷贝更是出现了2600 倍的性能差异!

    综上: 性能 setter > cglib > spring > apache PropertyUtils > apache BeanUtils

三、源码解析

此处只解析Apache BeanUtils 源码

解析源码

首先,抛出结论,Apache BeanUtils.copyProperties()性能差的原因是:

  1. 输出了大量的日志调试信息

  2. 重复的对象类型检查

开始分析

执行1000000次copy属性,然后通过jvisualvm查看方法耗时,如图:

img

发现最耗时的方法就是method.invoke(),但是spring的BeanUtils、PropertyUtils里也是采用反射来实现的,为什么效率相差这么大呢?img

看来Abstract.convert()和getIntrospectionData()占用了很大一部分时间.而且Apache BeanUtils中的日志输出也比较耗时。

与 Spring BeanUtils 进行对比

img

PropertyUtils和Apache BeanUtils核心代码区别在图中标注的地方。

Apache BeanUtils主要集中了各种丰富的功能(日志、转换、解析等等),导致性能变差。

而Spring BeanUtils则是直接通过反射来读取和写入,直抒胸臆,省去了其他繁杂的步骤,性能自然不差。

四、使用总结(重点)

对 Apache BeanUtils 的总结

  1. 拷贝性能 setter > Cglib BeanCopier > Spring BeanUtils > Apache PropertyUtils > Apache BeanUtils

  2. Apache BeanUtils.copyProperties()性能差的原因是对日志的大量操作和重复的对象类型检查

  3. Apache BeanUtils.copyProperties()坑点

    • 包装类默认值

    在进行属性拷贝时,低版本 BeanUtils 为了解决Date为空的问题会导致为目标对象的**原始类型的包装类属性赋予初始值**,如 Integer 属性默认赋值为 0,尽管你的来源对象该字段的值为 null。这个在我们的**包装类属性为 null 值时有特殊含义的场景**,非常容易踩坑!例如搜索条件对象,一般 null 值表示该字段不做限制,而 0 表示该字段的值必须为0。
    • 改用 Spring BeanUtils 时,要特别小心

    org.apache.commons.beanutils.BeanUtils.copyProperties(Object target, Object source);
    
    org.springframework.beans.BeanUtils.copyProperties(Object source, Object target);
    
    从方法签名上可以看出,这**两个工具类的名称相同,方法名也相同,甚至连参数个数、类型、名称都相同**。但是**参数的位置是相反的**。因此,如果你想更改的时候,千万要记得,将 target 和 source 两个参数也调换过来!
  4. Apache BeanUtils.copyProperties 有一种线程级别的“缓存”,第一次刷新缓存耗时较长,后续直接读”缓存”耗时较短,这种“缓存”是线程粒度

    • 单线程模型下,第一次访问BeanUtils.copyProperties耗时有200-300ms左右,后续访问几乎都是0ms,也就是微秒级别

    • 并发模型下,每个线程访问BeanUtils.copyProperties会有一次200-300ms耗时, 也就是高性能耗时次数与并发线程数一致

  5. Apache BeanUtils.copyProperties()可以在一定范围内进行类型转换(比如 int —> Integer),同时还要注意一些不能转换时候,会将默认null值转化成0;

  6. Apache BeanUtils 和 Apache PropertyUtils两个工具类都是对bean之间存在属性名相同的属性进行处理,无论是源bean或者是目标bean中多出来的属性均不处理

  7. Apache BeanUtils.copyProperties()可以在一定范围内进行类型转换(比如 int —> Integer),同时还要注意一些不能转换时候,会将默认null值转化成0;

  8. Property.copyProperties()则是严格的类型转化,必须类型和属性名完全一致才转化。PropertyUtils 支持为null的场景

对 Spring BeanUtils 的总结

  1. Spring BeanUtils 和 Apache BeanUtils 同名。从方法签名上可以看出,这两个工具类的名称相同,方法名也相同,甚至连参数个数、类型、名称都相同。但是参数的位置是相反的。因此,如果你想更改的时候,千万要记得,将 target 和 source 两个参数也调换过来!

  2. Spring BeanUtils.copryProperties()也可以在一定范围内进行类型转换(比如 int —> Integer)BeanUtils 对部分属性不支持 null,具体如下

    • java.util.Date 类型不支持,但是它的自雷java.sql.Date是被支持的。java.util.Date直接copy会报异常

    • Boolean,Integer,Long等不支持,会将null转化为0

    • String支持,转化后依然为null

  3. 如果存在属性完全相同的内部类,但是不是同一个内部类,即分别属于各自的内部类,则spring会认为属性不同,不会copy;

对 Cglib BeanCopier 的总结

  1. BeanCopier支持两种方式:

    • 一种是不使用Converter的方式,仅对两个bean间属性名和类型完全相同的变量进行拷贝;

    • 另一种则引入Converter,可以对某些特定属性值进行特殊操作

  2. Cglib 使用 BeanCopier.create()也是非常耗时,避免多次调用,尽可能做成全局初始化一次

  3. Cglib BeanCopier 的主要耗时方法就在 BeanCopier.create(),如果将该方法做成静态成员变量,则还可以大大缩小执行时间。BeanCopier是一种基于字节码的方式,其实就是通过字节码方式转换成性能最好的get、set方式,只需考虑创建BeanCopier的开销,如果我们将BeanCopier做成静态的,基本只需考虑get、set的开销,所以性能接近于get、set

  4. Cglib 的 BeanCopier 只拷贝名称和类型都相同的属性

    即使源类型是原始类型(int, shortchar等),目标类型是其包装类型(Integer, ShortCharacter等),或反之:都不会被拷贝

https://www.jianshu.com/p/2ca157963698)

拷贝严格性检验

* Apache BeanUtils.copyProperties()可以在一定范围内进行类型转换(比如 int ---> Integer),同时还要注意一些不能转换时候,会将默认null值转化成0;
* Property.copyProperties()则是严格的类型转化,必须类型和属性名完全一致才转化。PropertyUtils 支持为null的场景
* Spring BeanUtils.copryProperties()也可以在一定范围内进行类型转换(比如 int ---> Integer)BeanUtils 对部分属性不支持 null,具体如下
   a. java.util.Date 类型不支持,但是它的自雷java.sql.Date是被支持的。java.util.Date直接copy会报异常;
   b. Boolean,Integer,Long等不支持,会将null转化为0;
   c. String支持,转化后依然为null

参考链接:这四种对象属性拷贝方式,你都知道吗? – 风尘博客 – 博客园 (cnblogs.com)

BeanUtils.copyProperties(A,B)使用注意事项 – 龙-OSCAR – 博客园 (cnblogs.com)

BeanCopier、BeanUtils对象属性拷贝 – 简书 (jianshu.com)

Spring的BeanUtils的copyProperties方法需要注意的点 – 简书 (jianshu.com)

几种copyProperties工具类性能比较 – 简书 (jianshu.com)

Java Bean Copy框架性能对比-阿里云开发者社区 (aliyun.com)

为什么阿里要求避免使用 Apache BeanUtils 进行属性复制? – 简书 (jianshu.com)

基于内存保存消息

最近开发基于Bio的Socket项目的时候,想把简单的聊天室往消息队列的方向靠拢,因此在考虑怎么记录每个客户端所发送的消息(即实现聊天记录的保存功能)

客户端发送消息— 服务器端接收 转发 存储—-目标客户端

服务器接收转发我使用的是Read线程转发即可,那么存储呢?一开始的想法是,要不存储在内存中,即创建HashMap<senderName,msgList>来保存

具体如下:

<橙汁,List<“消息一”,“消息二”…>>

<小阮,List<“消息一”,”消息二”…>>

代码如下:

这样的实现好处在于:简单,明了。客户端将消息发送给服务器,服务器做转发,同时存入msgMemoryMap(全局变量);由于我的网络通信模型是使用的Bio,服务器循环监听,每来一个客户端就创建一个对应的读线程,而这些步骤也是在读线程里面完成的

也就是说 客户端A的消息存入 和客户端B的消息存入不是同一个线程实现的(当然已经使用了线程池进行优化,此处是有隐患的,如果客户端太多那么服务器就要创建很多线程),因为效率上不用太过担心

更高效的序列化

在存入msgMemoryMap之前,我将原来的Serialize序列化方式改为了Protostuff序列化,因为存入消息实在太占用内存了…

如果使用Serialize序列化,平均每条消息要占用300个字节(300B)这还得期望聊天用户发送的消息都是短消息,如果长消息那更恐怖,我计算了一下,如果同时有1000个用户在聊天,每人发送1条,也就是1*1000*300 = 300Kb!这个内存占用实在是太可怕了,因此我不得不考虑更高效的序列化方式,即Protostuff,关于Protostuff的文章会在之后写出,目前只需要知道它的序列化更高效且生成的byte数组大小更小,差不多是Serialize生成的十分之一,那么接下来直接看测试代码:

 

这样仿佛解决了写入的消息占用内存过大的问题?其实还可以进一步优化,比如再优化MessageRedis类的字段,或者先压缩再存入msgMemoryMap,等需要拿出来使用的时候再解压,也就是“时间换空间”的想法,听上去好像使用msgMemoryMap只要解决了内存占用问题就好了,其实并不然,因为内存具有掉电即失的特性。

任何实际开发的项目都不可能将数据简单的写在内存,必须要进行持久化,不然你的用户使用你的聊天室向其他人发送重要的资料和文件,等到后面他需要取查看这些消息的时候,却发现居然全丢了。持久化,欸,会做啊,我写入到数据库去不就不会丢失了嘛。

写入数据库有两种策略:

1.每来一条消息,服务器做转发后,将其写入到数据库;

2.每来一条消息,服务器将其写入到数据库后,再做转发;

我们来分析一下这两种策略,假设是 用户 橙汁 发送给 小阮的一条消息 ;

          策略1 :

           服务器先转发,那挺好,小阮可以立刻收到橙汁所发送的消息,然后橙汁的这条消息写入数据库,完美保存;橙汁第二条消息来的时候重复这个逻辑,因为橙汁发送消息中间是有间隔的,也就是不可能一直不停的发(假设消息有意义),那么这个间隔时间足够橙汁将第一条消息写入数据库了,到此,消息既转发了又保存了,服务器完成一次操作的时间为(T转发 + T存入)。

           似乎万事大吉?其实不然,现在都追求高可用,高并发,高可靠,我们的系统也不能落下。很显然目前的策略没有满足高并发和高可用,因为如果在服务器收到消息并转发后,断电了怎么办?消息并没有被写入数据库,如果小阮收到了消息,比如是 “明天一起去吃饭吧” 然后橙汁把这事忘了,第二天小阮来算账,说 “你昨天说的今天一起吃饭啊”;橙汁说:“噢 是吗? 我看看聊天记录”,结果消息记录居然真的没有!橙汁确实说了这句话,也就是说我们的系统会出现很多很多比这更复杂的问题,那么怎么解决呢?

                 策略2:

                  服务器先写入,再转发,如果写入失败则重试(或者其他策略),直到成功后再转发;这样的话,如果小阮收到了这条消息,一定和数据库中 的是一样的;同样的,如果写入数据库之后断电了,消息没被转发怎么办?只需要标记消息是否转发成功,如果没有的话就重新转发,可以从数据库去获取,不怕数据丢了,当然这里设计还可以详细展开。

分析了两种策略,如果是更追求消息一致性的话,优先选择策略2,当然策略2只是一个简版(悄咪咪的告诉你,这两个策略借鉴了Redis和Mysql的设计思路)

在上述策略的基础上继续思考,写入数据库是可以实现,但是不可忽略数据库的压力

数据库OS:对啊,你想的倒是好,每个线程都写数据库,如果采用策略2还需要从我身上拉取信息,线程压力小了,我数据库的压力大了喂,快找点人替我分担

 

是的,为了缓解数据库的压力,同时又可以完成我们的持久化功能,同时操作起来还快

缓存中间件:你他妈直接报我身份证得了

 

因此,我们可以采用Redis 来实现我们的需求—在转发消息的同时持久化保存消息

一样的:

1.每来一条消息,服务器做转发后,将其写入到Redis;

2.每来一条消息,服务器将其写入到Redis后,再做转发;

            策略1:服务器转发后,写入到Redis,Redis操作起来更快,对其上述写入数据库的策略1来说,就是T保存(保存需要的时间)更短了。如果转发完成后,断电了,没有写入到Redis,这里也会有问题,先按下不表

            策略2:先写入Redis,再转发,因为T保存的时间更短,写入Redis后转发失败的可能性就更小了,同样的,可以在接收消息的客户端设置一个接收反馈,接收到了反馈,没接收到的话这样服务器也知道,可以重发;

Redis的持久化功能保证了即使Redis所在的服务器机器掉电了,也不会丢失太多数据,丢失的数量取决了我们所采用的Redis持久化策略,比如是使用AOF还是使用RDB,其参数设置,以及是否做了Redis集群等等。

好啦,先介绍到这里啦~

0 109

静态变量并不是说其就不能改变值,不能改变值的量叫常量。 其拥有的值是可变的 ,而且它会保持最新的值。说其静态,是因为它不会随着函数的调用和退出而发生变化。即上次调用函数的时候,如果我们给静态变量赋予某个值的话,下次函数调用时,这个值保持不变。

静态变量 类型说明符是static。

静态变量属于静态存储方式,其存储空间为内存中的静态数据区(在 静态存储区内分配存储单元),该区域中的数据在整个程序的运行期间一直占用这些存储空间(在程序整个运行期间都不释放),也可以认为是其内存地址不变,直 到整个程序运行结束(相反,而auto自动变量,即动态局部变量,属于动态存储类别,占动态存储空间,函数调用结束后即释放)。静态变量虽在程序的整个执 行过程中始终存在,但是在它作用域之外不能使用。

另外,属于静态存储方式的量不一定就是静态变量。 例如:外部变量虽属于静态存储方式,但不一定是静态变量,必须由 static加以定义后才能成为静态外部变量,或称静态全局变量。

所有的全局变量都是静态变量,而局部变量只有定义时加上类型修饰符static,才为局部静态变量。

静态变量可以在任何可以申请的地方申请,一旦申请成功后,它将不再接受其他的同样申请。

 

转自:

https://zhidao.baidu.com/question/105782131.html

0 101

一、数据类型

数据分为基本数据类型(String, Number, Boolean, Null, Undefined,Symbol)和对象数据类型。

1、基本数据类型的特点:直接存储在栈(stack)中的数据

2、引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里

引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

堆内存

二、浅拷贝与深拷贝

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的

深拷贝和浅拷贝的示意图大致如下:

示意图

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

三、赋值和浅拷贝的区别

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

我们先来看两个例子,对比赋值与浅拷贝会对原对象带来哪些改变?

// 对象赋值
var obj1 = {
   'name' : 'zhangsan',
   'age' :  '18',
   'language' : [1,[2,3],[4,5]],
};
var obj2 = obj1;
obj2.name = "lisi";
obj2.language[1] = ["二","三"];
console.log('obj1',obj1)
console.log('obj2',obj2)
对象赋值

// 浅拷贝
var obj1 = {
   'name' : 'zhangsan',
   'age' :  '18',
   'language' : [1,[2,3],[4,5]],
};
var obj3 = shallowCopy(obj1);
obj3.name = "lisi";
obj3.language[1] = ["二","三"];
function shallowCopy(src) {
   var dst = {};
   for (var prop in src) {
       if (src.hasOwnProperty(prop)) {
           dst[prop] = src[prop];
       }
   }
   return dst;
}
console.log('obj1',obj1)
console.log('obj3',obj3)
浅拷贝

上面例子中,obj1是原始数据,obj2是赋值操作得到,而obj3浅拷贝得到。我们可以很清晰看到对原始数据的影响,具体请看下表:

对原始数据的影响

作者:grain先森
链接:https://www.jianshu.com/p/35d69cf24f1f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。