java核心系列(六)-java集合小结

本文主要从整体架构,功能特性,和其他方面介绍集合。

一.整体架构

上图:
集合架构
我们可以看到集合主要分为set,list和queue这三类,似乎还有一个Stack。它在哪里呢?
我们看JDK代码:public class Stack<E> extends Vector<E>,原来它继承了Vector。

看上面这个图,我们是不是有点疑问,为什么Linkedlist实现了List又实现了Queue。这是因为List可以模拟Queue的行为,所以也可以将他看成是Queue对象,只需要去掉List的随机访问需求即可,从而实现更高效的并发。

了解了Collection后,我们发现JDK中还有个Collections,它是什么鬼东西?查看源码,我们发现它是包含了对Collection的基本静态方法,例如排序,创建,复制等。同理,Arrays是对数组的操作,Executors是对Executor的操作。有没有一种感觉写jdk的人喜欢用名称加s来代表一个utils。

SET

一个不包括重复元素(包括可变对象)的Collection,是一种无序的集合。Set不包含满 a.equals(b) 的元素对a和b,并且最多有一个null。实现Set的接口有:EnumSet、HashSet、TreeSet等。下图是Set的JDK源码UML图。
set

List

一个有序的Collection(也称序列),元素可以重复。确切的讲,列表通常允许满足 e1.equals(e2) 的元素对 e1 和 e2,并且如果列表本身允许 null 元素的话,通常它们允许多个 null 元素。实现List的有:ArrayList、LinkedList、Vector、Stack等。下图是List的JDK源码UML图。
list

Queue

一种队列则是双端队列,支持在头、尾两端插入和移除元素,主要包括:ArrayDeque、LinkedBlockingDeque、LinkedList。另一种是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。虽然接口并未定义阻塞方法,但是实现类扩展了此接口。下图是Queue的JDK源码UML图。
queue

二,功能特性

以下比较基于ArrayList,linkedList,Vector,Stack,HashSet,TreeSet,HashMap,TreeMap

1,添加元素

在添加元素上,ArrayList,Vector的性能相对较差,原因是ArrayList在容量不够时需要扩充。

2,查找元素

在查找元素上,ArrayList,linkedList,Vector,Stack的性能略差一点点,这是由于他们在查找时需要遍历整个集合。而Set,Map类型的都是通过hash后再在链表上查找,因此速度比较快。

3,删除元素

在删除元素上,除了TreeMap和TreeSet外,其他集合类型的性能基本无差异,TreeSet基于TreeMap实现,TreeMap之所以性能相对较差的原因是它在删除时需要排序。
List适用于允许重复元素的单个对象集合场景,Set适用于不允许重复元素的单个对象集合场景,Map适合key-value结构的集合场景。

三,并发结构

1,ConcurrentHashMap

线程安全的haspmap,默认采用16个Segment存储对象。每个Segment一把锁,可允许16个线程同时并发的操作集合对象。

2,CopyOnWriteArrayList

CopyOnWriteArrayList是一个线程安全的,并且在读操作时无需加锁的ArrayList。

3,ArrayBlockingQueue

ArrayBlockingQueue是一个基于数组,先进先出的,线程安全的集合类,其特色为指定时间的阻塞读写,并且容量是可限制的。

offer(E,long,TimeUnit) 插入元素到数组的尾部,如果数组已满,则等待。poll(E,long,TimeUnit)获取数组中的第一个元素,如果数组中没有元素,则等待。

4,LinkedBlockingQueue

LinkedBlockingQueue采用链表的方式存储对象。

对于读操作take和poll,采用一把锁,对于写操作put和poll,采用另一把锁。因此在高并发读写操作多的情况下,性能好于ArrayBlockingQueue。但在遍历和删除元素时,需要2把锁同时锁住。

5,IndentityHashMap

简单说IdentityHashMap与常用的HashMap的区别是:前者比较key时是“引用相等”而后者是“对象相等”,即对于k1和k2,当k1==k2时,IdentityHashMap认为两个key相等,而HashMap只有在k1.equals(k2) == true 时才会认为两个key相等。

6,ConcurrentSkipListMap

SkipList是红黑树的一种简化替代方案,是个流行的有序集合算法。在4线程1.6万数据的条件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右。
但ConcurrentSkipListMap有几个ConcurrentHashMap 不能比拟的优点:

  1. ConcurrentSkipListMap 的key是有序的。
  2. ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。

四,其他特性

算法

  1. Colletions.sort(list) 与 Arrays.sort(T[])

Colletions.sort()实际会将list转为数组,然后调用Arrays.sort(),排完了再转回List。
PS. JDK8里,List有自己的sort()方法了,像ArrayList就直接用自己内部的数组来排,而LinkedList, CopyOnWriteArrayList还是要复制出一份数组。
而Arrays.sort(),对原始类型(int[],double[],char[],byte[]),JDK6里用的是快速排序,对于对象类型(Object[]),JDK6则使用归并排序。为什么要用不同的算法呢?

  1. JDK7的进步

