Skip to content
Open

B2 #2

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
language: java

install: mvn install -DskipTests=true -Dmaven.javadoc.skip=true
script: mvn -DskipTests=true clean install


branches:
only:
- master
16 changes: 8 additions & 8 deletions 79884.log
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,30 @@ Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.131-b11 mixed mode):

"Blocked2" #13 prio=5 os_prio=31 tid=0x00007ffd7b08b000 nid=0x5503 waiting for monitor entry [0x00007000083d1000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.crossoverjie.concurrent.ThreadState$Blocked.run(ThreadState.java:59)
- waiting to lock <0x000000079576cd60> (a java.lang.Class for com.crossoverjie.concurrent.ThreadState$Blocked)
at com.crossoverjie.thread.ThreadState$Blocked.run(ThreadState.java:59)
- waiting to lock <0x000000079576cd60> (a java.lang.Class for com.crossoverjie.thread.ThreadState$Blocked)
at java.lang.Thread.run(Thread.java:748)

"Blocked1" #12 prio=5 os_prio=31 tid=0x00007ffd7b08a000 nid=0x5303 waiting on condition [0x00007000082ce000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.crossoverjie.concurrent.ThreadState$Blocked.run(ThreadState.java:59)
- locked <0x000000079576cd60> (a java.lang.Class for com.crossoverjie.concurrent.ThreadState$Blocked)
at com.crossoverjie.thread.ThreadState$Blocked.run(ThreadState.java:59)
- locked <0x000000079576cd60> (a java.lang.Class for com.crossoverjie.thread.ThreadState$Blocked)
at java.lang.Thread.run(Thread.java:748)

"Waiting" #11 prio=5 os_prio=31 tid=0x00007ffd7b089800 nid=0x5103 in Object.wait() [0x00007000081cb000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000795768db0> (a java.lang.Class for com.crossoverjie.concurrent.ThreadState$Waiting)
- waiting on <0x0000000795768db0> (a java.lang.Class for com.crossoverjie.thread.ThreadState$Waiting)
at java.lang.Object.wait(Object.java:502)
at com.crossoverjie.concurrent.ThreadState$Waiting.run(ThreadState.java:42)
- locked <0x0000000795768db0> (a java.lang.Class for com.crossoverjie.concurrent.ThreadState$Waiting)
at com.crossoverjie.thread.ThreadState$Waiting.run(ThreadState.java:42)
- locked <0x0000000795768db0> (a java.lang.Class for com.crossoverjie.thread.ThreadState$Waiting)
at java.lang.Thread.run(Thread.java:748)

"TimeWaiting" #10 prio=5 os_prio=31 tid=0x00007ffd7b82c800 nid=0x4f03 waiting on condition [0x00007000080c8000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.crossoverjie.concurrent.ThreadState$TimeWaiting.run(ThreadState.java:27)
at com.crossoverjie.thread.ThreadState$TimeWaiting.run(ThreadState.java:27)
at java.lang.Thread.run(Thread.java:748)

"Monitor Ctrl-Break" #9 daemon prio=5 os_prio=31 tid=0x00007ffd7a97e000 nid=0x4d03 runnable [0x0000700007fc5000]
Expand Down
2 changes: 1 addition & 1 deletion MD/ArrayList.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ transient Object[] elementData;

## Vector

`Voctor` 也是实现于 `List` 接口,底层数据结构和 `ArrayList` 类似,也是一个动态数组存放数据。不过是在 `add()` 方法的时候使用 `synchronize` 进行同步写数据,但是开销较大,所以 `Vector` 是一个同步容器并不是一个并发容器。
`Vector` 也是实现于 `List` 接口,底层数据结构和 `ArrayList` 类似,也是一个动态数组存放数据。不过是在 `add()` 方法的时候使用 `synchronized` 进行同步写数据,但是开销较大,所以 `Vector` 是一个同步容器并不是一个并发容器。

