Java教程

在 Java 7 之前,String Pool 被放在运⾏行行时常量量池中,它属于永久代。⽽而在 Java 7,String Pool 被移到
堆中。这是因为永久代的空间有限,在⼤大量量使⽤用字符串串的场景下会导致 OutOfMemoryError 错误。
StackOverflow : What is String interning?
深⼊入解析 String#intern
new String(“abc”)
使⽤用这种⽅方式⼀一共会创建两个字符串串对象(前提是 String Pool 中还没有 “abc” 字符串串对象)。
“abc” 属于字符串串字⾯面量量,因此编译时期会在 String Pool 中创建⼀一个字符串串对象,指向这个 “abc”
字符串串字⾯面量量;
⽽而使⽤用 new 的⽅方式会在堆中创建⼀一个字符串串对象。
创建⼀一个测试类,其 main ⽅方法中使⽤用这种⽅方式来创建字符串串对象。
String s1 = new String(“aaa”);
String s2 = new String(“aaa”);
System.out.println(s1 == s2); // false
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s3 == s4); // true
String s5 = “bbb”;
String s6 = “bbb”;
System.out.println(s5 == s6); // true
public class NewStringTest {
public static void main(String[] args) {
String s = new String(“abc”);
}
}
使⽤用 javap -verbose 进⾏行行反编译,得到以下内容:
在 Constant Pool 中,#19 存储这字符串串字⾯面量量 “abc”,#3 是 String Pool 的字符串串对象,它指向 #19
这个字符串串字⾯面量量。在 main ⽅方法中,0: ⾏行行使⽤用 new #2 在堆中创建⼀一个字符串串对象,并且使⽤用 ldc #3
将 String Pool 中的字符串串对象作为 String 构造函数的参数。
以下是 String 构造函数的源码,可以看到,在将⼀一个字符串串对象作为另⼀一个字符串串对象的构造函数参数
时,并不不会完全复制 value 数组内容,⽽而是都会指向同⼀一个 value 数组。
三、运算
参数传递
Java 的参数是以值传递的形式传⼊入⽅方法中,⽽而不不是引⽤用传递。
// …
Constant pool:
// …
#2 = Class #18 // java/lang/String
#3 = String #19 // abc
// …
#18 = Utf8 java/lang/String
#19 = Utf8 abc
// …
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String abc
6: invokespecial #4 // Method java/lang/String."
":(Ljava/lang/String;)V
9: astore_1
// …
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
以下代码中 Dog dog 的 dog 是⼀一个指针,存储的是对象的地址。在将⼀一个参数传⼊入⼀一个⽅方法时,本质
上是将对象的地址以值的⽅方式传递到形参中。
在⽅方法中改变对象的字段值会改变原对象该字段值,因为引⽤用的是同⼀一个对象。
但是在⽅方法中将指针引⽤用了了其它对象,那么此时⽅方法⾥里里和⽅方法外的两个指针指向了了不不同的对象,在⼀一个
指针改变其所指向对象的内容对另⼀一个指针所指向的对象没有影响。
public class Dog {
String name;
Dog(String name) {
this.name = name;
}
String getName() {
return this.name;
}
void setName(String name) {
this.name = name;
}
String getObjectAddress() {
return super.toString();
}
}
class PassByValueExample {
public static void main(String[] args) {
Dog dog = new Dog(“A”);
func(dog);
System.out.println(dog.getName()); // B
}
private static void func(Dog dog) {
dog.setName(“B”);
}
}
public class PassByValueExample {
StackOverflow: Is Java “pass-by-reference” or “pass-by-value”?
float 与 double
Java 不不能隐式执⾏行行向下转型,因为这会使得精度降低。
1.1 字⾯面量量属于 double 类型,不不能直接将 1.1 直接赋值给 float 变量量,因为这是向下转型。
1.1f 字⾯面量量才是 float 类型。
隐式类型转换
因为字⾯面量量 1 是 int 类型,它⽐比 short 类型精度要⾼高,因此不不能隐式地将 int 类型向下转型为 short 类
型。
但是使⽤用 += 或者 ++ 运算符会执⾏行行隐式类型转换。
public static void main(String[] args) {
Dog dog = new Dog(“A”);
System.out.println(dog.getObjectAddress()); // Dog@4554617c
func(dog);
System.out.println(dog.getObjectAddress()); // Dog@4554617c
System.out.println(dog.getName()); // A
}
private static void func(Dog dog) {
System.out.println(dog.getObjectAddress()); // Dog@4554617c
dog = new Dog(“B”);
System.out.println(dog.getObjectAddress()); // Dog@74a14482
System.out.println(dog.getName()); // B
}
}
// float f = 1.1;
float f = 1.1f;
short s1 = 1;
// s1 = s1 + 1;
上⾯面的语句句相当于将 s1 + 1 的计算结果进⾏行行了了向下转型:
StackOverflow : Why don’t Java’s +=, -=, *=, /= compound assignment operators require casting?
switch
从 Java 7 开始,可以在 switch 条件判断语句句中使⽤用 String 对象。
switch 不不⽀支持 long、float、double,是因为 switch 的设计初衷是对那些只有少数⼏几个值的类型进⾏行行等
值判断,如果值过于复杂,那么还是⽤用 if ⽐比较合适。
StackOverflow : Why can’t your switch statement data type be long, Java?
四、关键字
s1 += 1;
s1++;
s1 = (short) (s1 + 1);
String s = “a”;
switch (s) {
case “a”:
System.out.println(“aaa”);
break;
case “b”:
System.out.println(“bbb”);
break;
}
// long x = 111;
// switch (x) { // Incompatible types. Found: ‘long’, required: ‘char,
byte, short, int, Character, Byte, Short, Integer, String, or an enum’
// case 111:
// System.out.println(111);
// break;
// case 222:
// System.out.println(222);
// break;
// }
final

  1. 数据
    声明数据为常量量,可以是编译时常量量,也可以是在运⾏行行时被初始化后不不能被改变的常量量。
    对于基本类型,final 使数值不不变;
    对于引⽤用类型,final 使引⽤用不不变,也就不不能引⽤用其它对象,但是被引⽤用的对象本身是可以修改的。
  2. ⽅方法
    声明⽅方法不不能被⼦子类重写。
    private ⽅方法隐式地被指定为 final,如果在⼦子类中定义的⽅方法和基类中的⼀一个 private ⽅方法签名相同,此
    时⼦子类的⽅方法不不是重写基类⽅方法,⽽而是在⼦子类中定义了了⼀一个新的⽅方法。

  3. 声明类不不允许被继承。
    static
  4. 静态变量量
    静态变量量:⼜又称为类变量量,也就是说这个变量量属于类的,类所有的实例例都共享静态变量量,可以直接
    通过类名来访问它。静态变量量在内存中只存在⼀一份。
    实例例变量量:每创建⼀一个实例例就会产⽣生⼀一个实例例变量量,它与该实例例同⽣生共死。
    final int x = 1;
    // x = 2; // cannot assign value to final variable ‘x’
    final A y = new A();
    y.a = 1;
  5. 静态⽅方法
    静态⽅方法在类加载的时候就存在了了,它不不依赖于任何实例例。所以静态⽅方法必须有实现,也就是说它不不能
    是抽象⽅方法。
    只能访问所属类的静态字段和静态⽅方法,⽅方法中不不能有 this 和 super 关键字,因此这两个关键字与具体
    对象关联。
    public class A {
    private int x; // 实例例变量量
    private static int y; // 静态变量量
    public static void main(String[] args) {
    // int x = A.x; // Non-static field ‘x’ cannot be referenced from
    a static context
    A a = new A();
    int x = a.x;
    int y = A.y;
    }
    }
    public abstract class A {
    public static void func1(){
    }
    // public abstract static void func2(); // Illegal combination of
    modifiers: ‘abstract’ and ‘static’
    }
    public class A {
    private static int x;
    private int y;
    public static void func1(){
    int a = x;
    // int b = y; // Non-static field ‘y’ cannot be referenced from a
    static context
    // int b = this.y; // ‘A.this’ cannot be referenced from a
    static context
    }
    }
  6. 静态语句句块
    静态语句句块在类初始化时运⾏行行⼀一次。
  7. 静态内部类
    ⾮非静态内部类依赖于外部类的实例例,也就是说需要先创建外部类实例例,才能⽤用这个实例例去创建⾮非静态内
    部类。⽽而静态内部类不不需要。
    静态内部类不不能访问外部类的⾮非静态的变量量和⽅方法。
  8. 静态导包
    public class A {
    static {
    System.out.println(“123”);
    }
    public static void main(String[] args) {
    A a1 = new A();
    A a2 = new A();
    }
    }
    123
    public class OuterClass {
    class InnerClass {
    }
    static class StaticInnerClass {
    }
    public static void main(String[] args) {
    // InnerClass innerClass = new InnerClass(); // ‘OuterClass.this’
    cannot be referenced from a static context
    OuterClass outerClass = new OuterClass();
    InnerClass innerClass = outerClass.new InnerClass();
    StaticInnerClass staticInnerClass = new StaticInnerClass();
    }
    }
    在使⽤用静态变量量和⽅方法时不不⽤用再指明 ClassName,从⽽而简化代码,但可读性⼤大⼤大降低。
  9. 初始化顺序
    静态变量量和静态语句句块优先于实例例变量量和普通语句句块,静态变量量和静态语句句块的初始化顺序取决于它们
    在代码中的顺序。
    最后才是构造函数的初始化。
    存在继承的情况下,初始化顺序为:
    ⽗父类(静态变量量、静态语句句块)
    ⼦子类(静态变量量、静态语句句块)
    ⽗父类(实例例变量量、普通语句句块)
    ⽗父类(构造函数)
    ⼦子类(实例例变量量、普通语句句块)
    ⼦子类(构造函数)
    五、Object 通⽤用⽅方法
    概览
    import static com.xxx.ClassName.*
    public static String staticField = “静态变量量”;
    static {
    System.out.println(“静态语句句块”);
    }
    public String field = “实例例变量量”;
    {
    System.out.println(“普通语句句块”);
    }
    public InitialOrderTest() {
    System.out.println(“构造函数”);
    }
    equals()
  10. 等价关系
    两个对象具有等价关系,需要满⾜足以下五个条件:
    Ⅰ ⾃自反性
    Ⅱ 对称性
    Ⅲ 传递性
    public native int hashCode()
    public boolean equals(Object obj)
    protected native Object clone() throws CloneNotSupportedException
    public String toString()
    public final native Class getClass()
    protected void finalize() throws Throwable {}
    public final native void notify()
    public final native void notifyAll()
    public final native void wait(long timeout) throws InterruptedException
    public final void wait(long timeout, int nanos) throws InterruptedException
    public final void wait() throws InterruptedException
    x.equals(x); // true
    x.equals(y) == y.equals(x); // true
    if (x.equals(y) && y.equals(z))
    x.equals(z); // true;
    Ⅳ ⼀一致性
    多次调⽤用 equals() ⽅方法结果不不变
    Ⅴ 与 null 的⽐比较
    对任何不不是 null 的对象 x 调⽤用 x.equals(null) 结果都为 false
  11. 等价与相等
    对于基本类型,== 判断两个值是否相等,基本类型没有 equals() ⽅方法。
    对于引⽤用类型,== 判断两个变量量是否引⽤用同⼀一个对象,⽽而 equals() 判断引⽤用的对象是否等价。
  12. 实现
    检查是否为同⼀一个对象的引⽤用,如果是直接返回 true;
    检查是否是同⼀一个类型,如果不不是,直接返回 false;
    将 Object 对象进⾏行行转型;
    判断每个关键域是否相等。
    x.equals(y) == x.equals(y); // true
    x.equals(null); // false;
    Integer x = new Integer(1);
    Integer y = new Integer(1);
    System.out.println(x.equals(y)); // true
    System.out.println(x == y); // false
    public class EqualExample {
    private int x;
    private int y;
    private int z;
    public EqualExample(int x, int y, int z) {
    this.x = x;
    this.y = y;
    this.z = z;
    }
    @Override
    hashCode()
    hashCode() 返回哈希值,⽽而 equals() 是⽤用来判断两个对象是否等价。等价的两个对象散列列值⼀一定相
    同,但是散列列值相同的两个对象不不⼀一定等价,这是因为计算哈希值具有随机性,两个值不不同的对象可能
    计算出相同的哈希值。
    在覆盖 equals() ⽅方法时应当总是覆盖 hashCode() ⽅方法,保证等价的两个对象哈希值也相等。
    HashSet 和 HashMap 等集合类使⽤用了了 hashCode() ⽅方法来计算对象应该存储的位置,因此要将对象添
    加到这些集合类中,需要让对应的类实现 hashCode() ⽅方法。
    下⾯面的代码中,新建了了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成⼀一样
    的,只在集合中添加⼀一个对象。但是 EqualExample 没有实现 hashCode() ⽅方法,因此这两个对象的哈
    希值是不不同的,最终导致集合添加了了两个等价的对象。
    理理想的哈希函数应当具有均匀性,即不不相等的对象应当均匀分布到所有可能的哈希值上。这就要求了了哈
    希函数要把所有域的值都考虑进来。可以将每个域都当成 R 进制的某⼀一位,然后组成⼀一个 R 进制的整
    数。
    R ⼀一般取 31,因为它是⼀一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相
    乘相当于向左移⼀一位,最左边的位丢失。并且⼀一个数与 31 相乘可以转换成移位和减法: 31*x ==
    (x<<5)-x ,编译器器会⾃自动进⾏行行这个优化。
    public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    EqualExample that = (EqualExample) o;
    if (x != that.x) return false;
    if (y != that.y) return false;
    return z == that.z;
    }
    }
    EqualExample e1 = new EqualExample(1, 1, 1);
    EqualExample e2 = new EqualExample(1, 1, 1);
    System.out.println(e1.equals(e2)); // true
    HashSet set = new HashSet<>();
    set.add(e1);
    set.add(e2);
    System.out.println(set.size()); // 2
    toString()
    默认返回 ToStringExample@4554617c 这种形式,其中 @ 后⾯面的数值为散列列码的⽆无符号⼗十六进制表
    示。
    clone()
  13. cloneable
    clone() 是 Object 的 protected ⽅方法,它不不是 public,⼀一个类不不显式去重写 clone(),其它类就不不能直接
    去调⽤用该类实例例的 clone() ⽅方法。
    @Override
    public int hashCode() {
    int result = 17;
    result = 31 * result + x;
    result = 31 * result + y;
    result = 31 * result + z;
    return result;
    }
    public class ToStringExample {
    private int number;
    public ToStringExample(int number) {
    this.number = number;
    }
    }
    ToStringExample example = new ToStringExample(123);
    System.out.println(example.toString());
    ToStringExample@4554617c
    public class CloneExample {
    private int a;
    private int b;
    }
    重写 clone() 得到以下实现:
    以上抛出了了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接⼝口。
    应该注意的是,clone() ⽅方法并不不是 Cloneable 接⼝口的⽅方法,⽽而是 Object 的⼀一个 protected ⽅方法。
    Cloneable 接⼝口只是规定,如果⼀一个类没有实现 Cloneable 接⼝口⼜又调⽤用了了 clone() ⽅方法,就会抛出
    CloneNotSupportedException。
    CloneExample e1 = new CloneExample();
    // CloneExample e2 = e1.clone(); // ‘clone()’ has protected access in
    ‘java.lang.Object’
    public class CloneExample {
    private int a;
    private int b;
    @Override
    public CloneExample clone() throws CloneNotSupportedException {
    return (CloneExample)super.clone();
    }
    }
    CloneExample e1 = new CloneExample();
    try {
    CloneExample e2 = e1.clone();
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    }
    java.lang.CloneNotSupportedException: CloneExample
    public class CloneExample implements Cloneable {
    private int a;
    private int b;
    @Override
    public Object clone() throws CloneNotSupportedException {
    return super.clone();
    }
    }
  14. 浅拷⻉贝
    拷⻉贝对象和原始对象的引⽤用类型引⽤用同⼀一个对象。
  15. 深拷⻉贝
    拷⻉贝对象和原始对象的引⽤用类型引⽤用不不同对象。
    public class ShallowCloneExample implements Cloneable {
    private int[] arr;
    public ShallowCloneExample() {
    arr = new int[10];
    for (int i = 0; i < arr.length; i++) {
    arr[i] = i;
    }
    }
    public void set(int index, int value) {
    arr[index] = value;
    }
    public int get(int index) {
    return arr[index];
    }
    @Override
    protected ShallowCloneExample clone() throws CloneNotSupportedException
    {
    return (ShallowCloneExample) super.clone();
    }
    }
    ShallowCloneExample e1 = new ShallowCloneExample();
    ShallowCloneExample e2 = null;
    try {
    e2 = e1.clone();
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    }
    e1.set(2, 222);
    System.out.println(e2.get(2)); // 222
  16. clone() 的替代⽅方案
    public class DeepCloneExample implements Cloneable {
    private int[] arr;
    public DeepCloneExample() {
    arr = new int[10];
    for (int i = 0; i < arr.length; i++) {
    arr[i] = i;
    }
    }
    public void set(int index, int value) {
    arr[index] = value;
    }
    public int get(int index) {
    return arr[index];
    }
    @Override
    protected DeepCloneExample clone() throws CloneNotSupportedException {
    DeepCloneExample result = (DeepCloneExample) super.clone();
    result.arr = new int[arr.length];
    for (int i = 0; i < arr.length; i++) {
    result.arr[i] = arr[i];
    }
    return result;
    }
    }
    DeepCloneExample e1 = new DeepCloneExample();
    DeepCloneExample e2 = null;
    try {
    e2 = e1.clone();
    } catch (CloneNotSupportedException e) {
    e.printStackTrace();
    }
    e1.set(2, 222);
    System.out.println(e2.get(2)); // 2
    使⽤用 clone() ⽅方法来拷⻉贝⼀一个对象即复杂⼜又有⻛风险,它会抛出异常,并且还需要类型转换。Effective
    Java 书上讲到,最好不不要去使⽤用 clone(),可以使⽤用拷⻉贝构造函数或者拷⻉贝⼯工⼚厂来拷⻉贝⼀一个对象。
    六、继承
    访问权限
    Java 中有三个访问权限修饰符:private、protected 以及 public,如果不不加访问修饰符,表示包级可
    ⻅见。
    可以对类或类中的成员(字段和⽅方法)加上访问修饰符。
    public class CloneConstructorExample {
    private int[] arr;
    public CloneConstructorExample() {
    arr = new int[10];
    for (int i = 0; i < arr.length; i++) {
    arr[i] = i;
    }
    }
    public CloneConstructorExample(CloneConstructorExample original) {
    arr = new int[original.arr.length];
    for (int i = 0; i < original.arr.length; i++) {
    arr[i] = original.arr[i];
    }
    }
    public void set(int index, int value) {
    arr[index] = value;
    }
    public int get(int index) {
    return arr[index];
    }
    }
    CloneConstructorExample e1 = new CloneConstructorExample();
    CloneConstructorExample e2 = new CloneConstructorExample(e1);
    e1.set(2, 222);
    System.out.println(e2.get(2)); // 2
    类可⻅见表示其它类可以⽤用这个类创建实例例对象。
    成员可⻅见表示其它类可以⽤用这个类的实例例对象访问到该成员;
    protected ⽤用于修饰成员,表示在继承体系中成员对于⼦子类可⻅见,但是这个访问修饰符对于类没有意
    义。
    设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们
    的 API 进⾏行行通信,⼀一个模块不不需要知道其他模块的内部⼯工作情况,这个概念被称为信息隐藏或封装。因
    此访问权限应当尽可能地使每个类或者成员不不被外界访问。
    如果⼦子类的⽅方法重写了了⽗父类的⽅方法,那么⼦子类中该⽅方法的访问级别不不允许低于⽗父类的访问级别。这是为
    了了确保可以使⽤用⽗父类实例例的地⽅方都可以使⽤用⼦子类实例例去代替,也就是确保满⾜足⾥里里⽒氏替换原则。
    字段决不不能是公有的,因为这么做的话就失去了了对这个字段修改⾏行行为的控制,客户端可以对其随意修
    改。例例如下⾯面的例例⼦子中,AccessExample 拥有 id 公有字段,如果在某个时刻,我们想要使⽤用 int 存储
    id 字段,那么就需要修改所有的客户端代码。
    可以使⽤用公有的 getter 和 setter ⽅方法来替换公有字段,这样的话就可以控制对字段的修改⾏行行为。
    但是也有例例外,如果是包级私有的类或者私有的嵌套类,那么直接暴暴露露成员不不会有特别⼤大的影响。
    public class AccessExample {
    public String id;
    }
    public class AccessExample {
    private int id;
    public String getId() {
    return id + “”;
    }
    public void setId(String id) {
    this.id = Integer.valueOf(id);
    }
    }
    public class AccessWithInnerClassExample {
    private class InnerClass {
    int x;
    }
    抽象类与接⼝口
  17. 抽象类
    抽象类和抽象⽅方法都使⽤用 abstract 关键字进⾏行行声明。如果⼀一个类中包含抽象⽅方法,那么这个类必须声明
    为抽象类。
    抽象类和普通类最⼤大的区别是,抽象类不不能被实例例化,只能被继承。
    private InnerClass innerClass;
    public AccessWithInnerClassExample() {
    innerClass = new InnerClass();
    }
    public int getValue() {
    return innerClass.x; // 直接访问
    }
    }
    public abstract class AbstractClassExample {
    protected int x;
    private int y;
    public abstract void func1();
    public void func2() {
    System.out.println(“func2”);
    }
    }
    public class AbstractExtendClassExample extends AbstractClassExample {
    @Override
    public void func1() {
    System.out.println(“func1”);
    }
    }
  18. 接⼝口
    接⼝口是抽象类的延伸,在 Java 8 之前,它可以看成是⼀一个完全抽象的类,也就是说它不不能有任何的⽅方
    法实现。
    从 Java 8 开始,接⼝口也可以拥有默认的⽅方法实现,这是因为不不⽀支持默认⽅方法的接⼝口的维护成本太⾼高
    了了。在 Java 8 之前,如果⼀一个接⼝口想要添加新的⽅方法,那么要修改所有实现了了该接⼝口的类,让它们都
    实现新增的⽅方法。
    接⼝口的成员(字段 + ⽅方法)默认都是 public 的,并且不不允许定义为 private 或者 protected。从 Java 9
    开始,允许将⽅方法定义为 private,这样就能定义某些复⽤用的代码⼜又不不会把⽅方法暴暴露露出去。
    接⼝口的字段默认都是 static 和 final 的。
    // AbstractClassExample ac1 = new AbstractClassExample(); //
    ‘AbstractClassExample’ is abstract; cannot be instantiated
    AbstractClassExample ac2 = new AbstractExtendClassExample();
    ac2.func1();
    public interface InterfaceExample {
    void func1();
    default void func2(){
    System.out.println(“func2”);
    }
    int x = 123;
    // int y; // Variable ‘y’ might not have been initialized
    public int z = 0; // Modifier ‘public’ is redundant for interface
    fields
    // private int k = 0; // Modifier ‘private’ not allowed here
    // protected int l = 0; // Modifier ‘protected’ not allowed here
    // private void fun3(); // Modifier ‘private’ not allowed here
    }
    public class InterfaceImplementExample implements InterfaceExample {
    @Override
    public void func1() {
    System.out.println(“func1”);
    }
    }
  19. ⽐比较
    从设计层⾯面上看,抽象类提供了了⼀一种 IS-A 关系,需要满⾜足⾥里里式替换原则,即⼦子类对象必须能够替换
    掉所有⽗父类对象。⽽而接⼝口更更像是⼀一种 LIKE-A 关系,它只是提供⼀一种⽅方法实现契约,并不不要求接⼝口
    和实现接⼝口的类具有 IS-A 关系。
    从使⽤用上来看,⼀一个类可以实现多个接⼝口,但是不不能继承多个抽象类。
    接⼝口的字段只能是 static 和 final 类型的,⽽而抽象类的字段没有这种限制。
    接⼝口的成员只能是 public 的,⽽而抽象类的成员可以有多种访问权限。
  20. 使⽤用选择
    使⽤用接⼝口:
    需要让不不相关的类都实现⼀一个⽅方法,例例如不不相关的类都可以实现 Comparable 接⼝口中的
    compareTo() ⽅方法;
    需要使⽤用多重继承。
    使⽤用抽象类:
    需要在⼏几个相关的类中共享代码。
    需要能控制继承来的成员的访问权限,⽽而不不是都为 public。
    需要继承⾮非静态和⾮非常量量字段。
    在很多情况下,接⼝口优先于抽象类。因为接⼝口没有抽象类严格的类层次结构要求,可以灵活地为⼀一个类
    添加⾏行行为。并且从 Java 8 开始,接⼝口也可以有默认的⽅方法实现,使得修改接⼝口的成本也变的很低。
    Abstract Methods and Classes
    深⼊入理理解 abstract class 和 interface
    When to Use Abstract Class and Interface
    Java 9 Private Methods in Interfaces
    super
    访问⽗父类的构造函数:可以使⽤用 super() 函数访问⽗父类的构造函数,从⽽而委托⽗父类完成⼀一些初始化
    的⼯工作。应该注意到,⼦子类⼀一定会调⽤用⽗父类的构造函数来完成初始化⼯工作,⼀一般是调⽤用⽗父类的默认
    构造函数,如果⼦子类需要调⽤用⽗父类其它构造函数,那么就可以使⽤用 super() 函数。
    访问⽗父类的成员:如果⼦子类重写了了⽗父类的某个⽅方法,可以通过使⽤用 super 关键字来引⽤用⽗父类的⽅方法
    实现。
    // InterfaceExample ie1 = new InterfaceExample(); // ‘InterfaceExample’ is
    abstract; cannot be instantiated
    InterfaceExample ie2 = new InterfaceImplementExample();
    ie2.func1();
    System.out.println(InterfaceExample.x);
    Using the Keyword super
    重写与重载
    public class SuperExample {
    protected int x;
    protected int y;
    public SuperExample(int x, int y) {
    this.x = x;
    this.y = y;
    }
    public void func() {
    System.out.println(“SuperExample.func()”);
    }
    }
    public class SuperExtendExample extends SuperExample {
    private int z;
    public SuperExtendExample(int x, int y, int z) {
    super(x, y);
    this.z = z;
    }
    @Override
    public void func() {
    super.func();
    System.out.println(“SuperExtendExample.func()”);
    }
    }
    SuperExample e = new SuperExtendExample(1, 2, 3);
    e.func();
    SuperExample.func()
    SuperExtendExample.func()
  21. 重写(Override)
    存在于继承体系中,指⼦子类实现了了⼀一个与⽗父类在⽅方法声明上完全相同的⼀一个⽅方法。
    为了了满⾜足⾥里里式替换原则,重写有以下三个限制:
    ⼦子类⽅方法的访问权限必须⼤大于等于⽗父类⽅方法;
    ⼦子类⽅方法的返回类型必须是⽗父类⽅方法返回类型或为其⼦子类型。
    ⼦子类⽅方法抛出的异常类型必须是⽗父类抛出异常类型或为其⼦子类型。
    使⽤用 @Override 注解,可以让编译器器帮忙检查是否满⾜足上⾯面的三个限制条件。
    下⾯面的示例例中,SubClass 为 SuperClass 的⼦子类,SubClass 重写了了 SuperClass 的 func() ⽅方法。其
    中:
    ⼦子类⽅方法访问权限为 public,⼤大于⽗父类的 protected。
    ⼦子类的返回类型为 ArrayList,是⽗父类返回类型 List 的⼦子类。
    ⼦子类抛出的异常类型为 Exception,是⽗父类抛出异常 Throwable 的⼦子类。
    ⼦子类重写⽅方法使⽤用 @Override 注解,从⽽而让编译器器⾃自动检查是否满⾜足限制条件。
    在调⽤用⼀一个⽅方法时,先从本类中查找看是否有对应的⽅方法,如果没有再到⽗父类中查看,看是否从⽗父类继
    承来。否则就要对参数进⾏行行转型,转成⽗父类之后看是否有对应的⽅方法。总的来说,⽅方法调⽤用的优先级
    为:
    this.func(this)
    super.func(this)
    this.func(super)
    super.func(super)
    class SuperClass {
    protected List func() throws Throwable {
    return new ArrayList<>();
    }
    }
    class SubClass extends SuperClass {
    @Override
    public ArrayList func() throws Exception {
    return new ArrayList<>();
    }
    }
    /*
    A
    |
    B
    |
    C
    |
    D
    */
    class A {
    public void show(A obj) {
    System.out.println(“A.show(A)”);
    }
    public void show(C obj) {
    System.out.println(“A.show©”);
    }
    }
    class B extends A {
    @Override
    public void show(A obj) {
    System.out.println(“B.show(A)”);
    }
    }
    class C extends B {
    }
    class D extends C {
    }
    public static void main(String[] args) {
    A a = new A();
    B b = new B();
    C c = new C();
    D d = new D();
    // 在 A 中存在 show(A obj),直接调⽤用
    a.show(a); // A.show(A)
    // 在 A 中不不存在 show(B obj),将 B 转型成其⽗父类 A
  22. 重载(Overload)
    存在于同⼀一个类中,指⼀一个⽅方法与已经存在的⽅方法名称上相同,但是参数类型、个数、顺序⾄至少有⼀一个
    不不同。
    应该注意的是,返回值不不同,其它都相同不不算是重载。
    七、反射
    每个类都有⼀一个 Class 对象,包含了了与类有关的信息。当编译⼀一个新类时,会产⽣生⼀一个同名的 .class
    ⽂文件,该⽂文件内容保存着 Class 对象。
    a.show(b); // A.show(A)
    // 在 B 中存在从 A 继承来的 show(C obj),直接调⽤用
    b.show©; // A.show©
    // 在 B 中不不存在 show(D obj),但是存在从 A 继承来的 show(C obj),将 D 转型成其
    ⽗父类 C
    b.show(d); // A.show©
    // 引⽤用的还是 B 对象,所以 ba 和 b 的调⽤用结果⼀一样
    A ba = new B();
    ba.show©; // A.show©
    ba.show(d); // A.show©
    }
    class OverloadingExample {
    public void show(int x) {
    System.out.println(x);
    }
    public void show(int x, String y) {
    System.out.println(x + " " + y);
    }
    }
    public static void main(String[] args) {
    OverloadingExample example = new OverloadingExample();
    example.show(1);
    example.show(1, “2”);
    }
    类加载相当于 Class 对象的加载,类在第⼀一次使⽤用时才动态加载到 JVM 中。也可以使⽤用
    Class.forName(“com.mysql.jdbc.Driver”) 这种⽅方式来控制类的加载,该⽅方法会返回⼀一个 Class 对
    象。
    反射可以提供运⾏行行时的类信息,并且这个类可以在运⾏行行时才加载进来,甚⾄至在编译时期该类的 .class 不不
    存在也可以加载进来。
    Class 和 java.lang.reflect ⼀一起对反射提供了了⽀支持,java.lang.reflect 类库主要包含了了以下三个类:
    Field :可以使⽤用 get() 和 set() ⽅方法读取和修改 Field 对象关联的字段;
    Method :可以使⽤用 invoke() ⽅方法调⽤用与 Method 对象关联的⽅方法;
    Constructor :可以⽤用 Constructor 的 newInstance() 创建新的对象。
    反射的优点:
    可扩展性 :应⽤用程序可以利利⽤用全限定名创建可扩展对象的实例例,来使⽤用来⾃自外部的⽤用户⾃自定义
    类。
    类浏览器器和可视化开发环境 :⼀一个类浏览器器需要可以枚举类的成员。可视化开发环境(如 IDE)
    可以从利利⽤用反射中可⽤用的类型信息中受益,以帮助程序员编写正确的代码。
    调试器器和测试⼯工具 : 调试器器需要能够检查⼀一个类⾥里里的私有成员。测试⼯工具可以利利⽤用反射来⾃自动地
    调⽤用类⾥里里定义的可被发现的 API 定义,以确保⼀一组测试中有较⾼高的代码覆盖率。
    反射的缺点:
    尽管反射⾮非常强⼤大,但也不不能滥⽤用。如果⼀一个功能可以不不⽤用反射完成,那么最好就不不⽤用。在我们使⽤用反
    射技术时,下⾯面⼏几条内容应该牢记于⼼心。
    性能开销 :反射涉及了了动态类型的解析,所以 JVM ⽆无法对这些代码进⾏行行优化。因此,反射操作的
    效率要⽐比那些⾮非反射操作低得多。我们应该避免在经常被执⾏行行的代码或对性能要求很⾼高的程序中使
    ⽤用反射。
    安全限制 :使⽤用反射技术要求程序必须在⼀一个没有安全限制的环境中运⾏行行。如果⼀一个程序必须在
    有安全限制的环境中运⾏行行,如 Applet,那么这就是个问题了了。
    内部暴暴露露 :由于反射允许代码执⾏行行⼀一些在正常情况下不不被允许的操作(⽐比如访问私有的属性和⽅方
    法),所以使⽤用反射可能会导致意料料之外的副作⽤用,这可能导致代码功能失调并破坏可移植性。反
    射代码破坏了了抽象性,因此当平台发⽣生改变的时候,代码的⾏行行为就有可能也随着变化。
    Trail: The Reflection API
    深⼊入解析 Java 反射(1)- 基础
    ⼋八、异常
    Throwable 可以⽤用来表示任何可以作为异常抛出的类,分为两种: Error 和 Exception。其中 Error
    ⽤用来表示 JVM ⽆无法处理理的错误,Exception 分为两种:
    受检异常 :需要⽤用 try…catch… 语句句捕获并进⾏行行处理理,并且可以从异常中恢复;
    ⾮非受检异常 :是程序运⾏行行时错误,例例如除 0 会引发 Arithmetic Exception,此时程序崩溃并且⽆无法
    恢复。
    Java ⼊入⻔门之异常处理理
    Java Exception Interview Questions and Answers
    九、泛型
    Java 泛型详解
    10 道 Java 泛型⾯面试题
    ⼗十、注解
    Java 注解是附加在代码中的⼀一些元信息,⽤用于⼀一些⼯工具在编译、运⾏行行时进⾏行行解析和使⽤用,起到说明、配
    置的功能。注解不不会也不不能影响代码的实际逻辑,仅仅起到辅助性的作⽤用。
    注解 Annotation 实现原理理与⾃自定义注解例例⼦子
    ⼗十⼀一、特性
    Java 各版本的新特性
    public class Box {
    // T stands for “Type”
    private T t;
    public void set(T t) { this.t = t; }
    public T get() { return t; }
    }
    New highlights in Java SE 8
  23. Lambda Expressions
  24. Pipelines and Streams
  25. Date and Time API
  26. Default Methods
  27. Type Annotations
  28. Nashhorn JavaScript Engine
  29. Concurrent Accumulators
  30. Parallel operations
  31. PermGen Error Removed
    New highlights in Java SE 7
  32. Strings in Switch Statement
  33. Type Inference for Generic Instance Creation
  34. Multiple Exception Handling
  35. Support for Dynamic Languages
  36. Try with Resources
  37. Java nio Package
  38. Binary Literals, Underscore in literals
  39. Diamond Syntax
    Difference between Java 1.8 and Java 1.7?
    Java 8 特性
    Java 与 C++ 的区别
    Java 是纯粹的⾯面向对象语⾔言,所有的对象都继承⾃自 java.lang.Object,C++ 为了了兼容 C 即⽀支持⾯面向
    对象也⽀支持⾯面向过程。
    Java 通过虚拟机从⽽而实现跨平台特性,但是 C++ 依赖于特定的平台。
    Java 没有指针,它的引⽤用可以理理解为安全指针,⽽而 C++ 具有和 C ⼀一样的指针。
    Java ⽀支持⾃自动垃圾回收,⽽而 C++ 需要⼿手动回收。
    Java 不不⽀支持多重继承,只能通过实现多个接⼝口来达到相同⽬目的,⽽而 C++ ⽀支持多重继承。
    Java 不不⽀支持操作符重载,虽然可以对两个 String 对象执⾏行行加法运算,但是这是语⾔言内置⽀支持的操
    作,不不属于操作符重载,⽽而 C++ 可以。
    Java 的 goto 是保留留字,但是不不可⽤用,C++ 可以使⽤用 goto。
    What are the main differences between Java and C++?
    JRE or JDK
    JRE:Java Runtime Environment,Java 运⾏行行环境的简称,为 Java 的运⾏行行提供了了所需的环境。它
    是⼀一个 JVM 程序,主要包括了了 JVM 的标准实现和⼀一些 Java 基本类库。
    JDK:Java Development Kit,Java 开发⼯工具包,提供了了 Java 的开发及运⾏行行环境。JDK 是 Java 开
    发的核⼼心,集成了了 JRE 以及⼀一些其它的⼯工具,⽐比如编译 Java 源码的编译器器 javac 等。
    参考资料料
    Eckel B. Java 编程思想[M]. 机械⼯工业出版社, 2002.
    Bloch J. Effective java[M]. Addison-Wesley Professional, 2017.
    Java 容器器
    Java 容器器
    ⼀一、概览
    Collection
    Map
    ⼆二、容器器中的设计模式
    迭代器器模式
    适配器器模式
    三、源码分析
    ArrayList
    Vector
    CopyOnWriteArrayList
    LinkedList
    HashMap
    ConcurrentHashMap
    LinkedHashMap
    WeakHashMap
    参考资料料
    ⼀一、概览
    容器器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,⽽而 Map 存储着键值对(两个对
    象)的映射表。
    Collection
  40. Set
    TreeSet:基于红⿊黑树实现,⽀支持有序性操作,例例如根据⼀一个范围查找元素的操作。但是查找效率不不
    如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
    HashSet:基于哈希表实现,⽀支持快速查找,但不不⽀支持有序性操作。并且失去了了元素的插⼊入顺序信
    息,也就是说使⽤用 Iterator 遍历 HashSet 得到的结果是不不确定的。
    LinkedHashSet:具有 HashSet 的查找效率,并且内部使⽤用双向链表维护元素的插⼊入顺序。
  41. List
    ArrayList:基于动态数组实现,⽀支持随机访问。
    Vector:和 ArrayList 类似,但它是线程安全的。
    LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插⼊入和删除元素。不不
    仅如此,LinkedList 还可以⽤用作栈、队列列和双向队列列。
  42. Queue
    LinkedList:可以⽤用它来实现双向队列列。
    PriorityQueue:基于堆结构实现,可以⽤用它来实现优先队列列。
    Map
    TreeMap:基于红⿊黑树实现。
    HashMap:基于哈希表实现。
    HashTable:和 HashMap 类似,但它是线程安全的,这意味着同⼀一时刻多个线程同时写⼊入
    HashTable 不不会导致数据不不⼀一致。它是遗留留类,不不应该去使⽤用它,⽽而是使⽤用 ConcurrentHashMap
    来⽀支持线程安全,ConcurrentHashMap 的效率会更更⾼高,因为 ConcurrentHashMap 引⼊入了了分段锁。
    LinkedHashMap:使⽤用双向链表来维护元素的顺序,顺序为插⼊入顺序或者最近最少使⽤用(LRU)顺
    序。
    ⼆二、容器器中的设计模式
    迭代器器模式
    Collection 继承了了 Iterable 接⼝口,其中的 iterator() ⽅方法能够产⽣生⼀一个 Iterator 对象,通过这个对象就可
    以迭代遍历 Collection 中的元素。
    从 JDK 1.5 之后可以使⽤用 foreach ⽅方法来遍历实现了了 Iterable 接⼝口的聚合对象。
    适配器器模式
    java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
    应该注意的是 asList() 的参数为泛型的变⻓长参数,不不能使⽤用基本类型数组作为参数,只能使⽤用相应的包
    装类型数组。
    也可以使⽤用以下⽅方式调⽤用 asList():
    三、源码分析
    如果没有特别说明,以下源码分析基于 JDK 1.8。
    在 IDEA 中 double shift 调出 Search EveryWhere,查找源码⽂文件,找到之后就可以阅读源码。
    ArrayList
  43. 概览
    因为 ArrayList 是基于数组实现的,所以⽀支持快速随机访问。RandomAccess 接⼝口标识着该类⽀支持快速
    随机访问。
    List list = new ArrayList<>();
    list.add(“a”);
    list.add(“b”);
    for (String item : list) {
    System.out.println(item);
    }
    @SafeVarargs
    public static List asList(T… a)
    Integer[] arr = {1, 2, 3};
    List list = Arrays.asList(arr);
    List list = Arrays.asList(1, 2, 3);
    数组的默认⼤大⼩小为 10。
  44. 扩容
    添加元素时使⽤用 ensureCapacityInternal() ⽅方法来保证容量量⾜足够,如果不不够时,需要使⽤用 grow() ⽅方法进
    ⾏行行扩容,新容量量的⼤大⼩小为 oldCapacity + (oldCapacity >> 1) ,即 oldCapacity+oldCapacity/2。其
    中 oldCapacity >> 1 需要取整,所以新容量量⼤大约是旧容量量的 1.5 倍左右。(oldCapacity 为偶数就是
    1.5 倍,为奇数就是 1.5 倍-0.5)
    扩容操作需要调⽤用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很⾼高,因此最好在
    创建 ArrayList 对象时就指定⼤大概的容量量⼤大⼩小,减少扩容操作的次数。
    public class ArrayList extends AbstractList
    implements List, RandomAccess, Cloneable, java.io.Serializable
    private static final int DEFAULT_CAPACITY = 10;
    public boolean add(E e) {
    ensureCapacityInternal(size + 1); // Increments modCount!!
    elementData[size++] = e;
    return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
    }
    private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
  45. 删除元素
    需要调⽤用 System.arraycopy() 将 index+1 后⾯面的元素都复制到 index 位置上,该操作的时间复杂度为
    O(N),可以看到 ArrayList 删除元素的代价是⾮非常⾼高的。
  46. 序列列化
    ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不不⼀一定都会被使⽤用,那么就没必
    要全部进⾏行行序列列化。
    保存元素的数组 elementData 使⽤用 transient 修饰,该关键字声明数组默认不不会被序列列化。
    ArrayList 实现了了 writeObject() 和 readObject() 来控制只序列列化数组中有元素填充那部分内容。
    grow(minCapacity);
    }
    private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
    }
    public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
    numMoved);
    elementData[–size] = null; // clear to let GC do its work
    return oldValue;
    }
    transient Object[] elementData; // non-private to simplify nested class
    access
    private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;
    // Read in size, and any hidden stuff
    s.defaultReadObject();
    // Read in capacity
    s.readInt(); // ignored
    if (size > 0) {
    // be like clone(), allocate array based upon size not capacity
    ensureCapacityInternal(size);
    Object[] a = elementData;
    // Read in all elements in the proper order.
    for (int i=0; i a[i] = s.readObject();
    }
    }
    }
    private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException{
    // Write out element count, and any hidden stuff
    int expectedModCount = modCount;
    s.defaultWriteObject();
    // Write out size as capacity for behavioural compatibility with
    clone()
    s.writeInt(size);
    // Write out all elements in the proper order.
    for (int i=0; i s.writeObject(elementData[i]);
    }
    if (modCount != expectedModCount) {
    throw new ConcurrentModificationException();
    }
    }
    序列列化时需要使⽤用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。⽽而 writeObject()
    ⽅方法在传⼊入的对象存在 writeObject() 的时候会去反射调⽤用该对象的 writeObject() 来实现序列列化。反序
    列列化使⽤用的是 ObjectInputStream 的 readObject() ⽅方法,原理理类似。
  47. Fail-Fast
    modCount ⽤用来记录 ArrayList 结构发⽣生变化的次数。结构发⽣生变化是指添加或者删除⾄至少⼀一个元素的
    所有操作,或者是调整内部数组的⼤大⼩小,仅仅只是设置元素的值不不算结构发⽣生变化。
    在进⾏行行序列列化或者迭代等操作时,需要⽐比较操作前后 modCount 是否改变,如果改变了了需要抛出
    ConcurrentModificationException。代码参考上节序列列化中的 writeObject() ⽅方法。
    Vector
  48. 同步
    它的实现与 ArrayList 类似,但是使⽤用了了 synchronized 进⾏行行同步。
  49. 扩容
    Vector 的构造函数可以传⼊入 capacityIncrement 参数,它的作⽤用是在扩容时使容量量 capacity 增⻓长
    capacityIncrement。如果这个参数的值⼩小于等于 0,扩容时每次都令 capacity 为原来的两倍。
    ArrayList list = new ArrayList();
    ObjectOutputStream oos = new ObjectOutputStream(new
    FileOutputStream(file));
    oos.writeObject(list);
    public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
    }
    public synchronized E get(int index) {
    if (index >= elementCount)
    throw new ArrayIndexOutOfBoundsException(index);
    return elementData(index);
    }
    调⽤用没有 capacityIncrement 的构造函数时,capacityIncrement 值被设置为 0,也就是说默认情况下
    Vector 每次扩容时容量量都会翻倍。
  50. 与 ArrayList 的⽐比较
    Vector 是同步的,因此开销就⽐比 ArrayList 要⼤大,访问速度更更慢。最好使⽤用 ArrayList ⽽而不不是
    Vector,因为同步操作完全可以由程序员⾃自⼰己来控制;
    Vector 每次扩容请求其⼤大⼩小的 2 倍(也可以通过构造函数设置增⻓长的容量量),⽽而 ArrayList 是 1.5
    倍。
  51. 替代⽅方案
    可以使⽤用 Collections.synchronizedList(); 得到⼀一个线程安全的 ArrayList。
    public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
    throw new IllegalArgumentException("Illegal Capacity: "+
    initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
    }
    private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
    capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
    }
    public Vector(int initialCapacity) {
    this(initialCapacity, 0);
    }
    public Vector() {
    this(10);
    }
    也可以使⽤用 concurrent 并发包下的 CopyOnWriteArrayList 类。
    CopyOnWriteArrayList
  52. 读写分离
    写操作在⼀一个复制的数组上进⾏行行,读操作还是在原始数组中进⾏行行,读写分离,互不不影响。
    写操作需要加锁,防⽌止并发写⼊入时导致写⼊入数据丢失。
    写操作结束之后需要把原始数组指向新的复制数组。
  53. 适⽤用场景
    List list = new ArrayList<>();
    List synList = Collections.synchronizedList(list);
    List list = new CopyOnWriteArrayList<>();
    public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    Object[] elements = getArray();
    int len = elements.length;
    Object[] newElements = Arrays.copyOf(elements, len + 1);
    newElements[len] = e;
    setArray(newElements);
    return true;
    } finally {
    lock.unlock();
    }
    }
    final void setArray(Object[] a) {
    array = a;
    }
    @SuppressWarnings(“unchecked”)
    private E get(Object[] a, int index) {
    return (E) a[index];
    }
    CopyOnWriteArrayList 在写操作的同时允许读操作,⼤大⼤大提⾼高了了读操作的性能,因此很适合读多写少的
    应⽤用场景。
    但是 CopyOnWriteArrayList 有其缺陷:
    内存占⽤用:在写操作时需要复制⼀一个新的数组,使得内存占⽤用为原来的两倍左右;
    数据不不⼀一致:读操作不不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
    所以 CopyOnWriteArrayList 不不适合内存敏敏感以及对实时性要求很⾼高的场景。
    LinkedList
  54. 概览
    基于双向链表实现,使⽤用 Node 存储链表节点信息。
    每个链表存储了了 first 和 last 指针:
  55. 与 ArrayList 的⽐比较
    ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。ArrayList 和 LinkedList 的区别可以归结
    为数组和链表的区别:
    数组⽀支持随机访问,但插⼊入删除的代价很⾼高,需要移动⼤大量量元素;
    private static class Node {
    E item;
    Node next;
    Node prev;
    }
    transient Node first;
    transient Node last;
    链表不不⽀支持随机访问,但插⼊入删除只需要改变指针。
    HashMap
    为了了便便于理理解,以下源码分析以 JDK 1.7 为主。
  56. 存储结构
    内部包含了了⼀一个 Entry 类型的数组 table。Entry 存储着键值对。它包含了了四个字段,从 next 字段我们
    可以看出 Entry 是⼀一个链表。即数组中的每个位置被当成⼀一个桶,⼀一个桶存放⼀一个链表。HashMap 使
    ⽤用拉链法来解决冲突,同⼀一个链表中存放哈希值和散列列桶取模运算结果相同的 Entry。
    transient Entry[] table;
    static class Entry implements Map.Entry {
    final K key;
    V value;
    Entry next;
    int hash;
    Entry(int h, K k, V v, Entry n) {
    value = v;
    next = n;
    key = k;
    hash = h;
  57. 拉链法的⼯工作原理理
    }
    public final K getKey() {
    return key;
    }
    public final V getValue() {
    return value;
    }
    public final V setValue(V newValue) {
    V oldValue = value;
    value = newValue;
    return oldValue;
    }
    public final boolean equals(Object o) {
    if (!(o instanceof Map.Entry))
    return false;
    Map.Entry e = (Map.Entry)o;
    Object k1 = getKey();
    Object k2 = e.getKey();
    if (k1 == k2 || (k1 != null && k1.equals(k2))) {
    Object v1 = getValue();
    Object v2 = e.getValue();
    if (v1 == v2 || (v1 != null && v1.equals(v2)))
    return true;
    }
    return false;
    }
    public final int hashCode() {
    return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }
    public final String toString() {
    return getKey() + “=” + getValue();
    }
    }
    新建⼀一个 HashMap,默认⼤大⼩小为 16;
    插⼊入 键值对,先计算 K1 的 hashCode 为 115,使⽤用除留留余数法得到所在的桶下标
    115%16=3。
    插⼊入 键值对,先计算 K2 的 hashCode 为 118,使⽤用除留留余数法得到所在的桶下标
    118%16=6。
    插⼊入 键值对,先计算 K3 的 hashCode 为 118,使⽤用除留留余数法得到所在的桶下标
    118%16=6,插在 前⾯面。
    应该注意到链表的插⼊入是以头插法⽅方式进⾏行行的,例例如上⾯面的 不不是插在 后⾯面,⽽而是插
    ⼊入在链表头部。
    查找需要分成两步进⾏行行:
    计算键值对所在的桶;
    在链表上顺序查找,时间复杂度显然和链表的⻓长度成正⽐比。
  58. put 操作
    HashMap map = new HashMap<>();
    map.put(“K1”, “V1”);
    map.put(“K2”, “V2”);
    map.put(“K3”, “V3”);
    public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
    HashMap 允许插⼊入键为 null 的键值对。但是因为⽆无法调⽤用 null 的 hashCode() ⽅方法,也就⽆无法确定该
    键值对的桶下标,只能通过强制指定⼀一个桶下标来存放。HashMap 使⽤用第 0 个桶存放键为 null 的键值
    对。
    inflateTable(threshold);
    }
    // 键为 null 单独处理理
    if (key == null)
    return putForNullKey(value);
    int hash = hash(key);
    // 确定桶下标
    int i = indexFor(hash, table.length);
    // 先找出是否已经存在键为 key 的键值对,如果存在的话就更更新这个键值对的值为 value
    for (Entry e = table[i]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    V oldValue = e.value;
    e.value = value;
    e.recordAccess(this);
    return oldValue;
    }
    }
    modCount++;
    // 插⼊入新键值对
    addEntry(hash, key, value, i);
    return null;
    }
    private V putForNullKey(V value) {
    for (Entry e = table[0]; e != null; e = e.next) {
    if (e.key == null) {
    V oldValue = e.value;
    e.value = value;
    e.recordAccess(this);
    return oldValue;
    }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
    }
    使⽤用链表的头插法,也就是新的键值对插在链表的头部,⽽而不不是链表的尾部。
  59. 确定桶下标
    很多操作都需要先确定⼀一个键值对所在的桶下标。
    4.1 计算 hash 值
    void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
    resize(2 * table.length);
    hash = (null != key) ? hash(key) : 0;
    bucketIndex = indexFor(hash, table.length);
    }
    createEntry(hash, key, value, bucketIndex);
    }
    void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry e = table[bucketIndex];
    // 头插法,链表头部指向新的键值对
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
    }
    Entry(int h, K k, V v, Entry n) {
    value = v;
    next = n;
    key = k;
    hash = h;
    }
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
    return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();
    4.2 取模
    令 x = 1<<4,即 x 为 2 的 4 次⽅方,它具有以下性质:
    令⼀一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:
    这个性质和 y 对 x 取模效果是⼀一样的:
    我们知道,位运算的代价⽐比求模运算⼩小的多,因此在进⾏行行这种计算时⽤用位运算的话能带来更更⾼高的性能。
    确定桶下标的最后⼀一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2
    的 n 次⽅方,那么就可以将这个操作转换为位运算。
  60. 扩容-基本原理理
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
    }
    public final int hashCode() {
    return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
    x : 00010000
    x-1 : 00001111
    y : 10110010
    x-1 : 00001111
    y&(x-1) : 00000010
    y : 10110010
    x : 00010000
    y%x : 00000010
    static int indexFor(int h, int length) {
    return h & (length-1);
    }
    参数含义
    capacity table 的容量量⼤大⼩小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次⽅方。
    size 键值对数量量。
    threshold size 的临界值,当 size ⼤大于等于 threshold 就必须进⾏行行扩容操作。
    loadFactor 装载因⼦子,table 能够使⽤用的⽐比例例,threshold = (int)(capacity* loadFactor)。
  61. 扩容-基本原理理
    设 HashMap 的 table ⻓长度为 M,需要存储的键值对数量量为 N,如果哈希函数满⾜足均匀性的要求,那么
    每条链表的⻓长度⼤大约为 N/M,因此查找的复杂度为 O(N/M)。
    为了了让查找的成本降低,应该使 N/M 尽可能⼩小,因此需要保证 M 尽可能⼤大,也就是说 table 要尽可能
    ⼤大。HashMap 采⽤用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。
    和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。
    从下⾯面的添加元素代码中可以看出,当需要扩容时,令 capacity 为原来的两倍。
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    static final int MAXIMUM_CAPACITY = 1 << 30;
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    transient Entry[] table;
    transient int size;
    int threshold;
    final float loadFactor;
    transient int modCount;
    void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    if (size++ >= threshold)
    resize(2 * table.length);
    }
    扩容使⽤用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插⼊入
    newTable 中,因此这⼀一步是很费时的。
  62. 扩容-重新计算桶下标
    在进⾏行行扩容时,需要把键值对重新计算桶下标,从⽽而放到对应的桶上。在前⾯面提到,HashMap 使⽤用
    hash%capacity 来确定桶下标。HashMap capacity 为 2 的 n 次⽅方这⼀一特点能够极⼤大降低重新计算桶下
    标操作的复杂度。
    假设原数组⻓长度 capacity 为 16,扩容之后 new capacity 为 32:
    void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
    threshold = Integer.MAX_VALUE;
    return;
    }
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
    }
    void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
    Entry e = src[j];
    if (e != null) {
    src[j] = null;
    do {
    Entry next = e.next;
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
    } while (e != null);
    }
    }
    }
    对于⼀一个 Key,它的哈希值 hash 在第 5 位:
    为 0,那么 hash%00010000 = hash%00100000,桶位置和原来⼀一致;
    为 1,hash%00010000 = hash%00100000 + 16,桶位置是原位置 + 16。
  63. 计算数组容量量
    HashMap 构造函数允许⽤用户传⼊入的容量量不不是 2 的 n 次⽅方,因为它可以⾃自动地将传⼊入的容量量转换为 2 的
    n 次⽅方。
    先考虑如何求⼀一个数的掩码,对于 10010000,它的掩码为 11111111,可以使⽤用以下⽅方法得到:
    mask+1 是⼤大于原始数字的最⼩小的 2 的 n 次⽅方。
    以下是 HashMap 中计算数组容量量的代码:
  64. 链表转红⿊黑树
    从 JDK 1.8 开始,⼀一个桶存储的链表⻓长度⼤大于等于 8 时会将链表转换为红⿊黑树。
  65. 与 Hashtable 的⽐比较
    capacity : 00010000
    new capacity : 00100000
    mask |= mask >> 1 11011000
    mask |= mask >> 2 11111110
    mask |= mask >> 4 11111111
    num 10010000
    mask+1 100000000
    static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n +
    1;
    }
    Hashtable 使⽤用 synchronized 来进⾏行行同步。
    HashMap 可以插⼊入键为 null 的 Entry。
    HashMap 的迭代器器是 fail-fast 迭代器器。
    HashMap 不不能保证随着时间的推移 Map 中的元素次序是不不变的。
    ConcurrentHashMap
  66. 存储结构
    ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采⽤用了了分段锁
    (Segment),每个分段锁维护着⼏几个桶(HashEntry),多个线程可以同时访问不不同分段锁上的桶,
    从⽽而使其并发度更更⾼高(并发度就是 Segment 的个数)。
    Segment 继承⾃自 ReentrantLock。
    static final class HashEntry {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry next;
    }
    默认的并发级别为 16,也就是说默认创建 16 个 Segment。
  67. size 操作
    每个 Segment 维护了了⼀一个 count 变量量来统计该 Segment 中的键值对个数。
    在执⾏行行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。
    ConcurrentHashMap 在执⾏行行 size 操作时先尝试不不加锁,如果连续两次不不加锁操作得到的结果⼀一致,那
    么可以认为这个结果是正确的。
    尝试次数使⽤用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。
    static final class Segment extends ReentrantLock implements
    Serializable {
    private static final long serialVersionUID = 2249069246763182397L;
    static final int MAX_SCAN_RETRIES =
    Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
    transient volatile HashEntry[] table;
    transient int count;
    transient int modCount;
    transient int threshold;
    final float loadFactor;
    }
    final Segment[] segments;
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    /**
  • The number of elements. Accessed only either within locks
  • or among other volatile reads that maintain visibility.
    /
    transient int count;
    如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
    /
    *
  • Number of unsynchronized retries in size and containsValue
  • methods before resorting to locking. This is used to avoid
  • unbounded retries if tables undergo continuous modification
  • which would make it impossible to obtain an accurate result.
    */
    static final int RETRIES_BEFORE_LOCK = 2;
    public int size() {
    // Try a few times to get accurate count. On failure due to
    // continuous async changes in table, resort to locking.
    final Segment[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
    long sum; // sum of modCounts
    long last = 0L; // previous sum
    int retries = -1; // first iteration isn’t retry
    try {
    for (; {
    // 超过尝试次数,则对每个 Segment 加锁
    if (retries++ == RETRIES_BEFORE_LOCK) {
    for (int j = 0; j < segments.length; ++j)
    ensureSegment(j).lock(); // force creation
    }
    sum = 0L;
    size = 0;
    overflow = false;
    for (int j = 0; j < segments.length; ++j) {
    Segment seg = segmentAt(segments, j);
    if (seg != null) {
    sum += seg.modCount;
    int c = seg.count;
    if (c < 0 || (size += c) < 0)
    overflow = true;
    }
    }
    // 连续两次得到的结果⼀一致,则认为这个结果是正确的
    if (sum == last)
    break;
    last = sum;
    }
  1. JDK 1.8 的改动
    JDK 1.7 使⽤用分段锁机制来实现并发更更新操作,核⼼心类为 Segment,它继承⾃自重⼊入锁 ReentrantLock,
    并发度与 Segment 数量量相等。
    JDK 1.8 使⽤用了了 CAS 操作来⽀支持更更⾼高的并发度,在 CAS 操作失败时使⽤用内置锁 synchronized。
    并且 JDK 1.8 的实现也在链表过⻓长时会转换为红⿊黑树。
    LinkedHashMap
    存储结构
    继承⾃自 HashMap,因此具有和 HashMap ⼀一样的快速查找特性。
    内部维护了了⼀一个双向链表,⽤用来维护插⼊入顺序或者 LRU 顺序。
    accessOrder 决定了了顺序,默认为 false,此时维护的是插⼊入顺序。
    } finally {
    if (retries > RETRIES_BEFORE_LOCK) {
    for (int j = 0; j < segments.length; ++j)
    segmentAt(segments, j).unlock();
    }
    }
    return overflow ? Integer.MAX_VALUE : size;
    }
    public class LinkedHashMap extends HashMap implements Map
    /**
  • The head (eldest) of the doubly linked list.
    /
    transient LinkedHashMap.Entry head;
    /
    *
  • The tail (youngest) of the doubly linked list.
    */
    transient LinkedHashMap.Entry tail;
    final boolean accessOrder;
    LinkedHashMap 最重要的是以下⽤用于维护顺序的函数,它们会在 put、get 等⽅方法中调⽤用。
    afterNodeAccess()
    当⼀一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部。也就是说指定为 LRU
    顺序之后,在每次访问⼀一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那
    么链表⾸首部就是最近最久未使⽤用的节点。
    afterNodeInsertion()
    在 put 等操作之后执⾏行行,当 removeEldestEntry() ⽅方法返回 true 时会移除最晚的节点,也就是链表⾸首部
    节点 first。
    evict 只有在构建 Map 的时候才为 false,在这⾥里里为 true。
    void afterNodeAccess(Node p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeAccess(Node e) { // move node to last
    LinkedHashMap.Entry last;
    if (accessOrder && (last = tail) != e) {
    LinkedHashMap.Entry p =
    (LinkedHashMap.Entry)e, b = p.before, a = p.after;
    p.after = null;
    if (b == null)
    head = a;
    else
    b.after = a;
    if (a != null)
    a.before = b;
    else
    last = b;
    if (last == null)
    head = p;
    else {
    p.before = last;
    last.after = p;
    }
    tail = p;
    ++modCount;
    }
    }
    removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个⽅方
    法的实现,这在实现 LRU 的缓存中特别有⽤用,通过移除最近最久未使⽤用的节点,从⽽而保证缓存空间⾜足
    够,并且缓存的数据都是热点数据。
    LRU 缓存
    以下是使⽤用 LinkedHashMap 实现的⼀一个 LRU 缓存:
    设定最⼤大缓存空间 MAX_ENTRIES 为 3;
    使⽤用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
    覆盖 removeEldestEntry() ⽅方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使⽤用的数据移
    除。
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
    K key = first.key;
    removeNode(hash(key), key, null, false, true);
    }
    }
    protected boolean removeEldestEntry(Map.Entry eldest) {
    return false;
    }
    class LRUCache extends LinkedHashMap {
    private static final int MAX_ENTRIES = 3;
    protected boolean removeEldestEntry(Map.Entry eldest) {
    return size() > MAX_ENTRIES;
    }
    LRUCache() {
    super(MAX_ENTRIES, 0.75f, true);
    }
    }
    WeakHashMap
    存储结构
    WeakHashMap 的 Entry 继承⾃自 WeakReference,被 WeakReference 关联的对象在下⼀一次垃圾回收时
    会被回收。
    WeakHashMap 主要⽤用来实现缓存,通过使⽤用 WeakHashMap 来引⽤用缓存对象,由 JVM 对这部分缓存
    进⾏行行回收。
    ConcurrentCache
    Tomcat 中的 ConcurrentCache 使⽤用了了 WeakHashMap 来实现缓存功能。
    ConcurrentCache 采取的是分代缓存:
    经常使⽤用的对象放⼊入 eden 中,eden 使⽤用 ConcurrentHashMap 实现,不不⽤用担⼼心会被回收(伊甸
    园);
    不不常⽤用的对象放⼊入 longterm,longterm 使⽤用 WeakHashMap 实现,这些⽼老老对象会被垃圾收集器器回
    收。
    当调⽤用 get() ⽅方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm
    获取到就把对象放⼊入 eden 中,从⽽而保证经常被访问的节点不不容易易被回收。
    当调⽤用 put() ⽅方法时,如果 eden 的⼤大⼩小超过了了 size,那么就将 eden 中的所有对象都放⼊入 longterm
    中,利利⽤用虚拟机回收掉⼀一部分不不经常使⽤用的对象。
    public static void main(String[] args) {
    LRUCache cache = new LRUCache<>();
    cache.put(1, “a”);
    cache.put(2, “b”);
    cache.put(3, “c”);
    cache.get(1);
    cache.put(4, “d”);
    System.out.println(cache.keySet());
    }
    [3, 1, 4]
    private static class Entry extends WeakReference implements
    Map.Entry
    public final class ConcurrentCache {
    参考资料料
    Eckel B. Java 编程思想 [M]. 机械⼯工业出版社, 2002.
    Java Collection Framework
    Iterator 模式
    Java 8 系列列之重新认识 HashMap
    What is difference between HashMap and Hashtable in Java?
    Java 集合之 HashMap
    The principle of ConcurrentHashMap analysis
    探索 ConcurrentHashMap ⾼高并发性的实现机制
    HashMap 相关⾯面试题及其解答
    Java 集合细节(⼆二):asList 的缺陷
    Java Collection Framework – The LinkedList Class
    private final int size;
    private final Map eden;
    private final Map longterm;
    public ConcurrentCache(int size) {
    this.size = size;
    this.eden = new ConcurrentHashMap<>(size);
    this.longterm = new WeakHashMap<>(size);
    }
    public V get(K k) {
    V v = this.eden.get(k);
    if (v == null) {
    v = this.longterm.get(k);
    if (v != null)
    this.eden.put(k, v);
    }
    return v;
    }
    public void put(K k, V v) {
    if (this.eden.size() >= size) {
    this.longterm.putAll(this.eden);
    this.eden.clear();
    }
    this.eden.put(k, v);
    }
    }
    Java 并发
    Java 并发
    ⼀一、使⽤用线程
    实现 Runnable 接⼝口
    实现 Callable 接⼝口
    继承 Thread 类
    实现接⼝口 VS 继承 Thread
    ⼆二、基础线程机制
    Executor
    Daemon
    sleep()
    yield()
    三、中断
    InterruptedException
    interrupted()
    Executor 的中断操作
    四、互斥同步
    synchronized
    ReentrantLock
    ⽐比较
    使⽤用选择
    五、线程之间的协作
    join()
    wait() notify() notifyAll()
    await() signal() signalAll()
    六、线程状态
    新建(NEW)
    可运⾏行行(RUNABLE)
    阻塞(BLOCKED)
    ⽆无限期等待(WAITING)
    限期等待(TIMED_WAITING)
    死亡(TERMINATED)
    七、J.U.C - AQS
    CountDownLatch
    CyclicBarrier
    Semaphore
    ⼋八、J.U.C - 其它组件
    FutureTask
    BlockingQueue
    ForkJoin
    九、线程不不安全示例例
    ⼗十、Java 内存模型
    主内存与⼯工作内存
    内存间交互操作
    内存模型三⼤大特性
    先⾏行行发⽣生原则
    ⼗十⼀一、线程安全
    不不可变
    互斥同步
    ⾮非阻塞同步
    ⽆无同步⽅方案
    ⼗十⼆二、锁优化
    ⾃自旋锁
    锁消除
    锁粗化
    轻量量级锁
    偏向锁
    ⼗十三、多线程开发良好的实践
    参考资料料
    ⼀一、使⽤用线程
    有三种使⽤用线程的⽅方法:
    实现 Runnable 接⼝口;
    实现 Callable 接⼝口;
    继承 Thread 类。
    实现 Runnable 和 Callable 接⼝口的类只能当做⼀一个可以在线程中运⾏行行的任务,不不是真正意义上的线程,
    因此最后还需要通过 Thread 来调⽤用。可以理理解为任务是通过线程驱动从⽽而执⾏行行的。
    实现 Runnable 接⼝口
    需要实现接⼝口中的 run() ⽅方法。
    使⽤用 Runnable 实例例再创建⼀一个 Thread 实例例,然后调⽤用 Thread 实例例的 start() ⽅方法来启动线程。
    实现 Callable 接⼝口
    与 Runnable 相⽐比,Callable 可以有返回值,返回值通过 FutureTask 进⾏行行封装。
    继承 Thread 类
    同样也是需要实现 run() ⽅方法,因为 Thread 类也实现了了 Runable 接⼝口。
    当调⽤用 start() ⽅方法启动⼀一个线程时,虚拟机会将该线程放⼊入就绪队列列中等待被调度,当⼀一个线程被调度
    时会执⾏行行该线程的 run() ⽅方法。
    public class MyRunnable implements Runnable {
    @Override
    public void run() {
    // …
    }
    }
    public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
    }
    public class MyCallable implements Callable {
    public Integer call() {
    return 123;
    }
    }
    public static void main(String[] args) throws ExecutionException,
    InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
    }
    实现接⼝口 VS 继承 Thread
    实现接⼝口会更更好⼀一些,因为:
    Java 不不⽀支持多重继承,因此继承了了 Thread 类就⽆无法继承其它类,但是可以实现多个接⼝口;
    类可能只要求可执⾏行行就⾏行行,继承整个 Thread 类开销过⼤大。
    ⼆二、基础线程机制
    Executor
    Executor 管理理多个异步任务的执⾏行行,⽽而⽆无需程序员显式地管理理线程的⽣生命周期。这⾥里里的异步是指多个任
    务的执⾏行行互不不⼲干扰,不不需要进⾏行行同步操作。
    主要有三种 Executor:
    CachedThreadPool:⼀一个任务创建⼀一个线程;
    FixedThreadPool:所有任务只能使⽤用固定⼤大⼩小的线程;
    SingleThreadExecutor:相当于⼤大⼩小为 1 的 FixedThreadPool。
    Daemon
    守护线程是程序运⾏行行时在后台提供服务的线程,不不属于程序中不不可或缺的部分。
    public class MyThread extends Thread {
    public void run() {
    // …
    }
    }
    public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
    }
    public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
    executorService.execute(new MyRunnable());
    }
    executorService.shutdown();
    }
    当所有⾮非守护线程结束时,程序也就终⽌止,同时会杀死所有守护线程。
    main() 属于⾮非守护线程。
    在线程启动之前使⽤用 setDaemon() ⽅方法可以将⼀一个线程设置为守护线程。
    sleep()
    Thread.sleep(millisec) ⽅方法会休眠当前正在执⾏行行的线程,millisec 单位为毫秒。
    sleep() 可能会抛出 InterruptedException,因为异常不不能跨线程传播回 main() 中,因此必须在本地进⾏行行
    处理理。线程中抛出的其它异常也同样需要在本地进⾏行行处理理。
    yield()
    对静态⽅方法 Thread.yield() 的调⽤用声明了了当前线程已经完成了了⽣生命周期中最重要的部分,可以切换给其
    它线程来执⾏行行。该⽅方法只是对线程调度器器的⼀一个建议,⽽而且也只是建议具有相同优先级的其它线程可以
    运⾏行行。
    三、中断
    ⼀一个线程执⾏行行完毕之后会⾃自动结束,如果在运⾏行行过程中发⽣生异常也会提前结束。
    InterruptedException
    public static void main(String[] args) {
    Thread thread = new Thread(new MyRunnable());
    thread.setDaemon(true);
    }
    public void run() {
    try {
    Thread.sleep(3000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    public void run() {
    Thread.yield();
    }
    通过调⽤用⼀一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者⽆无限期等待状态,
    那么就会抛出 InterruptedException,从⽽而提前结束该线程。但是不不能中断 I/O 阻塞和 synchronized 锁
    阻塞。
    对于以下代码,在 main() 中启动⼀一个线程之后再中断它,由于线程中调⽤用了了 Thread.sleep() ⽅方法,因此
    会抛出⼀一个 InterruptedException,从⽽而提前结束线程,不不执⾏行行之后的语句句。
    interrupted()
    如果⼀一个线程的 run() ⽅方法执⾏行行⼀一个⽆无限循环,并且没有执⾏行行 sleep() 等会抛出 InterruptedException 的
    操作,那么调⽤用线程的 interrupt() ⽅方法就⽆无法使线程提前结束。
    public class InterruptExample {
    private static class MyThread1 extends Thread {
    @Override
    public void run() {
    try {
    Thread.sleep(2000);
    System.out.println(“Thread run”);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
    }
    public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new MyThread1();
    thread1.start();
    thread1.interrupt();
    System.out.println(“Main run”);
    }
    Main run
    java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at InterruptExample.lambda$main 0 ( I n t e r r u p t E x a m p l e . j a v a : 5 ) a t I n t e r r u p t E x a m p l e 0(InterruptExample.java:5) at InterruptExample 0(InterruptExample.java:5)atInterruptExample$Lambda 1 / 713338599. r u n ( U n k n o w n S o u r c e ) a t j a v a . l a n g . T h r e a d . r u n ( T h r e a d . j a v a : 745 ) 但 是 调 ⽤ 用 i n t e r r u p t ( ) ⽅ 方 法 会 设 置 线 程 的 中 断 标 记 , 此 时 调 ⽤ 用 i n t e r r u p t e d ( ) ⽅ 方 法 会 返 回 t r u e 。 因 此 可 以 在 循 环 体 中 使 ⽤ 用 i n t e r r u p t e d ( ) ⽅ 方 法 来 判 断 线 程 是 否 处 于 中 断 状 态 , 从 ⽽ 而 提 前 结 束 线 程 。 E x e c u t o r 的 中 断 操 作 调 ⽤ 用 E x e c u t o r 的 s h u t d o w n ( ) ⽅ 方 法 会 等 待 线 程 都 执 ⾏ 行 行 完 毕 之 后 再 关 闭 , 但 是 如 果 调 ⽤ 用 的 是 s h u t d o w n N o w ( ) ⽅ 方 法 , 则 相 当 于 调 ⽤ 用 每 个 线 程 的 i n t e r r u p t ( ) ⽅ 方 法 。 以 下 使 ⽤ 用 L a m b d a 创 建 线 程 , 相 当 于 创 建 了 了 ⼀ 一 个 匿 匿 名 内 部 线 程 。 p u b l i c c l a s s I n t e r r u p t E x a m p l e p r i v a t e s t a t i c c l a s s M y T h r e a d 2 e x t e n d s T h r e a d @ O v e r r i d e p u b l i c v o i d r u n ( ) w h i l e ( ! i n t e r r u p t e d ( ) ) / / . . S y s t e m . o u t . p r i n t l n ( " T h r e a d e n d " ) ; p u b l i c s t a t i c v o i d m a i n ( S t r i n g [ ] a r g s ) t h r o w s I n t e r r u p t e d E x c e p t i o n T h r e a d t h r e a d 2 = n e w M y T h r e a d 2 ( ) ; t h r e a d 2. s t a r t ( ) ; t h r e a d 2. i n t e r r u p t ( ) ; T h r e a d e n d 如 果 只 想 中 断 E x e c u t o r 中 的 ⼀ 一 个 线 程 , 可 以 通 过 使 ⽤ 用 s u b m i t ( ) ⽅ 方 法 来 提 交 ⼀ 一 个 线 程 , 它 会 返 回 ⼀ 一 个 F u t u r e < ? > 对 象 , 通 过 调 ⽤ 用 该 对 象 的 c a n c e l ( t r u e ) ⽅ 方 法 就 可 以 中 断 线 程 。 四 、 互 斥 同 步 J a v a 提 供 了 了 两 种 锁 机 制 来 控 制 多 个 线 程 对 共 享 资 源 的 互 斥 访 问 , 第 ⼀ 一 个 是 J V M 实 现 的 s y n c h r o n i z e d , ⽽ 而 另 ⼀ 一 个 是 J D K 实 现 的 R e e n t r a n t L o c k 。 s y n c h r o n i z e d p u b l i c s t a t i c v o i d m a i n ( S t r i n g [ ] a r g s ) E x e c u t o r S e r v i c e e x e c u t o r S e r v i c e = E x e c u t o r s . n e w C a c h e d T h r e a d P o o l ( ) ; e x e c u t o r S e r v i c e . e x e c u t e ( ( ) − > t r y T h r e a d . s l e e p ( 2000 ) ; S y s t e m . o u t . p r i n t l n ( " T h r e a d r u n " ) ; c a t c h ( I n t e r r u p t e d E x c e p t i o n e ) e . p r i n t S t a c k T r a c e ( ) ; ) ; e x e c u t o r S e r v i c e . s h u t d o w n N o w ( ) ; S y s t e m . o u t . p r i n t l n ( " M a i n r u n " ) ; M a i n r u n j a v a . l a n g . I n t e r r u p t e d E x c e p t i o n : s l e e p i n t e r r u p t e d a t j a v a . l a n g . T h r e a d . s l e e p ( N a t i v e M e t h o d ) a t E x e c u t o r I n t e r r u p t E x a m p l e . l a m b d a 1/713338599.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) 但是调⽤用 interrupt() ⽅方法会设置线程的中断标记,此时调⽤用 interrupted() ⽅方法会返回 true。因此可以在 循环体中使⽤用 interrupted() ⽅方法来判断线程是否处于中断状态,从⽽而提前结束线程。 Executor 的中断操作 调⽤用 Executor 的 shutdown() ⽅方法会等待线程都执⾏行行完毕之后再关闭,但是如果调⽤用的是 shutdownNow() ⽅方法,则相当于调⽤用每个线程的 interrupt() ⽅方法。 以下使⽤用 Lambda 创建线程,相当于创建了了⼀一个匿匿名内部线程。 public class InterruptExample { private static class MyThread2 extends Thread { @Override public void run() { while (!interrupted()) { // .. } System.out.println("Thread end"); } } } public static void main(String[] args) throws InterruptedException { Thread thread2 = new MyThread2(); thread2.start(); thread2.interrupt(); } Thread end 如果只想中断 Executor 中的⼀一个线程,可以通过使⽤用 submit() ⽅方法来提交⼀一个线程,它会返回⼀一个 Future 对象,通过调⽤用该对象的 cancel(true) ⽅方法就可以中断线程。 四、互斥同步 Java 提供了了两种锁机制来控制多个线程对共享资源的互斥访问,第⼀一个是 JVM 实现的 synchronized, ⽽而另⼀一个是 JDK 实现的 ReentrantLock。 synchronized public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(() -> { try { Thread.sleep(2000); System.out.println("Thread run"); } catch (InterruptedException e) { e.printStackTrace(); } }); executorService.shutdownNow(); System.out.println("Main run"); } Main run java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at ExecutorInterruptExample.lambda 1/713338599.run(UnknownSource)atjava.lang.Thread.run(Thread.java:745)interrupt()线interrupted()true使interrupted()线线ExecutorExecutorshutdown()线shutdownNow()线interrupt()使Lambda线线publicclassInterruptExampleprivatestaticclassMyThread2extendsThread@Overridepublicvoidrun()while(!interrupted())//..System.out.println("Threadend");publicstaticvoidmain(String[]args)throwsInterruptedExceptionThreadthread2=newMyThread2();thread2.start();thread2.interrupt();ThreadendExecutor线使submit()线Future<?>cancel(true)线Java线访JVMsynchronizedJDKReentrantLocksynchronizedpublicstaticvoidmain(String[]args)ExecutorServiceexecutorService=Executors.newCachedThreadPool();executorService.execute(()>tryThread.sleep(2000);System.out.println("Threadrun");catch(InterruptedExceptione)e.printStackTrace(););executorService.shutdownNow();System.out.println("Mainrun");Mainrunjava.lang.InterruptedException:sleepinterruptedatjava.lang.Thread.sleep(NativeMethod)atExecutorInterruptExample.lambdamain 0 ( E x e c u t o r I n t e r r u p t E x a m p l e . j a v a : 9 ) a t E x e c u t o r I n t e r r u p t E x a m p l e 0(ExecutorInterruptExample.java:9) at ExecutorInterruptExample 0(ExecutorInterruptExample.java:9)atExecutorInterruptExample$Lambda$1/1160460865.run(Unknown Source)
    at
    java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1

at
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:
617)
at java.lang.Thread.run(Thread.java:745)
Future future = executorService.submit(() -> {
// …
});
future.cancel(true);

  1. 同步⼀一个代码块
    它只作⽤用于同⼀一个对象,如果调⽤用两个对象上的同步代码块,就不不会进⾏行行同步。
    对于以下代码,使⽤用 ExecutorService 执⾏行行了了两个线程,由于调⽤用的是同⼀一个对象的同步代码块,因此
    这两个线程会进⾏行行同步,当⼀一个线程进⼊入同步语句句块时,另⼀一个线程就必须等待。
    对于以下代码,两个线程调⽤用了了不不同对象的同步代码块,因此这两个线程就不不需要同步。从输出结果可
    以看出,两个线程交叉执⾏行行。
    public void func() {
    synchronized (this) {
    // …
    }
    }
    public class SynchronizedExample {
    public void func1() {
    synchronized (this) {
    for (int i = 0; i < 10; i++) {
    System.out.print(i + " ");
    }
    }
    }
    }
    public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
    }
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
  2. 同步⼀一个⽅方法
    它和同步代码块⼀一样,作⽤用于同⼀一个对象。
  3. 同步⼀一个类
    作⽤用于整个类,也就是说两个线程调⽤用同⼀一个类的不不同对象上的这种同步语句句,也会进⾏行行同步。
    public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
    }
    0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
    public synchronized void func () {
    // …
    }
    public void func() {
    synchronized (SynchronizedExample.class) {
    // …
    }
    }
    public class SynchronizedExample {
    public void func2() {
    synchronized (SynchronizedExample.class) {
    for (int i = 0; i < 10; i++) {
    System.out.print(i + " ");
    }
    }
    }
    }
  4. 同步⼀一个静态⽅方法
    作⽤用于整个类。
    ReentrantLock
    ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
    public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
    }
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
    public synchronized static void fun() {
    // …
    }
    public class LockExample {
    private Lock lock = new ReentrantLock();
    public void func() {
    lock.lock();
    try {
    for (int i = 0; i < 10; i++) {
    System.out.print(i + " ");
    }
    } finally {
    lock.unlock(); // 确保释放锁,从⽽而避免发⽣生死锁。
    }
    }
    }
    ⽐比较
  5. 锁的实现
    synchronized 是 JVM 实现的,⽽而 ReentrantLock 是 JDK 实现的。
  6. 性能
    新版本 Java 对 synchronized 进⾏行行了了很多优化,例例如⾃自旋锁等,synchronized 与 ReentrantLock ⼤大致
    相同。
  7. 等待可中断
    当持有锁的线程⻓长期不不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理理其他事情。
    ReentrantLock 可中断,⽽而 synchronized 不不⾏行行。
  8. 公平锁
    公平锁是指多个线程在等待同⼀一个锁时,必须按照申请锁的时间顺序来依次获得锁。
    synchronized 中的锁是⾮非公平的,ReentrantLock 默认情况下也是⾮非公平的,但是也可以是公平的。
  9. 锁绑定多个条件
    ⼀一个 ReentrantLock 可以同时绑定多个 Condition 对象。
    使⽤用选择
    除⾮非需要使⽤用 ReentrantLock 的⾼高级功能,否则优先使⽤用 synchronized。这是因为 synchronized 是
    JVM 实现的⼀一种锁机制,JVM 原⽣生地⽀支持它,⽽而 ReentrantLock 不不是所有的 JDK 版本都⽀支持。并且使
    ⽤用 synchronized 不不⽤用担⼼心没有释放锁⽽而导致死锁问题,因为 JVM 会确保锁的释放。
    五、线程之间的协作
    public static void main(String[] args) {
    LockExample lockExample = new LockExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> lockExample.func());
    executorService.execute(() -> lockExample.func());
    }
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
    当多个线程可以⼀一起⼯工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线
    程进⾏行行协调。
    join()
    在线程中调⽤用另⼀一个线程的 join() ⽅方法,会将当前线程挂起,⽽而不不是忙等待,直到⽬目标线程结束。
    对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调⽤用了了 a 线程的 join() ⽅方法,b 线程会等待 a
    线程结束才继续执⾏行行,因此最后能够保证 a 线程的输出先于 b 线程的输出。
    public class JoinExample {
    private class A extends Thread {
    @Override
    public void run() {
    System.out.println(“A”);
    }
    }
    private class B extends Thread {
    private A a;
    B(A a) {
    this.a = a;
    }
    @Override
    public void run() {
    try {
    a.join();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(“B”);
    }
    }
    public void test() {
    A a = new A();
    B b = new B(a);
    b.start();
    a.start();
    }
    wait() notify() notifyAll()
    调⽤用 wait() 使得线程等待某个条件满⾜足,线程在等待时会被挂起,当其他线程的运⾏行行使得这个条件满⾜足
    时,其它线程会调⽤用 notify() 或者 notifyAll() 来唤醒挂起的线程。
    它们都属于 Object 的⼀一部分,⽽而不不属于 Thread。
    只能⽤用在同步⽅方法或者同步控制块中使⽤用,否则会在运⾏行行时抛出 IllegalMonitorStateException。
    使⽤用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就⽆无法进⼊入对象的同
    步⽅方法或者同步控制块中,那么就⽆无法执⾏行行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
    }
    public static void main(String[] args) {
    JoinExample example = new JoinExample();
    example.test();
    }
    A
    B
    public class WaitNotifyExample {
    public synchronized void before() {
    System.out.println(“before”);
    notifyAll();
    }
    public synchronized void after() {
    try {
    wait();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(“after”);
    }
    }
    wait() 和 sleep() 的区别
    wait() 是 Object 的⽅方法,⽽而 sleep() 是 Thread 的静态⽅方法;
    wait() 会释放锁,sleep() 不不会。
    await() signal() signalAll()
    java.util.concurrent 类库中提供了了 Condition 类来实现线程之间的协调,可以在 Condition 上调⽤用
    await() ⽅方法使线程等待,其它线程调⽤用 signal() 或 signalAll() ⽅方法唤醒等待的线程。
    相⽐比于 wait() 这种等待⽅方式,await() 可以指定等待的条件,因此更更加灵活。
    使⽤用 Lock 来获取⼀一个 Condition 对象。
    public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    WaitNotifyExample example = new WaitNotifyExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
    }
    before
    after
    public class AwaitSignalExample {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    public void before() {
    lock.lock();
    try {
    System.out.println(“before”);
    condition.signalAll();
    } finally {
    lock.unlock();
    }
    }
    public void after() {
    lock.lock();
    try {
    condition.await();
    六、线程状态
    ⼀一个线程只能处于⼀一种状态,并且这⾥里里的线程状态特指 Java 虚拟机的线程状态,不不能反映线程在特定
    操作系统下的状态。
    新建(NEW)
    创建后尚未启动。
    可运⾏行行(RUNABLE)
    正在 Java 虚拟机中运⾏行行。但是在操作系统层⾯面,它可能处于运⾏行行状态,也可能等待资源调度(例例如处
    理理器器资源),资源调度完成就进⼊入运⾏行行状态。所以该状态的可运⾏行行是指可以被运⾏行行,具体有没有运⾏行行要
    看底层操作系统的资源调度。
    阻塞(BLOCKED)
    请求获取 monitor lock 从⽽而进⼊入 synchronized 函数或者代码块,但是其它线程已经占⽤用了了该 monitor
    lock,所以出于阻塞状态。要结束该状态进⼊入从⽽而 RUNABLE 需要其他线程释放 monitor lock。
    ⽆无限期等待(WAITING)
    等待其它线程显式地唤醒。
    System.out.println(“after”);
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    lock.unlock();
    }
    }
    }
    public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    AwaitSignalExample example = new AwaitSignalExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
    }
    before
    after
    进⼊入⽅方法退出⽅方法
    没有设置 Timeout 参数的 Object.wait() ⽅方法Object.notify() / Object.notifyAll()
    没有设置 Timeout 参数的 Thread.join() ⽅方法被调⽤用的线程执⾏行行完毕
    LockSupport.park() ⽅方法LockSupport.unpark(Thread)
    进⼊入⽅方法退出⽅方法
    Thread.sleep() ⽅方法时间结束
    设置了了 Timeout 参数的 Object.wait() ⽅方法时间结束 / Object.notify() / Object.notifyAll()
    设置了了 Timeout 参数的 Thread.join() ⽅方法时间结束 / 被调⽤用的线程执⾏行行完毕
    LockSupport.parkNanos() ⽅方法LockSupport.unpark(Thread)
    LockSupport.parkUntil() ⽅方法LockSupport.unpark(Thread)
    阻塞和等待的区别在于,阻塞是被动的,它是在等待获取 monitor lock。⽽而等待是主动的,通过调⽤用
    Object.wait() 等⽅方法进⼊入。
    限期等待(TIMED_WAITING)
    ⽆无需等待其它线程显式地唤醒,在⼀一定时间之后会被系统⾃自动唤醒。
    调⽤用 Thread.sleep() ⽅方法使线程进⼊入限期等待状态时,常常⽤用“使⼀一个线程睡眠”进⾏行行描述。调⽤用
    Object.wait() ⽅方法使线程进⼊入限期等待或者⽆无限期等待时,常常⽤用“挂起⼀一个线程”进⾏行行描述。睡眠和挂
    起是⽤用来描述⾏行行为,⽽而阻塞和等待⽤用来描述状态。
    死亡(TERMINATED)
    可以是线程结束任务之后⾃自⼰己结束,或者产⽣生了了异常⽽而结束。
    Java SE 9 Enum Thread.State
    七、J.U.C - AQS
    java.util.concurrent(J.U.C)⼤大⼤大提⾼高了了并发性能,AQS 被认为是 J.U.C 的核⼼心。
    CountDownLatch
    ⽤用来控制⼀一个或者多个线程等待多个线程。
    维护了了⼀一个计数器器 cnt,每次调⽤用 countDown() ⽅方法会让计数器器的值减 1,减到 0 的时候,那些因为调
    ⽤用 await() ⽅方法⽽而在等待的线程就会被唤醒。
    CyclicBarrier
    ⽤用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执⾏行行。
    和 CountdownLatch 相似,都是通过维护计数器器来实现的。线程执⾏行行 await() ⽅方法之后计数器器会减 1,
    并进⾏行行等待,直到计数器器为 0,所有调⽤用 await() ⽅方法⽽而在等待的线程才能继续执⾏行行。
    public class CountdownLatchExample {
    public static void main(String[] args) throws InterruptedException {
    final int totalThread = 10;
    CountDownLatch countDownLatch = new CountDownLatch(totalThread);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < totalThread; i++) {
    executorService.execute(() -> {
    System.out.print(“run…”);
    countDownLatch.countDown();
    });
    }
    countDownLatch.await();
    System.out.println(“end”);
    executorService.shutdown();
    }
    }
    run…run…run…run…run…run…run…run…run…run…end
    CyclicBarrier 和 CountdownLatch 的⼀一个区别是,CyclicBarrier 的计数器器通过调⽤用 reset() ⽅方法可以循
    环使⽤用,所以它才叫做循环屏障。
    CyclicBarrier 有两个构造函数,其中 parties 指示计数器器的初始值,barrierAction 在所有线程都到达屏
    障的时候会执⾏行行⼀一次。
    public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
    }
    public CyclicBarrier(int parties) {
    this(parties, null);
    }
    public class CyclicBarrierExample {
    public static void main(String[] args) {
    final int totalThread = 10;
    CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < totalThread; i++) {
    executorService.execute(() -> {
    System.out.print(“before…”);
    try {
    cyclicBarrier.await();
    } catch (InterruptedException | BrokenBarrierException e) {
    e.printStackTrace();
    }
    Semaphore
    Semaphore 类似于操作系统中的信号量量,可以控制对互斥资源的访问线程数。
    以下代码模拟了了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。
    ⼋八、J.U.C - 其它组件
    System.out.print(“after…”);
    });
    }
    executorService.shutdown();
    }
    }
    before…before…before…before…before…before…before…before…before…bef
    ore…after…after…after…after…after…after…after…after…after…after…
    public class SemaphoreExample {
    public static void main(String[] args) {
    final int clientCount = 3;
    final int totalRequestCount = 10;
    Semaphore semaphore = new Semaphore(clientCount);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < totalRequestCount; i++) {
    executorService.execute(()->{
    try {
    semaphore.acquire();
    System.out.print(semaphore.availablePermits() + " ");
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    semaphore.release();
    }
    });
    }
    executorService.shutdown();
    }
    }
    2 1 2 2 2 2 2 1 2 2
    FutureTask
    在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进⾏行行封装。FutureTask 实现了了
    RunnableFuture 接⼝口,该接⼝口继承⾃自 Runnable 和 Future 接⼝口,这使得 FutureTask 既可以当做⼀一
    个任务执⾏行行,也可以有返回值。
    FutureTask 可⽤用于异步获取执⾏行行结果或取消执⾏行行任务的场景。当⼀一个计算任务需要执⾏行行很⻓长时间,那么
    就可以⽤用 FutureTask 来封装这个任务,主线程在完成⾃自⼰己的任务之后再去获取结果。
    public class FutureTask implements RunnableFuture
    public interface RunnableFuture extends Runnable, Future
    public class FutureTaskExample {
    public static void main(String[] args) throws ExecutionException,
    InterruptedException {
    FutureTask futureTask = new FutureTask(new
    Callable() {
    @Override
    public Integer call() throws Exception {
    int result = 0;
    for (int i = 0; i < 100; i++) {
    Thread.sleep(10);
    result += i;
    }
    return result;
    }
    });
    Thread computeThread = new Thread(futureTask);
    computeThread.start();
    Thread otherThread = new Thread(() -> {
    System.out.println(“other task is running…”);
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    });
    otherThread.start();
    System.out.println(futureTask.get());
    BlockingQueue
    java.util.concurrent.BlockingQueue 接⼝口有以下阻塞队列列的实现:
    FIFO 队列列 :LinkedBlockingQueue、ArrayBlockingQueue(固定⻓长度)
    优先级队列列 :PriorityBlockingQueue
    提供了了阻塞的 take() 和 put() ⽅方法:如果队列列为空 take() 将阻塞,直到队列列中有内容;如果队列列为满
    put() 将阻塞,直到队列列有空闲位置。
    使⽤用 BlockingQueue 实现⽣生产者消费者问题
    }
    }
    other task is running…
    4950
    public class ProducerConsumer {
    private static BlockingQueue queue = new ArrayBlockingQueue<>
    (5);
    private static class Producer extends Thread {
    @Override
    public void run() {
    try {
    queue.put(“product”);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.print(“produce…”);
    }
    }
    private static class Consumer extends Thread {
    @Override
    public void run() {
    try {
    String product = queue.take();
    } catch (InterruptedException e) {
    e.printStackTrace();
    ForkJoin
    主要⽤用于并⾏行行计算中,和 MapReduce 原理理类似,都是把⼤大的计算任务拆分成多个⼩小任务并⾏行行计算。
    }
    System.out.print(“consume…”);
    }
    }
    }
    public static void main(String[] args) {
    for (int i = 0; i < 2; i++) {
    Producer producer = new Producer();
    producer.start();
    }
    for (int i = 0; i < 5; i++) {
    Consumer consumer = new Consumer();
    consumer.start();
    }
    for (int i = 0; i < 3; i++) {
    Producer producer = new Producer();
    producer.start();
    }
    }
    produce…produce…consume…consume…produce…consume…produce…consume…pro
    duce…consume…
    public class ForkJoinExample extends RecursiveTask {
    private final int threshold = 5;
    private int first;
    private int last;
    public ForkJoinExample(int first, int last) {
    this.first = first;
    this.last = last;
    }
    @Override
    protected Integer compute() {
    int result = 0;
    ForkJoin 使⽤用 ForkJoinPool 来启动,它是⼀一个特殊的线程池,线程数量量取决于 CPU 核数。
    ForkJoinPool 实现了了⼯工作窃取算法来提⾼高 CPU 的利利⽤用率。每个线程都维护了了⼀一个双端队列列,⽤用来存储
    需要执⾏行行的任务。⼯工作窃取算法允许空闲的线程从其它线程的双端队列列中窃取⼀一个任务来执⾏行行。窃取的
    任务必须是最晚的任务,避免和队列列所属线程发⽣生竞争。例例如下图中,Thread2 从 Thread1 的队列列中拿
    出最晚的 Task1 任务,Thread1 会拿出 Task2 来执⾏行行,这样就避免发⽣生竞争。但是如果队列列中只有⼀一个
    任务时还是会发⽣生竞争。
    if (last - first <= threshold) {
    // 任务⾜足够⼩小则直接计算
    for (int i = first; i <= last; i++) {
    result += i;
    }
    } else {
    // 拆分成⼩小任务
    int middle = first + (last - first) / 2;
    ForkJoinExample leftTask = new ForkJoinExample(first, middle);
    ForkJoinExample rightTask = new ForkJoinExample(middle + 1,
    last);
    leftTask.fork();
    rightTask.fork();
    result = leftTask.join() + rightTask.join();
    }
    return result;
    }
    }
    public static void main(String[] args) throws ExecutionException,
    InterruptedException {
    ForkJoinExample example = new ForkJoinExample(1, 10000);
    ForkJoinPool forkJoinPool = new ForkJoinPool();
    Future result = forkJoinPool.submit(example);
    System.out.println(result.get());
    }
    public class ForkJoinPool extends AbstractExecutorService
    九、线程不不安全示例例
    如果多个线程对同⼀一个共享数据进⾏行行访问⽽而不不采取同步操作的话,那么操作的结果是不不⼀一致的。
    以下代码演示了了 1000 个线程同时对 cnt 执⾏行行⾃自增操作,操作结束之后它的值有可能⼩小于 1000。
    public class ThreadUnsafeExample {
    private int cnt = 0;
    public void add() {
    cnt++;
    }
    public int get() {
    return cnt;
    }
    }
    public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    ThreadUnsafeExample example = new ThreadUnsafeExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
    executorService.execute(() -> {
    example.add();
    countDownLatch.countDown();
    });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
    }
    ⼗十、Java 内存模型
    Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达
    到⼀一致的内存访问效果。
    主内存与⼯工作内存
    处理理器器上的寄存器器的读写的速度⽐比内存快⼏几个数量量级,为了了解决这种速度⽭矛盾,在它们之间加⼊入了了⾼高速
    缓存。
    加⼊入⾼高速缓存带来了了⼀一个新的问题:缓存⼀一致性。如果多个缓存共享同⼀一块主内存区域,那么多个缓存
    的数据可能会不不⼀一致,需要⼀一些协议来解决这个问题。
    所有的变量量都存储在主内存中,每个线程还有⾃自⼰己的⼯工作内存,⼯工作内存存储在⾼高速缓存或者寄存器器
    中,保存了了该线程使⽤用的变量量的主内存副本拷⻉贝。
    线程只能直接操作⼯工作内存中的变量量,不不同线程之间的变量量值传递需要通过主内存来完成。
    997
    内存间交互操作
    Java 内存模型定义了了 8 个操作来完成主内存和⼯工作内存的交互操作。
    read:把⼀一个变量量的值从主内存传输到⼯工作内存中
    load:在 read 之后执⾏行行,把 read 得到的值放⼊入⼯工作内存的变量量副本中
    use:把⼯工作内存中⼀一个变量量的值传递给执⾏行行引擎
    assign:把⼀一个从执⾏行行引擎接收到的值赋给⼯工作内存的变量量
    store:把⼯工作内存的⼀一个变量量的值传送到主内存中
    write:在 store 之后执⾏行行,把 store 得到的值放⼊入主内存的变量量中
    lock:作⽤用于主内存的变量量
    unlock
    内存模型三⼤大特性
  10. 原⼦子性
    Java 内存模型保证了了 read、load、use、assign、store、write、lock 和 unlock 操作具有原⼦子性,例例如
    对⼀一个 int 类型的变量量执⾏行行 assign 赋值操作,这个操作就是原⼦子性的。但是 Java 内存模型允许虚拟机
    将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进⾏行行,即
    load、store、read 和 write 操作可以不不具备原⼦子性。
    有⼀一个错误认识就是,int 等原⼦子性的类型在多线程环境中不不会出现线程安全问题。前⾯面的线程不不安全示
    例例代码中,cnt 属于 int 类型变量量,1000 个线程对它进⾏行行⾃自增操作之后,得到的值为 997 ⽽而不不是
    1000。
    为了了⽅方便便讨论,将内存间的交互操作简化为 3 个:load、assign、store。
    下图演示了了两个线程同时对 cnt 进⾏行行操作,load、assign、store 这⼀一系列列操作整体上看不不具备原⼦子性,
    那么在 T1 修改 cnt 并且还没有将修改后的值写⼊入主内存,T2 依然可以读⼊入旧值。可以看出,这两个线
    程虽然执⾏行行了了两次⾃自增运算,但是主内存中 cnt 的值最后为 1 ⽽而不不是 2。因此对 int 类型读写操作满⾜足
    原⼦子性只是说明 load、assign、store 这些单个操作具备原⼦子性。
    AtomicInteger 能保证多个线程修改的原⼦子性。
    使⽤用 AtomicInteger 重写之前线程不不安全的代码之后得到以下线程安全实现:
    public class AtomicExample {
    private AtomicInteger cnt = new AtomicInteger();
    public void add() {
    cnt.incrementAndGet();
    }
    public int get() {
    return cnt.get();
    }
    }
    public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicExample example = new AtomicExample(); // 只修改这条语句句
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
    除了了使⽤用原⼦子类之外,也可以使⽤用 synchronized 互斥锁来保证操作的原⼦子性。它对应的内存间交互操
    作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
    executorService.execute(() -> {
    example.add();
    countDownLatch.countDown();
    });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
    }
    1000
    public class AtomicSynchronizedExample {
    private int cnt = 0;
    public synchronized void add() {
    cnt++;
    }
    public synchronized int get() {
    return cnt;
    }
    }
    public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    AtomicSynchronizedExample example = new AtomicSynchronizedExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
    executorService.execute(() -> {
    example.add();
    countDownLatch.countDown();
    });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
    }
  11. 可⻅见性
    可⻅见性指当⼀一个线程修改了了共享变量量的值,其它线程能够⽴立即得知这个修改。Java 内存模型是通过在变
    量量修改后将新值同步回主内存,在变量量读取前从主内存刷新变量量值来实现可⻅见性的。
    主要有三种实现可⻅见性的⽅方式:
    volatile
    synchronized,对⼀一个变量量执⾏行行 unlock 操作之前,必须把变量量值同步回主内存。
    final,被 final 关键字修饰的字段在构造器器中⼀一旦初始化完成,并且没有发⽣生 this 逃逸(其它线程通
    过 this 引⽤用访问到初始化了了⼀一半的对象),那么其它线程就能看⻅见 final 字段的值。
    对前⾯面的线程不不安全示例例中的 cnt 变量量使⽤用 volatile 修饰,不不能解决线程不不安全问题,因为 volatile 并不不
    能保证操作的原⼦子性。
  12. 有序性
    有序性是指:在本线程内观察,所有操作都是有序的。在⼀一个线程观察另⼀一个线程,所有操作都是⽆无序
    的,⽆无序是因为发⽣生了了指令重排序。在 Java 内存模型中,允许编译器器和处理理器器对指令进⾏行行重排序,重
    排序过程不不会影响到单线程程序的执⾏行行,却会影响到多线程并发执⾏行行的正确性。
    volatile 关键字通过添加内存屏障的⽅方式来禁⽌止指令重排,即重排序时不不能把后⾯面的指令放到内存屏障
    之前。
    也可以通过 synchronized 来保证有序性,它保证每个时刻只有⼀一个线程执⾏行行同步代码,相当于是让线
    程顺序执⾏行行同步代码。
    先⾏行行发⽣生原则
    上⾯面提到了了可以⽤用 volatile 和 synchronized 来保证有序性。除此之外,JVM 还规定了了先⾏行行发⽣生原则,
    让⼀一个操作⽆无需控制就能先于另⼀一个操作完成。
  13. 单⼀一线程原则
    Single Thread rule
    在⼀一个线程内,在程序前⾯面的操作先⾏行行发⽣生于后⾯面的操作。
    1000
  14. 管程锁定规则
    Monitor Lock Rule
    ⼀一个 unlock 操作先⾏行行发⽣生于后⾯面对同⼀一个锁的 lock 操作。
  15. volatile 变量量规则
    Volatile Variable Rule
    对⼀一个 volatile 变量量的写操作先⾏行行发⽣生于后⾯面对这个变量量的读操作。
  16. 线程启动规则
    Thread Start Rule
    Thread 对象的 start() ⽅方法调⽤用先⾏行行发⽣生于此线程的每⼀一个动作。
  17. 线程加⼊入规则
    Thread Join Rule
    Thread 对象的结束先⾏行行发⽣生于 join() ⽅方法返回。
  18. 线程中断规则
    Thread Interruption Rule
    对线程 interrupt() ⽅方法的调⽤用先⾏行行发⽣生于被中断线程的代码检测到中断事件的发⽣生,可以通过
    interrupted() ⽅方法检测到是否有中断发⽣生。
  19. 对象终结规则
    Finalizer Rule
    ⼀一个对象的初始化完成(构造函数执⾏行行结束)先⾏行行发⽣生于它的 finalize() ⽅方法的开始。
  20. 传递性
    Transitivity
    如果操作 A 先⾏行行发⽣生于操作 B,操作 B 先⾏行行发⽣生于操作 C,那么操作 A 先⾏行行发⽣生于操作 C。
    ⼗十⼀一、线程安全
    多个线程不不管以何种⽅方式访问某个类,并且在主调代码中不不需要进⾏行行同步,都能表现正确的⾏行行为。
    线程安全有以下⼏几种实现⽅方式:
    不不可变
    不不可变(Immutable)的对象⼀一定是线程安全的,不不需要再采取任何的线程安全保障措施。只要⼀一个不不
    可变的对象被正确地构建出来,永远也不不会看到它在多个线程之中处于不不⼀一致的状态。多线程环境下,
    应当尽量量使对象成为不不可变,来满⾜足线程安全。
    不不可变的类型:
    final 关键字修饰的基本数据类型
    String
    枚举类型
    Number 部分⼦子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等⼤大数据类
    型。但同为 Number 的原⼦子类 AtomicInteger 和 AtomicLong 则是可变的。
    对于集合类型,可以使⽤用 Collections.unmodifiableXXX() ⽅方法来获取⼀一个不不可变的集合。
    Collections.unmodifiableXXX() 先对原始的集合进⾏行行拷⻉贝,需要对集合进⾏行行修改的⽅方法都直接抛出异
    常。
    互斥同步
    synchronized 和 ReentrantLock。
    ⾮非阻塞同步
    互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
    互斥同步属于⼀一种悲观的并发策略略,总是认为只要不不去做正确的同步措施,那就肯定会出现问题。⽆无论
    共享数据是否真的会出现竞争,它都要进⾏行行加锁(这⾥里里讨论的是概念模型,实际上虚拟机会优化掉很⼤大
    ⼀一部分不不必要的加锁)、⽤用户态核⼼心态转换、维护锁计数器器和检查是否有被阻塞的线程需要唤醒等操
    作。
    随着硬件指令集的发展,我们可以使⽤用基于冲突检测的乐观并发策略略:先进⾏行行操作,如果没有其它线程
    争⽤用共享数据,那操作就成功了了,否则采取补偿措施(不不断地重试,直到成功为⽌止)。这种乐观的并发
    策略略的许多实现都不不需要将线程阻塞,因此这种同步操作称为⾮非阻塞同步。
  21. CAS
    乐观锁需要操作和冲突检测这两个步骤具备原⼦子性,这⾥里里就不不能再使⽤用互斥同步来保证了了,只能靠硬件
    来完成。硬件⽀支持的原⼦子性操作最典型的是:⽐比较并交换(Compare-and-Swap,CAS)。CAS 指令
    需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执⾏行行操作时,只有当 V 的值等于
    A,才将 V 的值更更新为 B。
    public class ImmutableExample {
    public static void main(String[] args) {
    Map map = new HashMap<>();
    Map unmodifiableMap =
    Collections.unmodifiableMap(map);
    unmodifiableMap.put(“a”, 1);
    }
    }
    Exception in thread “main” java.lang.UnsupportedOperationException
    at java.util.Collections$UnmodifiableMap.put(Collections.java:1457)
    at ImmutableExample.main(ImmutableExample.java:9)
    public V put(K key, V value) {
    throw new UnsupportedOperationException();
    }
  22. AtomicInteger
    J.U.C 包⾥里里⾯面的整数原⼦子类 AtomicInteger 的⽅方法调⽤用了了 Unsafe 类的 CAS 操作。
    以下代码使⽤用了了 AtomicInteger 执⾏行行了了⾃自增的操作。
    以下代码是 incrementAndGet() 的源码,它调⽤用了了 Unsafe 的 getAndAddInt() 。
    以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏
    移,var4 指示操作需要加的数值,这⾥里里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调⽤用
    compareAndSwapInt() 来进⾏行行 CAS ⽐比较,如果该字段内存地址中的值等于 var5,那么就更更新内存地址
    为 var1+var2 的变量量为 var5+var4。
    可以看到 getAndAddInt() 在⼀一个循环中进⾏行行,发⽣生冲突的做法是不不断的进⾏行行重试。
  23. ABA
    如果⼀一个变量量初次读取的时候是 A 值,它的值被改成了了 B,后来⼜又被改回为 A,那 CAS 操作就会误认
    为它从来没有被改变过。
    J.U.C 包提供了了⼀一个带有标记的原⼦子引⽤用类 AtomicStampedReference 来解决这个问题,它可以通过控
    制变量量值的版本来保证 CAS 的正确性。⼤大部分情况下 ABA 问题不不会影响程序并发的正确性,如果需要
    解决 ABA 问题,改⽤用传统的互斥同步可能会⽐比原⼦子类更更⾼高效。
    private AtomicInteger cnt = new AtomicInteger();
    public void add() {
    cnt.incrementAndGet();
    }
    public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
    var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
    }
    ⽆无同步⽅方案
    要保证线程安全,并不不是⼀一定就要进⾏行行同步。如果⼀一个⽅方法本来就不不涉及共享数据,那它⾃自然就⽆无须任
    何同步措施去保证正确性。
  24. 栈封闭
    多个线程访问同⼀一个⽅方法的局部变量量时,不不会出现线程安全问题,因为局部变量量存储在虚拟机栈中,属
    于线程私有的。
  25. 线程本地存储(Thread Local Storage)
    如果⼀一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同⼀一个
    线程中执⾏行行。如果能保证,我们就可以把共享数据的可⻅见范围限制在同⼀一个线程之内,这样,⽆无须同步
    也能保证线程之间不不出现数据争⽤用的问题。
    符合这种特点的应⽤用并不不少⻅见,⼤大部分使⽤用消费队列列的架构模式(如“⽣生产者-消费者”模式)都会将产品
    的消费过程尽量量在⼀一个线程中消费完。其中最重要的⼀一个应⽤用实例例就是经典 Web 交互模型中的“⼀一个请
    求对应⼀一个服务器器线程”(Thread-per-Request)的处理理⽅方式,这种处理理⽅方式的⼴广泛应⽤用使得很多 Web
    服务端应⽤用都可以使⽤用线程本地存储来解决线程安全问题。
    可以使⽤用 java.lang.ThreadLocal 类来实现线程本地存储功能。
    public class StackClosedExample {
    public void add100() {
    int cnt = 0;
    for (int i = 0; i < 100; i++) {
    cnt++;
    }
    System.out.println(cnt);
    }
    }
    public static void main(String[] args) {
    StackClosedExample example = new StackClosedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> example.add100());
    executorService.execute(() -> example.add100());
    executorService.shutdown();
    }
    100
    100
    对于以下代码,thread1 中设置 threadLocal 为 1,⽽而 thread2 设置 threadLocal 为 2。过了了⼀一段时间之
    后,thread1 读取 threadLocal 依然是 1,不不受 thread2 的影响。
    为了了理理解 ThreadLocal,先看以下代码:
    public class ThreadLocalExample {
    public static void main(String[] args) {
    ThreadLocal threadLocal = new ThreadLocal();
    Thread thread1 = new Thread(() -> {
    threadLocal.set(1);
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(threadLocal.get());
    threadLocal.remove();
    });
    Thread thread2 = new Thread(() -> {
    threadLocal.set(2);
    threadLocal.remove();
    });
    thread1.start();
    thread2.start();
    }
    }
    1
    public class ThreadLocalExample1 {
    public static void main(String[] args) {
    ThreadLocal threadLocal1 = new ThreadLocal();
    ThreadLocal threadLocal2 = new ThreadLocal();
    Thread thread1 = new Thread(() -> {
    threadLocal1.set(1);
    threadLocal2.set(1);
    });
    Thread thread2 = new Thread(() -> {
    threadLocal1.set(2);
    threadLocal2.set(2);
    });
    thread1.start();
    它所对应的底层结构图为:
    每个 Thread 都有⼀一个 ThreadLocal.ThreadLocalMap 对象。
    当调⽤用⼀一个 ThreadLocal 的 set(T value) ⽅方法时,先得到当前线程的 ThreadLocalMap 对象,然后将
    ThreadLocal->value 键值对插⼊入到该 Map 中。
    get() ⽅方法类似。
    thread2.start();
    }
    }
    /* ThreadLocal values pertaining to this thread. This map is maintained
  • by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
    map.set(this, value);
    else
    createMap(t, value);
    }
    ThreadLocal 从理理论上讲并不不是⽤用来解决多线程并发问题的,因为根本不不存在多线程竞争。
    在⼀一些场景 (尤其是使⽤用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致
    ThreadLocal 有内存泄漏漏的情况,应该尽可能在每次使⽤用 ThreadLocal 后⼿手动调⽤用 remove(),以避免出
    现 ThreadLocal 经典的内存泄漏漏甚⾄至是造成⾃自身业务混乱的⻛风险。
  1. 可重⼊入代码(Reentrant Code)
    这种代码也叫做纯代码(Pure Code),可以在代码执⾏行行的任何时刻中断它,转⽽而去执⾏行行另外⼀一段代码
    (包括递归调⽤用它本身),⽽而在控制权返回后,原来的程序不不会出现任何错误。
    可重⼊入代码有⼀一些共同的特征,例例如不不依赖存储在堆上的数据和公⽤用的系统资源、⽤用到的状态量量都由参
    数中传⼊入、不不调⽤用⾮非可重⼊入的⽅方法等。
    ⼗十⼆二、锁优化
    这⾥里里的锁优化主要是指 JVM 对 synchronized 的优化。
    ⾃自旋锁
    互斥同步进⼊入阻塞状态的开销都很⼤大,应该尽量量避免。在许多应⽤用中,共享数据的锁定状态只会持续很
    短的⼀一段时间。⾃自旋锁的思想是让⼀一个线程在请求⼀一个共享数据的锁时执⾏行行忙循环(⾃自旋)⼀一段时间,
    如果在这段时间内能获得锁,就可以避免进⼊入阻塞状态。
    ⾃自旋锁虽然能避免进⼊入阻塞状态从⽽而减少开销,但是它需要进⾏行行忙循环操作占⽤用 CPU 时间,它只适⽤用
    于共享数据的锁定状态很短的场景。
    在 JDK 1.6 中引⼊入了了⾃自适应的⾃自旋锁。⾃自适应意味着⾃自旋的次数不不再固定了了,⽽而是由前⼀一次在同⼀一个锁
    上的⾃自旋次数及锁的拥有者的状态来决定。
    锁消除
    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();
    }
    锁消除是指对于被检测出不不可能存在竞争的共享数据的锁进⾏行行消除。
    锁消除主要是通过逃逸分析来⽀支持,如果堆上的共享数据不不可能逃逸出去被其它线程访问到,那么就可
    以把它们当成私有数据对待,也就可以将它们的锁进⾏行行消除。
    对于⼀一些看起来没有加锁的代码,其实隐式的加了了很多锁。例例如下⾯面的字符串串拼接代码就隐式加了了锁:
    String 是⼀一个不不可变的类,编译器器会对 String 的拼接⾃自动优化。在 JDK 1.5 之前,会转化为
    StringBuffer 对象的连续 append() 操作:
    每个 append() ⽅方法中都有⼀一个同步块。虚拟机观察变量量 sb,很快就会发现它的动态作⽤用域被限制在
    concatString() ⽅方法内部。也就是说,sb 的所有引⽤用永远不不会逃逸到 concatString() ⽅方法之外,其他线
    程⽆无法访问到它,因此可以进⾏行行消除。
    锁粗化
    如果⼀一系列列的连续操作都对同⼀一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。
    上⼀一节的示例例代码中连续的 append() ⽅方法就属于这类情况。如果虚拟机探测到由这样的⼀一串串零碎的操
    作都对同⼀一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列列的外部。对于上⼀一节的示例例代
    码就是扩展到第⼀一个 append() 操作之前直⾄至最后⼀一个 append() 操作之后,这样只需要加锁⼀一次就可以
    了了。
    轻量量级锁
    JDK 1.6 引⼊入了了偏向锁和轻量量级锁,从⽽而让锁拥有了了四个状态:⽆无锁状态(unlocked)、偏向锁状态
    (biasble)、轻量量级锁状态(lightweight locked)和重量量级锁状态(inflated)。
    以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了了五个状
    态,这些状态在右侧的 state 表格中给出。除了了 marked for gc 状态,其它四个状态已经在前⾯面介绍过
    了了。
    public static String concatString(String s1, String s2, String s3) {
    return s1 + s2 + s3;
    }
    public static String concatString(String s1, String s2, String s3) {
    StringBuffer sb = new StringBuffer();
    sb.append(s1);
    sb.append(s2);
    sb.append(s3);
    return sb.toString();
    }
    下图左侧是⼀一个线程的虚拟机栈,其中有⼀一部分称为 Lock Record 的区域,这是在轻量量级锁运⾏行行过程创
    建的,⽤用于存放锁对象的 Mark Word。⽽而右侧就是⼀一个锁对象,包含了了 Mark Word 和其它信息。
    轻量量级锁是相对于传统的重量量级锁⽽而⾔言,它使⽤用 CAS 操作来避免重量量级锁使⽤用互斥量量的开销。对于绝
    ⼤大部分的锁,在整个同步周期内都是不不存在竞争的,因此也就不不需要都使⽤用互斥量量进⾏行行同步,可以先采
    ⽤用 CAS 操作进⾏行行同步,如果 CAS 失败了了再改⽤用互斥量量进⾏行行同步。
    当尝试获取⼀一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚
    拟机在当前线程的虚拟机栈中创建 Lock Record,然后使⽤用 CAS 操作将对象的 Mark Word 更更新为
    Lock Record 指针。如果 CAS 操作成功了了,那么线程就获取了了该对象上的锁,并且对象的 Mark Word
    的锁标记变为 00,表示该对象处于轻量量级锁状态。
    如果 CAS 操作失败了了,虚拟机⾸首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的
    话说明当前线程已经拥有了了这个锁对象,那就可以直接进⼊入同步块继续执⾏行行,否则说明这个锁对象已经
    被其他线程线程抢占了了。如果有两条以上的线程争⽤用同⼀一个锁,那轻量量级锁就不不再有效,要膨胀为重量量
    级锁。
    偏向锁
    偏向锁的思想是偏向于让第⼀一个获取锁对象的线程,这个线程在之后获取该锁就不不再需要进⾏行行同步操
    作,甚⾄至连 CAS 操作也不不再需要。
    当锁对象第⼀一次被线程获得的时候,进⼊入偏向状态,标记为 1 01。同时使⽤用 CAS 操作将线程 ID 记录到
    Mark Word 中,如果 CAS 操作成功,这个线程以后每次进⼊入这个锁相关的同步块就不不需要再进⾏行行任何
    同步操作。
    当有另外⼀一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后
    恢复到未锁定状态或者轻量量级锁状态。
    ⼗十三、多线程开发良好的实践
    给线程起个有意义的名字,这样可以⽅方便便找 Bug。
    缩⼩小同步范围,从⽽而减少锁争⽤用。例例如对于 synchronized,应该尽量量使⽤用同步块⽽而不不是同步⽅方法。
    多⽤用同步⼯工具少⽤用 wait() 和 notify()。⾸首先,CountDownLatch, CyclicBarrier, Semaphore 和
    Exchanger 这些同步类简化了了编码操作,⽽而⽤用 wait() 和 notify() 很难实现复杂控制流;其次,这些同
    步类是由最好的企业编写和维护,在后续的 JDK 中还会不不断优化和完善。
    使⽤用 BlockingQueue 实现⽣生产者消费者问题。
    多⽤用并发集合少⽤用同步集合,例例如应该使⽤用 ConcurrentHashMap ⽽而不不是 Hashtable。
    使⽤用本地变量量和不不可变类来保证线程安全。
    使⽤用线程池⽽而不不是直接创建线程,这是因为创建线程代价很⾼高,线程池可以有效地利利⽤用有限的线程
    来启动任务。
    参考资料料
    BruceEckel. Java 编程思想: 第 4 版 [M]. 机械⼯工业出版社, 2007.
    周志明. 深⼊入理理解 Java 虚拟机 [M]. 机械⼯工业出版社, 2011.
    Threads and Locks
    线程通信
    Java 线程⾯面试题 Top 50
    BlockingQueue
    thread state java
    CSC 456 Spring 2012/ch7 MN
    Java - Understanding Happens-before relationship
    6장 Thread Synchronization
    How is Java’s ThreadLocal implemented under the hood?
    Concurrent
    JAVA FORK JOIN EXAMPLE
    聊聊并发(⼋八)——Fork/Join 框架介绍
    Eliminating SynchronizationRelated Atomic Operations with Biased Locking and Bulk Rebiasing
    Java 虚拟机
    Java 虚拟机
    ⼀一、运⾏行行时数据区域
    程序计数器器
    Java 虚拟机栈
    本地⽅方法栈

    ⽅方法区
    运⾏行行时常量量池
    直接内存
    ⼆二、垃圾收集
    判断⼀一个对象是否可被回收
    引⽤用类型
    垃圾收集算法
    垃圾收集器器
    三、内存分配与回收策略略
    Minor GC 和 Full GC
    内存分配策略略
    Full GC 的触发条件
    四、类加载机制
    类的⽣生命周期
    类加载过程
    类初始化时机
    类与类加载器器
    类加载器器分类
    双亲委派模型
    ⾃自定义类加载器器实现
    参考资料料
    本⽂文⼤大部分内容参考 周志明《深⼊入理理解 Java 虚拟机》 ,想要深⼊入学习的话请看原书。
    ⼀一、运⾏行行时数据区域
    程序计数器器
    记录正在执⾏行行的虚拟机字节码指令的地址(如果正在执⾏行行的是本地⽅方法则为空)。
    Java 虚拟机栈
    每个 Java ⽅方法在执⾏行行的同时会创建⼀一个栈帧⽤用于存储局部变量量表、操作数栈、常量量池引⽤用等信息。从
    ⽅方法调⽤用直⾄至执⾏行行完成的过程,对应着⼀一个栈帧在 Java 虚拟机栈中⼊入栈和出栈的过程。
    可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存⼤大⼩小,在 JDK 1.4 中默认为
    256K,⽽而在 JDK 1.5+ 默认为 1M:
    该区域可能抛出以下异常:
    当线程请求的栈深度超过最⼤大值,会抛出 StackOverflowError 异常;
    栈进⾏行行动态扩展时如果⽆无法申请到⾜足够内存,会抛出 OutOfMemoryError 异常。
    本地⽅方法栈
    本地⽅方法栈与 Java 虚拟机栈类似,它们之间的区别只不不过是本地⽅方法栈为本地⽅方法服务。
    本地⽅方法⼀一般是⽤用其它语⾔言(C、C++ 或汇编语⾔言等)编写的,并且被编译为基于本机硬件和操作系统
    的程序,对待这些⽅方法需要特别处理理。

    所有对象都在这⾥里里分配内存,是垃圾收集的主要区域(“GC 堆”)。
    现代的垃圾收集器器基本都是采⽤用分代收集算法,其主要的思想是针对不不同类型的对象采取不不同的垃圾回
    收算法。可以将堆分成两块:
    java -Xss2M HackTheJava
    新⽣生代(Young Generation)
    ⽼老老年年代(Old Generation)
    堆不不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
    可以通过 -Xms 和 -Xmx 这两个虚拟机参数来指定⼀一个程序的堆内存⼤大⼩小,第⼀一个参数设置初始值,第
    ⼆二个参数设置最⼤大值。
    ⽅方法区
    ⽤用于存放已被加载的类信息、常量量、静态变量量、即时编译器器编译后的代码等数据。
    和堆⼀一样不不需要连续的内存,并且可以动态扩展,动态扩展失败⼀一样会抛出 OutOfMemoryError 异常。
    对这块区域进⾏行行垃圾回收的主要⽬目标是对常量量池的回收和对类的卸载,但是⼀一般⽐比较难实现。
    HotSpot 虚拟机把它当成永久代来进⾏行行垃圾回收。但很难确定永久代的⼤大⼩小,因为它受到很多因素影
    响,并且每次 Full GC 之后永久代的⼤大⼩小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了了更更
    容易易管理理⽅方法区,从 JDK 1.8 开始,移除永久代,并把⽅方法区移⾄至元空间,它位于本地内存中,⽽而不不是
    虚拟机内存中。
    ⽅方法区是⼀一个 JVM 规范,永久代与元空间都是其⼀一种实现⽅方式。在 JDK 1.8 之后,原来永久代的数据
    被分到了了堆和元空间中。元空间存储类的元信息,静态变量量和常量量池等放⼊入堆中。
    运⾏行行时常量量池
    运⾏行行时常量量池是⽅方法区的⼀一部分。
    Class ⽂文件中的常量量池(编译器器⽣生成的字⾯面量量和符号引⽤用)会在类加载后被放⼊入这个区域。
    除了了在编译期⽣生成的常量量,还允许动态⽣生成,例例如 String 类的 intern()。
    直接内存
    在 JDK 1.4 中新引⼊入了了 NIO 类,它可以使⽤用 Native 函数库直接分配堆外内存,然后通过 Java 堆⾥里里的
    DirectByteBuffer 对象作为这块内存的引⽤用进⾏行行操作。这样能在⼀一些场景中显著提⾼高性能,因为避免了了
    在堆内存和堆外内存来回拷⻉贝数据。
    ⼆二、垃圾收集
    垃圾收集主要是针对堆和⽅方法区进⾏行行。程序计数器器、虚拟机栈和本地⽅方法栈这三个区域属于线程私有
    的,只存在于线程的⽣生命周期内,线程结束之后就会消失,因此不不需要对这三个区域进⾏行行垃圾回收。
    java -Xms1M -Xmx2M HackTheJava
    判断⼀一个对象是否可被回收
  2. 引⽤用计数算法
    为对象添加⼀一个引⽤用计数器器,当对象增加⼀一个引⽤用时计数器器加 1,引⽤用失效时计数器器减 1。引⽤用计数为
    0 的对象可被回收。
    在两个对象出现循环引⽤用的情况下,此时引⽤用计数器器永远不不为 0,导致⽆无法对它们进⾏行行回收。正是因为
    循环引⽤用的存在,因此 Java 虚拟机不不使⽤用引⽤用计数算法。
    在上述代码中,a 与 b 引⽤用的对象实例例互相持有了了对象的引⽤用,因此当我们把对 a 对象与 b 对象的引⽤用
    去除之后,由于两个对象还存在互相之间的引⽤用,导致两个 Test 对象⽆无法被回收。
  3. 可达性分析算法
    以 GC Roots 为起始点进⾏行行搜索,可达的对象都是存活的,不不可达的对象可被回收。
    Java 虚拟机使⽤用该算法来判断对象是否可被回收,GC Roots ⼀一般包含以下内容:
    虚拟机栈中局部变量量表中引⽤用的对象
    本地⽅方法栈中 JNI 中引⽤用的对象
    ⽅方法区中类静态属性引⽤用的对象
    ⽅方法区中的常量量引⽤用的对象
    public class Test {
    public Object instance = null;
    public static void main(String[] args) {
    Test a = new Test();
    Test b = new Test();
    a.instance = b;
    b.instance = a;
    a = null;
    b = null;
    doSomething();
    }
    }
  4. ⽅方法区的回收
    因为⽅方法区主要存放永久代对象,⽽而永久代对象的回收率⽐比新⽣生代低很多,所以在⽅方法区上进⾏行行回收性
    价⽐比不不⾼高。
    主要是对常量量池的回收和对类的卸载。
    为了了避免内存溢出,在⼤大量量使⽤用反射和动态代理理的场景都需要虚拟机具备类卸载功能。
    类的卸载条件很多,需要满⾜足以下三个条件,并且满⾜足了了条件也不不⼀一定会被卸载:
    该类所有的实例例都已经被回收,此时堆中不不存在该类的任何实例例。
    加载该类的 ClassLoader 已经被回收。
    该类对应的 Class 对象没有在任何地⽅方被引⽤用,也就⽆无法在任何地⽅方通过反射访问该类⽅方法。
  5. finalize()
    类似 C++ 的析构函数,⽤用于关闭外部资源。但是 try-finally 等⽅方式可以做得更更好,并且该⽅方法运⾏行行代价
    很⾼高,不不确定性⼤大,⽆无法保证各个对象的调⽤用顺序,因此最好不不要使⽤用。
    当⼀一个对象可被回收时,如果需要执⾏行行该对象的 finalize() ⽅方法,那么就有可能在该⽅方法中让对象重新被
    引⽤用,从⽽而实现⾃自救。⾃自救只能进⾏行行⼀一次,如果回收的对象之前调⽤用了了 finalize() ⽅方法⾃自救,后⾯面回收时
    不不会再调⽤用该⽅方法。
    引⽤用类型
    ⽆无论是通过引⽤用计数算法判断对象的引⽤用数量量,还是通过可达性分析算法判断对象是否可达,判定对象
    是否可被回收都与引⽤用有关。
    Java 提供了了四种强度不不同的引⽤用类型。
  6. 强引⽤用
    被强引⽤用关联的对象不不会被回收。
    使⽤用 new ⼀一个新对象的⽅方式来创建强引⽤用。
  7. 软引⽤用
    被软引⽤用关联的对象只有在内存不不够的情况下才会被回收。
    使⽤用 SoftReference 类来创建软引⽤用。
  8. 弱引⽤用
    被弱引⽤用关联的对象⼀一定会被回收,也就是说它只能存活到下⼀一次垃圾回收发⽣生之前。
    使⽤用 WeakReference 类来创建弱引⽤用。
  9. 虚引⽤用
    ⼜又称为幽灵引⽤用或者幻影引⽤用,⼀一个对象是否有虚引⽤用的存在,不不会对其⽣生存时间造成影响,也⽆无法通
    过虚引⽤用得到⼀一个对象。
    为⼀一个对象设置虚引⽤用的唯⼀一⽬目的是能在这个对象被回收时收到⼀一个系统通知。
    使⽤用 PhantomReference 来创建虚引⽤用。
    垃圾收集算法
  10. 标记 - 清除
    Object obj = new Object();
    Object obj = new Object();
    SoftReference sf = new SoftReference(obj);
    obj = null; // 使对象只被软引⽤用关联
    Object obj = new Object();
    WeakReference wf = new WeakReference(obj);
    obj = null;
    Object obj = new Object();
    PhantomReference pf = new PhantomReference(obj, null);
    obj = null;
    在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。
    在清除阶段,会进⾏行行对象回收并取消标志位,另外,还会判断回收后的分块与前⼀一个空闲分块是否连
    续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链
    表,之后进⾏行行分配时只需要遍历这个空闲链表,就可以找到分块。
    在分配时,程序会搜索空闲链表寻找空间⼤大于等于新对象⼤大⼩小 size 的块 block。如果它找到的块等于
    size,会直接返回这个分块;如果找到的块⼤大于 size,会将块分割成⼤大⼩小为 size 与 (block - size) 的两部
    分,返回⼤大⼩小为 size 的分块,并把⼤大⼩小为 (block - size) 的块返回给空闲链表。
    不不⾜足:
    标记和清除过程效率都不不⾼高;
    会产⽣生⼤大量量不不连续的内存碎⽚片,导致⽆无法给⼤大对象分配内存。
  11. 标记 - 整理理
    让所有存活的对象都向⼀一端移动,然后直接清理理掉端边界以外的内存。
    优点:
    不不会产⽣生内存碎⽚片
    不不⾜足:
    需要移动⼤大量量对象,处理理效率⽐比较低。
  12. 复制
    将内存划分为⼤大⼩小相等的两块,每次只使⽤用其中⼀一块,当这⼀一块内存⽤用完了了就将还存活的对象复制到另
    ⼀一块上⾯面,然后再把使⽤用过的内存空间进⾏行行⼀一次清理理。
    主要不不⾜足是只使⽤用了了内存的⼀一半。
    现在的商业虚拟机都采⽤用这种收集算法回收新⽣生代,但是并不不是划分为⼤大⼩小相等的两块,⽽而是⼀一块较⼤大
    的 Eden 空间和两块较⼩小的 Survivor 空间,每次使⽤用 Eden 和其中⼀一块 Survivor。在回收时,将 Eden
    和 Survivor 中还存活着的对象全部复制到另⼀一块 Survivor 上,最后清理理 Eden 和使⽤用过的那⼀一块
    Survivor。
    HotSpot 虚拟机的 Eden 和 Survivor ⼤大⼩小⽐比例例默认为 8:1,保证了了内存的利利⽤用率达到 90%。如果每次回
    收有多于 10% 的对象存活,那么⼀一块 Survivor 就不不够⽤用了了,此时需要依赖于⽼老老年年代进⾏行行空间分配担
    保,也就是借⽤用⽼老老年年代的空间存储放不不下的对象。
  13. 分代收集
    现在的商业虚拟机采⽤用分代收集算法,它根据对象存活周期将内存划分为⼏几块,不不同块采⽤用适当的收集
    算法。
    ⼀一般将堆分为新⽣生代和⽼老老年年代。
    新⽣生代使⽤用:复制算法
    ⽼老老年年代使⽤用:标记 - 清除 或者 标记 - 整理理 算法
    垃圾收集器器
    以上是 HotSpot 虚拟机中的 7 个垃圾收集器器,连线表示垃圾收集器器可以配合使⽤用。
    单线程与多线程:单线程指的是垃圾收集器器只使⽤用⼀一个线程,⽽而多线程使⽤用多个线程;
    串串⾏行行与并⾏行行:串串⾏行行指的是垃圾收集器器与⽤用户程序交替执⾏行行,这意味着在执⾏行行垃圾收集的时候需要停
    顿⽤用户程序;并⾏行行指的是垃圾收集器器和⽤用户程序同时执⾏行行。除了了 CMS 和 G1 之外,其它垃圾收集
    器器都是以串串⾏行行的⽅方式执⾏行行。
  14. Serial 收集器器
    Serial 翻译为串串⾏行行,也就是说它以串串⾏行行的⽅方式执⾏行行。
    它是单线程的收集器器,只会使⽤用⼀一个线程进⾏行行垃圾收集⼯工作。
    它的优点是简单⾼高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最⾼高的单线程收集效
    率。
    它是 Client 场景下的默认新⽣生代收集器器,因为在该场景下内存⼀一般来说不不会很⼤大。它收集⼀一两百兆垃圾
    的停顿时间可以控制在⼀一百多毫秒以内,只要不不是太频繁,这点停顿时间是可以接受的。
  15. ParNew 收集器器
    它是 Serial 收集器器的多线程版本。
    它是 Server 场景下默认的新⽣生代收集器器,除了了性能原因外,主要是因为除了了 Serial 收集器器,只有它能
    与 CMS 收集器器配合使⽤用。
  16. Parallel Scavenge 收集器器
    与 ParNew ⼀一样是多线程收集器器。
    其它收集器器⽬目标是尽可能缩短垃圾收集时⽤用户线程的停顿时间,⽽而它的⽬目标是达到⼀一个可控制的吞吐
    量量,因此它被称为“吞吐量量优先”收集器器。这⾥里里的吞吐量量指 CPU ⽤用于运⾏行行⽤用户程序的时间占总时间的⽐比
    值。
    停顿时间越短就越适合需要与⽤用户交互的程序,良好的响应速度能提升⽤用户体验。⽽而⾼高吞吐量量则可以⾼高
    效率地利利⽤用 CPU 时间,尽快完成程序的运算任务,适合在后台运算⽽而不不需要太多交互的任务。
    缩短停顿时间是以牺牲吞吐量量和新⽣生代空间来换取的:新⽣生代空间变⼩小,垃圾回收变得频繁,导致吞吐
    量量下降。
    可以通过⼀一个开关参数打开 GC ⾃自适应的调节策略略(GC Ergonomics),就不不需要⼿手⼯工指定新⽣生代的⼤大
    ⼩小(-Xmn)、Eden 和 Survivor 区的⽐比例例、晋升⽼老老年年代对象年年龄等细节参数了了。虚拟机会根据当前系统
    的运⾏行行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最⼤大的吞吐量量。
  17. Serial Old 收集器器
    是 Serial 收集器器的⽼老老年年代版本,也是给 Client 场景下的虚拟机使⽤用。如果⽤用在 Server 场景下,它有两
    ⼤大⽤用途:
    在 JDK 1.5 以及之前版本(Parallel Old 诞⽣生以前)中与 Parallel Scavenge 收集器器搭配使⽤用。
    作为 CMS 收集器器的后备预案,在并发收集发⽣生 Concurrent Mode Failure 时使⽤用。
  18. Parallel Old 收集器器
    是 Parallel Scavenge 收集器器的⽼老老年年代版本。
    在注重吞吐量量以及 CPU 资源敏敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器器。
  19. CMS 收集器器
    CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
    分为以下四个流程:
    初始标记:仅仅只是标记⼀一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
    并发标记:进⾏行行 GC Roots Tracing 的过程,它在整个回收过程中耗时最⻓长,不不需要停顿。
    重新标记:为了了修正并发标记期间因⽤用户程序继续运作⽽而导致标记产⽣生变动的那⼀一部分对象的标记
    记录,需要停顿。
    并发清除:不不需要停顿。
    在整个过程中耗时最⻓长的并发标记和并发清除过程中,收集器器线程都可以与⽤用户线程⼀一起⼯工作,不不需要
    进⾏行行停顿。
    具有以下缺点:
    吞吐量量低:低停顿时间是以牺牲吞吐量量为代价的,导致 CPU 利利⽤用率不不够⾼高。
    ⽆无法处理理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于⽤用户线程
    继续运⾏行行⽽而产⽣生的垃圾,这部分垃圾只能到下⼀一次 GC 时才能进⾏行行回收。由于浮动垃圾的存在,因
    此需要预留留出⼀一部分内存,意味着 CMS 收集不不能像其它收集器器那样等待⽼老老年年代快满的时候再回
    收。如果预留留的内存不不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启
    ⽤用 Serial Old 来替代 CMS。
    标记 - 清除算法导致的空间碎⽚片,往往出现⽼老老年年代空间剩余,但⽆无法找到⾜足够⼤大连续空间来分配当
    前对象,不不得不不提前触发⼀一次 Full GC。
  20. G1 收集器器
    G1(Garbage-First),它是⼀一款⾯面向服务端应⽤用的垃圾收集器器,在多 CPU 和⼤大内存的场景下有很好的
    性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器器。
    堆被分为新⽣生代和⽼老老年年代,其它收集器器进⾏行行收集的范围都是整个新⽣生代或者⽼老老年年代,⽽而 G1 可以直接对
    新⽣生代和⽼老老年年代⼀一起回收。
    G1 把堆划分成多个⼤大⼩小相等的独⽴立区域(Region),新⽣生代和⽼老老年年代不不再物理理隔离。
    通过引⼊入 Region 的概念,从⽽而将原来的⼀一整块内存空间划分成多个的⼩小空间,使得每个⼩小空间可以单
    独进⾏行行垃圾回收。这种划分⽅方法带来了了很⼤大的灵活性,使得可预测的停顿时间模型成为可能。通过记录
    每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护⼀一
    个优先列列表,每次根据允许的收集时间,优先回收价值最⼤大的 Region。
    每个 Region 都有⼀一个 Remembered Set,⽤用来记录该 Region 对象的引⽤用对象所在的 Region。通过使
    ⽤用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
    如果不不计算维护 Remembered Set 的操作,G1 收集器器的运作⼤大致可划分为以下⼏几个步骤:
    初始标记
    并发标记
    最终标记:为了了修正在并发标记期间因⽤用户程序继续运作⽽而导致标记产⽣生变动的那⼀一部分标记记
    录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs ⾥里里⾯面,最终标记阶段需要把
    Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并⾏行行执
    ⾏行行。
    筛选回收:⾸首先对各个 Region 中的回收价值和成本进⾏行行排序,根据⽤用户所期望的 GC 停顿时间来
    制定回收计划。此阶段其实也可以做到与⽤用户程序⼀一起并发执⾏行行,但是因为只回收⼀一部分 Region,
    时间是⽤用户可控制的,⽽而且停顿⽤用户线程将⼤大幅度提⾼高收集效率。
    具备如下特点:
    空间整合:整体来看是基于“标记 - 整理理”算法实现的收集器器,从局部(两个 Region 之间)上来看是
    基于“复制”算法实现的,这意味着运⾏行行期间不不会产⽣生内存空间碎⽚片。
    可预测的停顿:能让使⽤用者明确指定在⼀一个⻓长度为 M 毫秒的时间⽚片段内,消耗在 GC 上的时间不不得
    超过 N 毫秒。
    三、内存分配与回收策略略
    Minor GC 和 Full GC
    Minor GC:回收新⽣生代,因为新⽣生代对象存活时间很短,因此 Minor GC 会频繁执⾏行行,执⾏行行的速度
    ⼀一般也会⽐比较快。
    Full GC:回收⽼老老年年代和新⽣生代,⽼老老年年代对象其存活时间⻓长,因此 Full GC 很少执⾏行行,执⾏行行速度会⽐比
    Minor GC 慢很多。
    内存分配策略略
  21. 对象优先在 Eden 分配
    ⼤大多数情况下,对象在新⽣生代 Eden 上分配,当 Eden 空间不不够时,发起 Minor GC。
  22. ⼤大对象直接进⼊入⽼老老年年代
    ⼤大对象是指需要连续内存空间的对象,最典型的⼤大对象是那种很⻓长的字符串串以及数组。
    经常出现⼤大对象会提前触发垃圾收集以获取⾜足够的连续空间分配给⼤大对象。
    -XX:PretenureSizeThreshold,⼤大于此值的对象直接在⽼老老年年代分配,避免在 Eden 和 Survivor 之间的⼤大
    量量内存复制。
  23. ⻓长期存活的对象进⼊入⽼老老年年代
    为对象定义年年龄计数器器,对象在 Eden 出⽣生并经过 Minor GC 依然存活,将移动到 Survivor 中,年年龄就
    增加 1 岁,增加到⼀一定年年龄则移动到⽼老老年年代中。
    -XX:MaxTenuringThreshold ⽤用来定义年年龄的阈值。
  24. 动态对象年年龄判定
    虚拟机并不不是永远要求对象的年年龄必须达到 MaxTenuringThreshold 才能晋升⽼老老年年代,如果在 Survivor
    中相同年年龄所有对象⼤大⼩小的总和⼤大于 Survivor 空间的⼀一半,则年年龄⼤大于或等于该年年龄的对象可以直接进
    ⼊入⽼老老年年代,⽆无需等到 MaxTenuringThreshold 中要求的年年龄。
  25. 空间分配担保
    在发⽣生 Minor GC 之前,虚拟机先检查⽼老老年年代最⼤大可⽤用的连续空间是否⼤大于新⽣生代所有对象总空间,如
    果条件成⽴立的话,那么 Minor GC 可以确认是安全的。
    如果不不成⽴立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续
    检查⽼老老年年代最⼤大可⽤用的连续空间是否⼤大于历次晋升到⽼老老年年代对象的平均⼤大⼩小,如果⼤大于,将尝试着进⾏行行
    ⼀一次 Minor GC;如果⼩小于,或者 HandlePromotionFailure 的值不不允许冒险,那么就要进⾏行行⼀一次 Full
    GC。
    Full GC 的触发条件
    对于 Minor GC,其触发条件⾮非常简单,当 Eden 空间满时,就将触发⼀一次 Minor GC。⽽而 Full GC 则相
    对复杂,有以下条件:
  26. 调⽤用 System.gc()
    只是建议虚拟机执⾏行行 Full GC,但是虚拟机不不⼀一定真正去执⾏行行。不不建议使⽤用这种⽅方式,⽽而是让虚拟机管
    理理内存。
  27. ⽼老老年年代空间不不⾜足
    ⽼老老年年代空间不不⾜足的常⻅见场景为前⽂文所讲的⼤大对象直接进⼊入⽼老老年年代、⻓长期存活的对象进⼊入⽼老老年年代等。
    为了了避免以上原因引起的 Full GC,应当尽量量不不要创建过⼤大的对象以及数组。除此之外,可以通过 -Xmn
    虚拟机参数调⼤大新⽣生代的⼤大⼩小,让对象尽量量在新⽣生代被回收掉,不不进⼊入⽼老老年年代。还可以通过 -
    XX:MaxTenuringThreshold 调⼤大对象进⼊入⽼老老年年代的年年龄,让对象在新⽣生代多存活⼀一段时间。
  28. 空间分配担保失败
    使⽤用复制算法的 Minor GC 需要⽼老老年年代的内存空间作担保,如果担保失败会执⾏行行⼀一次 Full GC。具体内
    容请参考上⾯面的第 5 ⼩小节。
  29. JDK 1.7 及以前的永久代空间不不⾜足
    在 JDK 1.7 及以前,HotSpot 虚拟机中的⽅方法区是⽤用永久代实现的,永久代中存放的为⼀一些 Class 的信
    息、常量量、静态变量量等数据。
    当系统中要加载的类、反射的类和调⽤用的⽅方法较多时,永久代可能会被占满,在未配置为采⽤用 CMS GC
    的情况下也会执⾏行行 Full GC。如果经过 Full GC 仍然回收不不了了,那么虚拟机会抛出
    java.lang.OutOfMemoryError。
    为避免以上原因引起的 Full GC,可采⽤用的⽅方法为增⼤大永久代空间或转为使⽤用 CMS GC。
  30. Concurrent Mode Failure
    执⾏行行 CMS GC 的过程中同时有对象要放⼊入⽼老老年年代,⽽而此时⽼老老年年代空间不不⾜足(可能是 GC 过程中浮动垃
    圾过多导致暂时性的空间不不⾜足),便便会报 Concurrent Mode Failure 错误,并触发 Full GC。
    四、类加载机制
    类是在运⾏行行期间第⼀一次使⽤用时动态加载的,⽽而不不是⼀一次性加载所有类。因为如果⼀一次性加载,那么会占
    ⽤用很多的内存。
    类的⽣生命周期
    包括以下 7 个阶段:
    加载(Loading)
    验证(Verification)
    准备(Preparation)
    解析(Resolution)
    初始化(Initialization)
    使⽤用(Using)
    卸载(Unloading)
    类加载过程
    包含了了加载、验证、准备、解析和初始化这 5 个阶段。
  31. 加载
    加载是类加载的⼀一个阶段,注意不不要混淆。
    加载过程完成以下三件事:
    通过类的完全限定名称获取定义该类的⼆二进制字节流。
    将该字节流表示的静态存储结构转换为⽅方法区的运⾏行行时存储结构。
    在内存中⽣生成⼀一个代表该类的 Class 对象,作为⽅方法区中该类各种数据的访问⼊入⼝口。
    其中⼆二进制字节流可以从以下⽅方式中获取:
    从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
    从⽹网络中获取,最典型的应⽤用是 Applet。
    运⾏行行时计算⽣生成,例例如动态代理理技术,在 java.lang.reflect.Proxy 使⽤用
    ProxyGenerator.generateProxyClass 的代理理类的⼆二进制字节流。
    由其他⽂文件⽣生成,例例如由 JSP ⽂文件⽣生成对应的 Class 类。
  32. 验证
    确保 Class ⽂文件的字节流中包含的信息符合当前虚拟机的要求,并且不不会危害虚拟机⾃自身的安全。
  33. 准备
    类变量量是被 static 修饰的变量量,准备阶段为类变量量分配内存并设置初始值,使⽤用的是⽅方法区的内存。
    实例例变量量不不会在这阶段分配内存,它会在对象实例例化时随着对象⼀一起被分配在堆中。应该注意到,实例例
    化不不是类加载的⼀一个过程,类加载发⽣生在所有实例例化操作之前,并且类加载只进⾏行行⼀一次,实例例化可以进
    ⾏行行多次。
    初始值⼀一般为 0 值,例例如下⾯面的类变量量 value 被初始化为 0 ⽽而不不是 123。
    如果类变量量是常量量,那么它将初始化为表达式所定义的值⽽而不不是 0。例例如下⾯面的常量量 value 被初始化为
    123 ⽽而不不是 0。
    public static int value = 123;
  34. 解析
    将常量量池的符号引⽤用替换为直接引⽤用的过程。
    其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了了⽀支持 Java 的动态绑定。
  35. 初始化
    初始化阶段才真正开始执⾏行行类中定义的 Java 程序代码。初始化阶段是虚拟机执⾏行行类构造器器 () ⽅方
    法的过程。在准备阶段,类变量量已经赋过⼀一次系统要求的初始值,⽽而在初始化阶段,根据程序员通过程
    序制定的主观计划去初始化类变量量和其它资源。
    () 是由编译器器⾃自动收集类中所有类变量量的赋值动作和静态语句句块中的语句句合并产⽣生的,编译器器收
    集的顺序由语句句在源⽂文件中出现的顺序决定。特别注意的是,静态语句句块只能访问到定义在它之前的类
    变量量,定义在它之后的类变量量只能赋值,不不能访问。例例如以下代码:
    由于⽗父类的 () ⽅方法先执⾏行行,也就意味着⽗父类中定义的静态语句句块的执⾏行行要优先于⼦子类。例例如以下
    代码:
    public static final int value = 123;
    public class Test {
    static {
    i = 0; // 给变量量赋值可以正常编译通过
    System.out.print(i); // 这句句编译器器会提示“⾮非法向前引⽤用”
    }
    static int i = 1;
    }
    static class Parent {
    public static int A = 1;
    static {
    A = 2;
    }
    }
    static class Sub extends Parent {
    public static int B = A;
    }
    public static void main(String[] args) {
    System.out.println(Sub.B); // 2
    }
    接⼝口中不不可以使⽤用静态语句句块,但仍然有类变量量初始化的赋值操作,因此接⼝口与类⼀一样都会⽣生成
    () ⽅方法。但接⼝口与类不不同的是,执⾏行行接⼝口的 () ⽅方法不不需要先执⾏行行⽗父接⼝口的 () ⽅方
    法。只有当⽗父接⼝口中定义的变量量使⽤用时,⽗父接⼝口才会初始化。另外,接⼝口的实现类在初始化时也⼀一样不不
    会执⾏行行接⼝口的 () ⽅方法。
    虚拟机会保证⼀一个类的 () ⽅方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化⼀一
    个类,只会有⼀一个线程执⾏行行这个类的 () ⽅方法,其它线程都会阻塞等待,直到活动线程执⾏行行
    () ⽅方法完毕。如果在⼀一个类的 () ⽅方法中有耗时的操作,就可能造成多个线程阻塞,在实
    际过程中此种阻塞很隐蔽。
    类初始化时机
  36. 主动引⽤用
    虚拟机规范中并没有强制约束何时进⾏行行加载,但是规范严格规定了了有且只有下列列五种情况必须对类进⾏行行
    初始化(加载、验证、准备都会随之发⽣生):
    遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进⾏行行过初始化,则
    必须先触发其初始化。最常⻅见的⽣生成这 4 条指令的场景是:使⽤用 new 关键字实例例化对象的时候;
    读取或设置⼀一个类的静态字段(被 final 修饰、已在编译期把结果放⼊入常量量池的静态字段除外)的时
    候;以及调⽤用⼀一个类的静态⽅方法的时候。
    使⽤用 java.lang.reflect 包的⽅方法对类进⾏行行反射调⽤用的时候,如果类没有进⾏行行初始化,则需要先触发
    其初始化。
    当初始化⼀一个类的时候,如果发现其⽗父类还没有进⾏行行过初始化,则需要先触发其⽗父类的初始化。
    当虚拟机启动时,⽤用户需要指定⼀一个要执⾏行行的主类(包含 main() ⽅方法的那个类),虚拟机会先初始
    化这个主类;
    当使⽤用 JDK 1.7 的动态语⾔言⽀支持时,如果⼀一个 java.lang.invoke.MethodHandle 实例例最后的解析结
    果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的⽅方法句句柄,并且这个⽅方法句句柄所对应的类
    没有进⾏行行过初始化,则需要先触发其初始化;
  37. 被动引⽤用
    以上 5 种场景中的⾏行行为称为对⼀一个类进⾏行行主动引⽤用。除此之外,所有引⽤用类的⽅方式都不不会触发初始化,
    称为被动引⽤用。被动引⽤用的常⻅见例例⼦子包括:
    通过⼦子类引⽤用⽗父类的静态字段,不不会导致⼦子类初始化。
    通过数组定义来引⽤用类,不不会触发此类的初始化。该过程会对数组类进⾏行行初始化,数组类是⼀一个由
    虚拟机⾃自动⽣生成的、直接继承⾃自 Object 的⼦子类,其中包含了了数组的属性和⽅方法。
    System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
    SuperClass[] sca = new SuperClass[10];
    常量量在编译阶段会存⼊入调⽤用类的常量量池中,本质上并没有直接引⽤用到定义常量量的类,因此不不会触发
    定义常量量的类的初始化。
    类与类加载器器
    两个类相等,需要类本身相等,并且使⽤用同⼀一个类加载器器进⾏行行加载。这是因为每⼀一个类加载器器都拥有⼀一
    个独⽴立的类名称空间。
    这⾥里里的相等,包括类的 Class 对象的 equals() ⽅方法、isAssignableFrom() ⽅方法、isInstance() ⽅方法的返回
    结果为 true,也包括使⽤用 instanceof 关键字做对象所属关系判定结果为 true。
    类加载器器分类
    从 Java 虚拟机的⻆角度来讲,只存在以下两种不不同的类加载器器:
    启动类加载器器(Bootstrap ClassLoader),使⽤用 C++ 实现,是虚拟机⾃自身的⼀一部分;
    所有其它类的加载器器,使⽤用 Java 实现,独⽴立于虚拟机,继承⾃自抽象类 java.lang.ClassLoader。
    从 Java 开发⼈人员的⻆角度看,类加载器器可以划分得更更细致⼀一些:
    启动类加载器器(Bootstrap ClassLoader)此类加载器器负责将存放在 \lib ⽬目录中的,
    或者被 -Xbootclasspath 参数所指定的路路径中的,并且是虚拟机识别的(仅按照⽂文件名识别,如
    rt.jar,名字不不符合的类库即使放在 lib ⽬目录中也不不会被加载)类库加载到虚拟机内存中。启动类加
    载器器⽆无法被 Java 程序直接引⽤用,⽤用户在编写⾃自定义类加载器器时,如果需要把加载请求委派给启动
    类加载器器,直接使⽤用 null 代替即可。
    扩展类加载器器(Extension ClassLoader)这个类加载器器是由
    ExtClassLoader(sun.misc.Launcher E x t C l a s s L o a d e r ) 实 现 的 。 它 负 责 将 < J A V A H O M E > / l i b / e x t 或 者 被 j a v a . e x t . d i r 系 统 变 量 量 所 指 定 路 路 径 中 的 所 有 类 库 加 载 到 内 存 中 , 开 发 者 可 以 直 接 使 ⽤ 用 扩 展 类 加 载 器 器 。 应 ⽤ 用 程 序 类 加 载 器 器 ( A p p l i c a t i o n C l a s s L o a d e r ) 这 个 类 加 载 器 器 是 由 A p p C l a s s L o a d e r ( s u n . m i s c . L a u n c h e r ExtClassLoader)实现的。它负责将 /lib/ext 或者被 java.ext.dir 系统变量量所指定路路径中的所有类库加载到内存中,开发者可以直接使⽤用扩展类加 载器器。 应⽤用程序类加载器器(Application ClassLoader)这个类加载器器是由 AppClassLoader(sun.misc.Launcher ExtClassLoader<JAVAHOME>/lib/extjava.ext.dir使ApplicationClassLoaderAppClassLoadersun.misc.LauncherAppClassLoader)实现的。由于这个类加载器器是
    ClassLoader 中的 getSystemClassLoader() ⽅方法的返回值,因此⼀一般称为系统类加载器器。它负责加
    载⽤用户类路路径(ClassPath)上所指定的类库,开发者可以直接使⽤用这个类加载器器,如果应⽤用程序
    中没有⾃自定义过⾃自⼰己的类加载器器,⼀一般情况下这个就是程序中默认的类加载器器。
    双亲委派模型
    应⽤用程序是由三种类加载器器互相配合从⽽而实现类加载,除此之外还可以加⼊入⾃自⼰己定义的类加载器器。
    下图展示了了类加载器器之间的层次关系,称为双亲委派模型(Parents Delegation Model)。该模型要求
    除了了顶层的启动类加载器器外,其它的类加载器器都要有⾃自⼰己的⽗父类加载器器。这⾥里里的⽗父⼦子关系⼀一般通过组合
    关系(Composition)来实现,⽽而不不是继承关系(Inheritance)。
    System.out.println(ConstClass.HELLOWORLD);
  38. ⼯工作过程
    ⼀一个类加载器器⾸首先将类加载请求转发到⽗父类加载器器,只有当⽗父类加载器器⽆无法完成时才尝试⾃自⼰己加载。
  39. 好处
    使得 Java 类随着它的类加载器器⼀一起具有⼀一种带有优先级的层次关系,从⽽而使得基础类得到统⼀一。
    例例如 java.lang.Object 存放在 rt.jar 中,如果编写另外⼀一个 java.lang.Object 并放到 ClassPath 中,程序
    可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object ⽐比在 ClassPath 中的 Object 优先
    级更更⾼高,这是因为 rt.jar 中的 Object 使⽤用的是启动类加载器器,⽽而 ClassPath 中的 Object 使⽤用的是应⽤用
    程序类加载器器。rt.jar 中的 Object 优先级更更⾼高,那么程序中所有的 Object 都是这个 Object。
  40. 实现
    以下是抽象类 java.lang.ClassLoader 的代码⽚片段,其中的 loadClass() ⽅方法运⾏行行过程如下:先检查类是
    否已经加载过,如果没有则让⽗父类加载器器去加载。当⽗父类加载器器加载失败时抛出
    ClassNotFoundException,此时尝试⾃自⼰己去加载。
    public abstract class ClassLoader {
    // The parent class loader for delegation
    private final ClassLoader parent;
    ⾃自定义类加载器器实现
    public Class loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
    }
    protected Class loadClass(String name, boolean resolve) throws
    ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
    try {
    if (parent != null) {
    c = parent.loadClass(name, false);
    } else {
    c = findBootstrapClassOrNull(name);
    }
    } catch (ClassNotFoundException e) {
    // ClassNotFoundException thrown if class not found
    // from the non-null parent class loader
    }
    if (c == null) {
    // If still not found, then invoke findClass in order
    // to find the class.
    c = findClass(name);
    }
    }
    if (resolve) {
    resolveClass©;
    }
    return c;
    }
    }
    protected Class findClass(String name) throws ClassNotFoundException
    {
    throw new ClassNotFoundException(name);
    }
    }
    以下代码中的 FileSystemClassLoader 是⾃自定义类加载器器,继承⾃自 java.lang.ClassLoader,⽤用于加载⽂文
    件系统上的类。它⾸首先根据类的全名在⽂文件系统上查找类的字节代码⽂文件(.class ⽂文件),然后读取该
    ⽂文件内容,最后通过 defineClass() ⽅方法来把这些字节代码转换成 java.lang.Class 类的实例例。
    java.lang.ClassLoader 的 loadClass() 实现了了双亲委派模型的逻辑,⾃自定义类加载器器⼀一般不不去重写它,
    但是需要重写 findClass() ⽅方法。
    public class FileSystemClassLoader extends ClassLoader {
    private String rootDir;
    public FileSystemClassLoader(String rootDir) {
    this.rootDir = rootDir;
    }
    protected Class findClass(String name) throws ClassNotFoundException
    {
    byte[] classData = getClassData(name);
    if (classData == null) {
    throw new ClassNotFoundException();
    } else {
    return defineClass(name, classData, 0, classData.length);
    }
    }
    private byte[] getClassData(String className) {
    String path = classNameToPath(className);
    try {
    InputStream ins = new FileInputStream(path);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    int bufferSize = 4096;
    byte[] buffer = new byte[bufferSize];
    int bytesNumRead;
    while ((bytesNumRead = ins.read(buffer)) != -1) {
    baos.write(buffer, 0, bytesNumRead);
    }
    return baos.toByteArray();
    } catch (IOException e) {
    e.printStackTrace();
    }
    return null;
    }
    private String classNameToPath(String className) {
    参考资料料
    周志明. 深⼊入理理解 Java 虚拟机 [M]. 机械⼯工业出版社, 2011.
    Chapter 2. The Structure of the Java Virtual Machine
    Jvm memory
    Getting Started with the G1 Garbage Collector
    JNI Part1: Java Native Interface Introduction and “Hello World” application
    Memory Architecture Of JVM(Runtime Data Areas)
    JVM Run-Time Data Areas
    Android on x86: Java Native Interface and the Android Native Development Kit
    深⼊入理理解 JVM(2)——GC 算法与内存分配策略略
    深⼊入理理解 JVM(3)——7 种垃圾收集器器
    JVM Internals
    深⼊入探讨 Java 类加载器器
    Guide to WeakHashMap in Java
    Tomcat example source code file (ConcurrentCache.java)
    Java IO
    Java IO
    ⼀一、概览
    ⼆二、磁盘操作
    三、字节操作
    实现⽂文件复制
    装饰者模式
    四、字符操作
    编码与解码
    String 的编码⽅方式
    Reader 与 Writer
    实现逐⾏行行输出⽂文本⽂文件的内容
    五、对象操作
    序列列化
    Serializable
    transient
    return rootDir + File.separatorChar
  • className.replace(’.’, File.separatorChar) + “.class”;
    }
    }
    六、⽹网络操作
    InetAddress
    URL
    Sockets
    Datagram
    七、NIO
    流与块
    通道与缓冲区
    缓冲区状态变量量
    ⽂文件 NIO 实例例
    选择器器
    套接字 NIO 实例例
    内存映射⽂文件
    对⽐比
    ⼋八、参考资料料
    ⼀一、概览
    Java 的 I/O ⼤大概可以分成以下⼏几类:
    磁盘操作:File
    字节操作:InputStream 和 OutputStream
    字符操作:Reader 和 Writer
    对象操作:Serializable
    ⽹网络操作:Socket
    新的输⼊入/输出:NIO
    ⼆二、磁盘操作
    File 类可以⽤用于表示⽂文件和⽬目录的信息,但是它不不表示⽂文件的内容。
    递归地列列出⼀一个⽬目录下所有⽂文件:
    从 Java7 开始,可以使⽤用 Paths 和 Files 代替 File。
    三、字节操作
    实现⽂文件复制
    装饰者模式
    Java I/O 使⽤用了了装饰者模式来实现。以 InputStream 为例例,
    InputStream 是抽象组件;
    FileInputStream 是 InputStream 的⼦子类,属于具体组件,提供了了字节流的输⼊入操作;
    public static void listAllFiles(File dir) {
    if (dir == null || !dir.exists()) {
    return;
    }
    if (dir.isFile()) {
    System.out.println(dir.getName());
    return;
    }
    for (File file : dir.listFiles()) {
    listAllFiles(file);
    }
    }
    public static void copyFile(String src, String dist) throws IOException {
    FileInputStream in = new FileInputStream(src);
    FileOutputStream out = new FileOutputStream(dist);
    byte[] buffer = new byte[20 * 1024];
    int cnt;
    // read() 最多读取 buffer.length 个字节
    // 返回的是实际读取的个数
    // 返回 -1 的时候表示读到 eof,即⽂文件尾
    while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
    out.write(buffer, 0, cnt);
    }
    in.close();
    out.close();
    }
    FilterInputStream 属于抽象装饰者,装饰者⽤用于装饰组件,为组件提供额外的功能。例例如
    BufferedInputStream 为 FileInputStream 提供缓存的功能。
    实例例化⼀一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套⼀一层
    BufferedInputStream 对象即可。
    DataInputStream 装饰者提供了了对更更多数据类型进⾏行行输⼊入的操作,⽐比如 int、double 等基本类型。
    四、字符操作
    编码与解码
    编码就是把字符转换为字节,⽽而解码是把字节重新组合成字符。
    如果编码和解码过程使⽤用不不同的编码⽅方式那么就出现了了乱码。
    GBK 编码中,中⽂文字符占 2 个字节,英⽂文字符占 1 个字节;
    UTF-8 编码中,中⽂文字符占 3 个字节,英⽂文字符占 1 个字节;
    UTF-16be 编码中,中⽂文字符和英⽂文字符都占 2 个字节。
    UTF-16be 中的 be 指的是 Big Endian,也就是⼤大端。相应地也有 UTF-16le,le 指的是 Little Endian,
    也就是⼩小端。
    Java 的内存编码使⽤用双字节编码 UTF-16be,这不不是指 Java 只⽀支持这⼀一种编码⽅方式,⽽而是说 char 这种
    类型使⽤用 UTF-16be 进⾏行行编码。char 类型占 16 位,也就是两个字节,Java 使⽤用这种双字节编码是为了了
    让⼀一个中⽂文或者⼀一个英⽂文都能使⽤用⼀一个 char 来存储。
    String 的编码⽅方式
    FileInputStream fileInputStream = new FileInputStream(filePath);
    BufferedInputStream bufferedInputStream = new
    BufferedInputStream(fileInputStream);
    String 可以看成⼀一个字符序列列,可以指定⼀一个编码⽅方式将它编码为字节序列列,也可以指定⼀一个编码⽅方式
    将⼀一个字节序列列解码为 String。
    在调⽤用⽆无参数 getBytes() ⽅方法时,默认的编码⽅方式不不是 UTF-16be。双字节编码的好处是可以使⽤用⼀一个
    char 存储中⽂文和英⽂文,⽽而将 String 转为 bytes[] 字节数组就不不再需要这个好处,因此也就不不再需要双字
    节编码。getBytes() 的默认编码⽅方式与平台有关,⼀一般为 UTF-8。
    Reader 与 Writer
    不不管是磁盘还是⽹网络传输,最⼩小的存储单元都是字节,⽽而不不是字符。但是在程序中操作的通常是字符形
    式的数据,因此需要提供对字符进⾏行行操作的⽅方法。
    InputStreamReader 实现从字节流解码成字符流;
    OutputStreamWriter 实现字符流编码成为字节流。
    实现逐⾏行行输出⽂文本⽂文件的内容
    五、对象操作
    String str1 = “中⽂文”;
    byte[] bytes = str1.getBytes(“UTF-8”);
    String str2 = new String(bytes, “UTF-8”);
    System.out.println(str2);
    byte[] bytes = str1.getBytes();
    public static void readFileContent(String filePath) throws IOException {
    FileReader fileReader = new FileReader(filePath);
    BufferedReader bufferedReader = new BufferedReader(fileReader);
    String line;
    while ((line = bufferedReader.readLine()) != null) {
    System.out.println(line);
    }
    // 装饰者模式使得 BufferedReader 组合了了⼀一个 Reader 对象
    // 在调⽤用 BufferedReader 的 close() ⽅方法时会去调⽤用 Reader 的 close() ⽅方法
    // 因此只要⼀一个 close() 调⽤用即可
    bufferedReader.close();
    }
    序列列化
    序列列化就是将⼀一个对象转换成字节序列列,⽅方便便存储和传输。
    序列列化:ObjectOutputStream.writeObject()
    反序列列化:ObjectInputStream.readObject()
    不不会对静态变量量进⾏行行序列列化,因为序列列化只是保存对象的状态,静态变量量属于类的状态。
    Serializable
    序列列化的类需要实现 Serializable 接⼝口,它只是⼀一个标准,没有任何⽅方法需要实现,但是如果不不去实现
    它的话⽽而进⾏行行序列列化,会抛出异常。
    public static void main(String[] args) throws IOException,
    ClassNotFoundException {
    A a1 = new A(123, “abc”);
    String objectFile = “file/a1”;
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new
    FileOutputStream(objectFile));
    objectOutputStream.writeObject(a1);
    objectOutputStream.close();
    ObjectInputStream objectInputStream = new ObjectInputStream(new
    FileInputStream(objectFile));
    A a2 = (A) objectInputStream.readObject();
    objectInputStream.close();
    System.out.println(a2);
    }
    private static class A implements Serializable {
    private int x;
    private String y;
    A(int x, String y) {
    this.x = x;
    this.y = y;
    }
    @Override
    public String toString() {
    return "x = " + x + " " + "y = " + y;
    transient
    transient 关键字可以使⼀一些属性不不会被序列列化。
    ArrayList 中存储数据的数组 elementData 是⽤用 transient 修饰的,因为这个数组是动态扩展的,并不不是
    所有的空间都被使⽤用,因此就不不需要所有的内容都被序列列化。通过重写序列列化和反序列列化⽅方法,使得可
    以只序列列化数组中有内容的那部分数据。
    六、⽹网络操作
    Java 中的⽹网络⽀支持:
    InetAddress:⽤用于表示⽹网络上的硬件资源,即 IP 地址;
    URL:统⼀一资源定位符;
    Sockets:使⽤用 TCP 协议实现⽹网络通信;
    Datagram:使⽤用 UDP 协议实现⽹网络通信。
    InetAddress
    没有公有的构造函数,只能通过静态⽅方法来创建实例例。
    URL
    可以直接从 URL 中读取字节流数据。
    }
    }
    private transient Object[] elementData;
    InetAddress.getByName(String host);
    InetAddress.getByAddress(byte[] address);
    public static void main(String[] args) throws IOException {
    URL url = new URL(“http://www.baidu.com”);
    /* 字节流 /
    InputStream is = url.openStream();
    /
    字符流 /
    InputStreamReader isr = new InputStreamReader(is, “utf-8”);
    Sockets
    ServerSocket:服务器器端类
    Socket:客户端类
    服务器器和客户端通过 InputStream 和 OutputStream 进⾏行行输⼊入输出。
    Datagram
    DatagramSocket:通信类
    DatagramPacket:数据包类
    七、NIO
    新的输⼊入/输出 (NIO) 库是在 JDK 1.4 中引⼊入的,弥补了了原来的 I/O 的不不⾜足,提供了了⾼高速的、⾯面向块的
    I/O。
    /
    提供缓存功能 /
    BufferedReader br = new BufferedReader(isr);
    String line;
    while ((line = br.readLine()) != null) {
    System.out.println(line);
    }
    br.close();
    }
    流与块
    I/O 与 NIO 最重要的区别是数据打包和传输的⽅方式,I/O 以流的⽅方式处理理数据,⽽而 NIO 以块的⽅方式处理理
    数据。
    ⾯面向流的 I/O ⼀一次处理理⼀一个字节数据:⼀一个输⼊入流产⽣生⼀一个字节数据,⼀一个输出流消费⼀一个字节数据。
    为流式数据创建过滤器器⾮非常容易易,链接⼏几个过滤器器,以便便每个过滤器器只负责复杂处理理机制的⼀一部分。不不
    利利的⼀一⾯面是,⾯面向流的 I/O 通常相当慢。
    ⾯面向块的 I/O ⼀一次处理理⼀一个数据块,按块处理理数据⽐比按流处理理数据要快得多。但是⾯面向块的 I/O 缺少⼀一
    些⾯面向流的 I/O 所具有的优雅性和简单性。
    I/O 包和 NIO 已经很好地集成了了,java.io.
    已经以 NIO 为基础重新实现了了,所以现在它可以利利⽤用 NIO 的
    ⼀一些特性。例例如,java.io.* 包中的⼀一些类包含以块的形式读写数据的⽅方法,这使得即使在⾯面向流的系统
    中,处理理速度也会更更快。
    通道与缓冲区
  1. 通道
    通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写⼊入数据。
    通道与流的不不同之处在于,流只能在⼀一个⽅方向上移动(⼀一个流必须是 InputStream 或者 OutputStream 的
    ⼦子类),⽽而通道是双向的,可以⽤用于读、写或者同时⽤用于读写。
    通道包括以下类型:
    FileChannel:从⽂文件中读写数据;
    DatagramChannel:通过 UDP 读写⽹网络中数据;
    SocketChannel:通过 TCP 读写⽹网络中数据;
    ServerSocketChannel:可以监听新进来的 TCP 连接,对每⼀一个新进来的连接都会创建⼀一个
    SocketChannel。
  2. 缓冲区
    发送给⼀一个通道的所有数据都必须⾸首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓
    冲区中。也就是说,不不会直接对通道进⾏行行读写数据,⽽而是要先经过缓冲区。
    缓冲区实质上是⼀一个数组,但它不不仅仅是⼀一个数组。缓冲区提供了了对数据的结构化访问,⽽而且还可以跟
    踪系统的读/写进程。
    缓冲区包括以下类型:
    ByteBuffer
    CharBuffer
    ShortBuffer
    IntBuffer
    LongBuffer
    FloatBuffer
    DoubleBuffer
    缓冲区状态变量量
    capacity:最⼤大容量量;
    position:当前已经读写的字节数;
    limit:还可以读写的字节数。
    状态变量量的改变过程举例例:
    ① 新建⼀一个⼤大⼩小为 8 个字节的缓冲区,此时 position 为 0,⽽而 limit = capacity = 8。capacity 变量量不不会
    改变,下⾯面的讨论会忽略略它。
    ② 从输⼊入通道中读取 5 个字节数据写⼊入缓冲区中,此时 position 为 5,limit 保持不不变。
    ③ 在将缓冲区的数据写到输出通道之前,需要先调⽤用 flip() ⽅方法,这个⽅方法将 limit 设置为当前
    position,并将 position 设置为 0。
    ④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
    ⑤ 最后需要调⽤用 clear() ⽅方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
    ⽂文件 NIO 实例例
    以下展示了了使⽤用 NIO 快速复制⽂文件的实例例:
    public static void fastCopy(String src, String dist) throws IOException {
    /* 获得源⽂文件的输⼊入字节流 /
    FileInputStream fin = new FileInputStream(src);
    /
    获取输⼊入字节流的⽂文件通道 /
    FileChannel fcin = fin.getChannel();
    /
    获取⽬目标⽂文件的输出字节流 /
    FileOutputStream fout = new FileOutputStream(dist);
    /
    获取输出字节流的⽂文件通道 /
    选择器器
    NIO 常常被叫做⾮非阻塞 IO,主要是因为 NIO 在⽹网络通信中的⾮非阻塞特性被⼴广泛使⽤用。
    NIO 实现了了 IO 多路路复⽤用中的 Reactor 模型,⼀一个线程 Thread 使⽤用⼀一个选择器器 Selector 通过轮询的⽅方
    式去监听多个通道 Channel 上的事件,从⽽而让⼀一个线程就可以处理理多个事件。
    通过配置监听的通道 Channel 为⾮非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不不会进⼊入阻塞状
    态⼀一直等待,⽽而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执⾏行行。
    因为创建和切换线程的开销很⼤大,因此使⽤用⼀一个线程来处理理多个事件⽽而不不是⼀一个线程处理理⼀一个事件,对
    于 IO 密集型的应⽤用具有很好地性能。
    应该注意的是,只有套接字 Channel 才能配置为⾮非阻塞,⽽而 FileChannel 不不能,为 FileChannel 配置⾮非
    阻塞也没有意义。
    FileChannel fcout = fout.getChannel();
    /
    为缓冲区分配 1024 个字节 /
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    while (true) {
    /
    从输⼊入通道中读取数据到缓冲区中 /
    int r = fcin.read(buffer);
    /
    read() 返回 -1 表示 EOF /
    if (r == -1) {
    break;
    }
    /
    切换读写 /
    buffer.flip();
    /
    把缓冲区的内容写⼊入输出⽂文件中 /
    fcout.write(buffer);
    /
    清空缓冲区 */
    buffer.clear();
    }
    }
  3. 创建选择器器
  4. 将通道注册到选择器器上
    通道必须配置为⾮非阻塞模式,否则使⽤用选择器器就没有任何意义了了,因为如果通道在某个事件上被阻塞,
    那么服务器器就不不能响应其它事件,必须等待这个事件处理理完毕才能去处理理其它事件,显然这和选择器器的
    作⽤用背道⽽而驰。
    在将通道注册到选择器器上时,还需要指定要注册的具体事件,主要有以下⼏几类:
    SelectionKey.OP_CONNECT
    SelectionKey.OP_ACCEPT
    SelectionKey.OP_READ
    SelectionKey.OP_WRITE
    它们在 SelectionKey 的定义如下:
    可以看出每个事件可以被当成⼀一个位域,从⽽而组成事件集整数。例例如:
    Selector selector = Selector.open();
    ServerSocketChannel ssChannel = ServerSocketChannel.open();
    ssChannel.configureBlocking(false);
    ssChannel.register(selector, SelectionKey.OP_ACCEPT);
    public static final int OP_READ = 1 << 0;
    public static final int OP_WRITE = 1 << 2;
    public static final int OP_CONNECT = 1 << 3;
    public static final int OP_ACCEPT = 1 << 4;
  5. 监听事件
    使⽤用 select() 来监听到达的事件,它会⼀一直阻塞直到有⾄至少⼀一个事件到达。
  6. 获取到达的事件
  7. 事件循环
    因为⼀一次 select() 调⽤用不不能处理理完所有的事件,并且服务器器端有可能需要⼀一直监听事件,因此服务器器端
    处理理事件的代码⼀一般会放在⼀一个死循环内。
    int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
    int num = selector.select();
    Set keys = selector.selectedKeys();
    Iterator keyIterator = keys.iterator();
    while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
    // …
    } else if (key.isReadable()) {
    // …
    }
    keyIterator.remove();
    }
    while (true) {
    int num = selector.select();
    Set keys = selector.selectedKeys();
    Iterator keyIterator = keys.iterator();
    while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
    // …
    } else if (key.isReadable()) {
    // …
    }
    keyIterator.remove();
    }
    }
    套接字 NIO 实例例
    public class NIOServer {
    public static void main(String[] args) throws IOException {
    Selector selector = Selector.open();
    ServerSocketChannel ssChannel = ServerSocketChannel.open();
    ssChannel.configureBlocking(false);
    ssChannel.register(selector, SelectionKey.OP_ACCEPT);
    ServerSocket serverSocket = ssChannel.socket();
    InetSocketAddress address = new InetSocketAddress(“127.0.0.1”,
    8888);
    serverSocket.bind(address);
    while (true) {
    selector.select();
    Set keys = selector.selectedKeys();
    Iterator keyIterator = keys.iterator();
    while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
    ServerSocketChannel ssChannel1 = (ServerSocketChannel)
    key.channel();
    // 服务器器会为每个新连接创建⼀一个 SocketChannel
    SocketChannel sChannel = ssChannel1.accept();
    sChannel.configureBlocking(false);
    // 这个新连接主要⽤用于从客户端读取数据
    sChannel.register(selector, SelectionKey.OP_READ);
    } else if (key.isReadable()) {
    SocketChannel sChannel = (SocketChannel) key.channel();
    System.out.println(readDataFromSocketChannel(sChannel));
    sChannel.close();
    }
    keyIterator.remove();
    }
    }
    }
    private static String readDataFromSocketChannel(SocketChannel sChannel)
    throws IOException {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    StringBuilder data = new StringBuilder();
    while (true) {
    buffer.clear();
    int n = sChannel.read(buffer);
    if (n == -1) {
    break;
    }
    buffer.flip();
    int limit = buffer.limit();
    char[] dst = new char[limit];
    for (int i = 0; i < limit; i++) {
    dst[i] = (char) buffer.get(i);
    }
    data.append(dst);
    buffer.clear();
    }
    return data.toString();
    }
    }
    内存映射⽂文件
    内存映射⽂文件 I/O 是⼀一种读和写⽂文件数据的⽅方法,它可以⽐比常规的基于流或者基于通道的 I/O 快得多。
    向内存映射⽂文件写⼊入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘
    上的⽂文件。修改数据与将数据保存到磁盘是没有分开的。
    下⾯面代码⾏行行将⽂文件的前 1024 个字节映射到内存中,map() ⽅方法返回⼀一个 MappedByteBuffer,它是
    ByteBuffer 的⼦子类。因此,可以像使⽤用其他任何 ByteBuffer ⼀一样使⽤用新映射的缓冲区,操作系统会在需
    要时负责执⾏行行映射。
    对⽐比
    NIO 与普通 I/O 的区别主要有以下两点:
    NIO 是⾮非阻塞的;
    NIO ⾯面向块,I/O ⾯面向流。
    ⼋八、参考资料料
    Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械⼯工业出版社, 2002.
    IBM: NIO ⼊入⻔门
    Java NIO Tutorial
    Java NIO 浅析
    IBM: 深⼊入分析 Java I/O 的⼯工作机制
    IBM: 深⼊入分析 Java 中的中⽂文编码问题
    IBM: Java 序列列化的⾼高级认识
    NIO 与传统 IO 的区别
    Decorator Design Pattern
    Socket Multicast
    public class NIOClient {
    public static void main(String[] args) throws IOException {
    Socket socket = new Socket(“127.0.0.1”, 8888);
    OutputStream out = socket.getOutputStream();
    String s = “hello world”;
    out.write(s.getBytes());
    out.close();
    }
    }
    MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
    one more thing
    如果你觉得这份资料料不不错的话,欢迎关注「沉默王⼆二」公众号,回复「Java」有更更多惊喜哦。

你可能感兴趣的:(java,java,缓存,开发语言)