到了JDK7,快速排序升级为双基准快排(双基准快排 vs 三路快排);归并排序升级为归并排序的改进版TimSort,一个JDK的自我进化。

  1. JDK8的进步

再到了JDK8, 对大集合增加了Arrays.parallelSort()函数,使用fork-Join框架,充分利用多核,对大的集合进行切分然后再归并排序,而在小的连续片段里,依然使用TimSort与DualPivotQuickSort。

五,参考资料

  1. 《分布式JAVA应用 基础与实践》—-林昊
  2. JDK78数则
  3. Java 容器 & 泛型(1):认识容器

java核心系列(五)——java I/O

一,概述

二,BIO

java I/O主要分为下面这几类:

  • 基于字节操作的 I/O 接口:InputStream 和 OutputStream
  • 基于字符操作的 I/O 接口:Writer 和 Reader
  • 基于磁盘操作的 I/O 接口:File
  • 基于网络操作的 I/O 接口:Socket

字符与字节:

  1. InputStream 类以二进制输入/输出,I/O速度快且效率搞,但是它的read()方法读到的是一个字节(二进制数据),很不利于人们阅读,而且无法直接对文件中的字符进行操作,比如替换,查找(必须以字节形式操作);
    而Reader类弥补了这个缺陷,可以以文本格式输入/输出,非常方便;比如可以使用while((ch = filereader.read())!=-1 )循环来读取文件;可以使用BufferedReader的readLine()方法一行一行的读取文本。
  2. InputStreamReader 是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。
  3. BufferedReader的最大特点就是缓冲区的设置。BufferReader类用来包装所有其 read() 操作可能开销很高的 Reader(如 FileReader 和InputStreamReader)。

三,NIO

nio基于块,io基于流。NIO 将最耗时的 I/O 操作(即填充和提取缓冲区)转移回操作系统,因而可以极大地提高速度。

  • 面向流的I/O系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。
  • 面向块的I/O系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

1,Buffer

1.1 Buffer类型

对于每一种基本 Java 类型都有一种缓冲区类型:

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

1.2 Buffer中的参数项:

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

1.3 Buffer中的常用方法

ByteBuffer buf = ByteBuffer.allocate(48); // create buffer

buf.flip();  //make buffer ready for read
buf.clear(); //make buffer ready for writing
buf.compact();//make buffer ready for writing,copies all unread data to the beginning of the Buffer

buf.rewind();//sets the position back to 0, so you can reread all the data in the buffer. 

buf.mark() ;//mark a given position 
buf.reset();//reset the position back to the marked position

buffer的读写模式

1.4 缓冲区分片

slice() 方法根据现有的缓冲区创建一种子缓冲区 。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。

1.5 直接和间接缓冲区

直接缓冲区 是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。

1.6 内存映射文件 I/O

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。一般来说,只有文件中实际读取或者写入的部分才会送入(或者 映射 )到内存中。

现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java 内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。

2,Channel

Channel是一个对象,可以通过它读取和写入数据。举个例子,channel就马路,buffer就是汽车,数据就是装在汽车上的沙子。
通道与流的不同之处在于通道是双向的。而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而 通道 可以用于读、写或者同时用于读写。

2.1 Channel的类型

  • FileChannel //文件
  • DatagramChannel //UDP
  • SocketChannel //TCP
  • ServerSocketChannel //TCP服务端

2.2 FileChannel

FileChannel可以实现从Channel到Channel间的直接数据传输.

  • FileChannel.transferFrom() 将数据从来源channel传输过来。
  • FileChannel.transferTo() 将数据传输到其他的channel中。

2.3 SocketChannel

SocketChannel是连接tcp网络的通道。其操作模式如下:

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("localhost", 80));
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
    channel.write(buf);
}

2.4 ServerSocketChannel

监听tcp连接。

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while(true){
    SocketChannel socketChannel =
            serverSocketChannel.accept();
    //do something with socketChannel...
}

2.5 DatagramChannel

UDP连接

2.6 Pipe

管道是一种线程之间进行数据交换的方式。
Pipe有一个source channel 和一个sink channel.你可以写数据到sink channel中,从source channel中读取数据.
pipe-internals

Pipe pipe = Pipe.open();
Pipe.SinkChannel sinkChannel = pipe.sink();
sinkChannel.write(buf);
Pipe.SourceChannel sourceChannel = pipe.source();
inChannel.read(buf);

2.7 分散和聚集

分散/聚集 I/O 是使用多个而不是单个缓冲区来保存数据的读写方法。
在 分散读取 中,通道依次填充每个缓冲区。填满一个缓冲区后,它就开始填充下一个。在某种意义上,缓冲区数组就像一个大缓冲区。

接口:

ScatteringByteChannel
long read( ByteBuffer[] dsts );
long read( ByteBuffer[] dsts, int offset, int length );

GatheringByteChannel
long write( ByteBuffer[] srcs );
long write( ByteBuffer[] srcs, int offset, int length );

