diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..314f02b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.txt \ No newline at end of file diff --git a/notes/Java 基础.md b/notes/Java 基础.md index 2a4e0221..53c59fab 100644 --- a/notes/Java 基础.md +++ b/notes/Java 基础.md @@ -4,8 +4,10 @@ * [static](#static) * [二、Object 通用方法](#二object-通用方法) * [概览](#概览) - * [clone()](#clone) * [equals()](#equals) + * [hashCode()](#hashcode) + * [toString()](#tostring) + * [clone()](#clone) * [四、继承](#四继承) * [访问权限](#访问权限) * [抽象类与接口](#抽象类与接口) @@ -44,7 +46,7 @@ ```java final int x = 1; -x = 2; // cannot assign value to final variable 'x' +// x = 2; // cannot assign value to final variable 'x' final A y = new A(); y.a = 1; ``` @@ -154,6 +156,147 @@ public final void wait() throws InterruptedException protected void finalize() throws Throwable {} ``` +## equals() + +**1. equals() 与 == 的区别** + +- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 +- 对于引用类型,== 判断两个实例是否引用同一个对象,而 equals() 判断引用的对象是否等价。 + +```java +Integer x = new Integer(1); +Integer y = new Integer(1); +System.out.println(x.equals(y)); // true +System.out.println(x == y); // false +``` + +**2. 等价关系** + +(一)自反性 + +```java +x.equals(x); // true +``` + +(二)对称性 + +```java +x.equals(y) == y.equals(x) // true +``` + +(三)传递性 + +```java +if(x.equals(y) && y.equals(z)) { + x.equals(z); // true; +} +``` + +(四)一致性 + +多次调用 equals() 方法结果不变 + +```java +x.equals(y) == x.equals(y); // true +``` + +(五)与 null 的比较 + +对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false + +```java +x.euqals(null); // false; +``` + +**3. 实现** + +- 检查是否为同一个对象的引用,如果是直接返回 true; +- 检查是否是同一个类型,如果不是,直接返回 false; +- 将 Object 实例进行转型; +- 判断每个关键域是否相等。 + +```java +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 + 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; + } +} +``` + +## hashCode() + +hasCode() 返回散列值,而 equals() 是用来判断两个实例是否相等。相等的两个实例散列值一定要相同,但是散列值相同的两个实例不一定相等。 + +在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证相等的两个实例散列值也相等。 + +下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例。 + +```java +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 +``` + +理想的散列函数应当具有均匀性,即不相等的实例应当均匀分不到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。 + +一个数与 31 相乘可以转换成移位和减法:31\*x == (x<<5)-x。 + +```java +@Override +public int hashCode() { + int result = 17; + result = 31 * result + x; + result = 31 * result + y; + result = 31 * result + z; + return result; +} +``` + +## toString() + +默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。 + +```java +public class ToStringExample { + private int number; + + public ToStringExample(int number) { + this.number = number; + } +} +``` + +```java +ToStringExample example = new ToStringExample(123); +System.out.println(example.toString()); +``` + +```html +ToStringExample@4554617c +``` + ## clone() **1. cloneable** @@ -161,21 +304,48 @@ protected void finalize() throws Throwable {} clone() 是 Object 的受保护方法,这意味着,如果一个类不显式去重载 clone() 就没有这个方法。 ```java -public class CloneTest { +public class CloneExample { private int a; private int b; } ``` ```java -CloneTest x = new CloneTest(); -CloneTest y = x.clone(); // 'clone()' has protected access in 'java.lang.Object' +CloneExample e1 = new CloneExample(); +CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object' ``` 接下来重载 Object 的 clone() 得到以下实现: ```java -public class CloneTest{ +public class CloneExample { + private int a; + private int b; + + @Override + protected CloneExample clone() throws CloneNotSupportedException { + return (CloneExample)super.clone(); + } +} +``` + +```java +CloneExample e1 = new CloneExample(); +try { + CloneExample e2 = e1.clone(); +} catch (CloneNotSupportedException e) { + e.printStackTrace(); +} +``` + +```html +java.lang.CloneNotSupportedException: CloneTest +``` + +以上抛出了 CloneNotSupportedException,这是因为 CloneTest 没有实现 Cloneable 接口。 + +```java +public class CloneExample implements Cloneable { private int a; private int b; @@ -186,44 +356,130 @@ public class CloneTest{ } ``` -```java -CloneTest x = new CloneTest(); -try { - CloneTest y = (CloneTest) x.clone(); -} catch (CloneNotSupportedException e) { - e.printStackTrace(); -} -``` - -```html -java.lang.CloneNotSupportedException: CloneTest -``` - -以上抛出了 CloneNotSupportedException,这是因为 CloneTest 没有实现 Cloneable 接口。应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。 +应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。 **2. 深拷贝与浅拷贝** -- 浅拷贝:拷贝对象和原对象的引用类型引用同一个对象; -- 深拷贝:引用不同对象。 +- 浅拷贝:拷贝实例和原始实例的引用类型引用同一个对象; +- 深拷贝:拷贝实例和原始实例的引用类型引用不同对象。 -实现深拷贝的方法: +```java +public class ShallowCloneExample implements Cloneable { + private int[] arr; -- [Defensive copying](http://www.javapractices.com/topic/TopicAction.do?Id=15) -- [copy constructors](http://www.javapractices.com/topic/TopicAction.do?Id=12) -- [static factory methods](http://www.javapractices.com/topic/TopicAction.do?Id=21). + public ShallowCloneExample() { + arr = new int[10]; + for (int i = 0; i < arr.length; i++) { + arr[i] = i; + } + } -> [How do I copy an object in Java?](https://stackoverflow.com/questions/869033/how-do-i-copy-an-object-in-java) + public void set(int index, int value) { + arr[index] = value; + } -## equals() + public int get(int index) { + return arr[index]; + } -**1. == 与 equals() 区别** + @Override + protected ShallowCloneExample clone() throws CloneNotSupportedException { + return (ShallowCloneExample) super.clone(); + } +} +``` -- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 -- 对于引用类型,== 判断两个引用是否引用同一个对象,而 equals() 判断引用的对象是否等价。 +```java +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 +``` -**2. 等价性** +```java +public class DeepCloneExample implements Cloneable { + private int[] arr; -> [散列](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Java%20%E5%AE%B9%E5%99%A8.md#%E4%B8%89%E6%95%A3%E5%88%977) + 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; + } +} +``` + +```java +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 +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]; + } +} +``` + +```java +CloneConstructorExample e1 = new CloneConstructorExample(); +CloneConstructorExample e2 = new CloneConstructorExample(e1); +e1.set(2, 222); +System.out.println(e2.get(2)); // 2 +``` # 四、继承 @@ -233,69 +489,150 @@ Java 中有三个访问权限修饰符:private、protected 以及 public,如 可以对类或类中的成员(字段以及方法)加上访问修饰符。 -- 成员可见表示其它类可该类的对象访问到该成员; +- 成员可见表示其它类可以用这个类的实例访问到该成员; - 类可见表示其它类可以用这个类创建对象。 -在理解类的可见性时,可以把类当做包中的一个成员,然后包表示一个类,那么就可以类比成员的可见性。 +protected 用于修饰成员,表示在继承体系中成员对于子类可见,但是这个访问修饰符对于类没有意义。 -protected 用于修饰成员,表示在继承体系中成员对于子类可见。但是这个访问修饰符对于类没有意义,因为包没有继承体系。 +设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问。 -> [浅析 Java 中的访问权限控制](http://www.importnew.com/18097.html) +如果子类的方法覆盖了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里式替换原则。 + +字段决不能是公有的,因为这么做的话就失去了对这个实例域修改行为的控制,客户端可以对其随意修改。可以使用共有的 getter 和 setter 方法来替换共有字段。 + +```java +public class AccessExample { + public int x; +} +``` + +```java +public class AccessExample { + private int x; + + public int getX() { + return x; + } + + public void setX(int x) { + this.x = x; + } +} +``` + +但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。 + +```java +public class AccessWithInnerClassExample { + private class InnerClass { + int x; + } + + private InnerClass innerClass; + + public AccessWithInnerClassExample() { + innerClass = new InnerClass(); + } + + public int getValue() { + return innerClass.x; // 直接访问 + } +} +``` ## 抽象类与接口 **1. 抽象类** -抽象类和抽象方法都使用 abstract 进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。 +抽象类和抽象方法都使用 abstract 进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。 + +抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。 ```java -public abstract class GenericServlet implements Servlet, ServletConfig, Serializable { - // abstract method - abstract void service(ServletRequest req, ServletResponse res); +public abstract class AbstractClassExample { - void init() { - // Its implementation + protected int x; + private int y; + + public abstract void func1(); + + public void func2() { + System.out.println("func2"); } - // other method related to Servlet } ``` -> [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/) +```java +public class AbstractExtendClassExample extends AbstractClassExample{ + @Override + public void func1() { + System.out.println("func1"); + } +} +``` + +```java +// AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated +AbstractClassExample ac2 = new AbstractExtendClassExample(); +ac2.func1(); +``` **2. 接口** -接口是抽象类的延伸。Java 为了安全性而不支持多重继承,一个类只能有一个父类。但是接口不同,一个类可以同时实现多个接口,不管这些接口之间有没有关系,所以接口弥补不支持多重继承的缺陷。 - -```java -public interface Externalizable extends Serializable { - - void writeExternal(ObjectOutput out) throws IOException; - - void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; -} -``` +接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。 从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。 +接口也可以包含域,并且这些域隐式都是 static 和 final 的。 + +接口中的方法默认都是 public 的,并且不允许定义为 private 或者 protected。 + ```java -public interface InterfaceDefaultTest { - default void func() { - System.out.println("default method in interface!"); +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 +} +``` + +```java +public class InterfaceImplementExample implements InterfaceExample { + @Override + public void func1() { + System.out.println("func1"); } } ``` +```java +// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated +InterfaceExample ie2 = new InterfaceImplementExample(); +ie2.func1(); +System.out.println(InterfaceExample.x); +``` + **3. 比较** - 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求子类和父类具有 IS-A 关系; - 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。 +- 接口的域只能是 static 和 final 类型的,而抽象类的域可以有多种访问权限。 +- 接口的方法只能是 public 的,而抽象类的方法可以由多种访问权限。 **4. 使用选择** 使用抽象类: - 需要在几个相关的类中共享代码; -- 需要能控制继承来的方法和字段的访问权限,而不是都为 public。 +- 需要能控制继承来的方法和域的访问权限,而不是都为 public。 - 需要继承非静态(non-static)和非常量(non-final)字段。 使用接口: @@ -303,46 +640,56 @@ public interface InterfaceDefaultTest { - 需要让不相关的类都实现一个方法,例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法; - 需要使用多重继承。 -> [When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface) +在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次接口要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。 + +> [深入理解 abstract class 和 interface](https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/)
[When to Use Abstract Class and Interface](https://dzone.com/articles/when-to-use-abstract-class-and-intreface) ## super -**1. 访问父类的成员** - -如果子类覆盖了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。 +- 访问父类的构造函数:可以使用 super() 函数访问父类的构造函数,从而完成一些初始化的工作。 +- 访问父类的成员:如果子类覆盖了父类的中某个方法的实现,可以通过使用 super 关键字来引用父类的方法实现。 ```java -public class Superclass { - public void printMethod() { - System.out.println("Printed in Superclass."); +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()"); } } ``` ```java -public class Subclass extends Superclass { - // Overrides printMethod in Superclass - public void printMethod() { - super.printMethod(); - System.out.println("Printed in Subclass"); +public class SuperExtendExample extends SuperExample { + private int z; + + public SuperExtendExample(int x, int y, int z) { + super(x, y); + this.z = z; } - public static void main(String[] args) { - Subclass s = new Subclass(); - s.printMethod(); + @Override + public void func() { + super.func(); + System.out.println("SuperExtendExample.func()"); } } ``` -**2. 访问父类的构造函数** - -可以使用 super() 函数访问父类的构造函数,从而完成一些初始化的工作。 - ```java -public MountainBike(int startHeight, int startCadence, int startSpeed, int startGear) { - super(startCadence, startSpeed, startGear); - seatHeight = startHeight; -} +SuperExample e = new SuperExtendExample(1, 2, 3); +e.func(); +``` + +```html +SuperExample.func() +SuperExtendExample.func() ``` > [Using the Keyword super](https://docs.oracle.com/javase/tutorial/java/IandI/super.html) @@ -374,7 +721,7 @@ public MountainBike(int startHeight, int startCadence, int startSpeed, int start **1. 可以缓存 hash 值** -因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 等情况。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。 +因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。 **2. String Pool 的需要** @@ -388,21 +735,50 @@ String 经常作为参数,String 不可变性可以保证参数不可变。例 **4. 线程安全** -String 不可变性天生具备线程安全,可以在多个线程中使用。 +String 不可变性天生具备线程安全,可以在多个线程中安全地使用。 > [Why String is immutable in Java?](https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/) ## String.intern() -使用 String.intern() 可以保证所有相同内容的字符串变量引用相同的内存对象。 +使用 String.intern() 可以保证相同内容的字符串实例引用相同的内存对象。 -> [揭开 String.intern() 那神秘的面纱](https://www.jianshu.com/p/95f516cb75ef) +下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用,这个方法首先把 s1 引用的对象放到 String Poll(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。 + +```java +String s1 = new String("aaa"); +String s2 = new String("aaa"); +System.out.println(s1 == s2); // false +String s3 = s1.intern(); +System.out.println(s1.intern() == s3); // true +``` + +如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Poll 中。 + +```java +String s4 = "bbb"; +String s5 = "bbb"; +System.out.println(s4 == s5); // true +``` + +Java 虚拟机将堆划分成新生代、老年代和永久代(PermGen Space)。在 Java 6 之前,字符串常量池被放在永久代中,而在 Java 7 时,它被放在堆的其它位置。这是因为永久代的空间有限,如果大量使用字符串的场景下会导致 OutOfMemoryError 错误。 + +> [What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning)
[深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html) # 六、基本类型与运算 ## 包装类型 -八个基本类型:boolean/1 byte/8 char/16 short/16 int/32 float/32 long/64 double/64 +八个基本类型: + +- boolean/1 +- byte/8 +- char/16 +- short/16 +- int/32 +- float/32 +- long/64 +- double/64 基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。 @@ -411,38 +787,70 @@ Integer x = 2; // 装箱 int y = x; // 拆箱 ``` -new Integer(123) 与 Integer.valueOf(123) 的区别在于,Integer.valueOf(123) 可能会使用缓存对象,因此多次使用 Integer.valueOf(123) 会取得同一个对象的引用。 +new Integer(123) 与 Integer.valueOf(123) 的区别在于,new Integer(123) 每次都会新建一个对象,而 Integer.valueOf(123) 可能会使用缓存对象,因此多次使用 Integer.valueOf(123) 会取得同一个对象的引用。 ```java -public static void main(String[] args) { - Integer a = new Integer(1); - Integer b = new Integer(1); - System.out.println("a==b? " + (a == b)); - - Integer c = Integer.valueOf(1); - Integer d = Integer.valueOf(1); - System.out.println("c==d? " + (c == d)); -} +Integer x = new Integer(123); +Integer y = new Integer(123); +System.out.println(x == y); // false +Integer z = Integer.valueOf(123); +Integer k = Integer.valueOf(123); +System.out.println(z == k); // true ``` -```html -a==b? false -c==d? true +编译器会在自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。 + +```java +Integer m = 123; +Integer n = 123; +System.out.println(m == n); // true ``` valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接使用缓存池的内容。 ```java public static Integer valueOf(int i) { - final int offset = 128; - if (i >= -128 && i <= 127) { - return IntegerCache.cache[i + offset]; - } + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } ``` -基本类型中可以使用缓存池的值如下: +在 Java 8 中,Integer 缓存池的大小默认为 -128\~127。 + +```java +static final int low = -128; +static final int high; +static final Integer cache[]; + +static { + // high value may be configured by property + int h = 127; + String integerCacheHighPropValue = + sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); + if (integerCacheHighPropValue != null) { + try { + int i = parseInt(integerCacheHighPropValue); + i = Math.max(i, 127); + // Maximum array size is Integer.MAX_VALUE + h = Math.min(i, Integer.MAX_VALUE - (-low) -1); + } catch( NumberFormatException nfe) { + // If the property cannot be parsed into an int, ignore it. + } + } + high = h; + + cache = new Integer[(high - low) + 1]; + int j = low; + for(int k = 0; k < cache.length; k++) + cache[k] = new Integer(j++); + + // range [-128, 127] must be interned (JLS7 5.1.7) + assert IntegerCache.high >= 127; +} +``` + +Java 还将一些其它基本类型的值放在缓冲池中,包含以下这些: - boolean values true and false - all byte values @@ -450,52 +858,42 @@ public static Integer valueOf(int i) { - int values between -128 and 127 - char in the range \u0000 to \u007F -自动装箱过程编译器会调用 valueOf() 方法,因此多个 Integer 对象使用装箱来创建并且值相同,那么就会引用相同的对象。这样做很显然是为了节省内存开销。 - -```java -Integer x = 1; -Integer y = 1; -System.out.println(c == d); // true -``` +因此在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。 > [Differences between new Integer(123), Integer.valueOf(123) and just 123 ](https://stackoverflow.com/questions/9030817/differences-between-new-integer123-integer-valueof123-and-just-123) ## switch -A switch works with the byte, short, char, and int primitive data types. It also works with enumerated types and a few special classes that "wrap" certain primitive types: Character, Byte, Short, and Integer. +从 Java 7 开始,可以在 switch 条件判断语句中使用 String 对象。 -In the JDK 7 release, you can use a String object in the expression of a switch statement. +```java +String s = "a"; +switch (s) { + case "a": + System.out.println("aaa"); + break; + case "b": + System.out.println("bbb"); + break; +} +``` switch 不支持 long,是因为 swicth 的设计初衷是为那些只需要对少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。 -> [Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java) - -switch 使用查找表的方式来实现,JVM 中使用的指令是 lookupswitch。 - ```java -public static void main(String... args) { - switch (1) { - case 1: - break; - case 2: - break; - } -} - -public static void main(java.lang.String[]); - Code: - Stack=1, Locals=1, Args_size=1 - 0: iconst_1 - 1: lookupswitch{ //2 - 1: 28; - 2: 31; - default: 31 } - 28: goto 31 - 31: return +// 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; +// } ``` -> [How does Java's switch work under the hood?](https://stackoverflow.com/questions/12020048/how-does-javas-switch-work-under-the-hood) +> [Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java) # 七、反射 @@ -513,8 +911,6 @@ Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect IDE 使用反射机制获取类的信息,在使用一个类的对象时,能够把类的字段、方法和构造函数等信息列出来供用户选择。 -> [深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/) - **Advantages of Using Reflection:** - **Extensibility Features** : An application may make use of external, user-defined classes by creating instances of extensibility objects using their fully-qualified names. @@ -529,7 +925,7 @@ Reflection is powerful, but should not be used indiscriminately. If it is possib - **Security Restrictions** : Reflection requires a runtime permission which may not be present when running under a security manager. This is in an important consideration for code which has to run in a restricted security context, such as in an Applet. - **Exposure of Internals** :Since reflection allows code to perform operations that would be illegal in non-reflective code, such as accessing private fields and methods, the use of reflection can result in unexpected side-effects, which may render code dysfunctional and may destroy portability. Reflective code breaks abstractions and therefore may change behavior with upgrades of the platform. -> [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html) +> [Trail: The Reflection API](https://docs.oracle.com/javase/tutorial/reflect/index.html)
[深入解析 Java 反射(1)- 基础](http://www.sczyh30.com/posts/Java/java-reflection-1/) # 八、异常 @@ -540,8 +936,7 @@ Throwable 可以用来表示任何可以作为异常抛出的类,分为两种

-> - [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception) -> - [Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html) +> [Java 入门之异常处理](https://www.tianmaying.com/tutorial/Java-Exception)
[Java 异常的面试问题及答案 -Part 1](http://www.importnew.com/7383.html) # 九、泛型 diff --git a/notes/Java 容器.md b/notes/Java 容器.md index 8c79de2c..7c31b8a2 100644 --- a/notes/Java 容器.md +++ b/notes/Java 容器.md @@ -5,8 +5,7 @@ * [二、容器中的设计模式](#二容器中的设计模式) * [迭代器模式](#迭代器模式) * [适配器模式](#适配器模式) -* [三、散列](#三散列) -* [四、源码分析](#四源码分析) +* [三、源码分析](#三源码分析) * [ArrayList](#arraylist) * [Vector](#vector) * [LinkedList](#linkedlist) @@ -15,7 +14,7 @@ * [LinkedHashMap](#linkedhashmap) * [ConcurrentHashMap - JDK 1.7](#concurrenthashmap---jdk-17) * [ConcurrentHashMap - JDK 1.8](#concurrenthashmap---jdk-18) -* [五、参考资料](#五参考资料) +* [参考资料](#参考资料) @@ -102,51 +101,7 @@ List list = Arrays.asList(arr); List list = Arrays.asList(1,2,3); ``` -# 三、散列 - -hasCode() 返回散列值,使用的是对象的地址。 - -而 equals() 是用来判断两个对象是否相等的,相等的两个对象散列值一定要相同,但是散列值相同的两个对象不一定相等。 - -相等必须满足以下五个性质: - -**1. 自反性** - -```java -x.equals(x); // true -``` - -**2. 对称性** - -```java -x.equals(y) == y.equals(x) // true -``` - -**3. 传递性** - -```java -if(x.equals(y) && y.equals(z)) { - x.equals(z); // true; -} -``` - -**4. 一致性** - -多次调用 equals() 方法结果不变 - -```java -x.equals(y) == x.equals(y); // true -``` - -**5. 与 null 的比较** - -对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false - -```java -x.euqals(null); // false; -``` - -# 四、源码分析 +# 三、源码分析 建议先阅读 [算法-查找](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E7%AE%97%E6%B3%95.md#%E6%9F%A5%E6%89%BE) 部分,对容器类源码的理解有很大帮助。 @@ -745,7 +700,7 @@ JDK 1.8 的实现不是用了 Segment,Segment 属于重入锁 ReentrantLock。 并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。 -# 五、参考资料 +# 参考资料 - Eckel B. Java 编程思想 [M]. 机械工业出版社, 2002. - [Java Collection Framework](https://www.w3resource.com/java-tutorial/java-collections.php) diff --git a/notes/Java 并发.md b/notes/Java 并发.md index e9efb1e8..96d344a7 100644 --- a/notes/Java 并发.md +++ b/notes/Java 并发.md @@ -1,37 +1,99 @@ -* [一、使用线程](#一使用线程) +* [一、线程状态转换](#一线程状态转换) + * [新建(New)](#新建new) + * [可运行(Runnable)](#可运行runnable) + * [阻塞(Blocking)](#阻塞blocking) + * [无限期等待(Waiting)](#无限期等待waiting) + * [限期等待(Timed Waiting)](#限期等待timed-waiting) + * [死亡(Terminated)](#死亡terminated) +* [二、使用线程](#二使用线程) * [实现 Runnable 接口](#实现-runnable-接口) * [实现 Callable 接口](#实现-callable-接口) * [继承 Thread 类](#继承-thread-类) * [实现接口 VS 继承 Thread](#实现接口-vs-继承-thread) -* [二、基础线程机制](#二基础线程机制) +* [三、基础线程机制](#三基础线程机制) + * [Executor](#executor) + * [Daemon](#daemon) * [sleep()](#sleep) * [yield()](#yield) +* [四、中断](#四中断) + * [InterruptedException](#interruptedexception) + * [interrupted()](#interrupted) + * [Executor 的中断操作](#executor-的中断操作) +* [五、互斥同步](#五互斥同步) + * [synchronized](#synchronized) + * [ReentrantLock](#reentrantlock) + * [synchronized 和 ReentrantLock 比较](#synchronized-和-reentrantlock-比较) +* [六、线程之间的协作](#六线程之间的协作) * [join()](#join) - * [deamon](#deamon) -* [三、结束线程](#三结束线程) - * [阻塞](#阻塞) - * [中断](#中断) -* [四、线程之间的协作](#四线程之间的协作) - * [同步与通信的概念理解](#同步与通信的概念理解) - * [线程同步](#线程同步) - * [线程通信](#线程通信) -* [五、线程状态转换](#五线程状态转换) -* [六、Executor](#六executor) -* [七、内存模型](#七内存模型) + * [wait() notify() notifyAll()](#wait-notify-notifyall) + * [await() signal() signalAll()](#await-signal-signalall) + * [BlockingQueue](#blockingqueue) +* [七、线程不安全示例](#七线程不安全示例) +* [八、Java 内存模型](#八java-内存模型) * [主内存与工作内存](#主内存与工作内存) + * [内存间交互操作](#内存间交互操作) * [内存模型三大特性](#内存模型三大特性) * [先行发生原则](#先行发生原则) -* [八、线程安全](#八线程安全) +* [九、线程安全](#九线程安全) * [线程安全分类](#线程安全分类) * [线程安全的实现方法](#线程安全的实现方法) - * [锁优化](#锁优化) +* [十、锁优化](#十锁优化) + * [自旋锁与自适应自旋](#自旋锁与自适应自旋) + * [锁消除](#锁消除) + * [锁粗化](#锁粗化) + * [轻量级锁](#轻量级锁) + * [偏向锁](#偏向锁) * [九、多线程开发良好的实践](#九多线程开发良好的实践) * [参考资料](#参考资料) -# 一、使用线程 +# 一、线程状态转换 + +

+ +## 新建(New) + +创建后尚未启动。 + +## 可运行(Runnable) + +可能正在运行,也可能正在等待 CPU 时间片。 + +包含了操作系统线程状态中的 Running 和 Ready。 + +## 阻塞(Blocking) + +等待获取一个排它锁,如果其线程释放了锁就会结束此状态。 + +## 无限期等待(Waiting) + +等待其它线程显示地唤醒,否则不会被分配 CPU 时间片; + +| 进入方法 | 退出方法 | +| --- | --- | +| 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() Object.notifyAll() | +| 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 | +| LockSupport.park() 方法 | - | + +## 限期等待(Timed Waiting) + +无需等待其它线程显示地唤醒,在一定时间之后会被系统自动唤醒。 + +| 进入方法 | 退出方法 | +| --- | --- | +| Thread.sleep() 方法 | 时间结束 | +| 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() Object.notifyAll() | +| 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 | +| LockSupport.parkNanos() 方法 | - | +| LockSupport.parkUntil() 方法 | - | + +## 死亡(Terminated) + +可以是线程结束任务之后自己结束,或者产生了异常而结束。 + +# 二、使用线程 有三种使用线程的方法: @@ -52,11 +114,14 @@ public class MyRunnable implements Runnable { public void run() { // ... } - public static void main(String[] args) { - MyRunnable instance = new MyRunnable(); - Tread thread = new Thread(instance); - thread.start(); - } +} +``` + +```java +public static void main(String[] args) { + MyRunnable instance = new MyRunnable(); + Thread thread = new Thread(instance); + thread.start(); } ``` @@ -67,18 +132,21 @@ public class MyRunnable implements Runnable { ```java public class MyCallable implements Callable { public Integer call() { - // ... - } - public static void main(String[] args) { - MyCallable mc = new MyCallable(); - FutureTask ft = new FutureTask<>(mc); - Thread thread = new Thread(ft); - thread.start(); - System.out.println(ft.get()); + return 123; } } ``` +```java +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()); +} +``` + ## 继承 Thread 类 同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。 @@ -88,10 +156,13 @@ public class MyThread extends Thread { public void run() { // ... } - public static void main(String[] args) { - MyThread mt = new MyThread(); - mt.start(); - } +} +``` + +```java +public static void main(String[] args) { + MyThread mt = new MyThread(); + mt.start(); } ``` @@ -102,218 +173,517 @@ public class MyThread extends Thread { 1. Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口; 2. 类可能只要求可执行就行,继承整个 Thread 类开销会过大。 +# 三、基础线程机制 -# 二、基础线程机制 +## Executor + +Executor 管理多个异步任务的执行,而无需程序员显示地管理线程的生命周期。 + +主要有三种 Executor: + +1. CachedTreadPool:一个任务创建一个线程; +2. FixedThreadPool:所有任务只能使用固定大小的线程; +3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 + +```java +public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + for (int i = 0; i < 5; i++) { + executorService.execute(new MyRunnable()); + } + executorService.shutdown(); +} +``` + +## Daemon + +守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。 + +当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。 + +main() 属于非守护线程。 + +使用 setDaemon() 方法将一个线程设置为守护线程。 + +```java +public static void main(String[] args) { + Thread thread = new Thread(new MyRunnable()); + thread.setDaemon(true); +} +``` ## sleep() -Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。也可以使用 TimeUnit.TILLISECONDS.sleep(millisec)。 +Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。 -sleep() 可能会抛出 InterruptedException。因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。 +sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。 ```java public void run() { try { - // ... - Thread.sleep(1000); - // ... + Thread.sleep(3000); } catch (InterruptedException e) { - System.err.println(e); + e.printStackTrace(); } } ``` ## yield() -对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。 +对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。 ```java public void run() { - // ... Thread.yield(); } ``` -## join() +# 四、中断 -在线程中调用另一个线程的 join() 方法,会将当前线程挂起,直到目标线程结束。 +一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。 -可以加一个超时参数。 +## InterruptedException -## deamon +通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。 -守护线程(deamon)是程序运行时在后台提供服务的线程,并不属于程序中不可或缺的部分。 - -当所有非后台线程结束时,程序也就终止,同时会杀死所有后台线程。 - -main() 属于非后台线程。 - -使用 setDaemon() 方法将一个线程设置为后台线程。 - -# 三、结束线程 - -## 阻塞 - -一个线程进入阻塞状态可能有以下原因: - -1. 调用 Thread.sleep() 使线程睡眠; -2. 调用 wait() 使线程挂起,直到线程得到 notify() 或 notifyAll() 消息(或者 java.util.concurrent 类库中等价的 signal() 或 signalAll() 消息; -3. 等待某个 I/O 的完成; -4. 试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个线程已经获得了这个锁。 - -**阻塞 睡眠 挂起** - -阻塞是一种状态,而睡眠和挂起是一种手段,通过睡眠和挂起可以让一个线程进入阻塞状态。 - -睡眠和挂起这两种手段的区别是,挂起手段会释放对象锁,而睡眠手段不会。 - -应该注意的是,睡眠和挂起都可以设置一个等待时间,超过等待时间之后,线程会退出阻塞状态。但是如果不为挂起设置等待时间,那么它只能等到通知的到来才能退出阻塞状态。 - -## 中断 - -使用中断机制即可终止阻塞的线程。 - -使用 **interrupt()** 方法来中断某个线程,它会设置线程的中断状态。Object.wait(), Thread.join() 和 Thread.sleep() 三种方法在收到中断请求的时候会清除中断状态,并抛出 InterruptedException。 - -应当捕获这个 InterruptedException 异常,从而做一些清理资源的操作。 - -**1. 不可中断的阻塞** - -不能中断 I/O 阻塞和 synchronized 锁阻塞。 - -**2. Executor 的中断操作** - -Executor 避免对 Thread 对象的直接操作,使用 shutdownNow() 方法来中断它里面的所有线程,shutdownNow() 方法会发送 interrupt() 调用给所有线程。 - -如果只想中断一个线程,那么使用 Executor 的 submit() 而不是 executor() 来启动线程,就可以持有线程的上下文。submit() 将返回一个泛型 Futrue,可以在它之上调用 cancel(),如果将 true 传递给 cancel(),那么它将会发送 interrupt() 调用给特定的线程。 - -**3. 检查中断** - -通过中断的方法来终止线程,需要线程进入阻塞状态才能终止。如果编写的 run() 方法循环条件为 true,但是该线程不发生阻塞,那么线程就永远无法终止。 - -interrupt() 方法会设置中断状态,可以通过 interrupted() 方法来检查中断状态,从而判断一个线程是否已经被中断。 - -interrupted() 方法在检查完中断状态之后会清除中断状态,这样做是为了确保一次中断操作只会产生一次影响。 - -# 四、线程之间的协作 - -## 同步与通信的概念理解 - -在操作系统中,有三个概念用来描述进程间的协作关系: - -1. 互斥:多个进程在同一时刻只有一个进程能进入临界区; -2. 同步:多个进程按一定顺序执行; -3. 通信:多个进程间的信息传递。 - -通信是一种手段,它可以用来实现同步。也就是说,通过在多个进程间传递信息,可以控制多个进程以一定顺序执行。 - -而同步又可以保证互斥。即进程按一定顺序执行,可以保证在同一时刻只有一个进程能访问临界资源。但是同步不止用来实现互斥,例如生成者消费者问题,生产者和消费者进程之间的同步不是用来控制对临界资源的访问。 - -总结起来就是:通信 -> 同步 -> 互斥。 - -进程和线程在一定程度上类似,也可以用这些概念来描述。 - -在 Java 语言中,这些概念描述有些差别: - -1. 同步:可以和操作系统的互斥等同; -2. 通信:可以和操作系统的同步等同。 - -很多时候这三个概念都会混在一起用,不同的文章有不同的解释,不能说哪个是对的哪个是错的,只要自己能理解就行。 - -## 线程同步 - -给定一个进程内的所有线程,都共享同一存储空间,这样有好处又有坏处。这些线程就可以共享数据,非常有用。不过,在两个线程同时修改某一资源时,这也会造成一些问题。Java 提供了同步机制,以控制对共享资源的互斥访问。 - -### 1. synchronized - -**同步一个方法** - -使多个线程不能同时访问该方法。 +对于以下代码,在 Main 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。 ```java -public synchronized void func(String name) { +public class InterruptExample { + public static void main(String[] args) throws InterruptedException { + Thread thread1 = new MyThread1(); + thread1.start(); + thread1.interrupt(); + System.out.println("Main run"); + } + + private static class MyThread1 extends Thread { + @Override + public void run() { + try { + Thread.sleep(2000); + System.out.println("Thread run"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} + +``` + +```html +Main run +java.lang.InterruptedException: sleep interrupted + at java.lang.Thread.sleep(Native Method) + at InterruptExample.lambda$main$0(InterruptExample.java:5) + at InterruptExample$$Lambda$1/713338599.run(Unknown Source) + at java.lang.Thread.run(Thread.java:745) +``` + +## interrupted() + +如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。 + +但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。 + +```java +public class InterruptExample { + public static void main(String[] args) throws InterruptedException { + Thread thread2 = new MyThread2(); + thread2.start(); + thread2.interrupt(); + } + + private static class MyThread2 extends Thread { + @Override + public void run() { + while (!interrupted()) { + // .. + } + System.out.println("Thread end"); + } + } +} +``` + +```html +Thread end +``` + +## Executor 的中断操作 + +调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。 + +以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。 + +```java +public class ExecutorInterruptExample { + 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"); + } +} +``` + +```html +Main run +java.lang.InterruptedException: sleep interrupted + at java.lang.Thread.sleep(Native Method) + at ExecutorInterruptExample.lambda$main$0(ExecutorInterruptExample.java:9) + at ExecutorInterruptExample$$Lambda$1/1160460865.run(Unknown Source) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) + at java.lang.Thread.run(Thread.java:745) +``` + +如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。 + +```java +Future future = executorService.submit(() -> { + // .. +}); +future.cancel(true); +``` + +# 五、互斥同步 + +Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。 + +## synchronized + +**1. 同步一个代码块** + +```java +public void func () { + synchronized (this) { + // ... + } +} +``` + +它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。 + +对于以下代码,使用 ExecutorService 执行了两个线程(这两个线程使用 Lambda 创建),由于调用的是同一个对象的同步语句块,因此这两个线程就需要进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。 + +```java +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()); + } +} +``` + +```html +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 +``` + +对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。 + +```java +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()); +} +``` + +```html +0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 +``` + + +**2. 同步一个方法** + +```java +public synchronized void func () { // ... } ``` -**同步一个代码块** +它和同步代码块一样,只作用于同一个对象。 + +**3. 同步一个类** ```java -public void func(String name) { - synchronized(this) { +public void func() { + synchronized (SynchronizedExample.class) { // ... } } ``` -### 2. ReentrantLock - -可以使用 Lock 来对一个语句块进行同步。 +作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也需要进行同步。 ```java -private Lock lock; -public int func(int value) { - try { - lock.lock(); - // ... - } finally { - lock.unlock(); - } +public class SynchronizedExample { + + public void func2() { + synchronized (SynchronizedExample.class) { + for (int i = 0; i < 10; i++) { + System.out.print(i + " "); + } + } + } + + 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()); + } } ``` +```html +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 +``` + +**4. 同步一个静态方法** + +```java +public synchronized static void fun() { + // ... +} +``` + +作用于整个类。 + +## ReentrantLock + +```java +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(); // 确保释放锁,从而避免发生死锁。 + } + } +} +``` + +```java +public static void main(String[] args) { + LockExample lockExample = new LockExample(); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> lockExample.func()); + executorService.execute(() -> lockExample.func()); +} +``` + +```html +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 +``` + ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了一些高级功能: -**等待可中断** +**1. 等待可中断** 当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。 -**可实现公平锁** +**2. 可实现公平锁** 公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。 -**锁绑定多个条件** +**3. 锁绑定多个条件** 一个 ReentrantLock 对象可以同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无须这样做,只需要多次调用 newCondition() 方法即可。 -如果需要使用上述功能,选用 ReentrantLock 是一个很好的选择。从性能上来看,在新版本的 JDK 中对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由,而且 synchronized 有更大的优化空间,因此优先考虑 synchronized。 +## synchronized 和 ReentrantLock 比较 -## 线程通信 +**1. 锁的实现** -### 1. wait() notify() notifyAll() +synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。 + +**2. 性能** + +从性能上来看,在新版本的 JDK 中对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由,而且 synchronized 有更大的优化空间,因此优先考虑 synchronized。 + +**3. 功能** + +ReentrantLock 多了一些高级功能。 + +**4. 使用选择** + +除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。 + +# 六、线程之间的协作 + +当多个线程可以一起工作去解决某个问题时,需要对它们进行协调,因为某些部分必须在其它部分之前完成。 + +## join() + +在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。 + +对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,因此 b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先与 b 线程的输出。 + +```java +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(); + } + + public static void main(String[] args) { + JoinExample example = new JoinExample(); + example.test(); + } +} +``` + +``` +A +B +``` + +## wait() notify() notifyAll() + +调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。 它们都属于 Object 的一部分,而不属于 Thread。 -wait() 会在等待时将线程挂起,而不是忙等待,并且只有在 notify() 或者 notifyAll() 到达时才唤醒。可以通过这种机制让一个线程阻塞,直到某种特定条件满足。 +只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。 -sleep() 和 yield() 并没有释放锁,但是 wait() 会释放锁。 - -只有在同步控制方法或同步控制块里才能调用 wait() 、notify() 和 notifyAll()。 +使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程。 ```java -private boolean flag = false; +public class WaitNotifyExample { + public synchronized void before() { + System.out.println("before"); + notifyAll(); + } -public synchronized void after() { - while(flag == false) { - wait(); - // ... + public synchronized void after() { + try { + wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("after"); + } + + public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + WaitNotifyExample example = new WaitNotifyExample(); + executorService.execute(() -> example.after()); + executorService.execute(() -> example.before()); } } +``` -public synchronized void before() { - flag = true; - notifyAll(); -} +```html +before +after ``` **wait() 和 sleep() 的区别** -这两种方法都能将线程阻塞,一种是使用挂起的方式,一种使用睡眠的方式。 - 1. wait() 是 Object 类的方法,而 sleep() 是 Thread 的静态方法; -2. 挂起会释放锁,睡眠不会。 +2. wait() 会释放锁,sleep() 不会。 -### 2. BlockingQueue +## await() signal() signalAll() + +java.util.confurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法时线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒挂起的线程。 + +使用 Lock 来获取一个 Condition 对象。 + +```java +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(); + 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()); + } +} +``` + +## BlockingQueue java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现: @@ -404,43 +774,79 @@ Consumer-3 is consuming product.( Made By Producer-3 ) Consumer-4 is consuming product.( Made By Producer-4 ) ``` -# 五、线程状态转换 +# 七、线程不安全示例 -

+如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。 -1. 新建(New):创建后尚未启动; -2. 可运行(Runnale):可能正在运行,也可能正在等待 CPU 时间片; -3. 无限期等待(Waiting):等待其它线程显示地唤醒,否则不会被分配 CPU 时间片; -4. 限期等待(Timed Waiting):无需等待其它线程显示地唤醒,在一定时间之后会被系统自动唤醒; -5. 阻塞(Blocking):等待获取一个排它锁,如果其线程释放了锁就会结束此状态; -6. 死亡(Terminated):可以是线程结束任务之后自己结束,或者产生了异常而结束,中断机制就是使用了抛出中断异常的方式让一个阻塞的线程结束。 - -# 六、Executor - -Executor 管理多个异步任务的执行,而无需程序员显示地管理线程的生命周期。 - -主要有三种 Executor: - -1. CachedTreadPool:一个任务创建一个线程; -2. FixedThreadPool:所有任务只能使用固定大小的线程; -3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 +以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值为 997 而不是 1000。 ```java -ExecutorService exec = Executors.newCachedThreadPool(); -for(int i = 0; i < 5; i++) { - exec.execute(new MyRunnable()); +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()); + } } ``` -# 七、内存模型 +```html +997 +``` + +# 八、Java 内存模型 + +Java 内存模型视图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。 ## 主内存与工作内存 -对处理器上的寄存器进行读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。 +处理器上的寄存器的读写的速度比内存快几个数量级,为了解决这种速度矛盾,在它们之间加入了高速缓存。 -所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存中,保存了被该线程使用到的变量的主内存副本拷贝,线程只能直接操作工作内存中的变量。 +加入高速缓存带来了一个新的问题:缓存一致性。如果多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致。CPU 使用一致性协议来解决一致性问题。 -

+

+ +所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝。 + +线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。 + +

+ +## 内存间交互操作 + +Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。 + +

+ +- read:把一个变量的值从主内存传输到工作内存中 +- load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中 +- use:把工作内存中一个变量的值传递给执行引擎 +- assign:把一个从执行引擎接收到的值赋给工作内存的变量 +- store:把工作内存的一个变量的值传送到主内存中 +- write:在 store 之后执行,把 store 得到的值放入主内存的变量中 +- lock:作用于主内存的变量 +- unlock ## 内存模型三大特性 @@ -448,39 +854,106 @@ for(int i = 0; i < 5; i++) { Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long,double)的读写操作划分为两次 32 位的操作来进行,也就是说对这部分数据的操作可以不具备原子性。 -AtomicInteger、AtomicLong、AtomicReference 等特殊的原子性变量类提供了下面形式的原子性条件更新语句,使得比较和更新这两个操作能够不可分割地执行。 +有一个错误认识就是,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 重写之前线程不安全的代码之后得到以下线程安全实现: ```java -boolean compareAndSet(expectedValue, updateValue); +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++) { + executorService.execute(() -> { + example.add(); + countDownLatch.countDown(); + }); + } + countDownLatch.await(); + executorService.shutdown(); + System.out.println(example.get()); + } +} + ``` -AtomicInteger 使用举例: +```html +1000 +``` + +除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的完整性,它对应的内存间交互操作为:lock 和 unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。 ```java -private AtomicInteger ai = new AtomicInteger(0); +public class AtomicSynchronizedExample { + private int cnt = 0; -public int next() { - return ai.addAndGet(2) + 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()); + } } ``` -也可以使用 synchronized 同步操作来保证操作具备原子性,它对应的虚拟机字节码指令为 monitorenter 和 monitorexit。 +```html +1000 +``` ### 2. 可见性 -如果没有及时地对主内存与工作内存的数据进行同步,那么就会出现不一致问题。如果存在不一致的问题,一个线程对一个共享数据所做的修改就不能被另一个线程看到。 +可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。 -volatile 可以保证可见性,它在修改一个共享数据时会将该值从工作内存同步到主内存,并且对一个共享数据进行读取时会先从主内存同步到工作内存。 - -synchronized 也能够保证可见性,他能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主内存当中。不过只有对共享变量的 set() 和 get() 方法都加上 synchronized 才能保证可见性,如果只有 set() 方法加了 synchronized,那么 get() 方法并不能保证会从内存中读取最新的数据。 +volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一般的对象),那么其它线程就能看见 final 字段的值。 ### 3. 有序性 +有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。 + 在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。 volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。 -也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。 +也可以通过 synchronized 来保证有序性,它保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行同步代码。 ## 先行发生原则 @@ -490,7 +963,7 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即 ### 1. 单一线程原则 -> Single thread rule +> Single Thread rule 在一个线程内,在程序前面的操作先行发生于后面的操作。 @@ -546,104 +1019,117 @@ join() 方法返回先行发生于 Thread 对象的结束。 如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。 -# 八、线程安全 - -当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。 《Java Concurrency In Practice》 +# 九、线程安全 ## 线程安全分类 +线程安全不是一个非真即假的命题,可以将共享数据按照安全程度的强弱顺序分成以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。 + ### 1. 不可变 不可变(Immutable)的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障措施,只要一个不可变的对象被正确地构建出来,那其外部的可见状态永远也不会改变,永远也不会看到它在多个线程之中处于不一致的状态。 -不可变的类: +不可变的类型: +- final 关键字修饰的基本数据类型; - String +- 枚举类型 - Number 部分子类,如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的子类型的原子类 AtomicInteger 和 AtomicLong 则并非不可变的。 -可以使用 final 关键字修饰一个基本数据类型的共享数据,使它具有不可变性。 - -### 2. 绝对线程安全 - -在 Java API 中标注自己是线程安全的类,大多数都不是绝对的线程安全。例如Vector 是一个线程安全的容器,它的方法被 synchronized 被修饰同步。即使是这样,也不意味着调用它的时候永远都不再需要同步手段了。 - -对于下面的代码,在多线程的环境中,如果不在方法调用端做额外的同步措施的话,使用这段代码仍然是不安全的。因为如果另一个线程恰好在错误的时间里删除了一个元素,导致序号 i 已经不再可用的话,再用 i 访问数组就会抛出一个 ArrayIndexOutOfBoundsException。 +对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。 ```java -private static Vector vector = new Vector(); - -public static void main(String[] args) { - while (true) { - for (int i = 0; i < 10; i++) { - vector.add(i); - } - - Thread removeThread = new Thread(new Runnable() { - @Override - public void run() { - for (int i = 0; i < vector.size(); i++) { - vector.remove(i); - } - } - }); - - Thread printThread = new Thread(new Runnable() { - @Override - public void run() { - for (int i = 0; i < vector.size(); i++) { - System.out.println((vector.get(i))); - } - } - }); - - removeThread.start(); - printThread.start(); - - while (Thread.activeCount() > 20); +public class ImmutableExample { + public static void main(String[] args) { + Map map = new HashMap<>(); + Map unmodifiableMap = Collections.unmodifiableMap(map); + unmodifiableMap.put("a", 1); } } ``` ```html -Exception in thread "Thread-132" java.lang.ArrayIndexOutOfBoundsException: -Array index out of range:17 -at java.util.Vector.remove(Vector.java:777) -at org.fenixsoft.mulithread.VectorTest$1.run(VectorTest.java:21) -at java.lang.Thread.run(Thread.java:662) +Exception in thread "main" java.lang.UnsupportedOperationException + at java.util.Collections$UnmodifiableMap.put(Collections.java:1457) + at ImmutableExample.main(ImmutableExample.java:9) ``` -如果要保证上面的代码能正确执行下去,就需要对 removeThread 和 printThread 中的方法进行同步。 +Collections.unmodifiableXXX() 先对原始的集合进行拷贝,需要对集合进行修改的方法都直接抛出异常。 ```java - Thread removeThread = new Thread(new Runnable() { - @Override - public void run() { - synchronized (vector) { - for (int i = 0; i < vector.size(); i++) { - vector.remove(i); - } - } - } -}); - -Thread printThread = new Thread(new Runnable() { - @Override - public void run() { - synchronized (vector) { - for (int i = 0; i < vector.size(); i++) { - System.out.println((vector.get(i))); - } - } - } -}); +public V put(K key, V value) { + throw new UnsupportedOperationException(); +} ``` +多线程环境下,应当尽量使对象称为不可变,来满足线程安全。 + +### 2. 绝对线程安全 + +不管运行时环境如何,调用者都不需要任何额外的同步措施。 + ### 3. 相对线程安全 -相对的线程安全需要保证对这个对象单独的操作是线程安全的,我们在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。 +相对的线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。 在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。 +对于下面的代码,如果删除元素的线程删除了一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。 + +```java +public class VectorUnsafeExample { + private static Vector vector = new Vector<>(); + + public static void main(String[] args) { + while (true) { + for (int i = 0; i < 100; i++) { + vector.add(i); + } + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> { + for (int i = 0; i < vector.size(); i++) { + vector.remove(i); + } + }); + executorService.execute(() -> { + for (int i = 0; i < vector.size(); i++) { + vector.get(i); + } + }); + executorService.shutdown(); + } + } +} +``` + +```html +Exception in thread "Thread-159738" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 3 + at java.util.Vector.remove(Vector.java:831) + at VectorUnsafeExample.lambda$main$0(VectorUnsafeExample.java:14) + at VectorUnsafeExample$$Lambda$1/713338599.run(Unknown Source) + at java.lang.Thread.run(Thread.java:745) +``` + + +如果要保证上面的代码能正确执行下去,就需要对删除元素和获取元素的代码进行同步。 + +```java +executorService.execute(() -> { + synchronized (vector) { + for (int i = 0; i < vector.size(); i++) { + vector.remove(i); + } + } +}); +executorService.execute(() -> { + synchronized (vector) { + for (int i = 0; i < vector.size(); i++) { + vector.get(i); + } + } +}); +``` + ### 4. 线程兼容 线程兼容是指对象本身并不是线程安全的,但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。 @@ -654,8 +1140,6 @@ Thread printThread = new Thread(new Runnable() { ## 线程安全的实现方法 -如何实现线程安全与代码编写有很大的关系,但虚拟机提供的同步和锁机制也起到了非常重要的作用。 - ### 1. 互斥同步 synchronized 和 ReentrantLock。 @@ -672,33 +1156,189 @@ CAS 指令需要有 3 个操作数,分别是内存位置(在 Java 中可以 J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。 -ABA :如果一个变量 V 初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 +在下面的代码 1 中,使用了 AtomicInteger 执行了自增的操作。代码 2 是 incrementAndGet() 的源码,它调用了 unsafe 的 getAndAddInt() 。代码 3 是 getAndAddInt() 源码,var1 指示内存位置,var2 指示新值,var4 指示操作需要加的数值,这里为 1。在代码 3 的实现中,通过 getIntVolatile(var1, var2) 得到旧的预期值。通过调用 compareAndSwapInt() 来进行 CAS 比较,如果 var2=var5,那么就更新内存地址为 var1 的变量为 var5+var4。可以看到代码 3 是在一个循环中进行,发生冲突的做法是不断的进行重试。 + +```java +// 代码 1 +private AtomicInteger cnt = new AtomicInteger(); + +public void add() { + cnt.incrementAndGet(); +} +``` + +```java +// 代码 2 +public final int incrementAndGet() { + return unsafe.getAndAddInt(this, valueOffset, 1) + 1; +} +``` + +```java +// 代码 3 +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; +} +``` + +ABA :如果一个变量 V 初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 ### 3. 无同步方案 要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。 -**可重入代码(Reentrant Code)** +**(一)可重入代码(Reentrant Code)** 这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。相对线程安全来说,可重入性是更基本的特性,它可以保证线程安全,即所有的可重入的代码都是线程安全的,但是并非所有的线程安全的代码都是可重入的。 可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。我们可以通过一个简单的原则来判断代码是否具备可重入性:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。 -**线程本地存储(Thread Local Storage)** +**(二)栈封闭** + +多个线程方法同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在栈中,属于线程私有的。 + +```java +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +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(); + } +} +``` + +```html +100 +100 +``` + +**(三)线程本地存储(Thread Local Storage)** 如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。 符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。 -Java 语言中,如果一个变量要被多线程访问,可以使用 volatile 关键字声明它为“易变的”;如果一个变量要被某个线程独享,Java 中就没有类似 C++中 \_\_declspec(thread)这样的关键字,不过还是可以通过 java.lang.ThreadLocal 类来实现线程本地存储的功能。每一个线程的 Thread 对象中都有一个 ThreadLocalMap 对象,这个对象存储了一组以 ThreadLocal.threadLocalHashCode 为键,以本地线程变量为值的 K-V 值对,ThreadLocal 对象就是当前线程的 ThreadLocalMap 的访问入口,每一个 ThreadLocal 对象都包含了一个独一无二的 threadLocalHashCode 值,使用这个值就可以在线程 K-V 值对中找回对应的本地线程变量。 +可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。 -ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。在一些场景 (尤其是使用线程池) 下, 由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。 +对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。 -## 锁优化 +```java +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(); + } +} +``` + +```html +1 +``` + +为了理解 ThreadLocal,先看以下代码: + +```java +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(); + thread2.start(); + } +} +``` + +它所对应的底层结构图为: + +

+ +每个 Thread 都有一个 TreadLocal.ThreadLocalMap 对象,Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员。 + +```java +/* ThreadLocal values pertaining to this thread. This map is maintained + * by the ThreadLocal class. */ +ThreadLocal.ThreadLocalMap threadLocals = null; +``` + +当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。 + +```java +public void set(T value) { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + createMap(t, value); +} +``` + +get() 方法类似。 + +```java +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(); +} +``` + +ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。 + +# 十、锁优化 高效并发是从 JDK 1.5 到 JDK 1.6 的一个重要改进,HotSpot 虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。 -### 1. 自旋锁与自适应自旋 +## 自旋锁与自适应自旋 前面我们讨论互斥同步的时候,提到了互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 @@ -706,7 +1346,7 @@ ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因 在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。 -### 2. 锁消除 +## 锁消除 锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判定在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把他们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。 @@ -731,7 +1371,7 @@ public static String concatString(String s1, String s2, String s3) { ``` 每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会 “逃逸” 到 concatString() 方法之外,其他线程无法访问到它。因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。 -### 3. 锁粗化 +## 锁粗化 原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小:只在共享数据的实际作用域中才进行同步。这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。 @@ -739,7 +1379,7 @@ public static String concatString(String s1, String s2, String s3) { 上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。 -### 4. 轻量级锁 +## 轻量级锁 轻量级锁是 JDK 1.6 之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重要级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。 @@ -755,7 +1395,7 @@ public static String concatString(String s1, String s2, String s3) { 轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。 -### 5. 偏向锁 +## 偏向锁 偏向锁也是 JDK 1.6 中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不做了。 @@ -783,6 +1423,7 @@ public static String concatString(String s1, String s2, String s3) { - BruceEckel. Java 编程思想: 第 4 版 [M]. 机械工业出版社, 2007. - 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011. +- [Threads and Locks](https://docs.oracle.com/javase/specs/jvms/se6/html/Threads.doc.html) - [线程通信](http://ifeve.com/thread-signaling/#missed_signal) - [Java 线程面试题 Top 50](http://www.importnew.com/12773.html) - [BlockingQueue](http://tutorials.jenkov.com/java-util-concurrent/blockingqueue.html) @@ -790,3 +1431,4 @@ public static String concatString(String s1, String s2, String s3) { - [CSC 456 Spring 2012/ch7 MN](http://wiki.expertiza.ncsu.edu/index.php/CSC_456_Spring_2012/ch7_MN) - [Java - Understanding Happens-before relationship](https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/happens-before.html) - [6장 Thread Synchronization](https://www.slideshare.net/novathinker/6-thread-synchronization) +- [How is Java's ThreadLocal implemented under the hood?](https://stackoverflow.com/questions/1202444/how-is-javas-threadlocal-implemented-under-the-hood/15653015) diff --git a/notes/数据库系统原理.md b/notes/数据库系统原理.md index 63d9fa30..46b5e1b8 100644 --- a/notes/数据库系统原理.md +++ b/notes/数据库系统原理.md @@ -311,19 +311,21 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回 ## 快照读与当前读 -### 1. 当前读 +### 1. 快照读 -读取最新的数据。 +读取快照中的数据。 + +引入快照读的目的主要是为了免去加锁操作带来的性能开销,但是当前读需要加锁。 ```sql select * from table ....; ``` -### 2. 快照读 +### 2. 当前读 -读取快照中的数据。 +读取最新的数据。 -引入快照读的目的主要是为了免去加锁操作带来的性能开销,但是当前读需要加锁。 +需要加锁,以下第一个语句加 S 锁,其它都加 X 锁。 ```sql select * from table where ? lock in share mode; diff --git a/notes/算法.md b/notes/算法.md index 27fb7023..161c2e79 100644 --- a/notes/算法.md +++ b/notes/算法.md @@ -483,7 +483,7 @@ private void exch(Comparable[] a, int i, int j) { ```java public class Selection { - public static void sort(Comparable[] a) { + public void sort(Comparable[] a) { int N = a.length; for (int i = 0; i < N; i++) { int min = i; @@ -506,7 +506,7 @@ public class Selection { ```java public class Insertion { - public static void sort(Comparable[] a) { + public void sort(Comparable[] a) { int N = a.length; for (int i = 1; i < N; i++) { for (int j = i; j > 0 && less(a[j], a[j - 1]); j--) { @@ -537,7 +537,7 @@ public class Insertion { ```java public class Shell { - public static void sort(Comparable[] a) { + public void sort(Comparable[] a) { int N = a.length; int h = 1; while (h < N / 3) { @@ -569,9 +569,9 @@ public class Shell { ```java public class MergeSort { - private static Comparable[] aux; + private Comparable[] aux; - private static void merge(Comparable[] a, int lo, int mid, int hi) { + private void merge(Comparable[] a, int lo, int mid, int hi) { int i = lo, j = mid + 1; for (int k = lo; k <= hi; k++) { @@ -594,12 +594,12 @@ public class MergeSort { ```java -public static void sort(Comparable[] a) { +public void sort(Comparable[] a) { aux = new Comparable[a.length]; sort(a, 0, a.length - 1); } -private static void sort(Comparable[] a, int lo, int hi) { +private void sort(Comparable[] a, int lo, int hi) { if (hi <= lo) return; int mid = lo + (hi - lo) / 2; sort(a, lo, mid); @@ -617,7 +617,7 @@ private static void sort(Comparable[] a, int lo, int hi) { 先归并那些微型数组,然后成对归并得到的子数组。 ```java -public static void busort(Comparable[] a) { +public void busort(Comparable[] a) { int N = a.length; aux = new Comparable[N]; for (int sz = 1; sz < N; sz += sz) { @@ -639,17 +639,23 @@ public static void busort(Comparable[] a) { ```java public class QuickSort { - public static void sort(Comparable[] a) { + public void sort(Comparable[] a) { shuffle(a); sort(a, 0, a.length - 1); } - private static void sort(Comparable[] a, int lo, int hi) { + private void sort(Comparable[] a, int lo, int hi) { if (hi <= lo) return; int j = partition(a, lo, hi); sort(a, lo, j - 1); sort(a, j + 1, hi); } + + private void shuffle(Comparable[] array) { + List list = Arrays.asList(array); + Collections.shuffle(list); + list.toArray(array); + } } ``` @@ -660,7 +666,7 @@ public class QuickSort {

```java -private static int partition(Comparable[] a, int lo, int hi) { +private int partition(Comparable[] a, int lo, int hi) { int i = lo, j = hi + 1; Comparable v = a[lo]; while (true) { @@ -700,7 +706,7 @@ private static int partition(Comparable[] a, int lo, int hi) { ```java public class Quick3Way { - public static void sort(Comparable[] a, int lo, int hi) { + public void sort(Comparable[] a, int lo, int hi) { if (hi <= lo) return; int lt = lo, i = lo + 1, gt = hi; Comparable v = a[lo]; @@ -975,7 +981,7 @@ public class BinarySearchST, Value> {

-**二叉查找树** (BST)是一颗二叉树,并且每个节点的值都大于其左子树中的所有节点的值而小于右子树的所有节点的值。 +**二叉查找树** (BST)是一颗二叉树,并且每个节点的值都大于等于其左子树中的所有节点的值而小于等于右子树的所有节点的值。

@@ -1119,6 +1125,7 @@ private int rank(Key key, Node x) { ```java private Node min(Node x) { + if (x == null) return null; if (x.left == null) return x; return min(x.left); } @@ -1551,19 +1558,17 @@ private void resize(int cap) { } ``` -虽然每次重新调整数组都需要重新把每个键值对插入到散列表,但是从摊还分析的角度来看,所需要的代价却是很小的。从下图可以看出,每次数组长度加倍后,累计平均值都会增加 1,这是因为散列表中每个键都需要重新计算散列值。随后平均值会下降。 - ## 应用 ### 1. 各种符号表实现的比较 | 算法 | 插入 | 查找 | 是否有序 | | :---: | :---: | :---: | :---: | -| 二分查找实现的有序表 | logN | N | yes | +| 二分查找实现的有序表 | N | logN | yes | | 二叉查找树 | logN | logN | yes | | 2-3 查找树 | logN | logN | yes | -| 拉链法实现的散列表 | logN | N/M | no | -| 线性探测法试下的删列表 | logN | 1 | no | +| 拉链法实现的散列表 | N/M | N/M | no | +| 线性探测法试下的散列表 | 1 | 1 | no | 应当优先考虑散列表,当需要有序性操作时使用红黑树。 diff --git a/notes/计算机网络.md b/notes/计算机网络.md index 8f71be19..56623655 100644 --- a/notes/计算机网络.md +++ b/notes/计算机网络.md @@ -29,6 +29,7 @@ * [路由选择协议](#路由选择协议) * [网际控制报文协议 ICMP](#网际控制报文协议-icmp) * [分组网间探测 PING](#分组网间探测-ping) + * [Traceroute](#traceroute) * [虚拟专用网 VPN](#虚拟专用网-vpn) * [网络地址转换 NAT](#网络地址转换-nat) * [五、运输层*](#五运输层) @@ -533,13 +534,14 @@ PING 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连 Ping 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报。 -Ping 的过程: +## Traceroute -1. 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,但 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文; -2. 源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTL 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。 -3. 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。 -4. 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。 +Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。 +1. 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,但 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文; +2. 源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTL 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。 +3. 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。 +4. 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。 ## 虚拟专用网 VPN diff --git a/notes/面向对象思想.md b/notes/面向对象思想.md index 7f67505e..5ab84adf 100644 --- a/notes/面向对象思想.md +++ b/notes/面向对象思想.md @@ -109,11 +109,13 @@ 利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。 -封装有三大好处: +优点: -1. 减少耦合 -2. 隐藏内部细节,因此内部结构可以自由修改 -3. 可以对成员进行更精确的控制 +- 减少耦合:可以独立地开发、测试、优化、使用、理解和修改 +- 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块 +- 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能 +- 提高软件的可重用性 +- 减低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的 以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。 diff --git a/pics/3646544a-cb57-451d-9e03-d3c4f5e4434a.png b/pics/3646544a-cb57-451d-9e03-d3c4f5e4434a.png new file mode 100644 index 00000000..76d45e19 Binary files /dev/null and b/pics/3646544a-cb57-451d-9e03-d3c4f5e4434a.png differ diff --git a/pics/47358f87-bc4c-496f-9a90-8d696de94cee.png b/pics/47358f87-bc4c-496f-9a90-8d696de94cee.png new file mode 100644 index 00000000..83d59359 Binary files /dev/null and b/pics/47358f87-bc4c-496f-9a90-8d696de94cee.png differ diff --git a/pics/4e760981-a0c5-4dbf-9fbf-ce963e0629fb.png b/pics/4e760981-a0c5-4dbf-9fbf-ce963e0629fb.png new file mode 100644 index 00000000..e5768980 Binary files /dev/null and b/pics/4e760981-a0c5-4dbf-9fbf-ce963e0629fb.png differ diff --git a/pics/536c6dfd-305a-4b95-b12c-28ca5e8aa043.png b/pics/536c6dfd-305a-4b95-b12c-28ca5e8aa043.png new file mode 100644 index 00000000..29bbc9de Binary files /dev/null and b/pics/536c6dfd-305a-4b95-b12c-28ca5e8aa043.png differ diff --git a/pics/68778c1b-15ab-4826-99c0-3b4fd38cb9e9.png b/pics/68778c1b-15ab-4826-99c0-3b4fd38cb9e9.png new file mode 100644 index 00000000..39d7ec60 Binary files /dev/null and b/pics/68778c1b-15ab-4826-99c0-3b4fd38cb9e9.png differ diff --git a/pics/6c0f4afb-20ab-49fd-837d-8144f4e38bfd.png b/pics/6c0f4afb-20ab-49fd-837d-8144f4e38bfd.png new file mode 100644 index 00000000..d86b4635 Binary files /dev/null and b/pics/6c0f4afb-20ab-49fd-837d-8144f4e38bfd.png differ diff --git a/pics/952afa9a-458b-44ce-bba9-463e60162945.png b/pics/952afa9a-458b-44ce-bba9-463e60162945.png new file mode 100644 index 00000000..db93c1ac Binary files /dev/null and b/pics/952afa9a-458b-44ce-bba9-463e60162945.png differ diff --git a/pics/ace830df-9919-48ca-91b5-60b193f593d2.png b/pics/ace830df-9919-48ca-91b5-60b193f593d2.png new file mode 100644 index 00000000..79efa287 Binary files /dev/null and b/pics/ace830df-9919-48ca-91b5-60b193f593d2.png differ diff --git a/pics/ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92.png b/pics/ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92.png new file mode 100644 index 00000000..f6256867 Binary files /dev/null and b/pics/ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92.png differ