以下是 `add()` 方法:
```java
Expand Down
4 changes: 2 additions & 2 deletions MD/Cache-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@

写缓存时也要注意,通常来说分为以下几步:

- 开启事物
- 开启事务
- 写入 DB 。
- 提交事物
- 提交事务
- 写入缓存。

这里可能会存在数据库写入成功但是缓存写入失败的情况,但是也不建议将写入缓存加入到事务中。
Expand Down
56 changes: 51 additions & 5 deletions MD/ConcurrentHashMap.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
**更多 HashMap 与 ConcurrentHashMap 相关请查看[这里](https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/)。**

# ConcurrentHashMap 实现原理

由于 `HashMap` 是一个线程不安全的容器,主要体现在容量大于`总量*负载因子`发生扩容时会出现环形链表从而导致死循环。

因此需要支持线程安全的并发容器 `ConcurrentHashMap` 。

## 数据结构
## JDK1.7 实现

### 数据结构
![](https://ws2.sinaimg.cn/large/006tNc79ly1fn2f5pgxinj30dw0730t7.jpg)

如图所示,是由 `Segment` 数组、`HashEntry` 数组组成,和 `HashMap` 一样,仍然是数组加链表组成。

`ConcurrentHashMap` 采用了分段锁技术,其中 `Segment` 继承于 `ReentrantLock`。不会像 `HashTable` 那样不管是 `put` 还是 `get` 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 `CurrencyLevel` (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 `Segment` 时,不会影响到其他的 `Segment`。

## get 方法
### get 方法
`ConcurrentHashMap` 的 `get` 方法是非常高效的,因为整个过程都不需要加锁。

只需要将 `Key` 通过 `Hash` 之后定位到具体的 `Segment` ,再通过一次 `Hash` 定位到具体的元素上。由于 `HashEntry` 中的 `value` 属性是用 `volatile` 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值([volatile 相关知识点](https://github.com/crossoverJie/Java-Interview/blob/master/MD/Threadcore.md#%E5%8F%AF%E8%A7%81%E6%80%A7))。

## put 方法
### put 方法

内部 `HashEntry` 类 :

```java
static final class HashEntry<K,V> {
final int hash;
Expand All @@ -39,12 +44,53 @@

首先也是通过 Key 的 Hash 定位到具体的 Segment,在 put 之前会进行一次扩容校验。这里比 HashMap 要好的一点是:HashMap 是插入元素之后再看是否需要扩容,有可能扩容之后后续就没有插入就浪费了本次扩容(扩容非常消耗性能)。

而 ConcurrentHashMap 不一样,它是先将数据插入之后再检查是否需要扩容,之后再做插入
而 ConcurrentHashMap 不一样,它是在将数据插入之前检查是否需要扩容,之后再做插入操作

## size 方法
### size 方法

每个 `Segment` 都有一个 `volatile` 修饰的全局变量 `count` ,求整个 `ConcurrentHashMap` 的 `size` 时很明显就是将所有的 `count` 累加即可。但是 `volatile` 修饰的变量却不能保证多线程的原子性,所有直接累加很容易出现并发问题。

但如果每次调用 `size` 方法将其余的修改操作加锁效率也很低。所以做法是先尝试两次将 `count` 累加,如果容器的 `count` 发生了变化再加锁来统计 `size`。

至于 `ConcurrentHashMap` 是如何知道在统计时大小发生了变化呢,每个 `Segment` 都有一个 `modCount` 变量,每当进行一次 `put remove` 等操作,`modCount` 将会 +1。只要 `modCount` 发生了变化就认为容器的大小也在发生变化。



## JDK1.8 实现

![](https://ws3.sinaimg.cn/large/006tNc79gy1fthpv4odbsj30lp0drmxr.jpg)

1.8 中的 ConcurrentHashMap 数据结构和实现与 1.7 还是有着明显的差异。

其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性。

![](https://ws3.sinaimg.cn/large/006tNc79gy1fthq78e5gqj30nr09mmz9.jpg)

也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。

其中的 `val next` 都用了 volatile 修饰,保证了可见性。

### put 方法

重点来看看 put 函数:

![](https://ws3.sinaimg.cn/large/006tNc79gy1fthrz8jlo8j30oc0rbte3.jpg)

- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- `f` 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的 `hashcode == MOVED == -1`,则需要进行扩容。
- 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于 `TREEIFY_THRESHOLD` 则要转换为红黑树。

### get 方法

![](https://ws1.sinaimg.cn/large/006tNc79gy1fthsnp2f35j30o409hwg7.jpg)

- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
- 如果是红黑树那就按照树的方式获取值。
- 都不满足那就按照链表的方式遍历获取值。

## 总结

1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(`O(logn)`),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。
4 changes: 2 additions & 2 deletions MD/GarbageCollection.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
### 标记-清除算法

标记清除算法分为两个步骤,标记和清除。
首先将需要回收的对象标记起来,然后统一清除。但是存在两个主要的问题:
首先将**不需要回收的对象**标记起来,然后再清除其余可回收对象。但是存在两个主要的问题:
- 标记和清除的效率都不高。
- 清除之后容易出现不连续内存,当需要分配一个较大内存时就不得不需要进行一次垃圾回收。

Expand All @@ -60,7 +60,7 @@

复制算法如果在存活对象较多时效率明显会降低,特别是在老年代中并没有多余的内存区域可以提供内存担保。

所以老年代中使用的时候`分配整理算法`,它的原理和`分配清除算法`类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后再将边界之外的内存全部回收。
所以老年代中使用的时候`标记整理算法`,它的原理和`标记清除算法`类似,只是最后一步的清除改为了将存活对象全部移动到一端,然后再将边界之外的内存全部回收。

![](https://ws3.sinaimg.cn/large/006tNc79gy1fmzbq55pfdj30fe08s3yx.jpg)

Expand Down
6 changes: 4 additions & 2 deletions MD/HashMap.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
**更多 HashMap 与 ConcurrentHashMap 相关请查看[这里](https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/)。**

# HashMap 底层分析

> 以下基于 JDK1.7 分析。
Expand All @@ -16,7 +18,7 @@

由于在计算中位运算比取模运算效率高的多,所以 HashMap 规定数组的长度为 `2^n` 。这样用 `2^n - 1` 做位运算与取模效果一致,并且效率还要高出许多。

由于数组的长度有限,所以难免会出现不同的 Key 通过运算得到的 index 相同,这种情况可以利用链表来解决,HashMap 会在 `table[index]`处形成环形链表,采用头插法将数据插入到链表中。
由于数组的长度有限,所以难免会出现不同的 Key 通过运算得到的 index 相同,这种情况可以利用链表来解决,HashMap 会在 `table[index]`处形成链表,采用头插法将数据插入到链表中。

## get 方法

Expand Down Expand Up @@ -64,7 +66,7 @@ map.forEach((key,value)->{
> 所以 HashMap 只能在单线程中使用,并且尽量的预设容量,尽可能的减少扩容。

在 `JDK1.8` 中对 `HashMap` 进行了优化:
当 `hash` 碰撞之后写入链表的长度超过了阈值(默认为8),链表将会转换为**红黑树**。
当 `hash` 碰撞之后写入链表的长度超过了阈值(默认为8)并且 `table` 的长度不小于64(否则扩容一次)时,链表将会转换为**红黑树**。

假设 `hash` 冲突非常严重,一个数组后面接了很长的链表,此时重新的时间复杂度就是 `O(n)` 。

Expand Down
4 changes: 2 additions & 2 deletions MD/Java-lock.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## 同一进程

### [重入锁](https://github.com/crossoverJie/Java-Interview/blob/master/MD/ReentrantLock.md)
使用 `ReentrantLock` 获取锁的时候会会判断当前线程是否为获取锁的线程,如果是则将同步的状态 +1 ,释放锁的时候则将状态 -1。只有将同步状态的次数置为 0 的时候才会最终释放锁。
使用 `ReentrantLock` 获取锁的时候会判断当前线程是否为获取锁的线程,如果是则将同步的状态 +1 ,释放锁的时候则将状态 -1。只有将同步状态的次数置为 0 的时候才会最终释放锁。

### 读写锁
使用 `ReentrantReadWriteLock` ,同时维护一对锁:读锁和写锁。当写线程访问时则其他所有锁都将阻塞,读线程访问时则不会。通过读写锁的分离可以很大程度的提高并发量和吞吐量。
Expand All @@ -30,7 +30,7 @@

### 基于 Redis

使用 `setNX(key) setEX(timeout)` 命令,只有在该 `key` 不存在的时候创建和这个 `key`,就相当于获取了锁。由于有超时时间,所以过了规定时间会自动删除,这样也可以避免死锁。
使用 `setNX(key) setEX(timeout)` 命令,只有在该 `key` 不存在的时候创建这个 `key`,就相当于获取了锁。由于有超时时间,所以过了规定时间会自动删除,这样也可以避免死锁。

可以参考:

Expand Down
2 changes: 1 addition & 1 deletion MD/LinkedList.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
}
```

由此可以看出是使用二分查找来看 `index` 离 size 中间距离来判断是从头结点正序查还是从尾节点倒序查
上述代码,利用了双向链表的特性,如果`index`离链表头比较近,就从节点头部遍历。否则就从节点尾部开始遍历。使用空间(双向链表)来换取时间

- `node()`会以`O(n/2)`的性能去获取一个结点
- 如果索引值大于链表大小的一半,那么将从尾结点开始遍历
Expand Down
69 changes: 64 additions & 5 deletions MD/MemoryAllocation.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,85 @@


## 虚拟机栈
虚拟机栈是有一个一个的栈帧组成,栈帧是在每一个方法调用时产生的。
虚拟机栈由一个一个的栈帧组成,栈帧是在每一个方法调用时产生的。

每一个栈帧由`局部变量区`、`操作数栈`等组成。每创建一个栈帧压栈,当一个方法执行完毕之后则出栈。

> 如果出现方法递归调用出现死循环的话就会造成栈帧过多,最终会抛出 `stackoverflow` 异常。
> - 如果出现方法递归调用出现死循环的话就会造成栈帧过多,最终会抛出 `StackOverflowError`。
> - 若线程执行过程中栈帧大小超出虚拟机栈限制,则会抛出 `StackOverflowError`。
> - 若虚拟机栈允许动态扩展,但在尝试扩展时内存不足,或者在为一个新线程初始化新的虚拟机栈时申请不到足够的内存,则会抛出
`OutOfMemoryError`。

**这块内存区域也是线程私有的。**

## Java 堆
`Java` 堆是整个虚拟机所管理的最大内存区域,所有的对象创建都是在这个区域进行内存分配。

可利用参数 `-Xms -Xmx` 进行堆内存控制。

这块区域也是垃圾回收器重点管理的区域,由于大多数垃圾回收器都采用`分代回收算法`,所有堆内存也分为 `新生代`、`老年代`,可以方便垃圾的准确回收。

**这块内存属于线程共享区域。**

## 方法区
## 方法区(JDK1.7)

方法区主要用于存放已经被虚拟机加载的类信息,如`常量,静态变量`。
这块区域也被称为`永久代`。

### 运行时常量池
可利用参数 `-XX:PermSize -XX:MaxPermSize` 控制初始化方法区和最大方法区大小。



## 元数据区(JDK1.8)

在 `JDK1.8` 中已经移除了方法区(永久代),并使用了一个元数据区域进行代替(`Metaspace`)。

默认情况下元数据区域会根据使用情况动态调整,避免了在 1.7 中由于加载类过多从而出现 `java.lang.OutOfMemoryError: PermGen`。

但也不能无限扩展,因此可以使用 `-XX:MaxMetaspaceSize`来控制最大内存。





## 运行时常量池

运行时常量池是方法区的一部分,其中存放了一些符号引用。当 `new` 一个对象时,会检查这个区域是否有这个符号的引用。



## 直接内存



直接内存又称为 `Direct Memory(堆外内存)`,它并不是由 `JVM` 虚拟机所管理的一块内存区域。

有使用过 `Netty` 的朋友应该对这块并内存不陌生,在 `Netty` 中所有的 IO(nio) 操作都会通过 `Native` 函数直接分配堆外内存。

它是通过在堆内存中的 `DirectByteBuffer` 对象操作的堆外内存,避免了堆内存和堆外内存来回复制交换复制,这样的高效操作也称为`零拷贝`。

既然是内存,那也得是可以被回收的。但由于堆外内存不直接受 `JVM` 管理,所以常规 `GC` 操作并不能回收堆外内存。它是借助于老年代产生的 `fullGC` 顺便进行回收。同时也可以显式调用 `System.gc()` 方法进行回收(前提是没有使用 `-XX:+DisableExplicitGC` 参数来禁止该方法)。

**值得注意的是**:由于堆外内存也是内存,是由操作系统管理。如果应用有使用堆外内存则需要平衡虚拟机的堆内存和堆外内存的使用占比。避免出现堆外内存溢出。


## 常用参数

![](https://ws1.sinaimg.cn/large/006tNbRwly1fxjcmnkuqyj30p009vjsn.jpg)

通过上图可以直观的查看各个区域的参数设置。

常见的如下:

- `-Xms64m` 最小堆内存 `64m`.
- `-Xmx128m` 最大堆内存 `128m`.
- `-XX:NewSize=30m` 新生代初始化大小为`30m`.
- `-XX:MaxNewSize=40m` 新生代最大大小为`40m`.
- `-Xss=256k` 线程栈大小。
- `-XX:+PrintHeapAtGC` 当发生 GC 时打印内存布局。
- `-XX:+HeapDumpOnOutOfMemoryError` 发送内存溢出时 dump 内存。


新生代和老年代的默认比例为 `1:2`,也就是说新生代占用 `1/3`的堆内存,而老年代占用 `2/3` 的堆内存。

运行时常量池是方法区的一部分,其中存放了一些符号引用。当 new 一个对象时,会检查这个区域是否有这个符号的引用
可以通过参数 `-XX:NewRatio=2` 来设置老年代/新生代的比例
2 changes: 1 addition & 1 deletion MD/MySQL-Index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@

观察树的结构,发现查询需要经历几次 IO 是由树的高度来决定的,而树的高度又由磁盘块,数据项的大小决定的。

磁盘块越大,数据项越小那么数的高度就越低。这也就是为什么索引字段要尽可能小的原因。
磁盘块越大,数据项越小那么树的高度就越低。这也就是为什么索引字段要尽可能小的原因。

> 索引使用的一些[原则](https://github.com/crossoverJie/Java-Interview/blob/master/MD/SQL-optimization.md)。
8 changes: 7 additions & 1 deletion MD/OOM-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
只要将`-Xms(最小堆)`,`-Xmx(最大堆)` 设置为一样禁止自动扩展堆内存。


当使用一个 `while(true)` 循环来不断创建对象就会发生 `OutOfMemory`,还可以使用 `-XX:+HeapDumpOutofMemoryErorr` 当发生 OOM 时会自动 dump 堆栈到文件中。
当使用一个 `while(true)` 循环来不断创建对象就会发生 `OutOfMemory`,还可以使用 `-XX:+HeapDumpOnOutOfMemoryError` 当发生 OOM 时会自动 dump 堆栈到文件中。

伪代码:

Expand Down Expand Up @@ -43,6 +43,12 @@ Process finished with exit code 1
`java.lang.OutOfMemoryError: Java heap space`表示堆内存溢出。



更多内存溢出相关实战请看这里:[强如 Disruptor 也发生内存溢出?](https://crossoverjie.top/2018/08/29/java-senior/OOM-Disruptor/)




## MetaSpace (元数据) 内存溢出

> `JDK8` 中将永久代移除,使用 `MetaSpace` 来保存类加载之后的类信息,字符串常量池也被移动到 Java 堆。
Expand Down
2 changes: 1 addition & 1 deletion MD/ReentrantLock.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ReentrantLock 实现原理

使用 `synchronize` 来做同步处理时,锁的获取和释放都是隐式的,实现的原理是通过编译后加上不同的机器指令来实现。
使用 `synchronized` 来做同步处理时,锁的获取和释放都是隐式的,实现的原理是通过编译后加上不同的机器指令来实现。

而 `ReentrantLock` 就是一个普通的类,它是基于 `AQS(AbstractQueuedSynchronizer)`来实现的。

Expand Down
Loading