分散/聚集 I/O 对于将数据划分为几个部分很有用。例如,您可能在编写一个使用消息对象的网络应用程序,每一个消息被划分为固定长度的头部和固定长度的正文。您可以创建一个刚好可以容纳头部的缓冲区和另一个刚好可以容纳正文的缓冲区。当您将它们放入一个数组中并使用分散读取来向它们读入消息时,头部和正文将整齐地划分到这两个缓冲区中。

3,Selector

selector用于多路复用,可以同时监听一组通信信道(Channel)上的 I/O 状态。

传统的套接字服务器的处理方式是对于每一个客户端套接字连接,都新创建一个线程来进行处理。创建线程是很耗时的操作,而有的实现会采用线程池。不过一个请求一个线程的处理模型并不是很理想。原因在于耗费时间创建的线程,在大部分时间可能处于等待的状态。而多路复用I/O的基本做法是由一个线程来管理多个套接字连接。该线程会负责根据连接的状态,来进行相应的处理。多路复用I/O依靠操作系统提供的select或相似系统调用的支持,选择那些已经就绪的套接字连接来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

while(true) {
int readyChannels = selector.select();
if(readyChannels == 0) continue;
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
}

四,AIO

五,其他

1,同步与异步

所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。

2,阻塞与非阻塞

阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。

3,网络I/O优化

  1. 一个是减少网络交互的次数,缓存。
  2. 减少网络传输数据量的大小,压缩。
  3. 尽量减少编码,最好直接以字节发送。
  4. 同步异步,阻塞非阻塞的选择。

六,参考资料

  1. 深入分析 Java I/O 的工作机制
  2. Java深度历险(八)——Java I/O
  3. Java I/O底层是如何工作的?
  4. Java NIO Overview
  5. NIO 入门

java核心系列(四)—java序列化

一,概述

本文主要介绍java序列化方面的一些基础知识。

二,java序列化类介绍

1,Serializable接口

1.1.triansant

当某个字段被声明为transient后,默认序列化机制就会忽略该字段。

1.2.serialVersionUID

序列化版本号,用来保证两个类是否是兼容的。默认情况下应该给一个值,不然系统会在运行的时候调用一个复杂的过程来生成这个值。

1.3writeObject和readObject

通过实现这2个方法来生成自己的自定义序列化方式。

1.4readObjectNoData

当没有获取到对象数据时,默认的初始化行为。

1.5writeReplace和readResolve

控制实例化对象的写入写出,可以指定任意对象。可通过此接口来禁止单例对象的多态化。

2,ObjectOutputStream和ObjectInputStream

实际的序列化和反序列化工作是通过ObjectOuputStream和ObjectInputStream来完成的。

ObjectInputStream:

  • readObject : 从流中读取一个Java对象
  • defaultWriteObject:默认的写入方法,注意这不是说默认会调用,而是采用默认的算法。

ObjectOutputStream:

  • writeObject:把一个Java对象写入到流中
  • defaultReadObject: 默认的读取算法。

3,Externalizable接口

无论是使用transient关键字,还是使用writeObject()和readObject()方法,其实都是基于Serializable接口的序列化。JDK中提供了另一个序列化接口—Externalizable,使用该接口之后,之前基于Serializable接口的序列化机制就将失效。

  • writeExternal:把一个Java对象写入到流中
  • readExternal:从流中读取一个Java对象

三,其他序列化器

1,json

json的序列化框架有fastjson,jackson,gson等。

适用性:

  • 公司之间传输数据量相对小,实时性要求相对低(例如秒级别)的服务。
  • 基于Web browser的Ajax请求。
  • 由于JSON具有非常强的前后兼容性,对于接口经常发生变化,并对可调式性要求高的场景,例如Mobile app与服务端的通讯。

2,xml

xml的序列化框架有XStream。

XML的序列化和反序列化的空间和时间开销都比较大,对于对性能要求在ms级别的服务,不推荐使用。

3,hessian

hessian主要用于java序列化。它的实现机制是着重于数据,附带简单的类型信息的方法:

  1. 对于简单的数据类型。就像Integer a = 1,hessian会序列化成I 1这样的流,I表示int or Integer,1就是数据内容。
  2. 对于复杂对象,通过Java的反射机制,hessian把对象所有的属性当成一个Map来序列化,产生类似M className propertyName1 I 1 propertyName S stringValue
  3. 对于引用对象,在序列化过程中,如果一个对象之前出现过,hessian会直接插入一个R index这样的块来表示一个引用位置,从而省去再次序列化和反序列化的时间。

4,thift

Thrift是Facebook开源提供的一个高性能,轻量级RPC服务框架,其产生正是为了满足当前大数据量、分布式、跨语言、跨平台数据通讯的需求。 但是,Thrift并不仅仅是序列化协议,而是一个RPC框架。相对于JSON和XML而言,Thrift在空间开销和解析性能上有了比较大的提升,对于对性能要求比较高的分布式系统,它是一个优秀的RPC解决方案;但是由于Thrift的序列化被嵌入到Thrift框架里面,Thrift框架本身并没有透出序列化和反序列化接口,这导致其很难和其他传输层协议共同使用(例如HTTP)。

