Java基础
# Java
- Oracle JDK 有部分源码是闭源的,如果确实需要可以查看 OpenJDK 的源码,可以在该网站获取。
- http://grepcode.com/snapshot/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/
- http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/73d5bcd0585d/src
- 上面这个还可以查看 native 方法。
# 1.1 JDK&JRE&JVM
- JDK(Java Development Kit)是针对 Java 开发员的产品,是整个 Java 的核心,包括了 Java 运行环境 JRE、Java 工具(编译、开发工具)和 Java 核心类库。
- Java Runtime Environment(JRE)是运行 JAVA 程序所必须的环境的集合,包含 JVM 标准实现及 Java 核心类库。
- JVM 是 Java Virtual Machine(Java 虚拟机)的缩写,是整个 java 实现跨平台的最核心的部分,能够运行以 Java 语言写作的软件程序。
- JDK 包含 JRE 和 Java 编译、开发工具;
- JRE 包含 JVM 和 Java 核心类库;
- 运行 Java 仅需要 JRE;而开发 Java 需要 JDK。
# 1.2 跨平台
- 字节码是在虚拟机上运行的,而不是编译器。换而言之,是因为 JVM 能跨平台安装,所以相应 JAVA 字节码便可以跟着在任何平台上运行。只要 JVM 自身的代码能在相应平台上运行,即 JVM 可行,则 JAVA 的程序员就可以不用考虑所写的程序要在哪里运行,反正都是在虚拟机上运行,然后变成相应平台的机器语言,而这个转变并不是程序员应该关心的。
# 1.3 基础数据类型
第一类:整型 byte short int long
第二类:浮点型 float double
第三类:逻辑型 boolean(它只有两个值可取 true false)
第四类:字符型 char
- byte(1)的取值范围为-128~127(-2 的 7 次方到 2 的 7 次方-1)
- short(2)的取值范围为-32768~32767(-2 的 15 次方到 2 的 15 次方-1)
- int(4)的取值范围为(-2147483648~2147483647)(-2 的 31 次方到 2 的 31 次方-1)
- long(8)的取值范围为(-9223372036854774808~9223372036854774807)(-2 的 63 次方到 2 的 63 次方-1)
- float(4)
- double(8)
- char(2)
- boolean(1/8)
内码是程序内部使用的字符编码,特别是某种语言实现其 char 或 String 类型在内存里用的内部编码;外码是程序与外部交互时外部使用的字符编码。“外部”相对“内部”而言;不是 char 或 String 在内存里用的内部编码的地方都可以认为是“外部”。例如,外部可以是序列化之后的 char 或 String,或者外部的文件、命令行参数之类的。
Java 语言规范规定,Java 的 char 类型是 UTF-16 的 code unit,也就是一定是 16 位(2 字节),然后字符串是 UTF-16 code unit 的序列。
Java 规定了字符的内码要用 UTF-16 编码。或者至少要让用户无法感知到 String 内部采用了非 UTF-16 的编码。
String.getBytes()是一个用于将 String 的内码转换为指定的外码的方法。无参数版使用平台的默认编码作为外码,有参数版使用参数指定的编码作为外码;将 String 的内容用外码编码好,结果放在一个新 byte[]返回。调用了 String.getBytes()之后得到的 byte[]只能表明该外码的性质,而无法碰触到 String 内码的任何特质。
- Java 标准库实现的对 char 与 String 的序列化规定使用 UTF-8 作为外码。Java 的 Class 文件中的字符串常量与符号名字也都规定用 UTF-8 编码。这大概是当时设计者为了平衡运行时的时间效率(采用定长编码的 UTF-16)与外部存储的空间效率(采用变长的 UTF-8 编码)而做的取舍。
# 1.4 引用类型
- 类、接口、数组都是引用类型
# 四种引用
- 目的:避免对象长期占用内存,
# 强引用
- StringReference GC 时不回收
- 当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
# 软引用
- SoftReference GC 时如果 JVM 内存不足时会回收
- 软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java 虚拟机就会把这个软引用加入到与之关联的引用队列中。
# 弱引用
- WeakReference GC 时立即回收
- 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
- 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
# 虚引用
- PhantomReference
- 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
- 在 Java 集合中有一种特殊的 Map 类型:WeakHashMap, 在这种 Map 中存放了键对象的弱引用,当一个键对象被垃圾回收,那么相应的值对象的引用会从 Map 中删除。WeakHashMap 能够节约存储空间,可用来缓存那些非必须存在的数据。
# 基础数据类型包装类
# 为什么需要
- 由于基本数据类型不是对象,所以 java 并不是纯面向对象的语言,好处是效率较高(全部包装为对象效率较低)。
- Java 是一个面向对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型 Collection 时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
# 有哪些
基本类型 包装器类型 boolean Boolean char Character int Integer byte Byte short Short long Long float Float double Double
- Number 是所有数字包装类的父类
# 自动装箱、自动拆箱(编译器行为)
- 自动装箱:可以将基础数据类型包装成对应的包装类
- Integer i = 10000; // 编译器会改为 new Integer(10000)
- 自动拆箱:可以将包装类转为对应的基础数据类型
- int i = new Integer(1000);//编译器会修改为 int i = new Integer(1000).intValue();
- 自动拆箱时如果包装类是 null,那么会抛出 NPE
# Integer.valueOf
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
2
3
4
5
- 调用 Integer.valueOf 时-128~127 的对象被缓存起来。
- 所以在此访问内的 Integer 对象使用==和 equals 结果是一样的。
- 如果 Integer 的值一致,且在此范围内,因为是同一个对象,所以==返回 true;但此访问之外的对象==比较的是内存地址,值相同,也是返回 false。
# 1.5 Object
# == 与 equals 的区别
- 如果两个引用类型变量使用==运算符,那么比较的是地址,它们分别指向的是否是同一地址的对象。结果一定是 false,因为两个对象不可能存放在同一地址处。
- 要求是两个对象都不是能空值,与空值比较返回 false。
- ==不能实现比较对象的值是否相同。
- 所有对象都有 equals 方法,默认是 Object 类的 equals,其结果与==一样。
- 如果希望比较对象的值相同,必须重写 equals 方法。
# hashCode 与 equals 的区别
- Object 中的 equals:
public boolean equals(Object obj) {
return (this == obj);
}
2
3
- equals 方法要求满足:
- 自反性 a.equals(a)
- 对称性 x.equals(y) y.equals(x)
- 一致性 x.equals(y) 多次调用结果一致
- 对于任意非空引用 x,x.equals(null) 应该返回 false
- Object 中的 hashCode:
public native int hashCode();
它是一个本地方法,它的实现与本地机器有关,这里我们暂且认为他返回的是对象存储的物理位置。
当 equals 方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规约定:值相同的对象必须有相同的 hashCode。
- object1.equals(object2)为 true,hashCode 也相同;
- hashCode 不同时,object1.equals(object2)为 false;
- hashCode 相同时,object1.equals(object2)不一定为 true;
当我们向一个 Hash 结构的集合中添加某个元素,集合会首先调用 hashCode 方法,这样就可以直接定位它所存储的位置,若该处没有其他元素,则直接保存。若该处已经有元素存在,就调用 equals 方法来匹配这两个元素是否相同,相同则不存,不同则链到后面(如果是链地址法)。
先调用 hashCode,唯一则存储,不唯一则再调用 equals,结果相同则不再存储,结果不同则散列到其他位置。因为 hashCode 效率更高(仅为一个 int 值),比较起来更快。
HashMap#put 源码
hash 是 key 的 hash 值,当该 hash 对应的位置已有元素时会执行以下代码(hashCode 相同)
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
如果 equals 返回结果相同,则值一定相同,不再存入。
# 如果重写 equals 不重写 hashCode 会怎样
- 两个值不同的对象的 hashCode 一定不一样,那么执行 equals,结果为 true,HashSet 或 HashMap 的键会放入值相同的对象。
# 1.6 String&StringBuffer&StringBuilder
- 都是 final 类,不允许继承;
- String 长度不可变,StringBuffer、StringBuilder 长度可变;
# String
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {}
2
# equals&hashCode
- String 重写了 Object 的 hashCode 和 equals。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 添加功能
- String 是 final 类,不可被继承,也不可重写一个 java.lang.String(类加载机制)。
- 一般是使用 StringUtils 来增强 String 的功能。
- 为什么只加载系统通过的 java.lang.String 类而不加载用户自定义的 java.lang.String 类呢?
- 双亲委派机制
- 因加载某个类时,优先使用父类加载器加载需要使用的类。如果我们自定义了 java.lang.String 这个类,
- 加载该自定义的 String 类,该自定义 String 类使用的加载器是 AppClassLoader,根据优先使用父类加载器原理,
- AppClassLoader 加载器的父类为 ExtClassLoader,所以这时加载 String 使用的类加载器是 ExtClassLoader,
- 但是类加载器 ExtClassLoader 在 jre/lib/ext 目录下没有找到 String.class 类。然后使用 ExtClassLoader 父类的加载器 BootStrap,
- 父类加载器 BootStrap 在 JRE/lib 目录的 rt.jar 找到了 String.class,将其加载到内存中。这就是类加载器的委托机制。
- 所以,用户自定义的 java.lang.String 不被加载,也就是不会被使用。
# + substring
- 会创建一个新的字符串;
- 编译时会将+转为 StringBuilder 的 append 方法。
- 注意新的字符串是在运行时在堆里创建的。
- String str1 = “ABC”;可能创建一个或者不创建对象,如果”ABC”这个字符串在 java String 池里不存在,会在 java String 池里创建一个创建一个 String 对象(“ABC”),然后 str1 指向这个内存地址,无论以后用这种方式创建多少个值为”ABC”的字符串对象,始终只有一个内存地址被分配,之后的都是 String 的拷贝,Java 中称为“字符串驻留”,所有的字符串常量都会在编译之后自动地驻留。
- 注意只有字符串常量是共享的,+和 substring 等操作的结果不是共享的,substring 也会在堆中重新创建字符串。
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 常量池
- String str = new String(“ABC”);
- 至少创建一个对象,也可能两个。因为用到 new 关键字,肯定会在 heap 中创建一个 str2 的 String 对象,它的 value 是“ABC”。同时如果这个字符串在字符串常量池里不存在,会在池里创建这个 String 对象“ABC”。
- String s1= “a”;
- String s2 = “a”;
- 此时 s1 == s2 返回 true
- String s1= new String(“a”);
- String s2 = new String(“a”);
- 此时 s1 == s2 返回 false
- ""创建的字符串在字符串池中。
- 如果引号中字符串存在在常量池中,则仅在堆中拷贝一份(new String);
- 如果不在,那么会先在常量池中创建一份("abc"),然后在堆中创建一份(new String),共创建两个对象。
# 编译优化
- 字面量,final 都会在编译期被优化,并且会被直接运算好。
- 1)注意 c 和 d 中,final 变量 b 已经被替换为其字符串常量了。
- 2)注意 f、g 中,b 被替换为其字符串常量,并且在编译时字符串常量的+运算会被执行,返回拼接后的字符串常量
- 3)注意 j,a1 作为 final 变量,在编译时被替换为其字符串常量
- 解释 c == h / d == h/ e== h 为 false:c 是运行时使用+拼接,创建了一个新的堆中的字符串 ab,与 ab 字符串常量不是同一个对象;
- 解释 f == h/ g == h 为 true:f 编译时进行优化,其值即为字符串常量 ab,h 也是,指向字符串常量池中的同一个对象;
- String#intern(JDK1.7 之后)
- JDK1.7 之后 JVM 里字符串常量池放入了堆中,之前是放在方法区。
- intern()方法设计的初衷,就是重用 String 对象,以节省内存消耗。
- 一定是 new 得到的字符串才会调用 intern,字符串常量没有必要去 intern。
- 当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,常量池中直接存储堆中该字符串的引用(1.7 之前是常量池中再保存一份该字符串)。
- 源码
public native String intern();
实例一:
- String s = new String("1"); s.intern(); String s2 = "1"; System.out.println(s == s2);// false
String s3 = new String("1") + new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3 == s4);// true
String s = newString("1"),生成了常量池中的“1” 和堆空间中的字符串对象。
s.intern(),这一行的作用是 s 对象去常量池中寻找后发现"1"已经存在于常量池中了。
String s2 = "1",这行代码是生成一个 s2 的引用指向常量池中的“1”对象。
结果就是 s 和 s2 的引用地址明显不同。因此返回了 false。
String s3 = new String("1") + newString("1"),这行代码在字符串常量池中生成“1” ,并在堆空间中生成 s3 引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。
s3.intern(),这一行代码,是将 s3 中的“11”字符串放入 String 常量池中,此时常量池中不存在“11”字符串,JDK1.6 的做法是直接在常量池中生成一个 "11" 的对象。
但是在 JDK1.7 中,常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用直接指向 s3 引用的对象,也就是说 s3.intern() ==s3 会返回 true。
String s4 = "11", 这一行代码会直接去常量池中创建,但是发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。因此 s3 == s4 返回了 true。
实例二:
- String s3 = new String("1") + new String("1"); String s4 = "11"; s3.intern(); System.out.println(s3 == s4);// false
String s3 = new String("1") + newString("1"),这行代码在字符串常量池中生成“1” ,并在堆空间中生成 s3 引用指向的对象(内容为"11")。注意此时常量池中是没有 “11”对象的。
String s4 = "11", 这一行代码会直接去生成常量池中的"11"。
s3.intern(),这一行在这里就没什么实际作用了。因为"11"已经存在了。
结果就是 s3 和 s4 的引用地址明显不同。因此返回了 false。
实例三:
- String str1 = new String("SEU") + new String("Calvin"); System.out.println(str1.intern() == str1);// true System.out.println(str1 == "SEUCalvin");// true
str1.intern() == str1 就是上面例子中的情况,str1.intern()发现常量池中不存在“SEUCalvin”,因此指向了 str1。 "SEUCalvin"在常量池中创建时,也就直接指向了 str1 了。两个都返回 true 就理所当然啦。
实例四:
- String str2 = "SEUCalvin";//新加的一行代码,其余不变 String str1 = new String("SEU") + new String("Calvin"); System.out.println(str1.intern() == str1);// false System.out.println(str1 == "SEUCalvin");// false
在实例三的基础上加了第一行
str2 先在常量池中创建了“SEUCalvin”,那么 str1.intern()当然就直接指向了 str2,你可以去验证它们两个是返回的 true。后面的"SEUCalvin"也一样指向 str2。所以谁都不搭理在堆空间中的 str1 了,所以都返回了 false。
# StringBuffer&StringBuilder
- StringBuffer 是线程安全的,StringBuilder 不是线程安全的,但它们两个中的所有方法都是相同的。StringBuffer 在 StringBuilder 的方法之上添加了 synchronized,保证线程安全。
- StringBuilder 比 StringBuffer 性能更好。
# 1.7 面向对象
# 抽象类与接口
区别:
- 1)抽象类中方法可以不是抽象的;接口中的方法必须是抽象方法;
- 2)抽象类中可以有普通的成员变量;接口中的变量必须是 static final 类型的,必须被初始化 , 接口中只有常量,没有变量。
- 3)抽象类只能单继承,接口可以继承多个父接口;
- 4)Java8 中接口中会有 default 方法,即方法可以被实现。
使用场景:
如果要创建不带任何方法定义和成员变量的基类,那么就应该选择接口而不是抽象类。
如果知道某个类应该是基类,那么第一个选择的应该是让它成为一个接口,只有在必须要有方法定义和成员变量的时候,才应该选择抽象类。因为抽象类中允许存在一个或多个被具体实现的方法,只要方法没有被全部实现该类就仍是抽象类。
# 三大特性
- 面向对象的三个特性:封装;继承;多态
- 封装:将数据与操作数据的方法绑定起来,隐藏实现细节,对外提供接口。
- 继承:代码重用;可扩展性
- 多态:允许不同子类对象对同一消息做出不同响应
- 多态的三个必要条件:继承、方法的重写、父类引用指向子类对象
# 重写和重载
- 根据对象对方法进行选择,称为分派
- 编译期的静态多分派:overloading 重载 根据调用引用类型和方法参数决定调用哪个方法(编译器)
- 运行期的动态单分派:overriding 重写 根据指向对象的类型决定调用哪个方法(JVM)
# 1.8 关键类
# ThreadLocal(线程局部变量)
- 在线程之间共享变量是存在风险的,有时可能要避免共享变量,使用 ThreadLocal 辅助类为各个线程提供各自的实例。
- 例如有一个静态变量
public static final SimpleDateFormat sdf = new SimpleDateFormat(“yyyy-MM-dd”);
- 如果两个线程同时调用 sdf.format(…)
- 那么可能会很混乱,因为 sdf 使用的内部数据结构可能会被并发的访问所破坏。当然可以使用线程同步,但是开销很大;或者也可以在需要时构造一个局部 SImpleDateFormat 对象。但这很浪费。
- 希望为每一个线程构造一个对象,即使该线程调用多次方法,也只需要构造一次,不必在局部每次都构造。
public static final ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
2
3
4
5
6
- 实现原理:
# 1)每个线程的变量副本是存储在哪里的
- ThreadLocal 的 get 方法就是从当前线程的 ThreadLocalMap 中取出当前线程对应的变量的副本。该 Map 的 key 是 ThreadLocal 对象,value 是当前线程对应的变量。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
2
3
4
5
6
7
8
9
10
11
12
13
- ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
- 【注意,变量是保存在线程中的,而不是保存在 ThreadLocal 变量中】。当前线程中,有一个变量引用名字是 threadLocals,这个引用是在 ThreadLocal 类中 createmap 函数内初始化的。
- void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
- 每个线程都有一个这样的名为 threadLocals 的 ThreadLocalMap,以 ThreadLocal 和 ThreadLocal 对象声明的变量类型作为 key 和 value。
- Thread
- ThreadLocal.ThreadLocalMap threadLocals = null;
- 这样,我们所使用的 ThreadLocal 变量的实际数据,通过 get 方法取值的时候,就是通过取出 Thread 中 threadLocals 引用的 map,然后从这个 map 中根据当前 threadLocal 作为参数,取出数据。现在,变量的副本从哪里取出来的(本文章提出的第一个问题)已经确认解决了。
- 每个线程内部都会维护一个类似 HashMap 的对象,称为 ThreadLocalMap,里边会包含若干了 Entry(K-V 键值对),相应的线程被称为这些 Entry 的属主线程;
- Entry 的 Key 是一个 ThreadLocal 实例,Value 是一个线程特有对象。Entry 的作用即是:为其属主线程建立起一个 ThreadLocal 实例与一个线程特有对象之间的对应关系;
- Entry 对 Key 的引用是弱引用;Entry 对 Value 的引用是强引用。
# 2)为什么 ThreadLocalMap 的 Key 是弱引用
- 如果是强引用,ThreadLocal 将无法被释放内存。
- 因为如果这里使用普通的 key-value 形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在 GC 分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是 Java 中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次 GC。当某个 ThreadLocal 已经没有强引用可达,则随着它被垃圾回收,在 ThreadLocalMap 里对应的 Entry 的键值会失效,这为 ThreadLocalMap 本身的垃圾清理提供了便利。
# 3)ThreadLocalMap 是何时初始化的(setInitialValue)
- 在 get 时最后一行调用了 setInitialValue,它又调用了我们自己重写的 initialValue 方法获得要线程局部变量对象。ThreadLocalMap 没有被初始化的话,便初始化,并设置 firstKey 和 firstValue;如果已经被初始化,那么将 key 和 value 放入 map。
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
2
3
4
5
6
7
8
9
10
# 4)ThreadLocalMap 原理
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
2
3
4
5
6
7
8
9
- 它也是一个类似 HashMap 的数据结构,但是并没实现 Map 接口。
- 也是初始化一个大小 16 的 Entry 数组,Entry 对象用来保存每一个 key-value 键值对,只不过这里的 key 永远都是 ThreadLocal 对象,通过 ThreadLocal 对象的 set 方法,结果把 ThreadLocal 对象自己当做 key,放进了 ThreadLoalMap 中。
- ThreadLoalMap 的 Entry 是继承 WeakReference,和 HashMap 很大的区别是,Entry 中没有 next 字段,所以就不存在链表的情况了。
# 构造方法
- ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
- // 表的大小始终为 2 的幂次 table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1;
- // 设定扩容阈值
setThreshold(INITIAL_CAPACITY);
}
- 在 ThreadLocalMap 中,形如 key.threadLocalHashCode & (table.length - 1)(其中 key 为一个 ThreadLocal 实例)这样的代码片段实质上就是在求一个 ThreadLocal 实例的哈希值,只是在源码实现中没有将其抽为一个公用函数。
- 对于& (INITIAL_CAPACITY - 1),相对于 2 的幂作为模数取模,可以用&(2^n-1)来替代%2^n,位运算比取模效率高很多。至于为什么,因为对 2^n 取模,只要不是低 n 位对结果的贡献显然都是 0,会影响结果的只能是低 n 位。
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
2
3
- getEntry(由 ThreadLocal#get 调用)
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
2
3
4
5
6
- // 因为用的是线性探测,所以往后找还是有可能能够找到目标 Entry 的。 return getEntryAfterMiss(key, i, e); }
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
2
3
4
5
6
7
8
9
- // 该 entry 对应的 ThreadLocal 已经被回收,调用 expungeStaleEntry 来清理无效的 entry
- expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
- i 是位置
- 从 staleSlot 开始遍历,将无效 key(弱引用指向对象被回收)清理,即对应 entry 中的 value 置为 null,将指向这个 entry 的 table[i]置为 null,直到扫到空 entry。
- 另外,在过程中还会对非空的 entry 作 rehash。
- 可以说这个函数的作用就是从 staleSlot 开始清理连续段中的 slot(断开强引用,rehash slot 等)
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 对于还没有被回收的情况,需要做一次 rehash。
如果对应的 ThreadLocal 的 ID 对 len 取模出来的索引 h 不为当前位置 i,
则从 h 向后线性探测到第一个空的 slot,把当前的 entry 给挪过去。 int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
# set(线性探测法解决 hash 冲突)
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
// 计算key的hash值
2
3
4
5
6
7
8
9
10
- int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
// 同一个 ThreadLocal 赋了新值,则替换原值为新值 e.value = value; return; }
if (k == null) {
// 该位置的 TheadLocal 已经被回收,那么会清理 slot 并在此位置放入当前 key 和 value(stale:陈旧的) replaceStaleEntry(key, value, i); return; } } // 下一个位置为空,那么就放到该位置上 tab[i] = new Entry(key, value); int sz = ++size;
// 启发式地清理一些 slot,并判断是否是否需要扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
每个 ThreadLocal 对象都有一个 hash 值 threadLocalHashCode,每初始化一个 ThreadLocal 对象,hash 值就增加一个固定的大小 0x61c88647。
private final int threadLocalHashCode = nextHashCode();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
2
3
- 由于 ThreadLocalMap 使用线性探测法来解决散列冲突,所以实际上 Entry[]数组在程序逻辑上是作为一个环形存在的。
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
2
3
- 在插入过程中,根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置 i,过程如下:
- 1、如果当前位置是空的,那么正好,就初始化一个 Entry 对象放在位置 i 上;
- 2、不巧,位置 i 已经有 Entry 对象了,如果这个 Entry 对象的 key 正好是即将设置的 key,那么重新设置 Entry 中的 value;
- 3、很不巧,位置 i 的 Entry 对象,和即将设置的 key 没关系,那么只能找下一个空位置;
- 这样的话,在 get 的时候,也会根据 ThreadLocal 对象的 hash 值,定位到 table 中的位置,然后判断该位置 Entry 对象中的 key 是否和 get 的 key 一致,如果不一致,就判断下一个位置
- 可以发现,set 和 get 如果冲突严重的话,效率很低,因为 ThreadLoalMap 是 Thread 的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为。
# cleanSomeSlots(启发式地清理 slot)
- i 是当前位置,n 是元素个数
- i 对应 entry 是非无效(指向的 ThreadLocal 没被回收,或者 entry 本身为空)
- n 是用于控制控制扫描次数的
- 正常情况下如果 log n 次扫描没有发现无效 slot,函数就结束了
- 但是如果发现了无效的 slot,将 n 置为 table 的长度 len,做一次连续段的清理
- 再从下一个空的 slot 开始继续扫描
- 这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效 slot 的时候可能会被调用,
- 区别是前者传入的 n 为元素个数,后者为 table 的容量
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# rehash
- 先全量清理,如果清理后现有元素个数超过负载,那么扩容
private void rehash() {
- // 进行一次全量清理
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
- 全量清理
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
2
3
4
5
6
7
8
9
- 扩容,因为需要保证 table 的容量 len 为 2 的幂,所以扩容即扩大 2 倍
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# remove
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
2
3
4
5
6
7
8
- // 显式断开弱引用 e.clear();
- // 进行段清理 expungeStaleEntry(i); return; } } }
- Reference#clear
public void clear() {
this.referent = null;
}
2
3
# 内存泄露
- 只有调用 TheadLocal 的 remove 或者 get、set 时才会采取措施去清理被回收的 ThreadLocal 对应的 value(但也未必会清理所有的需要被回收的 value)。假如一个局部的 ThreadLocal 不再需要,如果没有去调用 remove 方法清除,那么有可能会发生内存泄露。
- 既然已经发现有内存泄露的隐患,自然有应对的策略,在调用 ThreadLocal 的 get()、set()可能会清除 ThreadLocalMap 中 key 为 null 的 Entry 对象,这样对应的 value 就没有 GC Roots 可达了,下次 GC 的时候就可以被回收,当然如果调用 remove 方法,肯定会删除对应的 Entry 对象。
- 如果使用 ThreadLocal 的 set 方法之后,没有显式的调用 remove 方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完 ThreadLocal 之后,记得调用 remove 方法。
JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
# Iterator / ListIterator / Iterable
- 普通 for 循环时不能删除元素,否则会抛出异常;Iterator 可以
public interface Collection<E> extends Iterable<E> {}
- Collection 接口继承了 Iterable,Iterable 接口定义了 iterator 抽象方法和 forEach default 方法。所以 ArrayList、LinkedList 都可以使用迭代器和 forEach,包括增强 for 循环(编译时转为迭代器)。
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
2
3
4
5
6
7
8
- default Spliterator
<T>
spliterator() { return Spliterators.spliteratorUnknownSize(iterator(), 0); } - }
- 注意这些具体的容器类返回的迭代器对象是各不相同的,主要是因为不同的容器遍历方式不同,但是这些迭代器对象都实现 Iterator 接口,都可以使用一个 Iterator 对象来统一指向这些不同的子类对象。
- ArrayList#iterator
public Iterator<E> iterator() {
return new Itr();
}
2
3
- ArrayList#Itr
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
- ArrayList#listIterator
public ListIterator<E> listIterator() {
return new ListItr(0);
}
2
3
- ArrayList#ListItr
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# for /增强 for/ forEach
For-each loop Equivalent for loop
for (type var : arr) {
body-of-loop
} for (int i = 0; i < arr.length; i++) {
type var = arr[i];
body-of-loop
}
for (type var : coll) {
body-of-loop
} for (Iterator <type>
iter = coll.iterator(); iter.hasNext(); ) {
type var = iter.next();
body-of-loop
}
- 增强 for 循环在编译时被修改为 for 循环:数组会被修改为下标式的循环;集合会被修改为 Iterator 循环。
- 增强 for 循环不适合以下情况:(过滤、转换、平行迭代)
- 对 collection 或数组中的元素不能做赋值操作;
- 只能正向遍历,不能反向遍历;
- 遍历过程中,collection 或数组中同时只有一个元素可见,即只有“当前遍历到的元素”可见,而前一个或后一个元素是不可见的;
- forEach
- ArrayList#forEach 继承自
- Iterable 接口的 default 方法
- default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } }
# Comparable 与 Comparator
- 基本数据类型包装类和 String 类均已实现了 Comparable 接口。
- 实现了 Comparable 接口的类的对象的列表或数组可以通过 Collections.sort 或 Arrays.sort 进行自动排序,默认为升序。
- 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用 Comparator 来控制某些数据结构(如 TreeSet,TreeMap)的顺序。
# 1.9 继承
子类继承父类所有的成员变量(即使是private变量,有所有权,但是没有使用权,不能访问父类的private的成员变量)。
子类中可以直接调用父类非private方法,也可以用super.父类方法的形式调用。
- 子类构造方法中如果没有显式使用 super(父类构造方法参数)去构造父类对象的话(如果有必须是方法的第一行),编译器会在第一行添加 super()。
- 子类的构造函数可否不使用 super(父类构造方法参数)调用超类的构造方法?
- 可以不用显式的写出 super,但前提是“父类中有多个构造方法,且有一个是显式写出的无参的构造方法”。
# 1.10 内部类
- 在另一个类的里面定义的类就是内部类
- 内部类是编译器现象,与虚拟机无关。
- 编译器会将内部类编译成用$分割外部类名和内部类名的常规类文件,而虚拟机对此一无所知。
内部类可以是static的,也可用public,default,protected和private修饰。(而外部类即类名和文件名相同的只能使用public和default)。
# 优点
- 每个内部类都能独立地继承一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
- 接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。
- 用内部类还能够为我们带来如下特性:
- 1、内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外部对象的信息相互独立。
- 2、在单个外部类中,可以让多个内部类实现不同的接口,或者继承不同的类。外部类想要多继承的类可以分别由内部类继承,并进行 Override 或者直接复用。然后外部类通过创建内部类的对象来使用该内部对象的方法和成员,从而达到复用的目的,这样外部内就具有多个父类的所有特征。
- 3、创建内部类对象的时刻并不依赖于外部类对象的创建。
- 4、内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体。
- 5、内部类提供了更好的封装,除了该外部类,其他类都不能访问
- 只有静态内部类可以同时拥有静态成员和非静态成员,其他内部类只有拥有非静态成员。
# 成员内部类:就像外部类的一个成员变量
- 注意内部类的对象总有一个外部类的引用
- 当创建内部类对象时,会自动将外部类的 this 引用传递给当前的内部类的构造方法。
# 静态内部类:就像外部类的一个静态成员变量
public class OuterClass {
private static class StaticInnerClass {
int id;
static int increment = 1;
}
}
//调用方式:
//外部类.内部类 instanceName = new 外部类.内部类();
2
3
4
5
6
7
8
9
# 局部内部类:定义在一个方法或者一个块作用域里面的类
- 想创建一个类来辅助我们的解决方案,又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。
- JDK1.8 之前不能访问非 final 的局部变量!
- 生命周期不一致:
- 方法在栈中,对象在堆中;方法执行完,对象并没有死亡
- 如果可以使用方法的局部变量,如果方法执行完毕,就会访问一个不存在的内存区域。
- 而 final 是常量,就可以将该常量的值复制一份,即使不存在也不影响。
public Destination destination(String str) {
class PDestination implements Destination {
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
public String readLabel() {
return label;
}
}
return new PDestination(str);
}
2
3
4
5
6
7
8
9
10
11
12
13
# 匿名内部类:必须继承一个父类或实现一个接口
- 匿名内部类和局部内部类在 JDK1.8 之前都不能访问一个非 final 的局部变量,只能访问 final 的局部变量,原因是生命周期不同,可能栈中的局部变量已经被销毁,而堆中的对象仍存活,此时会访问一个不存在的内存区域。假如是 final 的变量,那么编译时会将其拷贝一份,延长其生命周期。
- 拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用 final 来让该引用不可改变。
- 但在 JDK1.8 之后可以访问一个非 final 的局部变量了,前提是非 final 的局部变量没有修改,表现得和 final 变量一样才可以!
interface AnonymousInner {
int add();
}
public class AnonymousOuter {
public AnonymousInner getAnonymousInner(){
int x = 100;
return new AnonymousInner() {
int y = 100;
@Override
public int add() {
return x + y;
}
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.11 关键字
# final
# try-finally-return
- 1、不管有没有出现异常,finally 块中代码都会执行;
- 2、当 try 和 catch 中有 return 时,finally 仍然会执行;无论 try 里执行了 return 语句、break 语句、还是 continue 语句,finally 语句块还会继续执行;如果执行 try 和 catch 时 JVM 退出(比如 System.exit(0)),那么 finally 不会被执行;
- finally 是在 return 后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管 finally 中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在 finally 执行前确定的;
- 【
- 如果 try 语句里有 return,那么代码的行为如下: 1.如果有返回值,就把返回值保存到局部变量中 2.执行 jsr 指令跳到 finally 语句里执行 3.执行完 finally 语句后,返回之前保存在局部变量表里的值
- 】
- 3、当 try 和 finally 里都有 return 时,会忽略 try 的 return,而使用 finally 的 return。
- 4、如果 try 块中抛出异常,执行 finally 块时又抛出异常,此时原始异常信息会丢失,只抛出在 finally 代码块中的异常。
- 实例一:
public static int test() {
int x = 1;
try {
x++;
return x; // 2
} finally {
x++;
}
}
2
3
4
5
6
7
8
9
- 实例二:
private static int test2() {
try {
System.out.println("try...");
return 80;
} finally {
System.out.println("finally...");
return 100; // 100
}
}
2
3
4
5
6
7
8
9
# static
- static 方法就是没有 this 的方法。在 static 方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用 static 方法。这实际上正是 static 方法的主要用途。
# 1)修饰成员方法:静态成员方法
- 在静态方法中不能访问类的非静态成员变量和非静态成员方法;
- 在非静态成员方法中是可以访问静态成员方法/变量的;
- 即使没有显式地声明为 static,类的构造器实际上也是静态方法
# 2)修饰成员变量:静态成员变量
- 静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
- 静态成员变量并发下不是线程安全的,并且对象是单例的情况下,非静态成员变量也不是线程安全的。
- 怎么保证变量的线程安全?
- 只有一个线程写,其他线程都是读的时候,加 volatile;线程既读又写,可以考虑 Atomic 原子类和线程安全的集合类;或者考虑 ThreadLocal
# 3)修饰代码块:静态代码块
- 用来构造静态代码块以优化程序性能。static 块可以置于类中的任何地方,类中可以有多个 static 块。在类初次被加载的时候,会按照 static 块的顺序来执行每个 static 块,并且只会执行一次。
# 4)修饰内部类:静态内部类
- 成员内部类和静态内部类的区别:
- 1)前者只能拥有非静态成员;后者既可拥有静态成员,又可拥有非静态成员
- 2)前者持有外部类的的引用,可以访问外部类的静态成员和非静态成员;后者不持有外部类的引用,只能访问外部类的静态成员
- 3)前者不能脱离外部类而存在;后者可以
# 5)修饰 import:静态导包
# switch
# switch 字符串实现原理
- 对比反编译之后的结果:
- 编译后 switch 还是基于整数,该整数来自于 String 的 hashCode。
- 先比较字符串的 hashCode,因为 hashCode 相同未必值相同,又再次检查了 equals 是否相同。
# 字节码实现原理(tableswitch / lookupswitch)
- 编译器会使用 tableswitch 和 lookupswitch 指令来生成 switch 语句的编译代码。当 switch 语句中的 case 分支的条件值比较稀疏时,tableswitch 指令的空间使用率偏低。这种情况下将使用 lookupswitch 指令来替代。lookupswitch 指令的索引表由 int 类型的键(来源于 case 语句块后面的数值)与对应的目标语句偏移量所构成。当 lookupswitch 指令执行时,switch 语句的条件值将和索引表中的键进行比较,如果某个键和条件值相符,那么将转移到这个键对应的分支偏移量继续执行,如果没有键值符合,执行将在 default 分支执行。
# abstract
- 只要含有抽象方法,这个类必须添加 abstract 关键字,定义为抽象类。
- 只要父类是抽象类,内含抽象方法,那么继承这个类的子类的相对应的方法必须重写。如果不重写,就需要把父类的声明抽象方法再写一遍,留给这个子类的子类去实现。同时将这个子类也定义为抽象类。
- 注意抽象类中可以有抽象方法,也可以有具体实现方法(当然也可以没有)。
- 抽象方法须加 abstract 关键字,而具体方法不可加
- 只要是抽象类,就不能存在这个类的对象(不可以 new 一个这个类的对象)。
# this & super
- this
- 自身引用;访问成员变量与方法;调用其他构造方法
- 通过 this 调用另一个构造方法,用法是 this(参数列表),这个仅在类的构造方法中可以使用
- 函数参数或者函数中的局部变量和成员变量同名的情况下,成员变量被屏蔽,此时要访问成员变量则需要用“this.成员变量名”的方式来引用成员变量。
- 需要引用当前对象时候,直接用 this(自身引用)
- super
- 父类引用;访问父类成员变量与方法;调用父类构造方法
- super 可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
- super 有三种用法:
- 1.普通的直接引用
- 与 this 类似,super 相当于是指向当前对象的父类,这样就可以用 super.xxx 来引用父类的成员,如果不冲突的话也可以不加 super。
- 2.子类中的成员变量或方法与父类中的成员变量或方法同名时,为了区别,调用父类的成员必须要加 super
- 3.调用父类的构造函数
# 访问权限
# 1.12 枚举
# JDK 实现
- 实例:
public enum Labels0 {
ENVIRONMENT("环保"), TRAFFIC("交通"), PHONE("手机");
private String name;
private Labels0(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- 编译后生成的字节码反编译:
- 可以清晰地看到枚举被编译后其实就是一个类,该类被声明成 final,说明其不能被继承,同时它继承了 Enum 类。枚举里面的元素被声明成 static final ,另外生成一个静态代码块 static{},最后还会生成 values 和 valueOf 两个方法。下面以最简单的 Labels 为例,一个一个模块来看。
# Enum 类
- Enum 类是一个抽象类,主要有 name 和 ordinal 两个属性,分别用于表示枚举元素的名称和枚举元素的位置索引,而构造函数传入的两个变量刚好与之对应。
- toString 方法直接返回 name。
- equals 方法直接用 == 比较两个对象。
- hashCode 方法调用的是父类的 hashCode 方法。
- 枚举不支持 clone、finalize 和 readObject 方法。
- compareTo 方法可以看到就是比较 ordinal 的大小。
- valueOf 方法,根据传入的字符串 name 来返回对应的枚举元素。
# 静态代码块的实现
- 在静态代码块中创建对象,对象是单例的!
- 可以看到静态代码块主要完成的工作就是先分别创建 Labels 对象,然后将“ENVIRONMENT”、“TRAFFIC”和“PHONE”字符串作为 name ,按照顺序分别分配位置索引 0、1、2 作为 ordinal,然后将其值设置给创建的三个 Labels 对象的 name 和 ordinal 属性,此外还会创建一个大小为 3 的 Labels 数组 ENUM$VALUES,将前面创建出来的 Labels 对象分别赋值给数组。
# values 的实现
- 可以看到它是一个静态方法,主要是使用了前面静态代码块中的 Labels 数组 ENUM$VALUES,调用 System.arraycopy 对其进行复制,然后返回该数组。所以通过 Labels.values()[2]就能获取到数组中索引为 2 的元素。
# valueOf 方法
- 该方法同样是个静态方法,可以看到该方法的实现是间接调用了父类 Enum 类的 valueOf 方法,根据传入的字符串 name 来返回对应的枚举元素,比如可以通过 Labels.valueOf("ENVIRONMENT")获取 Labels.ENVIRONMENT。
- 枚举本质其实也是一个类,而且都会继承 java.lang.Enum 类,同时还会生成一个静态代码块 static{},并且还会生成 values 和 valueOf 两个方法。而上述的工作都需要由编译器来完成,然后我们就可以像使用我们熟悉的类那样去使用枚举了。
# 用 enum 代替 int 常量
- 将 int 枚举常量翻译成可打印的字符串,没有很便利的方法。
- 要遍历一个枚举组中的所有 int 枚举常量,甚至获得 int 枚举组的大小。
- 使用枚举类型的 values 方法可以获得该枚举类型的数组
- 枚举类型没有可以访问的构造器,是真正的 final;是实例受控的,它们是单例的泛型化;本质上是单元素的枚举;提供了编译时的类型安全。
- 单元素的枚举是实现单例的最佳方法!
- 可以在枚举类型中放入这段代码,可以实现 String2Enum。
- 注意 Operation 是枚举类型名。
# 用实例域代替序数
- 这种实现不好,不推荐使用 ordinal 方法,推荐使用下面这种实现:
# 用 EnumSet 代替位域
- 位域是将几个常量合并到一个集合中,我们推荐用枚举代替常量,用 EnumSet 代替集合
- EnumSet.of(enum1,enum2) -> Set<枚举>
# 用 EnumMap 代替序数索引
- 将一个枚举类型的值与一个元素(或一组)对应起来,推荐使用 EnumMap 数据结构
- 如果是两个维度的变化,那么可以使用 EnumMap<Enum1,Map<Enum1,元素>>
# 1.13 序列化
# JDK 序列化(Serizalizable)
- 定义:将实现了 Serializable 接口(标记型接口)的对象转换成一个字节数组,并可以将该字节数组转为原来的对象。
- ObjectOutputStream 是专门用来输出对象的输出流;
- ObjectOutputStream 将 Java 对象写入 OutputStream。可以使用 ObjectInputStream 读取(重构)对象。
# serialVersionUID
Java 的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体(类)的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)。
- 1)如果没有添加 serialVersionUID,进行了序列化,而在反序列化的时候,修改了类的结构(添加或删除成员变量,修改成员变量的命名),此时会报错。
- 2)如果添加 serialVersionUID,进行了序列化,而在反序列化的时候,修改了类的结构(添加或删除成员变量,修改成员变量的命名),那么可能会恢复部分数据,或者恢复不了数据。
如果设置了 serialVersionUID 并且一致,那么可能会反序列化部分数据;如果没有设置,那么只要属性不同,那么无法反序列化。
# 其他序列化工具
- XML/JSON
- Thrift/Protobuf
# 对象深拷贝与浅拷贝
- 当拷贝一个变量时,原始引用和拷贝的引用指向同一个对象,改变一个引用所指向的对象会对另一个引用产生影响。
- 如果需要创建一个对象的浅拷贝,那么需要调用 clone 方法。
- Object 类本身不实现接口 Cloneable,直接调用 clone 会抛出异常。
如果要在自己定义类中调用clone方法,必须实现Cloneable接口(标记型接口),因为Object类中的clone方法为protected,所以需要自己重写clone方法,设置为public。
- protected native Object clone() throws CloneNotSupportedException;
public class Person implements Cloneable {
private int age;
private String name;
private Company company;
@Override
public Person clone() throws CloneNotSupportedException {
return (Person) super.clone();
}
2
3
4
5
6
7
8
- }
public class Company implements Cloneable{
private String name;
2
@Override
public Company clone() throws CloneNotSupportedException {
return (Company) super.clone();
}
2
3
4
- }
- 使用 super(即 Object)的 clone 方法只能进行浅拷贝。
- 如果希望实现深拷贝,需要修改实现,比如修改为:
@Override
public Person clone() throws CloneNotSupportedException {
Person person = (Person) super.clone();
person.setCompany(company.clone()); // 一个新的Company
return person;
}
2
3
4
5
6
- 假如说 Company 中还有持有其他对象的引用,那么 Company 中也要像 Person 这样做。
- 可以说:想要深拷贝一个子类,那么它的所有父类都必须可以实现深拷贝。
- 另一种实现对象深拷贝的方式是序列化。
- @Override protected Object clone() { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream os = new ObjectOutputStream(baos); os.writeObject(this); os.close(); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream in = new ObjectInputStream(bais); Object ret = in.readObject(); in.close(); return ret; }catch(Exception e) { e.printStackTrace(); } return null; }
# 1.14 异常
# Error、Exception
- Error 是程序无法处理的错误,它是由 JVM 产生和抛出的,比如 OutOfMemoryError、ThreadDeath 等。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
- Exception 是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。
# 常见 RuntimeException
- IllegalArgumentException - 方法的参数无效
- NullPointerException - 试图访问一空对象的变量、方法或空数组的元素
- ArrayIndexOutOfBoundsException - 数组越界访问
- ClassCastException - 类型转换异常
- NumberFormatException 继承 IllegalArgumentException,字符串转换为数字时出现。比如 int i= Integer.parseInt("ab3");
# RuntimeException 与非 Runtime Exception
- RuntimeException 是运行时异常,也称为未检查异常;
- 非 RuntimeException 也称为 CheckedException 受检异常
- 前者可以不必进行 try-catch,后者必须要进行 try-catch 或者 throw。
# 异常包装
- 在 catch 子句中可以抛出一个异常,这样做的目的是改变异常的类型
- try{
- …
- }catch(SQLException e){
- throw new ServletException(e.getMessage());
- }
- 这样的话 ServletException 会取代 SQLException。
- 有一种更好的方法,可以保存原有异常的信息,将原始异常设置为新的异常的原因
- try{
- …
- }catch(SQLException e){
- Throwable se = new ServletException(e.getMessage());
- se.initCause(e);
- throw se;
- }
- 当捕获到异常时,可以使用 getCause 方法来重新得到原始异常
- Throwable e = se.getCause();
- 建议使用这种包装技术,可以抛出系统的高级异常(自己 new 的),又不会丢失原始异常的细节。
- 早抛出,晚捕获。
# 1.15 泛型
- 泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
# 泛型接口/类/方法
# 泛型继承、实现
- 父类使用泛型,子类要么去指定具体类型参数,要么继续使用泛型
# 泛型的约束和局限性
- 1)只能使用包装器类型,不能使用基本数据类型;
- 2)运行时类型查询只适用于原始类型,不适用于带类型参数的类型;
if(a instanceof Pair
<String>
) //error- 3)不能创建带有类型参数的泛型类的数组
Pair
<String>
[] pairs = new Pair<String>
[10];//error只能使用反射来创建泛型数组
public static <T extends Comparable> T[] minmax(T… a){
- T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType(),个数);
- …复制
- }
# 通配符
- ? 未知类型 只可以用于声明时,声明类型或方法参数,不能用于定义时(指定类型参数时)
- List<?> unknownList;
- List<? extends Number> unknownNumberList;
- List<? super Integer> unknownBaseLineIntgerList;
- 对于参数值是未知类型的容器类,只能读取其中元素,不能向其中添加元素, 因为,其类型是未知,所以编译器无法识别添加元素的类型和容器的类型是否兼容,唯一的例外是 null。
- 通配符类型 List
<?> 与原始类型 List 和具体类型 List<String>都不相同,List<?>
表示这个 list 内的每个元素的类型都相同,但是这种类型具体是什么我们却不知道。注意,List<?>和List<Object>可不相同,由于Object是最高层的超类,List<Object>表示元素可以是任何类型的对象,但是List<?>
可不是这个意思(未知类型)。
# extends 指定类型必为自身或其子类
List<? extends Fruit>
这个引用变量如果作为参数,哪些引用可以传入?
本身及其子类
以及含有通配符及 extends 的本身及子类
不可传入只含通配符不含 extends+指定类 的引用
或者 extends 的不是指定类及其子类,而是其父类
// Number "extends" Number (in this context)
List<? extends Number> foo3 = new ArrayList<? extends Number>();
// Integer extends Number
List<? extends Number> foo3 = new ArrayList<? extends Integer>();
// Double extends Number
List<? extends Number> foo3 = new ArrayList<? extends Double>();
如果实现了多个接口,可以使用&来将接口隔开
T extends Comparable & Serializable
List<? extends Number> list = new ArrayList
<Number>
();- list.add(new Integer(1)); //error
list.add(new Float(1.2f)); //error
# super 指定类型必为自身或其父类
- 不能同时声明泛型通配符申明上界和下界
# PECS(读 extends,写 super)
- producer-extends, consumer-super.
- produce 是指参数是 producer,consumer 是指参数是 consumer。
- 要往泛型类写数据时,用 extends;
- 要从泛型类读数据时,用 super;
- 既要取又要写,就不用通配符(即 extends 与 super 都不用)比如 List
<Integer>
。 - 如果参数化类型表示一个 T 生产者,就是<? extends T>;如果它表示一个 T 消费者,就使用<? super E>。
- Stack 的 pushAll 的参数产生 E 实例供 Stack 使用,因此参数类型为 Iterable<? extends E>。
- popAll 的参数提供 Stack 消费 E 实例,因此参数类型为 Collection<? super E>。
public void pushAll(Iterable<? extends E> src) {
- for (E e : src)
- push(e);
- }
public void popAll(Collection<? super E> dst) {
- while (!isEmpty())
- dst.add(pop());
- }
- 在调用 pushAll 方法时生产了 E 实例(produces E instances),在调用 popAll 方法时 dst 消费了 E 实例(consumes E instances)。Naftalin 与 Wadler 将 PECS 称为 Get and Put Principle。
- Collections#copy
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 泛型擦除(编译时擦除)
编译器生成的 bytecode 是不包含泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程即泛型擦除。
擦除类型变量,并替换为限定类型(无限定的变量用 Object)。
比如 T extends Comparable
那么下面所有出现 T 的地方都会被替换为 Comparable
如果调用时指定某个类,比如 Pair
<String>
pair = new Pair<>();那么 Pair
<T>
中所有的 T 都替换为 String泛型擦除带来的问题:
- 1)无法使用具有不同类型参数的泛型进行方法重载
public void test(List<String> ls) {
- System.out.println("Sting");
- }
public void test(List<Integer> li) {
- System.ut.println("Integer");
- } // 编译出错
- 或者
public interface Builder<K,V> {
void add(List
<K>
keyList);void add(List
<V>
valueList);}
- 2)泛型类的静态变量是共享的
另外,因为 Java 泛型的擦除并不是对所有使用泛型的地方都会擦除的,部分地方会保留泛型信息,在运行时可以获得类型参数。
# 1.16 IO
# Unix IO 模型
- 异步 I/O 是指用户程序发起 IO 请求后,不等待数据,同时操作系统内核负责 I/O 操作把数据从内核拷贝到用户程序的缓冲区后通知应用程序。数据拷贝是由操作系统内核完成,用户程序从一开始就没有等待数据,发起请求后不参与任何 IO 操作,等内核通知完成。 - 同步 I/O 就是非异步 IO 的情况,也就是用户程序要参与把数据拷贝到程序缓冲区(例如 java 的 InputStream 读字节流过程)。 - 同步 IO 里的非阻塞 是指用户程序发起 IO 操作请求后不等待数据,而是调用会立即返回一个标志信息告知条件不满足,数据未准备好,从而用户请求程序继续执行其它任务。执行完其它任务,用户程序会主动轮询查看 IO 操作条件是否满足,如果满足,则用户程序亲自参与拷贝数据动作。
- Unix IO 模型的语境下,同步和异步的区别在于数据拷贝阶段是否需要完全由操作系统处理。阻塞和非阻塞操作是针对发起 IO 请求操作后是否有立刻返回一个标志信息而不让请求线程等待。
# BIO NIO AIO 介绍
- BIO:同步阻塞,每个客户端的连接会对应服务器的一个线程
- NIO:同步非阻塞,多路复用器轮询客户端的请求,每个客户端的 IO 请求会对应服务器的一个线程
- AIO: 异步非阻塞,客户端的 IO 请求由 OS 完成后再通知服务器启动线程处理(需要 OS 支持)
- 1、进程向操作系统请求数据
- 2、操作系统把外部数据加载到内核的缓冲区中,
- 3、操作系统把内核的缓冲区拷贝到进程的缓冲区
- 4、进程获得数据完成自己的功能
- Java NIO 属于同步非阻塞 IO,即 IO 多路复用,单个线程可以支持多个 IO
- 即询问时从 IO 没有完毕时直接阻塞,变成了立即返回一个是否完成 IO 的信号。
- 异步 IO 就是指 AIO,AIO 需要操作系统支持。
# Java BIO 使用
- Server
public class ChatServer {
- ServerSocket ss = null;
- boolean started = false;
- ArrayList
<Client>
clients = new ArrayList<Client>
();
public static void main(String[] args) {
- new ChatServer().start();
- }
public void start() {
try {
- ss = new ServerSocket(6666);
started = true;
} catch (BindException e) {
System.out.println("端口使用中...."); // 用于处理两次启动 Server 端
System.out.println("请重新运行服务器");
- System.exit(-1);
} catch (IOException e) {
System.out.println("服务器启动失败");
e.printStackTrace();
}
try {
while (started) {
Socket s = ss.accept();
Client c = new Client(s);
clients.add(c);
c.transmitToAll(c.name + "进入了聊天室");
new Thread(c).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally { // 主方法结束时应该关闭服务器 ServerSocket
if (ss != null)
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Client implements Runnable { // 包装给一个单独的客户端的线程类,应该保留自己的连接 Socket 和流
// 保留连接一般使用构造方法,将连接传入
// 一个客户端就 new 一个 Client 连接
private Socket s = null;
private DataInputStream dis = null;
private DataOutputStream dos = null;// 每个客户端的线程都有各自的输入输出流,输入流用于读来自当前客户端的数据,输出流用于保存当前客户端的流。
private boolean Connected = false;// 每个客户端都有一个开始结束的标志
private String name;
- Client(Socket s) { // new 一个 Client 对象时,要打开 Socket 和 DataInputStream 流
- this.s = s;
- try {
- dis = new DataInputStream(s.getInputStream());
- dos = new DataOutputStream(s.getOutputStream());
- Connected = true;
- this.name = dis.readUTF();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- // 如何实现一个客户端与其他客户端的通信?
- // 可以考虑在每连到一个客户端就保存与其的连接 Socket,当要发送给其他客户端信息时,遍历一遍所有其他客户端
public void run() {
- try {
- while (Connected) {
- String read = dis.readUTF();
- if (read.equals("EXIT")) {
- Connected = false;
- transmitToAll(this.name + "已退出");
- continue;
- } else if (read.startsWith("@")) {
- String[] msg = read.substring(1).split("😊;
- transmitToPerson(msg[0], msg[1]);
- continue;
- }
- transmitToAll(this.name+":"+read);
- }
- } catch (EOFException e1) {
- System.out.println("Client closed");
- } catch (IOException e1) {
- e1.printStackTrace();
- } finally { // 关闭资源应该放在 finally 中
- try {
- CloseUtil.close(dis, dos);
- if (s != null)
- s.close();
- } catch (IOException e1) {
- e1.printStackTrace();
- }
- }
- }
/**
- 将消息发送给所有人
- @param read
- */
public void transmitToAll(String read) {
- for (int i = 0; i < clients.size(); i++) {
- Client c = clients.get(i);
- if (c.Connected == true)
- c.send(read); // 调用每个客户端线程的 send 方法,一个对象的输出流与对应的客户端连接 dos -->
- // Client
- }
- }
/**
- 将消息发送给某个人,私聊
- @param read
- @param clientName
- */
public void transmitToPerson(String clientName, String read) {
- boolean isFind = false;
- for (int i = 0; i < clients.size(); i++) {
- Client client = clients.get(i);
- if (client.name.equals(clientName)) {
- client.send(this.name+":"+read);
- isFind = true;
- }
- }
- send(this.name+":"+read + (isFind ? "" : "\n 抱歉,没有找到此用户"));
- }
public void send(String str) {// 在哪里出错就在哪里捕获
- try {
- dos.writeUTF(str);
- } catch (SocketException e) {
- this.Connected = false;
- clients.remove(this);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }
- }
- Client(一个线程用于读取,一个线程用于发送)
public class ChatClient extends Frame {
- Socket s = null; //将某个对象使得在一个类的各个方法可用,将该对象设置为整个类的成员变量
- DataOutputStream dos = null;//在多个方法中都要使用
- DataInputStream dis = null;
- TextField tfText = new TextField(); // 设置为成员变量方便其他类进行访问
- TextArea taContent = new TextArea();
- boolean started = false;
- Thread recv = null;
- ChatClient(String name, int x, int y, int w, int h) {
- super(name);
- this.setBounds(x, y, w, h);
- this.setLayout(new BorderLayout());
- this.addWindowListener(new MonitorWindow());
- taContent.setEditable(false);
- this.add(tfText, BorderLayout.SOUTH);
- this.add(taContent, BorderLayout.NORTH);
- tfText.addActionListener(new MonitorText());//对于文本框的监视器必须添加在某个文本框上,只有窗口监视器才能添加到 Frame 上
- this.pack();
- this.setVisible(true); // 必须放在最后一行,否则之下的组件无法显示
- connect();
- ClientNameDialog dialog = new ClientNameDialog(this,"姓名提示框",true);
- }
private class ClientNameDialog extends JDialog implements ActionListener{
- JLabel jl = null;
- JTextField jf = null;
- JButton jb = null;
- ClientNameDialog(Frame owner,String title,boolean model){
- super(owner,title,model);
- this.setLayout(new BorderLayout());
- this.setBounds(300, 300, 200, 150);
- jl = new JLabel("请输入您的姓名或昵称:");
- jf = new JTextField();
- jb = new JButton("确定");
- jb.addActionListener(this);
- this.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent arg0) {
- setVisible(false);
- System.exit(0);
- }
- });
- this.add(jl,BorderLayout.NORTH);
- this.add(jf,BorderLayout.CENTER);
- this.add(jb, BorderLayout.SOUTH);
- this.setVisible(true);
- }
public void actionPerformed(ActionEvent e) {
- String name = "";
- name = jf.getText();
- if((name == null || name.equals(""))){
- JOptionPane.showMessageDialog(this, "姓名不可为空!");
- return;
- }
- this.setVisible(false);
- send(name);
- JOptionPane.showMessageDialog(this, "欢迎您,"+name);
- launchThread();
- }
- }
private class MonitorWindow extends WindowAdapter {
public void windowClosing(WindowEvent e) {
- setVisible(false);
- disConnect();
- System.exit(0);
- }
- }
private class MonitorText implements ActionListener {
- String str = null;
public void actionPerformed(ActionEvent e) {
- str = tfText.getText().trim();//注意这是内部类,要找到事件源对象直接引用外部类的 TextField 即可,不需要 getSource(平行类可用)
- tfText.setText(""); //trim 可以去掉开头和结尾的空格
- send(str);
- }
- }
public void send(String str){//为发送数据单独建立一个方法
- try{
- dos.writeUTF(str);
- dos.flush();
- }catch(IOException e1){
- e1.printStackTrace();
- }
- }
public void connect(){ //应为连接单独建立一个方法
- try{
- s = new Socket("localhost",6666);
- dos = new DataOutputStream(s.getOutputStream());//一连接就打开输出流
- dis = new DataInputStream(s.getInputStream()); //一连接就打开输入流
- started = true;
- }catch(IOException e){
- e.printStackTrace();
- }
- }
public void launchThread(){
- recv = new Thread(new Receive());
- recv.start();
- }
public void disConnect() {
- try{
- dos.writeUTF("EXIT");
- started = false;
- //加入到主线程,会等待子线程执行完毕,才会执行下面的语句。这就避免了在读数据的时候将流切断,但是在这里是无效的。但是将线程停止应该先考虑使用 join 方法
- CloseUtil.close(dis,dos);
- s.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
private class Receive implements Runnable { //同样原因 readUTF是阻塞式的,处于死循环中,不能执行其他语句,所以为其单独设置一个线程
public void run(){
- String str = null;
- try{
- while(started){
- str = dis.readUTF(); //如果在阻塞状态,程序被关闭,那么一定会报错 SocketException。关闭了 Socket 之后还在调用 readUTF 方法
- taContent.setText(taContent.getText()+str+'\n');//解决方法是在关闭程序的同时停止线程,不再读取
- (如果使用 JTextArea 可以使用 append 方法)
- }
- }catch (SocketException e){ //将 SocketException 视为退出。但这种想法是不好的,将异常视为程序正常的一部分
- System.out.println("Client has quitted!");
- }catch (EOFException e){
- System.out.println("Client has quitted!");
- }catch(IOException e){
- e.printStackTrace();
- }
- }
- }
public static void main(String[] args) {
- new ChatClient("Client", 200, 200, 300, 200);
- }
- }
# Java NIO 使用
- 传统的 IO 操作面向数据流,意味着每次从流中读一个或多个字节,直至完成,数据没有被缓存在任何地方。
- NIO 操作面向缓冲区,数据从 Channel 读取到 Buffer 缓冲区,随后在 Buffer 中处理数据。
- BIO 中的 accept 是没有客户端连接时阻塞,NIO 的 accept 是没有客户端连接时立即返回。
- NIO 的三个重要组件:Buffer、Channel、Selector。
- Buffer 是用于容纳数据的缓冲区,Channel 是与 IO 设备之间的连接,类似于流。
- 数据可以从 Channel 读到 Buffer 中,也可以从 Buffer 写到 Channel 中。
- Selector 是 Channel 的多路复用器。
# Buffer(缓冲区)
- clear 是将 position 置为 0,limit 置为 capacity;
- flip 是将 limit 置为 position,position 置为 0;
# MappedByteBuffer(对应 OS 中的内存映射文件)
- ByteBuffer 有两种模式:直接/间接。间接模式就是 HeapByteBuffer,即操作堆内存 (byte[])。
- 但是内存毕竟有限,如果我要发送一个 1G 的文件怎么办?不可能真的去分配 1G 的内存.这时就必须使用"直接"模式,即 MappedByteBuffer。
- OS 中内存映射文件是将一个文件映射为虚拟内存(文件没有真正加载到内存,只是作为虚存),不需要使用文件系统调用来读写数据,而是直接读写内存。
- Java 中是使用 MappedByteBuffer 来将文件映射为内存的。通常可以映射整个文件,如果文件比较大的话可以分段进行映射,只要指定文件的那个部分就可以。
- 优点:减少一次数据拷贝
- 之前是 进程空间<->内核的 IO 缓冲区<->文件
- 现在是 进程空间<->文件
- MappedByteBuffer 可以使用 FileChannel.map 方法获取。
- 它有更多的优点:
- a. 读取快
- b. 写入快
- c. 随机读写
- MappedByteBuffer 使用虚拟内存,因此分配(map)的内存大小不受 JVM 的-Xmx 参数限制,但是也是有大小限制的。
- 那么可用堆外内存到底是多少?,即默认堆外内存有多大:
- ① 如果我们没有通过-XX:MaxDirectMemorySize 来指定最大的堆外内存。则
- ② 如果我们没通过-Dsun.nio.MaxDirectMemorySize 指定了这个属性,且它不等于-1。则
- ③ 那么最大堆外内存的值来自于 directMemory = Runtime.getRuntime().maxMemory(),这是一个 native 方法。
- 在我们使用 CMS GC 的情况下的实现如下:其实是新生代的最大值-一个 survivor 的大小+老生代的最大值,也就是我们设置的-Xmx 的值里除去一个 survivor 的大小就是默认的堆外内存的大小了。
- 如果当文件过大,内存不足时,可以通过 position 参数重新 map 文件后面的内容。
- MappedByteBuffer 在处理大文件时的确性能很高,但也存在一些问题,如内存占用、文件关闭不确定,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的。
# DirectByteBuffer(堆外内存)
DirectByteBuffer 继承自 MappedByteBuffer,它们都是使用的堆外内存,不受 JVM 堆大小的限制,只是前者仅仅是分配内存,后者是将文件映射到内存中。
可以通过 ByteBuf.allocateDirect 方法获取。
堆外内存的特点(大对象;加快内存拷贝;减轻 GC 压力) - 对于大内存有良好的伸缩性(支持分配大块内存) - 对垃圾回收停顿的改善可以明显感觉到(堆外内存,减少 GC 对堆内存回收的压力) - 在进程间可以共享,减少虚拟机间的复制,加快复制速度(减少堆内内存拷贝到堆外内存的过程) - 还可以使用 池+堆外内存 的组合方式,来对生命周期较短,但涉及到 I/O 操作的对象进行堆外内存的再使用。( Netty 中就使用了该方式 )
堆外内存的一些问题
- 1)堆外内存回收问题(不手工回收会导致内存溢出,手工回收就失去了 Java 的优势);
- 数据结构变得有些别扭。要么就是需要一个简单的数据结构以便于直接映射到堆外内存,要么就使用复杂的数据结构并序列化及反序列化到内存中。很明显使用序列化的话会比较头疼且存在性能瓶颈。使用序列化比使用堆对象的性能还差。
# 堆外内存的释放
- java.nio.DirectByteBuffer 对象在创建过程中会先通过 Unsafe 接口直接通过 os::malloc 来分配内存,然后将内存的起始地址和大小存到 java.nio.DirectByteBuffer 对象里,这样就可以直接操作这些内存。这些内存只有在 DirectByteBuffer 回收掉之后才有机会被回收,因此如果这些对象大部分都移到了 old,但是一直没有触发 CMS GC 或者 Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize 来指定最大的堆外内存大小,当使用达到了阈值的时候将调用 System.gc 来做一次 full gc,以此来回收掉没有被使用的堆外内存。
- GC 方式:
- 存在于堆内的 DirectByteBuffer 对象很小,只存着基地址和大小等几个属性,和一个 Cleaner,但它代表着后面所分配的一大段内存,是所谓的冰山对象。通过前面说的 Cleaner,堆内的 DirectByteBuffer 对象被 GC 时,它背后的堆外内存也会被回收。
- 当新生代满了,就会发生 minor gc;如果此时对象还没失效,就不会被回收;撑过几次 minorgc 后,对象被迁移到老生代;当老生代也满了,就会发生 full gc。
- 这里可以看到一种尴尬的情况,因为 DirectByteBuffer 本身的个头很小,只要熬过了 minor gc,即使已经失效了也能在老生代里舒服的呆着,不容易把老生代撑爆触发 full gc,如果没有别的大块头进入老生代触发 full gc,就一直在那耗着,占着一大片堆外内存不释放。
- 这时,就只能靠前面提到的申请额度超限时触发的 System.gc()来救场了。但这道最后的保险其实也不很好,首先它会中断整个进程,然后它让当前线程睡了整整一百毫秒,而且如果 gc 没在一百毫秒内完成,它仍然会无情的抛出 OOM 异常。
- 那为什么 System.gc()会释放 DirectByteBuffer 呢?
- 每个 DirectByteBuffer 关联着其对应的 Cleaner,Cleaner 是 PhantomReference 的子类,虚引用主要被用来跟踪对象被垃圾回收的状态,通过查看 ReferenceQueue 中是否包含对象所对应的虚引用来判断它是否即将被垃圾回收。
- 当 GC 时发现 DirectByteBuffer 除了 PhantomReference 外已不可达,就会把它放进 Reference 类 pending list 静态变量里。然后另有一条 ReferenceHandler 线程,名字叫 "Reference Handler"的,关注着这个 pending list,如果看到有对象类型是 Cleaner,就会执行它的 clean(),其他类型就放入应用构造 Reference 时传入的 ReferenceQueue 中,这样应用的代码可以从 Queue 里拖出这些理论上已死的对象,做爱做的事情——这是一种比 finalizer 更轻量更好的机制。
- 手工方式:
- 如果想立即释放掉一个 MappedByteBuffer/DirectByteBuffer,因为 JDK 没有提供公开 API,只能使用反射的方法去 unmap;
- 或者使用 Cleaner 的 clean 方法。
public static void main(String[] args) {
try {
File f = File.createTempFile("Test", null);
f.deleteOnExit();
RandomAccessFile file = new RandomAccessFile(f, "rw");
file.setLength(1024);
FileChannel channel = file.getChannel();
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, 1024);
channel.close();
file.close();
// 手动unmap
Method m = FileChannelImpl.class.getDeclaredMethod("unmap",
MappedByteBuffer.class);
m.setAccessible(true);
m.invoke(FileChannelImpl.class, buffer);
if (f.delete())
System.out.println("Temporary file deleted: " + f);
else
System.err.println("Not yet deleted: " + f);
} catch (Exception ex) {
ex.printStackTrace();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Channel(通道)
- Channel 与 IO 设备的连接,与 Stream 是平级的概念。
# 流与通道的区别
- 1、流是单向的,通道是双向的,可读可写。
- 2、流读写是阻塞的,通道可以异步读写。
- 3、流中的数据可以选择性的先读到缓存中,通道的数据总是要先读到一个缓存中,或从缓存中写入
- 注意,FileChannel 不能设置为非阻塞模式。
# 分散与聚集
- 分散(scatter)从 Channel 中读取是指在读操作时将读取的数据写入多个 buffer 中。因此,Channel 将从 Channel 中读取的数据“分散(scatter)”到多个 Buffer 中。
- 聚集(gather)写入 Channel 是指在写操作时将多个 buffer 的数据写入同一个 Channel,因此,Channel 将多个 Buffer 中的数据“聚集(gather)”后发送到 Channel。
# Pipe
public class PipeTest {
public static void main(String[] args) {
Pipe pipe = null;
ExecutorService exec = Executors.newFixedThreadPool(2);
try {
pipe = Pipe.open();
final Pipe pipeTemp = pipe;
exec.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
Pipe.SinkChannel sinkChannel = pipeTemp.sink();//向通道中写数据
while (true) {
TimeUnit.SECONDS.sleep(1);
String newData = "Pipe Test At Time " + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while (buf.hasRemaining()) {
System.out.println(buf);
sinkChannel.write(buf);
}
}
}
});
exec.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
Pipe.SourceChannel sourceChannel = pipeTemp.source();//向通道中读数据
while (true) {
TimeUnit.SECONDS.sleep(1);
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.clear();
int bytesRead = sourceChannel.read(buf);
System.out.println("bytesRead=" + bytesRead);
while (bytesRead > 0) {
buf.flip();
byte b[] = new byte[bytesRead];
int i = 0;
while (buf.hasRemaining()) {
b[i] = buf.get();
System.out.printf("%X", b[i]);
i++;
}
String s = new String(b);
System.out.println("=================||" + s);
bytesRead = sourceChannel.read(buf);
}
}
}
});
} catch (IOException e) {
e.printStackTrace();
} finally {
exec.shutdown();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# FileChannel 与文件锁
- 在通道中我们可以对文件或者部分文件进行上锁。上锁和我们了解的线程锁差不多,都是为了保证数据的一致性。在文件通道 FileChannel 中可以对文件进行上锁,通过 FileLock 可以对文件进行锁的释放。
- 文件加锁是建立在文件通道(FileChannel)之上的,套接字通道(SockeChannel)不考虑文件加锁,因为它是不共享的。它对文件加锁有两种方式:
- ①lock
- ②tryLock
- 两种加锁方式默认都是对整个文件加锁,如果自己配置的话就可以控制加锁的文件范围:position 是加锁的开始位置,size 是加锁长度,shared 是用于控制该锁是共享的还是独占的。
- lock 是阻塞式的,当有进程对锁进行读取时会等待锁的释放,在此期间它会一直等待;tryLock 是非阻塞式的,它尝试获得锁,如果这个锁不能获得,那么它会立即返回。
- release 可以释放锁。
- 在一个进程中在锁没有释放之前是无法再次获得锁的
- 在 java 的 NIO 中,通道包下面有一个 FileLock 类,它主要是对文件锁工具的一个描述。在上一小节中对文件的锁获取其实是 FileChannel 获取的(lock 与 trylock 是 FileChannel 的方法),它们返回一个 FileLock 对象。这个类的核心方法有如下这些:
- boolean isShared() :判断锁是否为共享类型
- abstract boolean isValid() :判断锁是否有效
- boolean overlaps():判断此锁定是否与给定的锁定区域重叠
- long position():返回文件内锁定区域中第一个字节的位置。
- abstract void release() :释放锁
- long size() :返回锁定区域的大小,以字节为单位
- 在文件锁中有 3 种方式可以释放文件锁:① 锁类释放锁,调用 FileLock 的 release 方法; ② 通道类关闭通道,调用 FileChannel 的 close 方法;③jvm 虚拟机会在特定情况释放锁。
- 锁类型(独占式和共享式)
- 我们先区分一下在文件锁中两种锁的区别:① 独占式的锁就想我们上面测试的那样,只要有一个进程获取了独占锁,那么别的进程只能等待。② 共享锁在一个进程获取的情况下,别的进程还是可以读取被锁定的文件,但是别的进程不能写只能读。
# Selector (Channel 的多路复用器)
- Selector 可以用单线程去管理多个 Channel(多个连接)。
- 放在网络编程的环境下:Selector 使用单线程,轮询客户端对应的 Channel 的请求,如果某个 Channel 需要进行 IO,那么分配一个线程去执行 IO 操作。
- Selector 可以去监听的请求有以下几类:
- 1、connect:客户端连接服务端事件,对应值为 SelectionKey.OPCONNECT(8)
- 2、accept:服务端接收客户端连接事件,对应值为 SelectionKey.OPACCEPT(16)
- 3、read:读事件,对应值为 SelectionKey.OPREAD(1)
- 4、write:写事件,对应值为 SelectionKey.OPWRITE(4)
- 每次请求到达服务器,都是从 connect 开始,connect 成功后,服务端开始准备 accept,准备就绪,开始读数据,并处理,最后写回数据返回。
- SelectionKey 是一个复合事件,绑定到某个 selector 对应的某个 channel 上,可能是多个事件的复合或单一事件。
# Java NIO 实例(文件上传)
- 服务器主线程先创建 Socket,并注册到 selector,然后轮询 selector。
- 1)如果有客户端需要进行连接,那么 selector 返回 ACCEPT 事件,主线程建立连接(accept),并将该客户端连接注册到 selector,结束,继续轮询 selector 等待下一个客户端事件;
- 2)如果有已连接的客户端需要进行读写,那么 selector 返回 READ/WRITE 事件,主线程将该请求交给 IO 线程池中的某个线程执行操作,结束,继续轮询 selector 等待下一个客户端事件。
# 服务器
public class NIOTCPServer {
private ServerSocketChannel serverSocketChannel;
private final String FILE_PATH = "E:/uploads/";
private AtomicInteger i;
private final String RESPONSE_MSG = "服务器接收数据成功";
private Selector selector;
private ExecutorService acceptPool;
private ExecutorService readPool;
public NIOTCPServer() {
try {
serverSocketChannel = ServerSocketChannel.open();
//切换为非阻塞模式
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(9000));
//获得选择器
selector = Selector.open();
//将channel注册到selector上
//第二个参数是选择键,用于说明selector监控channel的状态
//可能的取值:SelectionKey.OP_READ OP_WRITE OP_CONNECT OP_ACCEPT
//监控的是channel的接收状态
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
acceptPool = new ThreadPoolExecutor(50, 100, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>());
readPool = new ThreadPoolExecutor(50, 100, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>());
i = new AtomicInteger(0);
System.out.println("服务器启动");
} catch (IOException e) {
e.printStackTrace();
}
}
public void receive() {
try {
//如果有一个及以上的客户端的数据准备就绪
while (selector.select() > 0) {
//获取当前选择器中所有注册的监听事件
for (Iterator<SelectionKey> it = selector.selectedKeys().iterator(); it.hasNext(); ) {
SelectionKey key = it.next();
//如果"接收"事件已就绪
if (key.isAcceptable()) {
//交由接收事件的处理器处理
acceptPool.submit(new ReceiveEventHander());
} else if (key.isReadable()) {
//如果"读取"事件已就绪
//交由读取事件的处理器处理
readPool.submit(new ReadEventHandler((SocketChannel) key.channel()));
}
//处理完毕后,需要取消当前的选择键
it.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
class ReceiveEventHander implements Runnable {
public ReceiveEventHander() {
}
@Override
public void run() {
SocketChannel client = null;
try {
client = serverSocketChannel.accept();
// 接收的客户端也要切换为非阻塞模式
client.configureBlocking(false);
// 监控客户端的读操作是否就绪
client.register(selector, SelectionKey.OP_READ);
System.out.println("服务器连接客户端:" + client.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ReadEventHandler implements Runnable {
private ByteBuffer buf;
private SocketChannel client;
public ReadEventHandler(SocketChannel client) {
this.client = client;
buf = ByteBuffer.allocate(1024);
}
@Override
public void run() {
FileChannel fileChannel = null;
try {
int index = 0;
synchronized (client) {
while (client.read(buf) != -1) {
if (fileChannel == null) {
index = i.getAndIncrement();
fileChannel = FileChannel.open(Paths.get(FILE_PATH, index + ".jpeg"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
}
buf.flip();
fileChannel.write(buf);
buf.clear();
}
}
if (fileChannel != null) {
fileChannel.close();
System.out.println("服务器写来自客户端" + client + " 文件" + index + " 完毕");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
NIOTCPServer server = new NIOTCPServer();
server.receive();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# 客户端
public class NIOTCPClient {
private SocketChannel clientChannel;
private ByteBuffer buf;
public NIOTCPClient() {
try {
clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9000));
//设置客户端为非阻塞模式
clientChannel.configureBlocking(false);
buf = ByteBuffer.allocate(1024);
} catch (IOException e) {
e.printStackTrace();
}
}
public void send(String fileName) {
try {
FileChannel fileChannel = FileChannel.open(Paths.get(fileName), StandardOpenOption.READ);
while (fileChannel.read(buf) != -1) {
buf.flip();
clientChannel.write(buf);
buf.clear();
}
System.out.println("客户端已发送文件" + fileName);
fileChannel.close();
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(50, 100, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>());
Instant begin = Instant.now();
for (int i = 0; i < 200; i++) {
pool.submit(() -> {
NIOTCPClient client = new NIOTCPClient();
client.send("E:/1.jpeg");
});
}
pool.shutdown();
Instant end = Instant.now();
System.out.println(Duration.between(begin,end));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# Java AIO 使用
- 对 AIO 来说,它不是在 IO 准备好时再通知线程,而是在 IO 操作已经完成后,再给线程发出通知。因此 AIO 是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待 IO 操作完成后,由系统自动触发。
- AIO 的四步:
- 1、进程向操作系统请求数据
- 2、操作系统把外部数据加载到内核的缓冲区中,
- 3、操作系统把内核的缓冲区拷贝到进程的缓冲区
- 4、进程获得数据完成自己的功能
- JDK1.7 主要增加了三个新的异步通道:
- AsynchronousFileChannel: 用于文件异步读写;
- AsynchronousSocketChannel: 客户端异步 socket;
- AsynchronousServerSocketChannel: 服务器异步 socket。
- 因为 AIO 的实施需充分调用 OS 参与,IO 需要操作系统支持、并发也同样需要操作系统的
- 在 AIO socket 编程中,服务端通道是 AsynchronousServerSocketChannel,这个类提供了一个 open()静态工厂,一个 bind()方法用于绑定服务端 IP 地址(还有端口号),另外还提供了 accept()用于接收用户连接请求。在客户端使用的通道是 AsynchronousSocketChannel,这个通道处理提供 open 静态工厂方法外,还提供了 read 和 write 方法。
- 在 AIO 编程中,发出一个事件(accept read write 等)之后要指定事件处理类(回调函数),AIO 中的事件处理类是 CompletionHandler<V,A>,这个接口定义了如下两个方法,分别在异步操作成功和失败时被回调。
- void completed(V result, A attachment);
- void failed(Throwable exc, A attachment);
public class AIOServer {
private static int PORT = 8080;
private static int BUFFER_SIZE = 1024;
private static String CHARSET = "utf-8"; //默认编码
private static CharsetDecoder decoder = Charset.forName(CHARSET).newDecoder(); //解码
private AsynchronousServerSocketChannel serverChannel;
public AIOServer() {
this.decoder = Charset.forName(CHARSET).newDecoder();
}
private void listen() throws Exception {
//打开一个服务通道
//绑定服务端口
this.serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT), 100);
this.serverChannel.accept(this, new AcceptHandler());
}
/**
* accept到一个请求时的回调
*/
private class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AIOServer> {
@Override
public void completed(final AsynchronousSocketChannel client, AIOServer server) {
try {
System.out.println("远程地址:" + client.getRemoteAddress());
//tcp各项参数
client.setOption(StandardSocketOptions.TCP_NODELAY, true);
client.setOption(StandardSocketOptions.SO_SNDBUF, 1024);
client.setOption(StandardSocketOptions.SO_RCVBUF, 1024);
if (client.isOpen()) {
System.out.println("client.isOpen:" + client.getRemoteAddress());
final ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
buffer.clear();
client.read(buffer, client, new ReadHandler(buffer));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
server.serverChannel.accept(server, this);// 监听新的请求,递归调用。
}
}
@Override
public void failed(Throwable e, AIOServer attachment) {
try {
e.printStackTrace();
} finally {
attachment.serverChannel.accept(attachment, this);// 监听新的请求,递归调用。
}
}
}
/**
* Read到请求数据的回调
*/
private class ReadHandler implements CompletionHandler<Integer, AsynchronousSocketChannel> {
private ByteBuffer buffer;
public ReadHandler(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public void completed(Integer result, AsynchronousSocketChannel client) {
try {
if (result < 0) {// 客户端关闭了连接
AIOServer.close(client);
} else if (result == 0) {
System.out.println("空数据"); // 处理空数据
} else {
// 读取请求,处理客户端发送的数据
buffer.flip();
CharBuffer charBuffer = AIOServer.decoder.decode(buffer);
System.out.println(charBuffer.toString()); //接收请求
//响应操作,服务器响应结果
buffer.clear();
String res = "hellworld";
buffer = ByteBuffer.wrap(res.getBytes());
client.write(buffer, client, new WriteHandler(buffer));//Response:响应。
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
exc.printStackTrace();
AIOServer.close(attachment);
}
}
/**
* Write响应完请求的回调
*/
private class WriteHandler implements CompletionHandler<Integer, AsynchronousSocketChannel> {
private ByteBuffer buffer;
public WriteHandler(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public void completed(Integer result, AsynchronousSocketChannel attachment) {
buffer.clear();
AIOServer.close(attachment);
}
@Override
public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
exc.printStackTrace();
AIOServer.close(attachment);
}
}
private static void close(AsynchronousSocketChannel client) {
try {
client.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
try {
System.out.println("正在启动服务...");
AIOServer AIOServer = new AIOServer();
AIOServer.listen();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
}
}
});
t.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# Java NIO 源码
- 关于 Selector 源码过于难以理解,可以先放过。
# Buffer
public abstract class Buffer {
/**
* The characteristics of Spliterators that traverse and split elements
* maintained in Buffers.
*/
static final int SPLITERATOR_CHARACTERISTICS =
Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;
// Creates a new buffer with the given mark, position, limit, and capacity,
// after checking invariants.
//
Buffer(int mark, int pos, int lim, int cap) { // package-private
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
this.capacity = cap;
limit(lim);
position(pos);
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- }
- ByteBuffer 有两种实现:HeapByteBuffer 和 DirectByteBuffer。
- ByteBuffer#allocate
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
2
3
4
5
- ByteBuffer#allocateDirect
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
2
3
# HeapByteBuffer(间接模式)
- 底层基于 byte 数组。
# 初始化
HeapByteBuffer(int cap, int lim) { // package-private
super(-1, 0, lim, cap, new byte[cap], 0);
}
2
3
- 调用的是 ByteBuffer 的初始化方法
ByteBuffer(int mark, int pos, int lim, int cap, // package-private
byte[] hb, int offset)
{
super(mark, pos, lim, cap);
this.hb = hb;
this.offset = offset;
}
2
3
4
5
6
7
- ByteBuffer 的独有成员变量:
- final byte[] hb; // Non-null only for heap buffers final int offset; boolean isReadOnly; // Valid only for heap buffers
# get
public byte get() {
return hb[ix(nextGetIndex())];
}
2
3
final int nextGetIndex() { // package-private
if (position >= limit)
throw new BufferUnderflowException();
return position++;
}
2
3
4
5
- protected int ix(int i) { return i + offset; }
# put
public ByteBuffer put(byte x) {
hb[ix(nextPutIndex())] = x;
return this;
}
2
3
4
final int nextPutIndex() { // package-private
if (position >= limit)
throw new BufferOverflowException();
return position++;
}
2
3
4
5
# DirectByteBuffer(直接模式)
- 底层基于 c++的 malloc 分配的堆外内存,是使用 Unsafe 类分配的,底层调用了 native 方法。
- 在创建 DirectByteBuffer 的同时,创建一个与其对应的 cleaner,cleaner 是一个虚引用。
- 回收堆外内存的几种情况:
- 1)程序员手工释放,需要使用 sun 的非公开 API 实现。
- 2)申请新的堆外内存而内存不足时,会进行调用 Cleaner(作为一个 Reference)的静态方法 tryHandlePending(false),它又会调用 cleaner 的 clean 方法释放内存。
- 3)当 DirectByteBuffer 失去强引用,只有虚引用时,当等到某一次 System.gc(full gc)(比如堆外内存达到 XX:MaxDirectMemorySize)时,当 DirectByteBuffer 对象从 pending 状态 -> enqueue 状态时,会触发 Cleaner 的 clean(),而 Cleaner 的 clean()的方法会实现通过 unsafe 对堆外内存的释放。
# 初始化
- 重要成员变量:
private final Cleaner cleaner;
2
- // Cached unsafe-access object protected static final Unsafe unsafe = Bits.unsafe();
- Unsafe 中很多都是 native 方法,底层调用 c++代码。
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
2
3
// 内存是否按页分配对齐 boolean pa = VM.isDirectMemoryPageAligned();
// 获取每页内存大小 int ps = Bits.pageSize();
// 分配内存的大小,如果是按页对齐方式,需要再加一页内存的容量 long size = Math.max(1L, (long)cap + (pa ? ps : 0));
// 用 Bits 类保存总分配内存(按页分配)的大小和实际内存的大小 Bits.reserveMemory(size, cap);
long base = 0; try {
// 在堆外内存的基地址,指定内存大小 base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte) 0);
- // 计算堆外内存的基地址 if (pa && (base % ps != 0)) { // Round up to page boundary address = base + ps - (base & (ps - 1)); } else { address = base; } cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); att = null; }
第一行 super 调用的是其父类 MappedByteBuffer 的构造方法
MappedByteBuffer(int mark, int pos, int lim, int cap) { // package-private
super(mark, pos, lim, cap);
this.fd = null;
}
2
3
4
- 而它的 super 又调用了 ByteBuffer 的构造方法
ByteBuffer(int mark, int pos, int lim, int cap) { // package-private
this(mark, pos, lim, cap, null, 0);
}
2
3
Bits#reserveMemory
该方法用于在系统中保存总分配内存(按页分配)的大小和实际内存的大小。
总的来说,Bits.reserveMemory(size, cap)方法在可用堆外内存不足以分配给当前要创建的堆外内存大小时,会实现以下的步骤来尝试完成本次堆外内存的创建:
① 触发一次非堵塞的 Reference#tryHandlePending(false)。该方法会将已经被 JVM 垃圾回收的 DirectBuffer 对象的堆外内存释放。
② 如果进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则进行 System.gc()。System.gc()会触发一个 full gc,但你需要知道,调用 System.gc()并不能够保证 full gc 马上就能被执行。所以在后面打代码中,会进行最多 9 次尝试,看是否有足够的可用堆外内存来分配堆外内存。并且每次尝试之前,都对延迟等待时间,已给 JVM 足够的时间去完成 full gc 操作。
这里之所以用使用 full gc 的很重要的一个原因是:System.gc()会对新生代和老生代都会进行内存回收,这样会比较彻底地回收 DirectByteBuffer 对象以及他们关联的堆外内存.
DirectByteBuffer 对象本身其实是很小的,但是它后面可能关联了一个非常大的堆外内存,因此我们通常称之为冰山对象.
我们做 young gc 的时候会将新生代里的不可达的 DirectByteBuffer 对象及其堆外内存回收了,但是无法对 old 里的 DirectByteBuffer 对象及其堆外内存进行回收,这也是我们通常碰到的最大的问题。(并且堆外内存多用于生命期中等或较长的对象)
如果有大量的 DirectByteBuffer 对象移到了 old,但是又一直没有做 cms gc 或者 full gc,而只进行 ygc,那么我们的物理内存可能被慢慢耗光,但是我们还不知道发生了什么,因为 heap 明明剩余的内存还很多(前提是我们禁用了 System.gc – JVM 参数 DisableExplicitGC)。
注意,如果你设置了-XX:+DisableExplicitGC,将会禁用显示 GC,这会使 System.gc()调用无效。
③ 如果 9 次尝试后依旧没有足够的可用堆外内存来分配本次堆外内存,则抛出 OutOfMemoryError("Direct buffer memory”)异常。
static void reserveMemory(long size, int cap) {
if (!memoryLimitSet && VM.isBooted()) { maxMemory = VM.maxDirectMemory(); memoryLimitSet = true; }
// optimist! if (tryReserveMemory(size, cap)) { return; }
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess(); // 如果系统中内存( 即,堆外内存 )不够的话:
jlra.tryHandlePendingReference()会触发一次非堵塞的 Reference#tryHandlePending(false)。该方法会将已经被 JVM 垃圾回收的 DirectBuffer 对象的堆外内存释放。
// retry while helping enqueue pending Reference objects // which includes executing pending Cleaner(s) which includes // Cleaner(s) that free direct buffer memory while (jlra.tryHandlePendingReference()) { if (tryReserveMemory(size, cap)) { return; } } // 如果在进行一次堆外内存资源回收后,还不够进行本次堆外内存分配的话,则 // trigger VM's Reference processing System.gc();
// a retry loop with exponential back-off delays // (this gives VM some time to do it's job) boolean interrupted = false; try { long sleepTime = 1; int sleeps = 0; while (true) { if (tryReserveMemory(size, cap)) { return; }
// 9 if (sleeps >= MAX_SLEEPS) { break; } if (!jlra.tryHandlePendingReference()) { try { Thread.sleep(sleepTime); sleepTime <<= 1; sleeps++; } catch (InterruptedException e) { interrupted = true; } } } // 如果 9 次尝试后依旧没有足够的可用堆外内存来分配本次堆外内存,则抛出 OutOfMemoryError("Direct buffer memory”)异常。 // no luck throw new OutOfMemoryError("Direct buffer memory");
} finally { if (interrupted) { // don't swallow interrupts Thread.currentThread().interrupt(); } } }
Reference#tryHandlePending
static boolean tryHandlePending(boolean waitForNotify) { Reference
<Object>
r; Cleaner c; try { synchronized (lock) { if (pending != null) { r = pending; // 'instanceof' might throw OutOfMemoryError sometimes // so do this before un-linking 'r' from the 'pending' chain... c = r instanceof Cleaner ? (Cleaner) r : null; // unlink 'r' from 'pending' chain pending = r.discovered; r.discovered = null; } else { // The waiting on the lock may cause an OutOfMemoryError // because it may try to allocate exception objects. if (waitForNotify) { lock.wait(); } // retry if waited return waitForNotify; } } } catch (OutOfMemoryError x) { // Give other threads CPU time so they hopefully drop some live references // and GC reclaims some space. // Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above // persistently throws OOME for some time... Thread.yield(); // retry return true; } catch (InterruptedException x) { // retry return true; }// Fast path for cleaners if (c != null) { c.clean(); return true; }
ReferenceQueue<? super Object> q = r.queue; if (q != ReferenceQueue.NULL) q.enqueue(r); return true; }
# Deallocator
- 后面是调用 unsafe 的分配堆外内存的方法,然后初始化了该 DirectByteBuffer 对应的 cleaner。
- 注:在 Cleaner 内部中通过一个列表,维护了一个针对每一个 directBuffer 的一个回收堆外内存的 线程对象(Runnable),回收操作是发生在 Cleaner 的 clean() 方法中。
private static class Deallocator
implements Runnable
{
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address;
private long size;
private int capacity;
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Cleaner(回收)
public class Cleaner extends PhantomReference<Object> {
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
private static Cleaner first = null;
private Cleaner next = null;
private Cleaner prev = null;
private final Runnable thunk;
private static synchronized Cleaner add(Cleaner var0) {
if (first != null) {
var0.next = first;
first.prev = var0;
}
first = var0;
return var0;
}
private static synchronized boolean remove(Cleaner var0) {
if (var0.next == var0) {
return false;
} else {
if (first == var0) {
if (var0.next != null) {
first = var0.next;
} else {
first = var0.prev;
}
}
if (var0.next != null) {
var0.next.prev = var0.prev;
}
if (var0.prev != null) {
var0.prev.next = var0.next;
}
var0.next = var0;
var0.prev = var0;
return true;
}
}
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}
// var0是DirectByteBuffer,var1是Deallocator线程对象
public static Cleaner create(Object var0, Runnable var1) {
return var1 == null ? null : add(new Cleaner(var0, var1));
}
public void clean() {
if (remove(this)) {
try {
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 回收该DirectByteBuffer对应的堆外内存
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- Cleaner 的构造方法中又调用了父类虚引用的构造方法:
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
2
3
# get
public byte get() {
return ((unsafe.getByte(ix(nextGetIndex()))));
}
2
3
# put
public ByteBuffer put(byte x) {
unsafe.putByte(ix(nextPutIndex()), ((x)));
return this;
}
2
3
4
# FileChannel(阻塞式)
- FileChannel 的 read、write 和 map 通过其实现类 FileChannelImpl 实现。
- FileChannelImpl 的 Oracle JDK 没有提供源码,只能在 OpenJDK 中查看。
# open
public static FileChannel open(Path path, OpenOption... options)
throws IOException
{
Set<OpenOption> set = new HashSet<OpenOption>(options.length);
Collections.addAll(set, options);
return open(path, set, NO_ATTRIBUTES);
}
2
3
4
5
6
7
public static FileChannel open(Path path,
Set<? extends OpenOption> options,
FileAttribute<?>... attrs)
throws IOException
{
FileSystemProvider provider = path.getFileSystem().provider();
return provider.newFileChannel(path, options, attrs);
}
2
3
4
5
6
7
8
- WindowsFileSystemProvider#newFileChannel
public FileChannel newFileChannel(Path path,
Set<? extends OpenOption> options,
FileAttribute<?>... attrs)
throws IOException
{
if (path == null)
throw new NullPointerException();
if (!(path instanceof WindowsPath))
throw new ProviderMismatchException();
WindowsPath file = (WindowsPath)path;
WindowsSecurityDescriptor sd = WindowsSecurityDescriptor.fromAttribute(attrs);
try {
return WindowsChannelFactory
.newFileChannel(file.getPathForWin32Calls(),
file.getPathForPermissionCheck(),
options,
sd.address());
} catch (WindowsException x) {
x.rethrowAsIOException(file);
return null;
} finally {
if (sd != null)
sd.release();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
WindowsChannelFactory#newFileChannel
static FileChannel newFileChannel(String pathForWindows, String pathToCheck, Set<? extends OpenOption> options, long pSecurityDescriptor) throws WindowsException { Flags flags = Flags.toFlags(options);
// default is reading; append => writing if (!flags.read && !flags.write) { if (flags.append) { flags.write = true; } else { flags.read = true; } }
// validation if (flags.read && flags.append) throw new IllegalArgumentException("READ + APPEND not allowed"); if (flags.append && flags.truncateExisting) throw new IllegalArgumentException("APPEND + TRUNCATE_EXISTING not allowed");
FileDescriptor fdObj = open(pathForWindows, pathToCheck, flags, pSecurityDescriptor); return FileChannelImpl.open(fdObj, pathForWindows, flags.read, flags.write, flags.append, null); }
/**
* Opens file based on parameters and options, returning a FileDescriptor
* encapsulating the handle to the open file.
*/
private static FileDescriptor open(String pathForWindows,
String pathToCheck,
Flags flags,
long pSecurityDescriptor)
throws WindowsException
{
// set to true if file must be truncated after open
boolean truncateAfterOpen = false;
// map options
int dwDesiredAccess = 0;
if (flags.read)
dwDesiredAccess |= GENERIC_READ;
if (flags.write)
dwDesiredAccess |= GENERIC_WRITE;
int dwShareMode = 0;
if (flags.shareRead)
dwShareMode |= FILE_SHARE_READ;
if (flags.shareWrite)
dwShareMode |= FILE_SHARE_WRITE;
if (flags.shareDelete)
dwShareMode |= FILE_SHARE_DELETE;
int dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL;
int dwCreationDisposition = OPEN_EXISTING;
if (flags.write) {
if (flags.createNew) {
dwCreationDisposition = CREATE_NEW;
// force create to fail if file is orphaned reparse point
dwFlagsAndAttributes |= FILE_FLAG_OPEN_REPARSE_POINT;
} else {
if (flags.create)
dwCreationDisposition = OPEN_ALWAYS;
if (flags.truncateExisting) {
// Windows doesn't have a creation disposition that exactly
// corresponds to CREATE + TRUNCATE_EXISTING so we use
// the OPEN_ALWAYS mode and then truncate the file.
if (dwCreationDisposition == OPEN_ALWAYS) {
truncateAfterOpen = true;
} else {
dwCreationDisposition = TRUNCATE_EXISTING;
}
}
}
}
if (flags.dsync || flags.sync)
dwFlagsAndAttributes |= FILE_FLAG_WRITE_THROUGH;
if (flags.overlapped)
dwFlagsAndAttributes |= FILE_FLAG_OVERLAPPED;
if (flags.deleteOnClose)
dwFlagsAndAttributes |= FILE_FLAG_DELETE_ON_CLOSE;
// NOFOLLOW_LINKS and NOFOLLOW_REPARSEPOINT mean open reparse point
boolean okayToFollowLinks = true;
if (dwCreationDisposition != CREATE_NEW &&
(flags.noFollowLinks ||
flags.openReparsePoint ||
flags.deleteOnClose))
{
if (flags.noFollowLinks || flags.deleteOnClose)
okayToFollowLinks = false;
dwFlagsAndAttributes |= FILE_FLAG_OPEN_REPARSE_POINT;
}
// permission check
if (pathToCheck != null) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
if (flags.read)
sm.checkRead(pathToCheck);
if (flags.write)
sm.checkWrite(pathToCheck);
if (flags.deleteOnClose)
sm.checkDelete(pathToCheck);
}
}
// open file
long handle = CreateFile(pathForWindows,
dwDesiredAccess,
dwShareMode,
pSecurityDescriptor,
dwCreationDisposition,
dwFlagsAndAttributes);
// make sure this isn't a symbolic link.
if (!okayToFollowLinks) {
try {
if (WindowsFileAttributes.readAttributes(handle).isSymbolicLink())
throw new WindowsException("File is symbolic link");
} catch (WindowsException x) {
CloseHandle(handle);
throw x;
}
}
// truncate file (for CREATE + TRUNCATE_EXISTING case)
if (truncateAfterOpen) {
try {
SetEndOfFile(handle);
} catch (WindowsException x) {
CloseHandle(handle);
throw x;
}
}
// make the file sparse if needed
if (dwCreationDisposition == CREATE_NEW && flags.sparse) {
try {
DeviceIoControlSetSparse(handle);
} catch (WindowsException x) {
// ignore as sparse option is hint
}
}
// create FileDescriptor and return
FileDescriptor fdObj = new FileDescriptor();
fdAccess.setHandle(fdObj, handle);
return fdObj;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
- static long CreateFile(String path, int dwDesiredAccess, int dwShareMode, long lpSecurityAttributes, int dwCreationDisposition, int dwFlagsAndAttributes) throws WindowsException { NativeBuffer buffer = asNativeBuffer(path); try { return CreateFile0(buffer.address(), dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes); } finally { buffer.release(); } }
private static native long CreateFile0(long lpFileName,
int dwDesiredAccess,
int dwShareMode,
long lpSecurityAttributes,
int dwCreationDisposition,
int dwFlagsAndAttributes)
throws WindowsException;
2
3
4
5
6
7
# read
public int read(ByteBuffer dst) throws IOException {
ensureOpen();
if (!readable)
throw new NonReadableChannelException();
synchronized (positionLock) {
int n = 0;
int ti = -1;
try {
begin();
ti = threads.add();
if (!isOpen())
return 0;
do {
n = IOUtil.read(fd, dst, -1, nd);
} while ((n == IOStatus.INTERRUPTED) && isOpen());
return IOStatus.normalize(n);
} finally {
threads.remove(ti);
end(n > 0);
assert IOStatus.check(n);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
IOUtil.read
static int read(FileDescriptor fd, ByteBuffer dst, long position, NativeDispatcher nd) IOException { if (dst.isReadOnly()) throw new IllegalArgumentException("Read-only buffer"); if (dst instanceof DirectBuffer) return readIntoNativeBuffer(fd, dst, position, nd);
// Substitute a native buffer ByteBuffer bb = Util.getTemporaryDirectBuffer(dst.remaining()); try { int n = readIntoNativeBuffer(fd, bb, position, nd); bb.flip(); if (n > 0) dst.put(bb); return n; } finally { Util.offerFirstTemporaryDirectBuffer(bb); } }
通过上述实现可以看出,基于 channel 的文件数据读取步骤如下:
1、申请一块和缓存同大小的 DirectByteBuffer bb。
2、读取数据到缓存 bb,底层由 NativeDispatcher 的 read 实现。
3、把 bb 的数据读取到 dst(用户定义的缓存,在 jvm 中分配内存)。
read 方法导致数据复制了两次。
# write
public int write(ByteBuffer src) throws IOException {
ensureOpen();
if (!writable)
throw new NonWritableChannelException();
synchronized (positionLock) {
int n = 0;
int ti = -1;
try {
begin();
ti = threads.add();
if (!isOpen())
return 0;
do {
n = IOUtil.write(fd, src, -1, nd);
} while ((n == IOStatus.INTERRUPTED) && isOpen());
return IOStatus.normalize(n);
} finally {
threads.remove(ti);
end(n > 0);
assert IOStatus.check(n);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- IOUtil.write
- static int write(FileDescriptor fd, ByteBuffer src, long position, NativeDispatcher nd) throws IOException { if (src instanceof DirectBuffer) return writeFromNativeBuffer(fd, src, position, nd); // Substitute a native buffer int pos = src.position(); int lim = src.limit(); assert (pos <= lim); int rem = (pos <= lim ? lim - pos : 0); ByteBuffer bb = Util.getTemporaryDirectBuffer(rem); try { bb.put(src); bb.flip(); // Do not update src until we see how many bytes were written src.position(pos); int n = writeFromNativeBuffer(fd, bb, position, nd); if (n > 0) { // now update src src.position(pos + n); } return n; } finally { Util.offerFirstTemporaryDirectBuffer(bb); } }
- 基于 channel 的文件数据写入步骤如下:
- 1、申请一块 DirectByteBuffer,bb 大小为 byteBuffer 中的 limit - position。
- 2、复制 byteBuffer 中的数据到 bb 中。
- 3、把数据从 bb 中写入到文件,底层由 NativeDispatcher 的 write 实现,具体如下:
private static int writeFromNativeBuffer(FileDescriptor fd,
ByteBuffer bb, long position, NativeDispatcher nd)
throws IOException {
int pos = bb.position();
int lim = bb.limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
int written = 0;
if (rem == 0)
return 0;
if (position != -1) {
written = nd.pwrite(fd,
((DirectBuffer) bb).address() + pos,
rem, position);
} else {
written = nd.write(fd, ((DirectBuffer) bb).address() + pos, rem);
}
if (written > 0)
bb.position(pos + written);
return written;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- write 方法也导致了数据复制了两次。
# ServerSocketChannel
- 它的实现类是 ServerSocketChannelImpl,同样是闭源的。
# open
public static ServerSocketChannel open() throws IOException {
return SelectorProvider.provider().openServerSocketChannel();
}
2
3
- SelectorProvider.provider()方法在 windows 平台下返回的是 SelectorProvider 的实现类 WindowsSelectorProvider 类的实例。
- WindowsSelectorProvider 类的直接父类为 SelectorProviderImpl;
- SelectorProviderImpl 的直接父类是 SelectorProvider。
- SelectorProviderImpl# openServerSocketChannel
public ServerSocketChannel openServerSocketChannel() throws IOException {
return new ServerSocketChannelImpl(this);
}
2
3
- ServerSocketChannelImpl(SelectorProvider var1) throws IOException {
super(var1);
this.fd = Net.serverSocket(true);
this.fdVal = IOUtil.fdVal(this.fd);
this.state = 0;
} - super(var1)实际上是父类的构造方法
- protected AbstractSelectableChannel(SelectorProvider provider) { this.provider = provider; }
- Net#serverSocket 创建一个 FileDescriptor
- static FileDescriptor serverSocket(boolean stream) { return IOUtil.newFD(socket0(isIPv6Available(), stream, true, fastLoopback)); }
# bind
public ServerSocketChannel bind(SocketAddress local, int backlog) throws IOException {
synchronized (lock) {
if (!isOpen())
throw new ClosedChannelException();
if (isBound())
throw new AlreadyBoundException();
InetSocketAddress isa = (local == null) ? new InetSocketAddress(0) :
Net.checkAddress(local);
SecurityManager sm = System.getSecurityManager();
if (sm != null)
sm.checkListen(isa.getPort());
NetHooks.beforeTcpBind(fd, isa.getAddress(), isa.getPort());
Net.bind(fd, isa.getAddress(), isa.getPort());
Net.listen(fd, backlog < 1 ? 50 : backlog);
synchronized (stateLock) {
localAddress = Net.localAddress(fd);
}
}
return this;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# register
- Selector 是通过 Selector.open 方法获得的。
- 将这个通道 channel 注册到指定的 selector 中,返回一个 SelectionKey 对象实例。
- register 这个方法在实现代码上的逻辑有以下四点:
- 1、首先检查通道 channel 是否是打开的,如果不是打开的,则抛异常,如果是打开的,则进行 2。
- 2、检查指定的 interest 集合是否是有效的。如果没效,则抛异常。否则进行 3。这里要特别强调一下:对于 ServerSocketChannel 仅仅支持”新的连接”,因此 interest 集合 ops 满足 ops&~sectionKey.OP_ACCEPT!=0,即对于 ServerSocketChannel 注册到 Selector 中时的事件只能包括 SelectionKey.OP_ACCEPT。
- 3、对通道进行了阻塞模式的检查,如果不是阻塞模式,则抛异常,否则进行 4.
- 4、得到当前通道在指定 Selector 上的 SelectionKey,假设结果用 k 表示。下面对 k 是否为 null 有不同的处理。如果 k 不为 null,则说明此通道 channel 已经在 Selector 上注册过了,则直接将指定的 ops 添加进 SelectionKey 中即可。如果 k 为 null,则说明此通道还没有在 Selector 上注册,则需要先进行注册,然后为其对应的 SelectionKey 设置给定值 ops。
- 与 Selector 一起使用时,Channel 必须处于非阻塞模式下。这意味着不能将 FileChannel 与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式。而套接字通道都可以。
public final SelectionKey register(Selector sel, int ops,
Object att)
throws ClosedChannelException
{
synchronized (regLock) {
if (!isOpen())
throw new ClosedChannelException();
if ((ops & ~validOps()) != 0)
throw new IllegalArgumentException();
if (blocking)
throw new IllegalBlockingModeException();
2
3
4
5
6
7
8
9
10
11
- // 得到当前通道在指定 Selector 上的 SelectionKey(复合事件)
- SelectionKey k = findKey(sel);
- 如果 k 不为 null,则说明此通道已经在 Selector 上注册过了,则直接将指定的 ops 添加进 SelectionKey 中即可。
- 如果 k 为 null,则说明此通道还没有在 Selector 上注册,则需要先进行注册,然后添加 SelectionKey。 if (k != null) { k.interestOps(ops); k.attach(att); } if (k == null) { // New registration synchronized (keyLock) { if (!isOpen()) throw new ClosedChannelException(); k = ((AbstractSelector)sel).register(this, ops, att); addKey(k); } } return k; }
private SelectionKey findKey(Selector sel) {
synchronized (keyLock) {
if (keys == null)
return null;
for (int i = 0; i < keys.length; i++)
if ((keys[i] != null) && (keys[i].selector() == sel))
return keys[i];
return null;
}
}
2
3
4
5
6
7
8
9
10
# Selector(如何实现 Channel 多路复用)
- SocketChannel、ServerSocketChannel 和 Selector 的实例初始化都通过 SelectorProvider 类实现,其中 Selector 是整个 NIO Socket 的核心实现。
- SelectorProvider 在 windows 和 linux 下有不同的实现,provider 方法会返回对应的实现。
# 成员变量
1.- final class WindowsSelectorImpl extends SelectorImpl 2.- { 3.
private final int INIT_CAP = 8;//选择key集合,key包装集合初始化容量
private static final int MAX_SELECTABLE_FDS = 1024;//最大选择key数量
private SelectionKeyImpl channelArray[];//选择器关联通道集合
private PollArrayWrapper pollWrapper;//存放所有文件描述对象(选择key,唤醒管道的source与sink通道)的集合
private int totalChannels;//注册到选择的通道数量
private int threadsCount;//选择线程数
private final List threads = new ArrayList();//选择操作线程集合
private final Pipe wakeupPipe = Pipe.open();//唤醒等待选择操作的管道
private final int wakeupSourceFd;//唤醒管道源通道文件描述
private final int wakeupSinkFd;//唤醒管道sink通道文件描述
private Object closeLock;//选择器关闭同步锁
private final FdMap fdMap = new FdMap();//存放选择key文件描述与选择key映射关系的Map
private final SubSelector subSelector = new SubSelector();//子选择器
private long timeout;//超时时间,具体什么意思,现在还没明白,在后面在看
private final Object interruptLock = new Object();//中断同步锁,在唤醒选择操作线程时,用于同步
private volatile boolean interruptTriggered;//是否唤醒等待选择操的线程
private final StartLock startLock = new StartLock();//选择操作开始锁
private final FinishLock finishLock = new FinishLock();//选择操作结束锁
private long updateCount;//更新数量,具体什么意思,现在还没明白,在后面在看
22.- static final boolean $assertionsDisabled = !sun/nio/ch/WindowsSelectorImpl.desiredAssertionStatus(); 23.- static 24.- { 25.- //加载 nio,net 资源库 26.- Util.load(); 27.- } 28.- }
# open
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
2
3
- WindowsSelectorProvider#openSelector
public AbstractSelector openSelector() throws IOException {
return new WindowsSelectorImpl(this);
}
2
3
初始化一个 wakeupPipe
- WindowsSelectorImpl(SelectorProvider var1) throws IOException { super(var1); this.wakeupSourceFd = ((SelChImpl)this.wakeupPipe.source()).getFDVal(); SinkChannelImpl var2 = (SinkChannelImpl)this.wakeupPipe.sink(); var2.sc.socket().setTcpNoDelay(true); this.wakeupSinkFd = var2.getFDVal(); this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0); }
SelectorImpl#register
第一个参数是 ServerSocketChannel,第二个参数是复合事件,第三个是附件。
1、以当前 channel 和 selector 为参数,初始化 SelectionKeyImpl 对象 selectionKeyImpl ,并添加附件 attachment。
2、如果当前 channel 的数量 totalChannels 等于 SelectionKeyImpl 数组大小,对 SelectionKeyImpl 数组和 pollWrapper 进行扩容操作。
3、如果 totalChannels % MAXSELECTABLEFDS == 0,则多开一个线程处理 selector。
4、pollWrapper.addEntry 将把 selectionKeyImpl 中的 socket 句柄添加到对应的 pollfd。
5、k.interestOps(ops)方法最终也会把 event 添加到对应的 pollfd。
所以,不管 serverSocketChannel,还是 socketChannel,在 selector 注册的事件,最终都保存在 pollArray 中。
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment) {
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
k.attach(attachment);
synchronized (publicKeys) {
implRegister(k);
}
k.interestOps(ops);
return k;
}
2
3
4
5
6
7
8
9
10
11
12
13
- WindowsSelectorImpl#implRegister
- protected void implRegister(SelectionKeyImpl ski) { synchronized (closeLock) { if (pollWrapper == null) throw new ClosedSelectorException(); growIfNeeded(); channelArray[totalChannels] = ski; ski.setIndex(totalChannels); fdMap.put(ski); keys.add(ski); pollWrapper.addEntry(totalChannels, ski); totalChannels++; } }
# select(返回有事件发生的 SelectionKey 数量)
- var1 是 timeout 时间,无参数的版本对应的 timeout 为 0.
- select(long timeout)和 select()一样,除了最长会阻塞 timeout 毫秒(参数)。
- 这个方法并不能提供精确时间的保证,和当执行 wait(long timeout)方法时并不能保证会延时 timeout 道理一样。
- 这里的 timeout 说明如下:
- 如果 timeout 为正,则 select(long timeout)在等待有通道被选择时至多会阻塞 timeout 毫秒
- 如果 timeout 为零,则永远阻塞直到有至少一个通道准备就绪。
- timeout 不能为负数。
public int select(long timeout)
throws IOException
{
if (timeout < 0)
throw new IllegalArgumentException("Negative timeout");
return lockAndDoSelect((timeout == 0) ? -1 : timeout);
}
2
3
4
5
6
7
- selectNow(非阻塞版本)
public int selectNow() throws IOException {
return this.lockAndDoSelect(0L);
}
2
3
private int lockAndDoSelect(long timeout) throws IOException {
synchronized (this) {
if (!isOpen())
throw new ClosedSelectorException();
synchronized (publicKeys) {
synchronized (publicSelectedKeys) {
return doSelect(timeout);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
- WindowsSelectorImpl#doSelect
- protected int doSelect(long timeout) throws IOException { if (channelArray == null) throw new ClosedSelectorException(); this.timeout = timeout; // set selector timeout processDeregisterQueue(); if (interruptTriggered) { resetWakeupSocket(); return 0; } // Calculate number of helper threads needed for poll. If necessary // threads are created here and start waiting on startLock adjustThreadsCount(); finishLock.reset(); // reset finishLock // Wakeup helper threads, waiting on startLock, so they start polling. // Redundant threads will exit here after wakeup. startLock.startThreads(); // do polling in the main thread. Main thread is responsible for // first MAX_SELECTABLE_FDS entries in pollArray. try { begin(); try { subSelector.poll(); } catch (IOException e) { finishLock.setException(e); // Save this exception } // Main thread is out of poll(). Wakeup others and wait for them if (threads.size() > 0) finishLock.waitForHelperThreads(); } finally { end(); } // Done with poll(). Set wakeupSocket to nonsignaled for the next run. finishLock.checkForException(); processDeregisterQueue(); int updated = updateSelectedKeys(); // Done with poll(). Set wakeupSocket to nonsignaled for the next run. resetWakeupSocket(); return updated; }
- 其中 subSelector.poll() 是 select 的核心,由 native 函数 poll0 实现,readFds、writeFds 和 exceptFds 数组用来保存底层 select 的结果,数组的第一个位置都是存放发生事件的 socket 的总数,其余位置存放发生事件的 socket 句柄 fd。
private int poll() throws IOException {
return this.poll0(WindowsSelectorImpl.this.pollWrapper.pollArrayAddress, Math.min(WindowsSelectorImpl.this.totalChannels, 1024), this.readFds, this.writeFds, this.exceptFds, WindowsSelectorImpl.this.timeout);
}
2
3
private native int poll0(long pollAddress, int numfds,
int[] readFds, int[] writeFds, int[] exceptFds, long timeout);
2
- 在 src/windows/native/sun/nio/ch/WindowsSelectorImpl.c 中找到了该方法的实现
- #define FD_SETSIZE 1024
Java_sun_nio_ch_WindowsSelectorImpl_00024SubSelector_poll0(JNIEnv *env, jobject this,
jlong pollAddress, jint numfds,
jintArray returnReadFds, jintArray returnWriteFds,
jintArray returnExceptFds, jlong timeout)
{
DWORD result = 0;
pollfd *fds = (pollfd *) pollAddress;
int i;
FD_SET readfds, writefds, exceptfds;
struct timeval timevalue, *tv;
static struct timeval zerotime = {0, 0};
int read_count = 0, write_count = 0, except_count = 0;
#ifdef _WIN64
int resultbuf[FD_SETSIZE + 1];
#endif
if (timeout == 0) {
tv = &zerotime;
} else if (timeout < 0) {
tv = NULL;
} else {
tv = &timevalue;
tv->tv_sec = (long)(timeout / 1000);
tv->tv_usec = (long)((timeout % 1000) * 1000);
}
/* Set FD_SET structures required for select */
for (i = 0; i < numfds; i++) {
if (fds[i].events & POLLIN) {
readfds.fd_array[read_count] = fds[i].fd;
read_count++;
}
if (fds[i].events & (POLLOUT | POLLCONN))
{
writefds.fd_array[write_count] = fds[i].fd;
write_count++;
}
exceptfds.fd_array[except_count] = fds[i].fd;
except_count++;
}
readfds.fd_count = read_count;
writefds.fd_count = write_count;
exceptfds.fd_count = except_count;
/* Call select */
if ((result = select(0 , &readfds, &writefds, &exceptfds, tv))
== SOCKET_ERROR) {
/* Bad error - this should not happen frequently */
/* Iterate over sockets and call select() on each separately */
FD_SET errreadfds, errwritefds, errexceptfds;
readfds.fd_count = 0;
writefds.fd_count = 0;
exceptfds.fd_count = 0;
for (i = 0; i < numfds; i++) {
/* prepare select structures for the i-th socket */
errreadfds.fd_count = 0;
errwritefds.fd_count = 0;
if (fds[i].events & POLLIN) {
errreadfds.fd_array[0] = fds[i].fd;
errreadfds.fd_count = 1;
}
if (fds[i].events & (POLLOUT | POLLCONN))
{
errwritefds.fd_array[0] = fds[i].fd;
errwritefds.fd_count = 1;
}
errexceptfds.fd_array[0] = fds[i].fd;
errexceptfds.fd_count = 1;
/* call select on the i-th socket */
if (select(0, &errreadfds, &errwritefds, &errexceptfds, &zerotime)
== SOCKET_ERROR) {
/* This socket causes an error. Add it to exceptfds set */
exceptfds.fd_array[exceptfds.fd_count] = fds[i].fd;
exceptfds.fd_count++;
} else {
/* This socket does not cause an error. Process result */
if (errreadfds.fd_count == 1) {
readfds.fd_array[readfds.fd_count] = fds[i].fd;
readfds.fd_count++;
}
if (errwritefds.fd_count == 1) {
writefds.fd_array[writefds.fd_count] = fds[i].fd;
writefds.fd_count++;
}
if (errexceptfds.fd_count == 1) {
exceptfds.fd_array[exceptfds.fd_count] = fds[i].fd;
exceptfds.fd_count++;
}
}
}
}
/* Return selected sockets. */
/* Each Java array consists of sockets count followed by sockets list */
#ifdef _WIN64
resultbuf[0] = readfds.fd_count;
for (i = 0; i < (int)readfds.fd_count; i++) {
resultbuf[i + 1] = (int)readfds.fd_array[i];
}
(*env)->SetIntArrayRegion(env, returnReadFds, 0,
readfds.fd_count + 1, resultbuf);
resultbuf[0] = writefds.fd_count;
for (i = 0; i < (int)writefds.fd_count; i++) {
resultbuf[i + 1] = (int)writefds.fd_array[i];
}
(*env)->SetIntArrayRegion(env, returnWriteFds, 0,
writefds.fd_count + 1, resultbuf);
resultbuf[0] = exceptfds.fd_count;
for (i = 0; i < (int)exceptfds.fd_count; i++) {
resultbuf[i + 1] = (int)exceptfds.fd_array[i];
}
(*env)->SetIntArrayRegion(env, returnExceptFds, 0,
exceptfds.fd_count + 1, resultbuf);
#else
(*env)->SetIntArrayRegion(env, returnReadFds, 0,
readfds.fd_count + 1, (jint *)&readfds);
(*env)->SetIntArrayRegion(env, returnWriteFds, 0,
writefds.fd_count + 1, (jint *)&writefds);
(*env)->SetIntArrayRegion(env, returnExceptFds, 0,
exceptfds.fd_count + 1, (jint *)&exceptfds);
#endif
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
- 执行 selector.select() ,poll0 函数把指向 socket 句柄和事件的内存地址传给底层函数。
- 1、如果之前没有发生事件,程序就阻塞在 select 处,当然不会一直阻塞,因为 epoll 在 timeout 时间内如果没有事件,也会返回;
- 2、一旦有对应的事件发生,poll0 方法就会返回;
- 3、processDeregisterQueue 方法会清理那些已经 cancelled 的 SelectionKey;
- 4、updateSelectedKeys 方法统计有事件发生的 SelectionKey 数量,并把符合条件发生事件的 SelectionKey 添加到 selectedKeys 哈希表中,提供给后续使用。
- 如何判断是否有事件发生?(native)
- poll0()会监听 pollWrapper 中的 FD 有没有数据进出,这会造成 IO 阻塞,直到有数据读写事件发生。
- 比如,由于 pollWrapper 中保存的也有 ServerSocketChannel 的 FD,所以只要 ClientSocket 发一份数据到 ServerSocket,那么 poll0()就会返回;
- 又由于 pollWrapper 中保存的也有 pipe 的 write 端的 FD,所以只要 pipe 的 write 端向 FD 发一份数据,也会造成 poll0()返回;
- 如果这两种情况都没有发生,那么 poll0()就一直阻塞,也就是 selector.select()会一直阻塞;如果有任何一种情况发生,那么 selector.select()就会返回。
- 在早期的 JDK1.4 和 1.5 update10 版本之前,Selector 基于 select/poll 模型实现,是基于 IO 复用技术的非阻塞 IO,不是异步 IO。在 JDK1.5 update10 和 linux core2.6 以上版本,sun 优化了 Selctor 的实现,底层使用 epoll 替换了 select/poll。
- epoll 是 Linux 下的一种 IO 多路复用技术,可以非常高效的处理数以百万计的 socket 句柄。
- 在 Windows 下是 IOCP
# WindowsSelectorImpl.wakeup()
public Selector wakeup() {
synchronized (interruptLock) {
if (!interruptTriggered) {
setWakeupSocket();
interruptTriggered = true;
}
}
return this;
}
2
3
4
5
6
7
8
9
private void setWakeupSocket() {
setWakeupSocket0(wakeupSinkFd);
}
2
3
private native void setWakeupSocket0(int wakeupSinkFd);
Java_sun_nio_ch_WindowsSelectorImpl_setWakeupSocket0(JNIEnv *env, jclass this,
jint scoutFd)
{
/* Write one byte into the pipe */
const char byte = 1;
send(scoutFd, &byte, 1, 0);
}
2
3
4
5
6
7
- 这里完成了向最开始建立的 pipe 的 sink 端写入了一个字节,source 文件描述符就会处于就绪状态,poll 方法会返回,从而导致 select 方法返回。(原来自己建立一个 socket 链着自己另外一个 socket 就是为了这个目的)
# Java AIO 源码
# AsynchronousFileChannle(AIO,基于 CompletionHandler 回调)
- 在 Java 7 中,AsynchronousFileChannel 被添加到 Java NIO。AsynchronousFileChannel 使读取数据,并异步地将数据写入文件成为可能。
# open
- Path path = Paths.get("data/test.xml");
- AsynchronousFileChannel fileChannel =
- AsynchronousFileChannel.open(path, StandardOpenOption.READ);
public static AsynchronousFileChannel open(Path file,
Set<? extends OpenOption> options,
ExecutorService executor,
FileAttribute<?>... attrs)
throws IOException
{
FileSystemProvider provider = file.getFileSystem().provider();
return provider.newAsynchronousFileChannel(file, options, executor, attrs);
}
2
3
4
5
6
7
8
9
WindowsChannelFactory#newAsynchronousFileChannel
static AsynchronousFileChannel newAsynchronousFileChannel(String pathForWindows, String pathToCheck, Set<? extends OpenOption> options, long pSecurityDescriptor, ThreadPool pool) throws IOException { Flags flags = Flags.toFlags(options);
// Overlapped I/O required flags.overlapped = true;
// default is reading if (!flags.read && !flags.write) { flags.read = true; }
// validation if (flags.append) throw new UnsupportedOperationException("APPEND not allowed");
// open file for overlapped I/O FileDescriptor fdObj; try { fdObj = open(pathForWindows, pathToCheck, flags, pSecurityDescriptor); } catch (WindowsException x) { x.rethrowAsIOException(pathForWindows); return null; }
// create the AsynchronousFileChannel try { return WindowsAsynchronousFileChannelImpl.open(fdObj, flags.read, flags.write, pool); } catch (IOException x) { // IOException is thrown if the file handle cannot be associated // with the completion port. All we can do is close the file. long handle = fdAccess.getHandle(fdObj); CloseHandle(handle); throw x; }
WindowsAsynchronousFileChannelImpl#open
public static AsynchronousFileChannel open(FileDescriptor fdo,
boolean reading,
boolean writing,
ThreadPool pool)
throws IOException
{
Iocp iocp;
boolean isDefaultIocp;
if (pool == null) {
iocp = DefaultIocpHolder.defaultIocp;
isDefaultIocp = true;
} else {
iocp = new Iocp(null, pool).start();
isDefaultIocp = false;
}
try {
return new
WindowsAsynchronousFileChannelImpl(fdo, reading, writing, iocp, isDefaultIocp);
} catch (IOException x) {
// error binding to port so need to close it (if created for this channel)
if (!isDefaultIocp)
iocp.implClose();
throw x;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# read
# write
# Netty NIO
- 基于这个语境,Netty 目前的版本是没有把 IO 操作交过操作系统处理的,所以是属于同步的。如果别人说 Netty 是异步非阻塞,如果要深究,那真要看看 Netty 新的版本是否把 IO 操作交过操作系统处理,或者看看有否使用 JDK1.7 中的 AIO API,否则他们说的异步其实是指客户端程序调用 Netty 的 IO 操作 API“不停顿等待”。
- 很多人所讲的异步其实指的是编程模型上的异步(即回调),而非应用程序的异步。
# NIO 与 Epoll
Linux2.6 之后支持 epoll
windows 支持 select 而不支持 epoll
不同系统下 nio 的实现是不一样的,包括 Sunos linux 和 windows
select 的复杂度为 O(N)
select 有最大 fd 限制,默认为 1024
修改 sys/select.h 可以改变 select 的 fd 数量限制
- epoll 的事件模型,无 fd 数量限制,复杂度 O(1),不需要遍历 fd
# 1.17 动态代理
- 静态代理:代理类是在编译时就实现好的。也就是说 Java 编译完成后代理类是一个实际的 class 文件。
- 动态代理:代理类是在运行时生成的。也就是说 Java 编译完之后并没有实际的 class 文件,而是在运行时动态生成的类字节码,并加载到 JVM 中。
- JDK 动态代理是由 Java 内部的反射机制+动态生成字节码来实现的,cglib 动态代理底层则是借助 asm 来实现的。总的来说,反射机制在生成类的过程中比较高效,而 asm 在生成类之后的相关执行过程中比较高效(可以通过将 asm 生成的类进行缓存,这样解决 asm 生成类过程低效问题)。还有一点必须注意:JDK 动态代理的应用前提,必须是目标类基于统一的接口。如果没有上述前提,JDK 动态代理不能应用。由此可以看出,JDK 动态代理有一定的局限性,cglib 这种第三方类库实现的动态代理应用更加广泛,且在效率上更有优势。
- 前者必须基于接口,后者不需要接口,是基于继承的,但是不能代理 final 类和 final 方法;
- JDK 采用反射机制调用委托类的方法,CGLIB 采用类似索引的方式直接调用委托类方法;
- 前者效率略低于后者效率,CGLIB 效率略高(不是一定的)
# JDK 动态代理 使用
- Proxy 类(代理类)的设计用到代理模式的设计思想,Proxy 类对象实现了代理目标的所有接口,并代替目标对象进行实际的操作。代理的目的是在目标对象方法的基础上作增强,这种增强的本质通常就是对目标对象的方法进行拦截。所以,Proxy 应该包括一个方法拦截器,来指示当拦截到方法调用时作何种处理。InvocationHandler 就是拦截器的接口。
- Proxy (代理) 提供用于创建动态代理类和实例的静态方法,它还是由这些方法创建的所有动态代理类的超类。
- 动态代理类(代理类)是一个实现在创建类时在运行时指定的接口列表的类 ,代理接口是代理类实现的一个接口。代理实例 是代理类的一个实例。
- 每个代理实例都有一个关联的调用处理程序对象,它可以实现接口 InvocationHandler。(拦截器)
- 在 Java 中怎样实现动态代理呢?
- 第一步,我们要有一个接口,还要有一个接口的实现类,而这个实现类呢就是我们要代理的对象, 所谓代理呢也就是在调用实现类的方法时,可以在方法执行前后做额外的工作。
- 第二步,我们要自己写一个在代理类的方法要执行时,能够做额外工作的类(拦截器),而这个类必须继承 InvocationHandler 接口, 为什么要继承它呢?因为代理类的实例在调用实现类的方法的时候,不会调用真正的实现类的这个方法, 而是转而调用这个类的 invoke 方法(继承时必须实现的方法),在这个方法中你可以调用真正的实现类的这个方法。
# JDK 动态代理 原理
- Proxy#newProxyInstance
- 会返回一个实现了指定接口的代理对象,对该对象的所有方法调用都会转发给 InvocationHandler.invoke()方法。
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
/*
* Look up or generate the designated proxy class.
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 生成代理类的class
Class<?> cl = getProxyClass0(loader, intfs);
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
// 获取代理对象的构造方法(也就是$Proxy0(InvocationHandler h))
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- // 生成代理类的实例并把 InvocationHandlerImpl 的实例传给它的构造方法 return cons.newInstance(new Object[]{h}); } catch (IllegalAccessException|InstantiationException e) { throw new InternalError(e.toString(), e); } catch (InvocationTargetException e) { Throwable t = e.getCause(); if (t instanceof RuntimeException) { throw (RuntimeException) t; } else { throw new InternalError(t.toString(), t); } } catch (NoSuchMethodException e) { throw new InternalError(e.toString(), e); } }
# 1)getProxyClass0(生成代理类的 class)
- 最终生成是通过 ProxyGenerator 的 generateProxyClass 方法实现的。
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}
2
3
4
5
6
7
8
9
10
11
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
2
- @param
<K>
type of keys
- @param
- @param
<P>
type of parameters - @param
<V>
type of values */ final class WeakCache<K, P, V> {}
public V get(K key, P parameter) {
Objects.requireNonNull(parameter);
expungeStaleEntries();
Object cacheKey = CacheKey.valueOf(key, refQueue);
// lazily install the 2nd level valuesMap for the particular cacheKey
ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
if (valuesMap == null) {
ConcurrentMap<Object, Supplier<V>> oldValuesMap
= map.putIfAbsent(cacheKey,
valuesMap = new ConcurrentHashMap<>());
if (oldValuesMap != null) {
valuesMap = oldValuesMap;
}
}
// create subKey and retrieve the possible Supplier<V> stored by that
// subKey from valuesMap
Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
Supplier<V> supplier = valuesMap.get(subKey);
Factory factory = null;
while (true) {
if (supplier != null) {
// supplier might be a Factory or a CacheValue<V> instance
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// supplier 是 Factory,这个类定义在 WeakCache 的内部。 V value = supplier.get(); if (value != null) { return value; } } // else no supplier in cache // or a supplier that returned null (could be a cleared CacheValue // or a Factory that wasn't successful in installing the CacheValue)
// lazily construct a Factory if (factory == null) { factory = new Factory(key, parameter, subKey, valuesMap); }
if (supplier == null) { supplier = valuesMap.putIfAbsent(subKey, factory); if (supplier == null) { // successfully installed Factory supplier = factory; } // else retry with winning supplier } else { if (valuesMap.replace(subKey, supplier, factory)) { // successfully replaced // cleared CacheEntry / unsuccessful Factory // with our Factory supplier = factory; } else { // retry with current supplier supplier = valuesMap.get(subKey); } } } }
Factory
private final class Factory implements Supplier<V> {
private final K key;
private final P parameter;
private final Object subKey;
private final ConcurrentMap<Object, Supplier<V>> valuesMap;
Factory(K key, P parameter, Object subKey,
ConcurrentMap<Object, Supplier<V>> valuesMap) {
this.key = key;
this.parameter = parameter;
this.subKey = subKey;
this.valuesMap = valuesMap;
}
@Override
public synchronized V get() { // serialize access
// re-check
Supplier<V> supplier = valuesMap.get(subKey);
if (supplier != this) {
// something changed while we were waiting:
// might be that we were replaced by a CacheValue
// or were removed because of failure ->
// return null to signal WeakCache.get() to retry
// the loop
return null;
}
// else still us (supplier == this)
// create new value
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 创建新的 class V value = null; try { value = Objects.requireNonNull(valueFactory.apply(key, parameter)); } finally { if (value == null) { // remove us on failure valuesMap.remove(subKey, this); } } // the only path to reach here is with non-null value assert value != null;
// wrap value with CacheValue (WeakReference) CacheValue
<V>
cacheValue = new CacheValue<>(value);// try replacing us with CacheValue (this should always succeed) if (valuesMap.replace(subKey, this, cacheValue)) { // put also in reverseMap reverseMap.put(cacheValue, Boolean.TRUE); } else { throw new AssertionError("Should not reach here"); }
// successfully replaced us with new CacheValue -> return the value // wrapped by it return value; } }
private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// prefix for all proxy class names
private static final String proxyClassNamePrefix = "$Proxy";
// next number to use for generation of unique proxy class names
private static final AtomicLong nextUniqueNumber = new AtomicLong();
@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
/*
* Verify that the class loader resolves the name of this
* interface to the same Class object.
*/
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
/*
* Verify that the Class object actually represents an
* interface.
*/
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
/*
* Verify that this interface is not a duplicate.
*/
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}
String proxyPkg = null; // package to define proxy class in
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
/*
* Record the package of a non-public proxy interface so that the
* proxy class will be defined in the same package. Verify that
* all non-public proxy interfaces are in the same package.
*/
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}
if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
/*
* Generate the specified proxy class.
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
- 重点!
/**
* Generate a proxy class given a name and a list of proxy interfaces.
*
* @param name the class name of the proxy class
* @param interfaces proxy interfaces
* @param accessFlags access flags of the proxy class
*/
public static byte[] generateProxyClass(final String name,
Class<?>[] interfaces,
int accessFlags)
{
ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags);
final byte[] classFile = gen.generateClassFile();
if (saveGeneratedFiles) {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
try {
int i = name.lastIndexOf('.');
Path path;
if (i > 0) {
Path dir = Paths.get(name.substring(0, i).replace('.', File.separatorChar));
Files.createDirectories(dir);
path = dir.resolve(name.substring(i+1, name.length()) + ".class");
} else {
path = Paths.get(name + ".class");
}
Files.write(path, classFile);
return null;
} catch (IOException e) {
throw new InternalError(
"I/O exception saving generated file: " + e);
}
}
});
}
return classFile;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- ProxyGenerator#generateClassFIle
/**
* Generate a class file for the proxy class. This method drives the
* class file generation process.
*/
private byte[] generateClassFile() {
/* ============================================================
* Step 1: Assemble ProxyMethod objects for all methods to
* generate proxy dispatching code for.
*/
/*
* Record that proxy methods are needed for the hashCode, equals,
* and toString methods of java.lang.Object. This is done before
* the methods from the proxy interfaces so that the methods from
* java.lang.Object take precedence over duplicate methods in the
* proxy interfaces.
*/
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);
/*
* Now record all of the methods from the proxy interfaces, giving
* earlier interfaces precedence over later ones with duplicate
* methods.
*/
for (Class<?> intf : interfaces) {
for (Method m : intf.getMethods()) {
addProxyMethod(m, intf);
}
}
/*
* For each set of proxy methods with the same signature,
* verify that the methods' return types are compatible.
*/
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
checkReturnTypes(sigmethods);
}
/* ============================================================
* Step 2: Assemble FieldInfo and MethodInfo structs for all of
* fields and methods in the class we are generating.
*/
try {
methods.add(generateConstructor());
for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
for (ProxyMethod pm : sigmethods) {
// add static field for method's Method object
fields.add(new FieldInfo(pm.methodFieldName,
"Ljava/lang/reflect/Method;",
ACC_PRIVATE | ACC_STATIC));
// generate code for proxy method and add it
methods.add(pm.generateMethod());
}
}
methods.add(generateStaticInitializer());
} catch (IOException e) {
throw new InternalError("unexpected I/O Exception", e);
}
if (methods.size() > 65535) {
throw new IllegalArgumentException("method limit exceeded");
}
if (fields.size() > 65535) {
throw new IllegalArgumentException("field limit exceeded");
}
/* ============================================================
* Step 3: Write the final class file.
*/
/*
* Make sure that constant pool indexes are reserved for the
* following items before starting to write the final class file.
*/
cp.getClass(dotToSlash(className));
cp.getClass(superclassName);
for (Class<?> intf: interfaces) {
cp.getClass(dotToSlash(intf.getName()));
}
/*
* Disallow new constant pool additions beyond this point, since
* we are about to write the final constant pool table.
*/
cp.setReadOnly();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
try {
/*
* Write all the items of the "ClassFile" structure.
* See JVMS section 4.1.
*/
// u4 magic;
dout.writeInt(0xCAFEBABE);
// u2 minor_version;
dout.writeShort(CLASSFILE_MINOR_VERSION);
// u2 major_version;
dout.writeShort(CLASSFILE_MAJOR_VERSION);
cp.write(dout); // (write constant pool)
// u2 access_flags;
dout.writeShort(accessFlags);
// u2 this_class;
dout.writeShort(cp.getClass(dotToSlash(className)));
// u2 super_class;
dout.writeShort(cp.getClass(superclassName));
// u2 interfaces_count;
dout.writeShort(interfaces.length);
// u2 interfaces[interfaces_count];
for (Class<?> intf : interfaces) {
dout.writeShort(cp.getClass(
dotToSlash(intf.getName())));
}
// u2 fields_count;
dout.writeShort(fields.size());
// field_info fields[fields_count];
for (FieldInfo f : fields) {
f.write(dout);
}
// u2 methods_count;
dout.writeShort(methods.size());
// method_info methods[methods_count];
for (MethodInfo m : methods) {
m.write(dout);
}
// u2 attributes_count;
dout.writeShort(0); // (no ClassFile attributes for proxy classes)
} catch (IOException e) {
throw new InternalError("unexpected I/O Exception", e);
}
return bout.toByteArray();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# 2)getConstructor(获取代理类的构造方法)
# 3)newInstance(初始化代理对象)
# CGLIB 动态代理 使用
- CGLIB(Code Generation Library)是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。
- CGLIB 的核心类:
- net.sf.cglib.proxy.Enhancer – 主要的增强类
- net.sf.cglib.proxy.MethodInterceptor – 主要的方法拦截类,它是 Callback 接口的子接口,需要用户实现
- net.sf.cglib.proxy.MethodProxy – JDK 的 java.lang.reflect.Method 类的代理类,可以方便的实现对源对象方法的调用,如使用:
- Object o = methodProxy.invokeSuper(proxy, args);//虽然第一个参数是被代理对象,也不会出现死循环的问题。
- net.sf.cglib.proxy.MethodInterceptor 接口是最通用的回调(callback)类型,它经常被基于代理的 AOP 用来实现拦截(intercept)方法的调用。这个接口只定义了一个方法
public Object intercept(Object object, java.lang.reflect.Method method,
- Object[] args, MethodProxy proxy) throws Throwable;
- 第一个参数是代理对像,第二和第三个参数分别是拦截的方法和方法的参数。原来的方法可能通过使用 java.lang.reflect.Method 对象的一般反射调用,或者使用 net.sf.cglib.proxy.MethodProxy 对象调用。net.sf.cglib.proxy.MethodProxy 通常被首选使用,因为它更快。
public class CglibProxy implements MethodInterceptor {
- @Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
- System.out.println("++++++before " + methodProxy.getSuperName() + "++++++");
- System.out.println(method.getName());
- Object o1 = methodProxy.invokeSuper(o, args);
- System.out.println("++++++before " + methodProxy.getSuperName() + "++++++");
- return o1;
- }
- }
public class Main {
public static void main(String[] args) {
- CglibProxy cglibProxy = new CglibProxy();
- Enhancer enhancer = new Enhancer();
- enhancer.setSuperclass(UserServiceImpl.class);
- enhancer.setCallback(cglibProxy);
- UserService o = (UserService)enhancer.create();
- o.getName(1);
- o.getAge(1);
- }
- }
- 我们通过 CGLIB 的 Enhancer 来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用 create()方法得到代理对象,对这个对象所有非 final 方法的调用都会转发给 MethodInterceptor.intercept()方法,在 intercept()方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;通过调用 MethodProxy.invokeSuper()方法,我们将调用转发给原始对象,具体到本例,就是 HelloConcrete 的具体方法。CGLIG 中 MethodInterceptor 的作用跟 JDK 代理中的 InvocationHandler 很类似,都是方法调用的中转站。
- 注意:对于从 Object 中继承的方法,CGLIB 代理也会进行代理,如 hashCode()、equals()、toString()等,但是 getClass()、wait()等方法不会,因为它是 final 方法,CGLIB 无法代理。
- 既然是继承就不得不考虑 final 的问题。我们知道 final 类型不能有子类,所以 CGLIB 不能代理 final 类型。
- final 方法是不能重载的,所以也不能通过 CGLIB 代理,遇到这种情况不会抛异常,而是会跳过 final 方法只代理其他方法。
# CGLIB 动态代理 原理
- 1、生成代理类 Class 的二进制字节码(基于 ASM);
- 2、通过 Class.forName 加载二进制字节码,生成 Class 对象;
- 3、通过反射机制获取实例构造,并初始化代理类对象。
- 调用委托类的方法是使用 invokeSuper
public Object invokeSuper(Object obj, Object[] args) throws Throwable {
try {
init();
FastClassInfo fci = fastClassInfo;
return fci.f2.invoke(fci.i2, obj, args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
2
3
4
5
6
7
8
9
private static class FastClassInfo
{
FastClass f1;
FastClass f2;
int i1;
int i2;
}
2
3
4
5
6
7
- f1 指向委托类对象,f2 指向代理类对象
- i1 是被代理的方法在对象中的索引位置
- i2 是 CGLIB$被代理的方法$0 在对象中的索引位置
# FastClass 实现机制
- FastClass 其实就是对 Class 对象进行特殊处理,提出下标概念 index,通过索引保存方法的引用信息,将原先的反射调用,转化为方法的直接调用,从而体现所谓的 fast。
- 在 FastTest 中有两个方法, getIndex 中对 Test 类的每个方法根据 hash 建立索引, invoke 根据指定的索引,直接调用目标方法,避免了反射调用。
# 1.18 反射
- Java 的动态性体现在:反射机制、动态执行脚本语言、动态操作字节码
- 反射:在运行时加载、探知、使用编译时未知的类。
- Class.forName 使用的类加载器是调用者的类加载器
# Class
- 表示 Java 中的类型(class、interface、enum、annotation、primitive type、void)本身。
- 一个类被加载之后,JVM 会创建一个对应该类的 Class 对象,类的整个结构信息会放在相应的 Class 对象中。
- 这个 Class 对象就像一个镜子一样,从中可以看到类的所有信息。
- 反射的核心就是 Class
- 如果多次执行 forName 等加载类的方法,类只会被加载一次;一个类只会形成一个 Class 对象,无论执行多少次加载类的方法,获得的 Class 都是一样的。
# 用途
# 性能
反射带来灵活性的同时,也有降低程序执行效率的弊端
setAccessible 方法不仅可以标记某些私有的属性方法为可访问的属性方法,并且可以提高程序的执行效率
实际上是启用和禁用访问安全检查的开关。如果做检查就会降低效率;关闭检查就可以提高效率。
反射调用方法比直接调用要慢大约 30 倍,如果跳过安全检查的话比直接调用要慢大约 7 倍
开启和不开启安全检查对于反射而言可能会差 4 倍的执行效率。
为什么慢?
- 1)验证等防御代码过于繁琐,这一步本来在 link 阶段,现在却在计算时进行验证
- 2)产生很多临时对象,造成 GC 与计算时间消耗
- 3)由于缺少上下文,丢失了很多运行时的优化,比如 JIT(它可以看作 JVM 的重要评测标准之一)
当然,现代 JVM 也不是非常慢了,它能够对反射代码进行缓存以及通过方法计数器同样实现 JIT 优化,所以反射不一定慢。
# 实现
- 反射在 Java 中可以直接调用,不过最终调用的仍是 native 方法,以下为主流反射操作的实现。
# Class.forName 的实现
- Class.forName 可以通过包名寻找 Class 对象,比如 Class.forName("java.lang.String")。
- 在 JDK 的源码实现中,可以发现最终调用的是 native 方法 forName0(),它在 JVM 中调用的实际是 FindClassFromCaller(),原理与 ClassLoader 的流程一样。
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
2
3
4
5
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller)
throws ClassNotFoundException;
2
3
4
Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname,
jboolean initialize, jobject loader, jclass caller)
{
char *clname;
jclass cls = 0;
char buf[128];
jsize len;
jsize unicode_len;
if (classname == NULL) {
JNU_ThrowNullPointerException(env, 0);
return 0;
}
len = (*env)->GetStringUTFLength(env, classname);
unicode_len = (*env)->GetStringLength(env, classname);
if (len >= (jsize)sizeof(buf)) {
clname = malloc(len + 1);
if (clname == NULL) {
JNU_ThrowOutOfMemoryError(env, NULL);
return NULL;
}
} else {
clname = buf;
}
(*env)->GetStringUTFRegion(env, classname, 0, unicode_len, clname);
if (VerifyFixClassname(clname) == JNI_TRUE) {
/* slashes present in clname, use name b4 translation for exception */
(*env)->GetStringUTFRegion(env, classname, 0, unicode_len, clname);
JNU_ThrowClassNotFoundException(env, clname);
goto done;
}
if (!VerifyClassname(clname, JNI_TRUE)) { /* expects slashed name */
JNU_ThrowClassNotFoundException(env, clname);
goto done;
}
cls = JVM_FindClassFromCaller(env, clname, initialize, loader, caller);
done:
if (clname != buf) {
free(clname);
}
return cls;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
JVM_ENTRY(jclass, JVM_FindClassFromClass(JNIEnv env, const char name, jboolean init, jclass from)) JVMWrapper2("JVM_FindClassFromClass %s", name); if (name == NULL || (int)strlen(name) > Symbol::max_length()) { // It's impossible to create this class; the name cannot fit // into the constant pool. THROW_MSG_0(vmSymbols::java_lang_NoClassDefFoundError(), name); } TempNewSymbol h_name = SymbolTable::new_symbol(name, CHECK_NULL); oop from_class_oop = JNIHandles::resolve(from); Klass from_class = (from_class_oop == NULL) ? (Klass)NULL : java_lang_Class::as_Klass(from_class_oop); oop class_loader = NULL; oop protection_domain = NULL; if (from_class != NULL) { class_loader = from_class->class_loader(); protection_domain = from_class->protection_domain(); } Handle h_loader(THREAD, class_loader); Handle h_prot (THREAD, protection_domain); jclass result = find_class_from_class_loader(env, h_name, init, h_loader, h_prot, true, thread);
if (TraceClassResolution && result != NULL) { // this function is generally only used for class loading during verification. ResourceMark rm; oop from_mirror = JNIHandles::resolve_non_null(from); Klass* from_class = java_lang_Class::as_Klass(from_mirror); const char * from_name = from_class->external_name();
oop mirror = JNIHandles::resolve_non_null(result); Klass* to_class = java_lang_Class::as_Klass(mirror); const char * to = to_class->external_name(); tty->print("RESOLVE %s %s (verification)\n", from_name, to); }
return result; JVM_END
# getDeclaredFields 的实现
在 JDK 源码中,可以知道 class.getDeclaredFields()方法实际调用的是 native 方法 getDeclaredFields0(),它在 JVM 主要实现步骤如下:
- 1)根据 Class 结构体信息,获取 field_count 与 fields[]字段,这个字段早已在 load 过程中被放入了
- 2)根据 field_count 的大小分配内存、创建数组
- 3)将数组进行 forEach 循环,通过 fields[]中的信息依次创建 Object 对象
- 4)返回数组指针
主要慢在如下方面:
创建、计算、分配数组对象
对字段进行循环赋值
public Field[] getDeclaredFields() throws SecurityException {
checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
return copyFields(privateGetDeclaredFields(false));
}
2
3
4
private Field[] privateGetDeclaredFields(boolean publicOnly) {
checkInitted();
Field[] res;
ReflectionData<T> rd = reflectionData();
if (rd != null) {
res = publicOnly ? rd.declaredPublicFields : rd.declaredFields;
if (res != null) return res;
}
// No cached value available; request value from VM
res = Reflection.filterFields(this, getDeclaredFields0(publicOnly));
if (rd != null) {
if (publicOnly) {
rd.declaredPublicFields = res;
} else {
rd.declaredFields = res;
}
}
return res;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static Field[] copyFields(Field[] arg) {
Field[] out = new Field[arg.length];
ReflectionFactory fact = getReflectionFactory();
for (int i = 0; i < arg.length; i++) {
out[i] = fact.copyField(arg[i]);
}
return out;
}
2
3
4
5
6
7
8
# Method.invoke 的实现
以下为无同步、无异常的情况下调用的步骤
- 1)创建 Frame
- 2)如果对象 flag 为 native,交给 native_handler 进行处理
- 3)在 frame 中执行 java 代码
- 4)弹出 Frame
- 5)返回执行结果的指针
主要慢在如下方面:
需要完全执行 ByteCode 而缺少 JIT 等优化
检查参数非常多,这些本来可以在编译器或者加载时完成
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- NativeMethodAccessorImpl#invoke
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
// We can't inflate methods belonging to vm-anonymous classes because
// that kind of class can't be referred to by name, hence can't be
// found from the generated bytecode.
if (++numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
return invoke0(method, obj, args);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static native Object invoke0(Method m, Object obj, Object[] args);
- Java_sun_reflect_NativeMethodAccessorImpl_invoke0 (JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args) { return JVM_InvokeMethod(env, m, obj, args); }
- JVM_ENTRY(jobject, JVM_InvokeMethod(JNIEnv *env, jobject method, jobject obj, jobjectArray args0)) JVMWrapper("JVM_InvokeMethod"); Handle method_handle; if (thread->stack_available((address) &method_handle) >= JVMInvokeMethodSlack) { method_handle = Handle(THREAD, JNIHandles::resolve(method)); Handle receiver(THREAD, JNIHandles::resolve(obj)); objArrayHandle args(THREAD, objArrayOop(JNIHandles::resolve(args0))); oop result = Reflection::invoke_method(method_handle(), receiver, args, CHECK_NULL); jobject res = JNIHandles::make_local(env, result); if (JvmtiExport::should_post_vm_object_alloc()) { oop ret_type = java_lang_reflect_Method::return_type(method_handle()); assert(ret_type != NULL, "sanity check: ret_type oop must not be NULL!"); if (java_lang_Class::is_primitive(ret_type)) { // Only for primitive type vm allocates memory for java object. // See box() method. JvmtiExport::post_vm_object_alloc(JavaThread::current(), result); } } return res; } else { THROW_0(vmSymbols::java_lang_StackOverflowError()); } JVM_END
# class.newInstance 的实现
- 1)检测权限、预分配空间大小等参数
- 2)创建Object对象,并分配空间
- 3)通过Method.invoke调用构造函数(`<init>`())
- 4)返回Object指针
- 主要慢在如下方面:
- 参数检查不能优化或者遗漏
<init>
()的查表- Method.invoke 本身耗时
public T newInstance()
throws InstantiationException, IllegalAccessException
{
if (System.getSecurityManager() != null) {
checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), false);
}
// NOTE: the following code may not be strictly correct under
// the current Java memory model.
// Constructor lookup
if (cachedConstructor == null) {
if (this == Class.class) {
throw new IllegalAccessException(
"Can not call newInstance() on the Class for java.lang.Class"
);
}
try {
Class<?>[] empty = {};
final Constructor<T> c = getConstructor0(empty, Member.DECLARED);
// Disable accessibility checks on the constructor
// since we have to do the security check here anyway
// (the stack depth is wrong for the Constructor's
// security check to work)
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
c.setAccessible(true);
return null;
}
});
cachedConstructor = c;
} catch (NoSuchMethodException e) {
throw (InstantiationException)
new InstantiationException(getName()).initCause(e);
}
}
Constructor<T> tmpConstructor = cachedConstructor;
// Security check (same as in java.lang.reflect.Constructor)
int modifiers = tmpConstructor.getModifiers();
if (!Reflection.quickCheckMemberAccess(this, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
if (newInstanceCallerCache != caller) {
Reflection.ensureMemberAccess(caller, this, null, modifiers);
newInstanceCallerCache = caller;
}
}
// Run constructor
try {
return tmpConstructor.newInstance((Object[])null);
} catch (InvocationTargetException e) {
Unsafe.getUnsafe().throwException(e.getTargetException());
// Not reached
return null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 1.19 XML
# DOM
- OM 是用与平台和语言无关的方式表示 XML 文档的官方 W3C 标准。DOM 是以层次结构组织的节点或信息片断的集合。这个层次结构允许开发人员在树中寻找特定信息。分析该结构通常需要加载整个文档和构造层次结构,然后才能做任何工作。由于它是基于信息层次的,因而 DOM 被认为是基于树或基于对象的。
- 优点
- ① 允许应用程序对数据和结构做出更改。
- ② 访问是双向的,可以在任何时候在树中上下导航,获取和操作任意部分的数据。
- 缺点
- ① 通常需要加载整个 XML 文档来构造层次结构,消耗资源大。
# SAX
- SAX 处理的优点非常类似于流媒体的优点。分析能够立即开始,而不是等待所有的数据被处理。而且,由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中。这对于大型文档来说是个巨大的优点。事实上,应用程序甚至不必解析整个文档;它可以在某个条件得到满足时停止解析。一般来说,SAX 还比它的替代者 DOM 快许多。
- 优点
- ① 不需要等待所有数据都被处理,分析就能立即开始。
- ② 只在读取数据时检查数据,不需要保存在内存中。
- ③ 可以在某个条件得到满足时停止解析,不必解析整个文档。
- ④ 效率和性能较高,能解析大于系统内存的文档。
- 缺点
- ① 需要应用程序自己负责 TAG 的处理逻辑(例如维护父/子关系等),文档越复杂程序就越复杂。
- ② 单向导航,无法定位文档层次,很难同时访问同一文档的不同部分数据,不支持 XPath。
- JDOM
- DOM4J
# 1.20 Java8
# Lambda 表达式&函数式接口&方法引用&Stream API
Java8 stream 迭代的优势和区别;lambda 表达式?为什么要引入它
- 1)流(高级 Iterator):对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation),隐式迭代等,代码简洁
- 2)方便地实现并行(并行流),比如实现 MapReduce
- 3)Lamdba:简化匿名内部类的实现,代码更加紧凑
- 4)方法引用:方法引用是 lambda 表达式的另一种表达方式
对象::实例方法
类::静态方法
类::实例方法名
# Optional
- Optional 仅仅是一个容易:存放 T 类型的值或者 null。它提供了一些有用的接口来避免显式的 null 检查。
# CompletableFuture
- 1)实现异步API(将任务交给另一线程完成,该线程与调用方异步,通过回调函数或阻塞的方式取得任务结果)
- 2)将批量同步操作转为异步操作(并行流/CompletableFuture)
- 3)多个异步任务合并
# 时间日期 API
- 新的 java.time 包包含了所有关于日期、时间、时区、Instant(跟日期类似但是精确到纳秒)、duration(持续时间)和时钟操作的类。新设计的 API 认真考虑了这些类的不变性(从 java.util.Calendar 吸取的教训),如果某个实例需要修改,则返回一个新的对象。
# 接口中的默认方法与静态方法
- 默认方法使得开发者可以在不破坏二进制兼容性的前提下,往现存接口中添加新的方法,即不强制那些实现了该接口的类也同时实现这个新加的方法。
- 默认方法允许在不打破现有继承体系的基础上改进接口。该特性在官方库中的应用是:给 java.util.Collection 接口添加新方法,如 stream()、parallelStream()、forEach()和 removeIf()等等。
# 1.21 Java9
# 模块化
- 提供了类似于 OSGI 框架的功能,模块之间存在相互的依赖关系,可以导出一个公共的 API,并且隐藏实现的细节,Java 提供该功能的主要的动机在于,减少内存的开销,在 JVM 启动的时候,至少会有 30 ~ 60MB 的内存加载,主要原因是 JVM 需要加载 rt.jar,不管其中的类是否被 classloader 加载,第一步整个 jar 都会被 JVM 加载到内存当中去,模块化可以根据模块的需要加载程序运行需要的 class。
- 在引入了模块系统之后,JDK 被重新组织成 94 个模块。Java 应用可以通过新增的 jlink 工具,创建出只包含所依赖的 JDK 模块的自定义运行时镜像。这样可以极大的减少 Java 运行时环境的大小。使得 JDK 可以在更小的设备中使用。采用模块化系统的应用程序只需要这些应用程序所需的那部分 JDK 模块,而非是整个 JDK 框架了。
# HTTP/2
- Java 9 的版本中引入了一个新的 package:java.net.http,里面提供了对 Http 访问很好的支持,不仅支持 Http1.1 而且还支持 HTTP/2,以及 WebSocket,据说性能特别好。
# JShell
- java9 引入了 jshell 这个交互性工具,让 Java 也可以像脚本语言一样来运行,可以从控制台启动 jshell ,在 jshell 中直接输入表达式并查看其执行结果。当需要测试一个方法的运行效果,或是快速的对表达式进行求值时,jshell 都非常实用。
- 除了表达式之外,还可以创建 Java 类和方法。jshell 也有基本的代码完成功能。
# 不可变集合工厂方法
- Java 9 增加了 List.of()、Set.of()、Map.of()和 Map.ofEntries()等工厂方法来创建不可变集合。
# 私有接口方法
- Java 8 为我们提供了接口的默认方法和静态方法,接口也可以包含行为,而不仅仅是方法定义。
- 默认方法和静态方法可以共享接口中的私有方法,因此避免了代码冗余,这也使代码更加清晰。如果私有方法是静态的,那这个方法就属于这个接口的。并且没有静态的私有方法只能被在接口中的实例调用。
# 多版本兼容 JAR
- 当一个新版本的 Java 出现的时候,你的库用户要花费很长时间才会切换到这个新的版本。这就意味着库要去向后兼容你想要支持的最老的 Java 版本 (许多情况下就是 Java 6 或者 7)。这实际上意味着未来的很长一段时间,你都不能在库中运用 Java 9 所提供的新特性。幸运的是,多版本兼容 JAR 功能能让你创建仅在特定版本的 Java 环境中运行库程序时选择使用的 class 版本。
# 统一 JVM 日志
- Java 9 中 ,JVM 有了统一的日志记录系统,可以使用新的命令行选项-Xlog 来控制 JVM 上 所有组件的日志记录。该日志记录系统可以设置输出的日志消息的标签、级别、修饰符和输出目标等。
# 垃圾收集机制
- Java 9 移除了在 Java 8 中 被废弃的垃圾回收器配置组合,同时把 G1 设为默认的垃圾回收器实现。替代了之前默认使用的 Parallel GC,对于这个改变,evens 的评论是酱紫的:这项变更是很重要的,因为相对于 Parallel 来说,G1 会在应用线程上做更多的事情,而 Parallel 几乎没有在应用线程上做任何事情,它基本上完全依赖 GC 线程完成所有的内存管理。这意味着切换到 G1 将会为应用线程带来额外的工作,从而直接影响到应用的性能
# I/O 流新特性
- java.io.InputStream 中增加了新的方法来读取和复制 InputStream 中包含的数据。 - readAllBytes:读取 InputStream 中的所有剩余字节。 - readNBytes: 从 InputStream 中读取指定数量的字节到数组中。 - transferTo:读取 InputStream 中的全部字节并写入到指定的 OutputStream 中 。