diff --git a/README.md b/README.md index 92a1317e..80528a71 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,10 @@ Leetcode 上数据库题目的解题记录。 ## Java :coffee: +> [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md) + +整理了一些常见考点。 + > [Java 虚拟机](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20虚拟机.md) 整理自《深入理解 Java 虚拟机》 @@ -93,10 +97,6 @@ Leetcode 上数据库题目的解题记录。 File, InputStream OutputStream, Reader Writer, Serializable, Socket, NIO -> [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md) - -整理了一些常见考点。 - > [JDK 中的设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/JDK%20中的设计模式.md) 对每种设计模式做了一个总结,并给出在 JDK 中的使用实例。 diff --git a/notes/Java 基础.md b/notes/Java 基础.md index b1a0b9b7..6424600a 100644 --- a/notes/Java 基础.md +++ b/notes/Java 基础.md @@ -13,7 +13,6 @@ * [抽象类与接口](#抽象类与接口) * [super](#super) * [覆盖与重载](#覆盖与重载) - * [内部类](#内部类) * [五、String](#五string) * [String, StringBuffer and StringBuilder](#string,-stringbuffer-and-stringbuilder) * [String 不可变的原因](#string-不可变的原因) @@ -52,7 +51,7 @@ final A y = new A(); y.a = 1; ``` -**2. 方法**
+**2. 方法** 声明方法不能被子类覆盖。 @@ -80,7 +79,7 @@ public class A { **2. 静态方法** -静态方法在类加载的时候就存在了,它不依赖于任何实例,所以 static 方法必须实现,也就是说它不能是抽象方法(abstract)。 +静态方法在类加载的时候就存在了,它不依赖于任何实例,所以静态方法必须有实现,也就是说它不能是抽象方法(abstract)。 **3. 静态语句块** @@ -88,11 +87,11 @@ public class A { **4. 静态内部类** -内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非 static 变量和方法。 +内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非静态的变量和方法。 **5. 静态导包** -```source-java +```java import static com.xxx.ClassName.* ``` @@ -132,12 +131,12 @@ public InitialOrderTest() { 存在继承的情况下,初始化顺序为: -1. 父类(静态变量、静态语句块) -2. 子类(静态变量、静态语句块) -3. 父类(实例变量、普通语句块) -4. 父类(构造函数) -5. 子类(实例变量、普通语句块) -6. 子类(构造函数) +- 父类(静态变量、静态语句块) +- 子类(静态变量、静态语句块) +- 父类(实例变量、普通语句块) +- 父类(构造函数) +- 子类(实例变量、普通语句块) +- 子类(构造函数) # 二、Object 通用方法 @@ -192,15 +191,14 @@ x.equals(x); // true (二)对称性 ```java -x.equals(y) == y.equals(x) // true +x.equals(y) == y.equals(x); // true ``` (三)传递性 ```java -if(x.equals(y) && y.equals(z)) { +if (x.equals(y) && y.equals(z)) x.equals(z); // true; -} ``` (四)一致性 @@ -256,7 +254,7 @@ public class EqualExample { hasCode() 返回散列值,而 equals() 是用来判断两个实例是否等价。等价的两个实例散列值一定要相同,但是散列值相同的两个实例不一定等价。 -在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证相等的两个实例散列值也等价。 +在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个实例散列值也相等。 下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例。 @@ -272,7 +270,7 @@ System.out.println(set.size()); // 2 理想的散列函数应当具有均匀性,即不相等的实例应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。 -一个数与 31 相乘可以转换成移位和减法:31\*x == (x<<5)-x。 +一个数与 31 相乘可以转换成移位和减法:`31\*x == (x<<5)-x`,编译器会自动进行这个优化。 ```java @Override @@ -594,9 +592,9 @@ ac2.func1(); 从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。 -接口也可以包含字段,并且这些字段隐式都是 static 和 final 的。 +接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。 -接口中的方法默认都是 public 的,并且不允许定义为 private 或者 protected。 +接口的字段默认都是 static 和 final 的。 ```java public interface InterfaceExample { @@ -607,7 +605,7 @@ public interface InterfaceExample { } int x = 123; - //int y; // Variable 'y' might not have been initialized + // 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 @@ -711,228 +709,6 @@ SuperExtendExample.func() - 重载(Overload)存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。 -## 内部类 - -在Java中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类。 -为什么在Java中需要内部类?总结一下主要有以下四点: - -1. 每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整, - -2. 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。 - -3. 方便编写事件驱动程序 - -4. 方便编写线程代码 - -个人觉得第一点是最重要的原因之一,内部类的存在使得Java的多继承机制变得更加完善。在这之前,Java 的继承机制主要由接口和单根继承实现,通过实现多个接口里的方法,看似能够实现多继承,但是并不总是高效的,因为一旦我们继承了一个接口就必然要实现它内部定义的所有方法。现在我们可以通过内部类多次继承某个具体类或者接口,省去一些不必要的实现动作。 - -先来看一段示例代码来方便理解利用内部类完善多继承机制: - -```java -public class Father { - public String powerFul = "市长"; -} - -public class Mother { - public String wealthy = "一百万"; -} -``` - -```java -public class Son { - class Extends_Father extends Father{ - } - - class Extends_Mother extends Mother{ - } - - public void sayHello(){ - String father = new Extends_Father().powerFul; - String mother = new Extends_Mother().wealthy; - System.out.println("my father is:" + father + "my mother has:" + mother); - } -} -``` - -显然,我们的 Son 类是不可能同时继承 Father 和 Mother 的,但是我们却可以通过在其内部定义内部类继承了 Father 和 Mother,必要的情况下,我们还能够重写继承而来的各个类的属性或者方法。 -这就是典型的一种通过内部类实现多继承的实现方式,但是同时你也会发现,单单从 Son 来外表看,你根本不知道它内部多继承了 Father 和 Mother,从而往往会给我们带来一些错觉。所以你看,内部类并不绝对是一个好东西,它破坏了封装性,用的不好反而会适得其反,让你的程序一团糟,所以谨慎! - -广泛意义上的内部类一般来说包括这四种:成员内部类、局部内部类、匿名内部类和静态内部类。下面就先来了解一下这四种内部类的用法。 - -**1. 成员内部类** - -成员内部类即内部类当做外部类得一个普通成员。 - -成员内部类可以无条件访问外部类得所有成员属性和成员方法 - -```java -class Circle { - private double radius = 0; - public static int count =1; - public Circle(double radius) { - this.radius = radius; - } - - class Draw { //内部类 - public void drawSahpe() { - System.out.println(radius); //外部类的private成员 - System.out.println(count); //外部类的静态成员 - } - } -} -``` - -不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问: - -`外部类.this.成员方法` - -`外部类.this.成员变量` - -虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问,由于成员内部类是依附于外部类而存在的,也就是说,如果要创建内部类的对象,前提是必须存在一个外部类的对象,方式如下所示: - -```java -public class Test { - public static void main(String[] args) { - //第一种方式: - Outter outter = new Outter(); - Outter.Inner inner = outter.new Inner(); //必须通过Outter对象来创建 - - //第二种方式: - Outter.Inner inner1 = outter.getInnerInstance(); - } -} - -class Outter { - private Inner inner = null; - public Outter() { - - } - - public Inner getInnerInstance() { - if(inner == null) - inner = new Inner(); - return inner; - } - - class Inner { - public Inner() { - - } - } -} -``` - -**2. 局部内部类** - -局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。 - -```java -class People{ - public People() { - - } -} - -class Man{ - public Man(){ - - } - - public People getWoman(){ - class Woman extends People{ //局部内部类 - int age =0; - } - return new Woman(); - } -} -``` - -注意,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。 - -**3. 匿名内部类** - -匿名内部类,顾名思义,是没有名字的类,那么既然它没有名字,自然也就无法显式的创建出其实例对象了,所以匿名内部类适合那种只使用一次的情境,匿名内部类是唯一一种没有构造器的类。 - -典型得匿名内部类使用例子如下: - -```java -public class Test{ - public static void main(String[] args){ - Object obj=new Object(){ - @Override - public String toString(){ - return "hello world"; - } - } - } -} -``` - -上述代码效果等同于如下代码: - -```java -public class MyObj extends Object{ - @Override - public String toString(){ - return "hello world"; - } -} -``` - -```java -public static void main(String[] args){ - Object obj = new MyObj(); -} -``` - -为了一个只使用一次的类而单独创建一个 .java 文件,是否有些浪费和繁琐? -在我看来,匿名内部类最大的好处就在于能够简化代码块。 - -匿名内部类往往是对某个父类或者接口的继承与实现,匿名类的基本使用语法格式如下: - -```java -new 父类/接口{ - //匿名类的实现 -} -``` -常用场景如自定义比较器实现规则排序: - -```java - Arrays.sort(data, new Comparator(){ - public int compare(Integer a,Integer b) - { - return (a+""+b).compareTo(b+""+a);//构建ab的比较器 - } - }); -``` - -**4. 静态内部类** - -静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非 static 成员必须依附于具体的对象。 - -常见使用场景如,静态内部类实现单例模式: - -```java -/** - * 懒汉式单例类线程安全可可能有问题,性能会受到影响 - * 饿汉式不能实现延迟加载,不管将来用不用始终占据内存 - * 内部类的实现方式可以很好的解决上面的缺点 - */ -public class InnerClassSingleton { - private static class SingletonHolder{ - private static final InnerClassSingleton SINGLETON=new InnerClassSingleton(); - } - private InnerClassSingleton() {} - public static final InnerClassSingleton getSingleton(){ - return SingletonHolder.SINGLETON; - } -} -``` - -此外,我们 Integer 类中的 IntegerCache 就是一个静态的内部类,它不需要访问外围类中任何成员,却通过内部定义的一些属性和方法为外围类提供缓存服务。 - -> [Java内部类详解](https://www.cnblogs.com/latter/p/5665015.html) - # 五、String ## String, StringBuffer and StringBuilder @@ -1115,15 +891,15 @@ switch (s) { switch 不支持 long,是因为 swicth 的设计初衷是为那些只需要对少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。 ```java -// 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; -// } +// 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; +// } ``` > [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) @@ -1182,7 +958,7 @@ public class Box { } ``` -> [Java 泛型详解](https://www.ziwenxie.site/2017/03/01/java-generic/)
[10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693) +> [Java 泛型详解](http://www.importnew.com/24029.html)
[10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693) # 十、注解 diff --git a/notes/Java 并发.md b/notes/Java 并发.md index 61c9fdb8..8a2c8fcf 100644 --- a/notes/Java 并发.md +++ b/notes/Java 并发.md @@ -47,7 +47,7 @@ * [线程安全分类](#线程安全分类) * [线程安全的实现方法](#线程安全的实现方法) * [十二、锁优化](#十二锁优化) - * [自旋锁与自适应自旋](#自旋锁与自适应自旋) + * [自旋锁](#自旋锁) * [锁消除](#锁消除) * [锁粗化](#锁粗化) * [轻量级锁](#轻量级锁) @@ -93,6 +93,10 @@ 调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。 +睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。 + +阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁;而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。 + | 进入方法 | 退出方法 | | --- | --- | | Thread.sleep() 方法 | 时间结束 | @@ -109,9 +113,9 @@ 有三种使用线程的方法: -1. 实现 Runnable 接口; -2. 实现 Callable 接口; -3. 继承 Thread 类。 +- 实现 Runnable 接口; +- 实现 Callable 接口; +- 继承 Thread 类。 实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。 @@ -182,8 +186,8 @@ public static void main(String[] args) { 实现接口会更好一些,因为: -1. Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口; -2. 类可能只要求可执行就行,继承整个 Thread 类开销会过大。 +- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口; +- 类可能只要求可执行就行,继承整个 Thread 类开销过大。 # 三、基础线程机制 @@ -193,9 +197,9 @@ Executor 管理多个异步任务的执行,而无需程序员显式地管理 主要有三种 Executor: -1. CachedThreadPool:一个任务创建一个线程; -2. FixedThreadPool:所有任务只能使用固定大小的线程; -3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 +- CachedThreadPool:一个任务创建一个线程; +- FixedThreadPool:所有任务只能使用固定大小的线程; +- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 ```java public static void main(String[] args) { @@ -258,16 +262,10 @@ public void run() { 通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。 -对于以下代码,在 Main 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。 +对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。 ```java 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 @@ -281,7 +279,15 @@ public class InterruptExample { } } } +``` +```java +public static void main(String[] args) throws InterruptedException { + Thread thread1 = new MyThread1(); + thread1.start(); + thread1.interrupt(); + System.out.println("Main run"); +} ``` ```html @@ -301,11 +307,6 @@ java.lang.InterruptedException: sleep 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 @@ -319,6 +320,14 @@ public class InterruptExample { } ``` +```java +public static void main(String[] args) throws InterruptedException { + Thread thread2 = new MyThread2(); + thread2.start(); + thread2.interrupt(); +} +``` + ```html Thread end ``` @@ -330,20 +339,18 @@ Thread end 以下使用 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"); - } +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"); } ``` @@ -385,7 +392,7 @@ public void func () { 它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。 -对于以下代码,使用 ExecutorService 执行了两个线程(这两个线程使用 Lambda 创建),由于调用的是同一个对象的同步语句块,因此这两个线程就需要进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。 +对于以下代码,使用 ExecutorService 执行了两个线程(这两个线程使用 Lambda 创建),由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。 ```java public class SynchronizedExample { @@ -397,13 +404,15 @@ public class SynchronizedExample { } } } +} +``` - public static void main(String[] args) { - SynchronizedExample e1 = new SynchronizedExample(); - ExecutorService executorService = Executors.newCachedThreadPool(); - executorService.execute(() -> e1.func1()); - executorService.execute(() -> e1.func1()); - } +```java +public static void main(String[] args) { + SynchronizedExample e1 = new SynchronizedExample(); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> e1.func1()); + executorService.execute(() -> e1.func1()); } ``` @@ -460,14 +469,16 @@ public class SynchronizedExample { } } } +} +``` - 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()); - } +```java +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()); } ``` @@ -518,7 +529,7 @@ public static void main(String[] args) { 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,它多了一些高级功能: +ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了以下高级功能: **1. 等待可中断** @@ -540,7 +551,7 @@ synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。 **2. 性能** -从性能上来看,在新版本的 JDK 中对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由,而且 synchronized 有更大的优化空间,因此优先考虑 synchronized。 +从性能上来看,新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。 **3. 功能** @@ -556,7 +567,7 @@ ReentrantLock 多了一些高级功能。 ## join() -在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。 +在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待, 直到目标线程结束。 对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,因此 b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先与 b 线程的输出。 @@ -595,11 +606,13 @@ public class JoinExample { b.start(); a.start(); } +} +``` - public static void main(String[] args) { - JoinExample example = new JoinExample(); - example.test(); - } +```java +public static void main(String[] args) { + JoinExample example = new JoinExample(); + example.test(); } ``` @@ -633,13 +646,15 @@ public class WaitNotifyExample { } 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()); - } +```java +public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + WaitNotifyExample example = new WaitNotifyExample(); + executorService.execute(() -> example.after()); + executorService.execute(() -> example.before()); } ``` @@ -685,13 +700,15 @@ public class AwaitSignalExample { lock.unlock(); } } +} +``` - public static void main(String[] args) { - ExecutorService executorService = Executors.newCachedThreadPool(); - AwaitSignalExample example = new AwaitSignalExample(); - executorService.execute(() -> example.after()); - executorService.execute(() -> example.before()); - } +```java +public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + AwaitSignalExample example = new AwaitSignalExample(); + executorService.execute(() -> example.after()); + executorService.execute(() -> example.before()); } ``` @@ -875,9 +892,6 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现: **使用 BlockingQueue 实现生产者消费者问题** ```java -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; - public class ProducerConsumer { private static BlockingQueue queue = new ArrayBlockingQueue<>(5); @@ -906,20 +920,22 @@ public class ProducerConsumer { 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(); - } +```java +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(); } } ``` @@ -1002,22 +1018,24 @@ public class ThreadUnsafeExample { 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 +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()); } ``` @@ -1089,24 +1107,25 @@ public class AtomicExample { 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()); - } } +``` +```java +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()); +} ``` ```html @@ -1126,22 +1145,24 @@ public class AtomicSynchronizedExample { 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()); +```java +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()); } ``` @@ -1155,9 +1176,7 @@ public class AtomicSynchronizedExample { volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。 -对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题。因为 volatile 并不能保证操作的原子性。 - -// TODO:volatile 不能解决线程不安全问题的示例代码。 +对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。 ### 3. 有序性 @@ -1404,7 +1423,7 @@ public final int getAndAddInt(Object var1, long var2, int var4) { } ``` -ABA :如果一个变量 V 初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 +ABA :如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 ### 3. 无同步方案 @@ -1432,14 +1451,16 @@ public class StackClosedExample { } 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(); - } +```java +public static void main(String[] args) { + StackClosedExample example = new StackClosedExample(); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> example.add100()); + executorService.execute(() -> example.add100()); + executorService.shutdown(); } ``` @@ -1554,21 +1575,23 @@ ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因 # 十二、锁优化 -高效并发是从 JDK 1.5 到 JDK 1.6 的一个重要改进,HotSpot 虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。 +这里的锁优化主要是指虚拟机对 synchronized 的优化。 -## 自旋锁与自适应自旋 +## 自旋锁 -前面我们讨论互斥同步的时候,提到了互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 +互斥同步的进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。 -自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时候很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是 10 次,用户可以使用参数 -XX:PreBlockSpin 来更改。 +自选锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。自旋次数的默认值是 10 次,用户可以使用虚拟机参数 -XX:PreBlockSpin 来更改。 -在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。 +在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。 ## 锁消除 -锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判定在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把他们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。 +锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。 -也许读者会有疑问,变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?答案是有许多同步措施并不是程序员自己加入的。同步的代码在 Java 程序中的普遍程度也许超过了大部分读者的想象。下面段非常简单的代码仅仅是输出 3 个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。 +锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们上的锁进行消除。 + +对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁: ```java public static String concatString(String s1, String s2, String s3) { @@ -1576,7 +1599,7 @@ public static String concatString(String s1, String s2, String s3) { } ``` -我们也知道,由于 String 是一个不可变的类,对字符串的连接操作总是通过生成新的 String 对象来进行的,因此 Javac 编译器会对 String 连接做自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,即上面的代码可能会变成下面的样子: +String 是一个不可变的类,Javac 编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,即上面的代码可能会变成下面的样子: ```java public static String concatString(String s1, String s2, String s3) { @@ -1587,46 +1610,47 @@ public static String concatString(String s1, String s2, String s3) { return sb.toString(); } ``` -每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会 “逃逸” 到 concatString() 方法之外,其他线程无法访问到它。因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。 + +每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会“逃逸”到 concatString() 方法之外,其他线程无法访问到它。因此,虽然这里有锁,但是可以被安全地消除掉。 ## 锁粗化 -原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小:只在共享数据的实际作用域中才进行同步。这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。 +如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。 -大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。 - -上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。 +上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。 ## 轻量级锁 -轻量级锁是 JDK 1.6 之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。 +JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。 -要理解轻量级锁,以及后面会讲到的偏向锁的原理和运作过程,必须从 HotSpot 虚拟机的对象(对象头部分)的内存布局开始介绍。HotSpot 虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄(Generational GC Age)等,这部分数据是长度在 32 位和 64 位的虚拟机中分别为 32 bit 和 64 bit,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。 +以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 mark word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出,应该注意的是 state 表格不是存储在对象头中的。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。 -简单地介绍了对象的内存布局后,我们把话题返回到轻量级锁的执行过程上。在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 “01” 状态)虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝(官方把这份拷贝加上了一个 Displaced 前缀,即 Displaced Mark Word)。然后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位(Mark Word 的最后 2bit)将转变为 “00”,即表示此对象处于轻量级锁定状态。 +

-

+下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。 -如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是的话只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,所标志的状态变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 +

-上面描述的是轻量级锁的加锁过程,它的解锁过程也是通过 CAS 操作来进行的,如果对象的 Mark Word 仍然指向着线程的锁记录,那就用 CAS 操作把对象当前的 Mark Word 和线程中复制的 Displaced Mark Word 替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要释放锁的同时,唤醒被挂起的线程。 +轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。 -轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。 +当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。 + +

+ +如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。 ## 偏向锁 -偏向锁也是 JDK 1.6 中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不做了。 +偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。 -偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。 +可以使用 -XX:+UseBiasedLocking=true 开启偏向锁,不过在 JDK 1.6 中它是默认开启的。 -假设当前虚拟机启用了偏向锁(启用参数 -XX:+UseBiasedLocking,这是 JDK 1.6 的默认值),那么,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用 CAS 操作把获取到这个锁的线程 ID 记录在对象的 Mark Word 之中,如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行如何同步操作(例如 Locking、Unlocking 及对 Mark Word 的 Update 等)。 +当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。 -当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行。偏向锁、轻量级锁的状态转换及对象 Mark Word 的关系如图 13-5 所示。 +当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。

-偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。 - # 十三、多线程开发良好的实践 - 给线程起个有意义的名字,这样可以方便找 Bug。 @@ -1659,3 +1683,4 @@ public static String concatString(String s1, String s2, String s3) { - [Concurrent](https://sites.google.com/site/webdevelopart/21-compile/06-java/javase/concurrent?tmpl=%2Fsystem%2Fapp%2Ftemplates%2Fprint%2F&showPrintDialog=1) - [JAVA FORK JOIN EXAMPLE](http://www.javacreed.com/java-fork-join-example/ "Java Fork Join Example") - [聊聊并发(八)——Fork/Join 框架介绍](http://ifeve.com/talk-concurrency-forkjoin/) +- [Eliminating SynchronizationRelated Atomic Operations with Biased Locking and Bulk Rebiasing](http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf) diff --git a/pics/051e436c-0e46-4c59-8f67-52d89d656182.png b/pics/051e436c-0e46-4c59-8f67-52d89d656182.png new file mode 100644 index 00000000..e3054539 Binary files /dev/null and b/pics/051e436c-0e46-4c59-8f67-52d89d656182.png differ diff --git a/pics/baaa681f-7c52-4198-a5ae-303b9386cf47.png b/pics/baaa681f-7c52-4198-a5ae-303b9386cf47.png new file mode 100644 index 00000000..6664d398 Binary files /dev/null and b/pics/baaa681f-7c52-4198-a5ae-303b9386cf47.png differ diff --git a/pics/bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png b/pics/bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png new file mode 100644 index 00000000..07d8692d Binary files /dev/null and b/pics/bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png differ