5,protobuf

  • 序列化数据非常简洁,紧凑.
  • 解析速度非常快
  • 提供了非常友好的动态库,使用非常简介,反序列化只需要一行代码。

6,avro

Avro的产生解决了JSON的冗长和没有IDL的问题。 Avro提供两种序列化格式:JSON格式或者Binary格式。Binary格式在空间开销和解析性能方面可以和Protobuf媲美,JSON格式方便测试阶段的调试。

  • 动态类型:Avro并不需要生成代码,模式和数据存放在一起,而模式使得整个数据的处理过程并不生成代码、静态数据类型等等。这方便了数据处理系统和语言的构造。
  • 未标记的数据:由于读取数据的时候模式是已知的,那么需要和数据一起编码的类型信息就很少了,这样序列化的规模也就小了。
  • 不需要用户指定字段号:即使模式改变,处理数据时新旧模式都是已知的,所以通过使用字段名称可以解决差异问题。

四,参考资料

  1. 序列化的秘密.pdf —-liujia
  2. jvm-serializers
  3. 序列化和反序列化
  4. Java对象序列化与RMI
  5. Google Protocol Buffer 的使用和原理
  6. Avro:大数据的二进制传输中间件

java核心系列(三)—java类加载器

一,概览

类加载

1,类的生命周期

一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况,如图所示:

2,连接

链接过程:验证、准备和解析。

  1. 验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。
  2. 准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。
  3. 解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。

3,类的使用

类的使用包括主动引用和被动引用。

a,主动引用

通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
通过反射方式执行以上三种行为。
初始化子类的时候,会触发父类的初始化。
作为程序入口直接运行时(也就是直接调用main方法)。

b,被动引用

引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
定义类数组,不会引起类的初始化。
引用类的常量,不会引起类的初始化。

4,卸载

如果满足下面的情况,类就会被卸载:

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

二,问答

1,什么是双亲委派?

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

2,由不同的类加载器加载的指定类还是相同的类型吗?

在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。

3,在代码中直接调用Class.forName(String name)方法,到底会触发那个类加载器进行类加载行为?

Class.forName(String name)默认会使用调用类的类加载器来进行类加载。

4,自定义类的默认父加载器是谁?

AppClassLoder

5,线程上下文类加载器

使用线程上下文类加载器,可以在执行线程中抛弃双亲委派加载链模式,使用线程上下文里的类加载器加载类。

6,SPI

SPI(Service Provider Interface) 是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

7,如何在系统或程序的类加载器、当前类加载器、以及当前线程的上下文类加载器中选择?

  1. 系统类加载器通常不会使用。此类加载器处理启动应用程序时classpath指定的类,可以通过ClassLoader.getSystemClassLoader()来获得。所有的ClassLoader.getSystemXXX()接口也是通过这个类加载器加载的。一般不要显式调用这些方法,应该让其他类加载器代理到系统类加载器上。
  2. 上下文类加载器要比当前类加载器更适合于框架编程。
  3. 当前类加载器则更适合于业务逻辑编程。

8,java中使用的类加载器

  1. JNDI使用线程上下文类加载器。
  2. Class.getResource()和Class.forName()使用当前类加载器。
  3. JAXP使用上下文类加载器。
  4. java.util.ResourceBundle使用调用者的当前类加载器。
  5. URL协议处理器使用java.protocol.handler.pkgs系统属性并只使用系统类加载器。
  6. Java序列化API缺省使用调用者当前的类加载器。

9,getResourceAsStream辨析

  1. Class.getResourceAsStream(String path) : path 不以’/‘开头时默认是从此类所在的包下取资源,以’/‘开头则是从
    ClassPath根下获取。其只是通过path构造一个绝对路径,最终还是由ClassLoader获取资源。
  2. Class.getClassLoader.getResourceAsStream(String path) :默认则是从ClassPath根下获取,path不能以’/‘开头,最终是由ClassLoader获取资源。
  3. ServletContext.getResourceAsStream(String path):默认从WebAPP根目录下取资源,Tomcat下path是否以’/‘开头无所谓,当然这和具体的容器实现有关。

10,Web容器设计原则

  1. 每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes和 WEB-INF/lib目录下面。
  2. 多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
  3. 当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。

11,tomcat的类加载规则

  1. 查找本地缓存
  2. 使用系统类加载器加载系统类
  3. 使用本地类加载器加载类
  4. 如果没有找到,使用父类加载器加载类

12,OSGi

OSGi 中的每个模块都有对应的一个类加载器。它负责加载模块自己包含的 Java 包和类。当它需要加载 Java 核心库的类时(以 java开头的包和类),它会代理给父类加载器(通常是启动类加载器)来完成。当它需要加载所导入的 Java 类时,它会代理给导出此 Java 类的模块来完成加载。

三,参考资料

  1. 详解java类的生命周期
  2. 深入探讨 Java 类加载器
  3. 深入理解Java类加载器(1):Java类加载原理解析
  4. Tomcat源码解读系列(四)——Tomcat类加载机制概述

java核心系列(二)—java反射

一,概述

java动态代理只能对方法进行反射,为什么?

二,Class

getXXX()返回的为public的XXX(方法,构造方法,属性,注解),包括继承的和接口实现的。

getDeclaredXXX()返回的为本类的所有的XXX(方法,构造方法,属性,注解),不包括继承的。

1,Methods

  • public Method[] getMethods()
    返回某个类的所有公用(public)方法包括其继承类的公用方法,当然也包括它所实现接口的方法。

  • public Method[] getDeclaredMethods()
    返回某个类的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。当然也包括它所实现接口的方法。

2,Constructors

  • public Constructor getConstructor(Class<?>… parameterTypes)
    根据参数返回公共构造方法。
  • public Constructor<?>[] getConstructors()
    得到所有的公共构造方法
  • public Constructor<?>[] getDeclaredConstructors()
    得到声明的构造方法

3,Fields

同上

4,Annotations

同上,只能获取注解集合,暂时不能获取指定参数注解。

三,AccessibleObject


从图中我们可以看到Methods,Constructors,Fields继承了这个类。
我们看这个类的方法:

从图中可以看出,这个对象主要包含了2类方法:

  • 第一类是获取对象注解,getAnnotations()。
  • 第二类是设置对象是否可访问,setAccessible(boolean flag),通过设置访问标志来访问非public对象(方法,构造方法,属性)。

五.Modifiers

我们可以看到修饰符使用的是二进制来表示的,访问修饰符用01,10,100,1000来表示的,总共容纳32个修饰符,每一位使用mod&修饰符来判断当前值是否包含修饰符。

public static final int PUBLIC           = 0x00000001;
public static final int PRIVATE          = 0x00000002;
public static final int PROTECTED        = 0x00000004;
public static final int STATIC           = 0x00000008;
...
Modifier.isAbstract(int modifiers)
Modifier.isFinal(int modifiers)
Modifier.isInterface(int modifiers)
Modifier.isNative(int modifiers)
Modifier.isPrivate(int modifiers)
Modifier.isProtected(int modifiers)
Modifier.isPublic(int modifiers)
Modifier.isStatic(int modifiers)
Modifier.isStrict(int modifiers)
Modifier.isSynchronized(int modifiers)
Modifier.isTransient(int modifiers)
Modifier.isVolatile(int modifiers)

六.InvocationHandler

1
2
3
4
5
6
7
8
Proxy
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)

上述方法与下面的方法 完全等同,都是构造一个代理实例。
Proxy.getProxyClass(loader, interfaces).
getConstructor(new Class[] { InvocationHandler.class }).
newInstance(new Object[] { handler });

从这里可以看出,任何一个代理类都会有一个以InvocationHandler为参数的构造方法,通过这个方法,我们可以创建一个代理类。
然后我们看看InvocationHandler的接口实现:

1
public Object invoke(Object proxy, Method method, Object[] args)

从这里看出,参数仅仅能传Method,所以只能对方法进行代理。

七,Array

注意不要混淆java.lang.reflect.Array与java.util.Arrays。

1,创建数组

int[] intArray = (int[]) Array.newInstance(int.class, 3);

创建一个大小为3,类型为int的数组。

2,访问数组

Array.set(intArray, 0, 123);

设置数组的第一个值为123。

3,从数组中获取类对象

Class stringArrayClass = String[].class;
Class intArray = Class.forName("[I");
Class stringArrayClass = Class.forName("[Ljava.lang.String;");

八,jdk8改进

反射异常ClassNotFoundException、NoSuchMethodException、IllegalAccessException,InvocationTargetExcetpion等有一个父类异常叫ReflectiveOperationExcetpion。

旧版JDK,还有个很莫名其妙的地方,就是所有反射,都拿不到参数名,无论名字叫啥,都返回arg0,arg1,所以在CXF,SpringMVC里,你都要把参数名字用annotation再写一遍:
Person getEmployee(@PathParam(“dept”) Long dept, @QueryParam(“id”) Long id)
现在,JDK8新提供的类java.lang.reflect.Parameter可以反射参数名了,编译时要加参数,如 javac -parameters xxx.java,或者Eclipse里设置。然后就可以写成:
Person getEmployee(@PathParam Long dept, @QueryParam Long id)

九,参考资料

Java Reflection Tutorial

java核心系列(一)—介绍

图1

一,概述

俗话说,只有地基打的好,楼层才能盖得高。对于程序员来说,也是一样。我们也需要不断反复的理解以前的东西,打好自己的基础,才能在技术上又更大的进步。

本系列主要介绍j2se的核心部分,希望对大家梳理自己的知识体系有所帮助。本系列并不完善,以后会逐步添加新的东西。

当然,本系列暂时不涉及计算机基础知识,这些知识也需要程序员去理解。

二,目标

本系列主要从图1中绘制的几大主线来讲解java核心。
初步设计采用问答式和总结式来介绍各个组件的特性,以加深读者对java核心的理解。
本系列只是个引子,更深入的内容请参考文中的参考资料。

三,适用人群

java爱好者

三,总结

暂时没有了,就这么点,如果大家有好的意见可以提出来。

性能调优总结

性能调优主要是对CPU,磁盘IO,网络IO,内存等方面进行调优。
性能调优

一,CPU

1,上下文切换

上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程或线程切换到另一个进程或线程。

上下文是指某一时间点 CPU 寄存器和程序计数器的内容。寄存器是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置,具体依赖于特定的系统。

稍微详细描述一下,上下文切换可以认为是内核(操作系统的核心)在 CPU 上对于进程(包括线程)进行以下的活动:

  1. 挂起一个进程,将这个进程在 CPU 中的状态(上下文)存储于内存中的某处,
  2. 在内存中检索下一个进程的上下文并 将其在 CPU 的寄存器中恢复,
  3. 跳转到程序计数器所指向的位置(即跳转到进程被中断时的代码行),以恢复该进程。

上下文切换的消耗

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 号称的相比于其他的操作系统(包括其他类 Unix 系统)的很多优点中,有一项就是,其上下文切换和模式切换的时间消耗非常少。

2,利用率

利用率为CPU在用户进程,内核,中断处理,IO等待以及空闲5个部分使用百分比。Linux And System Network Performance Monitoring建议用户进程和内核进程的CPU消耗分别是65%-70%,30%-35%。
在liunx中可以使用top或者pidstat查看CPU的消耗情况。
在java应用中,CPU消耗主要在us,sy上。

US

US:用户进程处理所占的百分比
当us过高时,表明应用消耗了大量的CPU。在这种情况下,需要找到具体消耗CPU的线程所执行的代码。

SY

SY:内核线程所占的百分比
当sy过高时,表示linux花费了过多的时间在进程切换上面。java应用造成这种现象的主要原因是启动的线程比较多,且这些线程多数处于不断阻塞的状态和执行状态的变化过程中,产生了大量的上下文切换。
通过jstack -l pid 查看线程争用。可以看到TIMED_WAITING(on object monitor)状态和Runnable转换中。

二,磁盘IO

在linux中,要查看线程的磁盘IO消耗,主要方法是通过pidstat。

使用pidstat找到文件IO操作多的线程,之后结合jstack找到对应的java代码。

三,网络IO

在linux中,通过sar来分析网络IO的状况。如果需要跟踪tcp的通信信息,需要使用tcpdump来进行。

四,内存消耗

通过jmap,jstat,mat,visualVM来分析

PS:本文总结自《分布式JAVA应用 基础与实践》—-林昊

String字符串小结

String可以说是java编程中最常用的对象。用了这么久,大家对它的印象有多深呢?下面我就谈谈我自己对它的一些认知吧。

不可变对象

我们可以看到,String被声明成了final,这样我们可以得到String是不可继承的对象,也是线程安全的对象。

public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
}

concat

接下来我们看String的concat方法。由于String是不可变的,所以当我们连接一个字符串时,它会合并2哥字符串的value数组,然后用这个数组创建一个新的字符串。

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
  }

“+”连接符的处理

Java 语言提供对字符串串联符号(”+”)以及将其他对象转换为字符串的特殊支持。字符串串联是通过 StringBuilder(或 StringBuffer)类及其 append 方法实现的。

String str1 = "aabc0";
String str2 = "aa" + "bc"+"0";
System.out.println(str1+"  "+str2);
System.out.println(str1==str2);
for (int i = 0; i < 1; i++) {
    String str3 = "aa" + "bc"+i;
    System.out.println(str1+"  "+str3);
    System.out.println(str1==str3);
}

打印结果:

aabc0  aabc0
true
aabc0  aabc0
false

如果在编译期间就能确定字符串,编译器就会优化String,合并到一起。如上例所示str1==str2为true,表明为同一字符串。

intern浅析

对于字符串常量池,看了网上的很多文章,大部分人貌似都不对。现在在此在总结一遍。

String str1 = new StringBuilder("chaofan").append("wei").toString();
System.out.println(str1.intern() == str1);

String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);

打印结果:

jdk6false false
jdk7 下true false

产生的差异在于在jdk1.6中 intern 方法会把首次遇到的字符串实例复制到永久待(常量池)中,并返回此引用;但在jdk1.7中,只是会把首次遇到的字符串实例的引用添加到常量池中(没有复制),并返回此引用。
所以在jdk1.7中执行上面代码,str1返回true是引用他们指向的都是str1对象(堆中)(池中不存在,返回原引用),而str2返回false是因为池中已经存在”java”了(关键词),所以返回的池的对象,因此不相等。

相信很多 JAVA 程序员都做做类似 String s = new String(“abc”)这个语句创建了几个对象的题目。 这种题目主要就是为了考察程序员对字符串对象的常量池掌握与否。上述的语句中是创建了2个对象,第一个对象是”abc”字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。接下来我们分析另一端代码。

String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);

String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);

打印结果是

  • jdk6 下false false
  • jdk7 下false true

为什么会这样呢?美团网给出了很好的解释?我觉得上面的说法比美团网的解释更好,大家可以参考http://tech.meituan.com/in_depth_understanding_string_intern.html。
最根本的原因应该是:
jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括2点:

  • 将String常量池 从 Perm 区移动到了 Java Heap区
  • String#intern 方法时,如果存在堆中的对象,会直接保存对象的引用,而不会重新创建对象。

intern需要注意的地方

String的String Pool是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。

在 jdk6中StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:

  • -XX:StringTableSize=99991

例如:在fastjson 中对所有的 json 的 key 使用了 intern 方法,缓存到了字符串常量池中,这样每次读取的时候就会非常快,大大减少时间和空间。而且 json 的 key 通常都是不变的。这个地方没有考虑到大量的 json key 如果是变化的,那就会给字符串常量池带来很大的负担。

这个问题 fastjson 在1.1.24版本中已经将这个漏洞修复了。程序加入了一个最大的缓存大小,超过这个大小后就不会再往字符串常量池中放了。
更多详情请参考http://tech.meituan.com/in_depth_understanding_string_intern.html

subString内存泄露

首先我们看subString方法。

public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

在这个方法中,新建了一个String对象。

在jdk1.7,1.8中,如下:

public String(char value[], int offset, int count) {
     ....
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}

在jdk1.6中

String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}

我们可以看到,1.6中还是引用了原来的字符串,存在内存泄露问题。1.7及以上使用了数组复制的方法创建了一个新的数组,不存在内存泄露。

其他

字符串分割

在jdk1.6中:优先考虑使用indexOf方法,最后才使用spilt方法。
在jdk1.8中:优先考虑spilt方法,最后是indexOf方法。
为什么?我们对下面代码进行测试:

StringBuffer sb = new StringBuffer();
int i = 1000;
for (int i1 = i; i1 > 0; i1--) {
    sb.append(i1).append(";");
}
String old = sb.toString();
int k = 10000;
long l = System.currentTimeMillis();
for (int k1 = k; k1 > 0; k1--) {
    old.split(";");
}
System.out.println(System.currentTimeMillis() - l);
long l1 = System.currentTimeMillis();
String tmp = old;
for (int k1 = k; k1 > 0; k1--) {
    while (true) {
        int j = tmp.indexOf(";");
        if (j < 0) {
            break;
        }
        tmp.substring(0, j);
        tmp = tmp.substring(j + 1);
    }
    tmp = old;
}
System.out.println(System.currentTimeMillis() - l1);

结果如下:

  • jdk1.6中 1073 295
  • jdk1.8中 606 7177

对比1073与606,在1.8中由于取消了offset和count2个字段,所以spilt的性能有了较大的提升。
对比295与7177,在1.6中,由于substring方法不会产生新字符串,所以速度更快。在1.8中每次spilt都会产生新的字符串,造成了性能瓶颈。

startWith替代

我们看看下面一段代码:

String ss = "abcdefgg";
int i = 10000000;
long l = System.currentTimeMillis();
for (int i1 = i; i1 > 0; i1--) {
    boolean a = ss.startsWith("abc");
}
System.out.println(System.currentTimeMillis() - l);
long l1 = System.currentTimeMillis();
for (int i1 = i; i1 > 0; i1--) {
    boolean c = ss.charAt(0)=='a'&&ss.charAt(1)=='b'&&ss.charAt(2)=='c';
}
System.out.println(System.currentTimeMillis() - l1);

打印结果如下:

  • 在jdk1.6中 109 26
  • 在jdk1.8中 22 21

为什么会出现这样的结果呢,我们分析代码,发现

//1.6
public boolean startsWith(String prefix, int toffset) {
    char ta[] = value;
    int to = offset + toffset;
    char pa[] = prefix.value;
    int po = prefix.offset;
    int pc = prefix.count;
    // Note: toffset might be near -1>>>1.
    if ((toffset < 0) || (toffset > count - pc)) {
        return false;
    }
    while (--pc >= 0) {
        if (ta[to++] != pa[po++]) {
            return false;
        }
    }
    return true;
}

//1.8
public boolean startsWith(String prefix, int toffset) {
    char ta[] = value;
    int to = toffset;
    char pa[] = prefix.value;
    int po = 0;
    int pc = prefix.value.length;
    // Note: toffset might be near -1>>>1.
    if ((toffset < 0) || (toffset > value.length - pc)) {
        return false;
    }
    while (--pc >= 0) {
        if (ta[to++] != pa[po++]) {
            return false;
        }
    }
    return true;
}

在1.6中第四行,int to = offset + toffset;进行了整形相加操作,这里影响了方法的执行时间。1.7及以后去掉了offset和count,性能有了极大的提升。

参考资料

mysql索引总结

索引是什么?索引是存储引擎用于快速找到记录的一种数据结构。

索引分类

B-Tree索引

MyISAM使用前缀压缩技术使得索引更小。InnoDB按照原数据存储索引。
MyISAM索引通过数据的物理位置引用被索引的行。InnoDB根据主键引用被索引的行。
可以使用B-Tree索引的查询类型:

  • 全值匹配:和索引中的所有列进行匹配
  • 匹配最左前缀:匹配第一个索引
  • 匹配列前缀:匹配某一列的值的开头部分,只使用第一个索引
  • 匹配范围值:匹配某个范围,只使用第一个索引
  • 精确匹配某一列并范围匹配另外一列
  • 只访问索引的查询
  • 排序操作:索引的有序性

哈希索引

哈希索引基于哈希表实现,只有精确匹配所有列的查询才有效。对于每一行数据,存储引擎会对所有的索引列计算一个hash值。
哈希索引的限制:

  • 哈希索引只能包含哈希值和行指针,不能使用索引的值避免行读取。
  • 哈希索引数据并不是按照索引值顺序存储的,无法用于排序。
  • 哈希索引也不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。
  • 哈希索引只支持等值查找。
  • 访问哈希索引的数据非常快,除非有很多哈希冲突。

空间数据索引

地理数据索引。(略)

全文索引

全文索引查找文本中的关键词,一般采用倒排索引。

其他

索引的优点

索引可以让服务器快速的定位到指定的位置。

  • 索引大大减少了服务器需要扫描的数量。
  • 索引可以帮助服务器避免排序和临时表。
  • 索引可以将随机IO变为顺序IO。

参考:Relational Database Index Design and the Optimizers.

索引是最好的解决方案吗?
对于非常小的表,大部分情况下简单的全表扫描更高效。
对于中大型的表,索引就非常有效。
对于特大型的表,建立和使用索引的代价随之增长。可以采用分区技术等。

索引策略

独立的列

索引列不能是表达式的一部分,也不能使函数的参数。

前缀索引和索引选择性

有时候需要索引很长的字符列,会让索引变得很大,这是我们可以使用哈希索引。我们是否还有更好的方法?

通常可以索引开始的部分字符,这样可以节省索引空间,从而提高索引效率。但这样也会降低索引的选择性。

多列索引

很多人对多列索引的理解都不够。一个常见的错误就是,为每个列创建独立的索引,或者按照错误的顺序创建多列索引。

在多个列上建立独立的单列索引大部分情况下并不能提高MySql的查询性能。MySql5.0以上引入了一种“索引合并”的策略,一定程度上可以使用表上的多个单列索引来定位指定的行。

选择合适的索引列顺序

经验法则:将选择性最高的列放在索引最前列。

聚簇索引

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。InnoDB的聚簇索引索引实际上在同一个结构中保存了B-Tree索引和物理行。
当表有聚簇索引时,实际上它的数据行保存在索引的叶子节点中。所以,一个表中只能有一个聚簇索引。

聚簇索引的优点:

  • 把相关数据保存在一起
  • 数据访问更快,提高了IO的性能

缺点:

  • 插入速度依赖于插入顺序。
  • 更新索引的代价很高。
  • 插入新行,更新主键或者移动行的时候,面临“页分裂”的问题。
  • 可能导致全表扫描变慢,尤其是行稀疏,也分裂导致数据不连续
  • 二级索引(非聚簇索引)可能比想象的大,因为包含了主键列。

覆盖索引

如果索引的叶子节点已经包含了需要查询的字段,那么还有什么必要再回表查询?如果一个索引包含所有需要查询的字段的值,我们就称为“覆盖索引”。

适应场景:

  • 索引条目通常远小于数据行的大小。
  • 对于IO密集型的范围查询会比随机从磁盘读取每一行数据的IO要小的多。
  • 可内存缓存索引,避免系统调用

使用索引进行排序

当索引的列序列和order by子句的顺序完全一致,并且所有列的排序方向(倒序或正序)都一样时,MySql才能够使用索引来对结果进行排序。如果查询关联多个表时,则只有当ORDER BY子句引用的字段全部为第一个表时,才能使用索引进行排序。ORDER BY 子句和查找型查询的限制是一样的:需要全部满足索引的最左前缀的要求;否则,MySql都需要执行排序操纵,而无法利用索引进行排序。

有一种情况下ORDER BY子句可以不满足索引的最左前缀要求,就是前导列为常量的时候。如果WHERE子句或者JOIN子句中对这些列指定了常量,就可以“弥补”索引的不足。

索引和锁

索引可以让查询锁定更少的行。如果你的查询不需要访问那些不需要的行,那么就会锁定更少的行。

设计原则

选择性特别低,并且经常用到的的作为索引前列

例如性别只有2种选择,但是我们经常会用到。如果没有用到性别查询,我们也可以使用 sex in(‘m’,‘f’)来让mysql选择该索引。

有一点需要注意的是,使用in的方式覆盖不在where条件中查询的列,不能过度使用。因为每增加一个in条件,优化器都需要以指数的形式增加,abc。

对于经常性范围查询,应放到索引后列

对于范围查询,MySql无法再使用范围列后面的其他索引了,但是对于多个“等值条件查询”则没有这个限制。
例如年龄查询,可能是查询某个范围的,我们就要放到后面。

避免多个范围查询

使用in来代替多范围查询

优化排序

对于选择性非常低的列,可以添加一些特殊的索引进行排序。
例如sex进行排序时,可以使用(sex,rating)索引进行排序。
select from table where sex=’M’ order by rating ;

PS:本文总结来自《mysql高性能》一书