Skip to content

Commit

Permalink
update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
dunwu committed Jun 30, 2020
1 parent 814d844 commit a5d1ae4
Show file tree
Hide file tree
Showing 17 changed files with 459 additions and 122 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
- [Java 开发环境](docs/basics/java-develop-env.md)
- [Java 基础语法特性](docs/basics/java-basic-grammar.md)
- [Java 基本数据类型](docs/basics/java-data-type.md)
- [Java String 类型](docs/basics/java-string.md)
- [Java 类和对象](docs/basics/java-class.md)
- [Java 方法](docs/basics/java-method.md)
- [Java 数组](docs/basics/java-array.md)
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ footer: CC-BY-SA-4.0 Licensed | Copyright © 2018-Now Dunwu
- [Java 开发环境](basics/java-develop-env.md)
- [Java 基础语法特性](basics/java-basic-grammar.md)
- [Java 基本数据类型](basics/java-data-type.md)
- [Java String 类型](basics/java-string.md)
- [Java 类和对象](basics/java-class.md)
- [Java 方法](basics/java-method.md)
- [Java 数组](basics/java-array.md)
Expand Down
1 change: 1 addition & 0 deletions docs/basics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [Java 开发环境](java-develop-env.md)
- [Java 基础语法特性](java-basic-grammar.md)
- [Java 基本数据类型](java-data-type.md)
- [Java String 类型](java-string.md)
- [Java 类和对象](java-class.md)
- [Java 方法](java-method.md)
- [Java 数组](java-array.md)
Expand Down
91 changes: 91 additions & 0 deletions docs/basics/java-string.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# 深入理解 Java String 类型

> **📦 本文以及示例源码已归档在 [javacore](https://github.com/dunwu/javacore/)**
>
> String 类型可能是 Java 中应用最频繁的引用类型,但它的性能问题却常常被忽略。高效的使用字符串,可以提升系统的整体性能。当然,要做到高效使用字符串,需要深入了解其特性。
思考题:结果是什么?

```
String str1= "abc";
String str2= new String("abc");
String str3= str2.intern();
assertSame(str1==str2);
assertSame(str2==str3);
assertSame(str1==str3)
```

## String 的不可变性

我们先来看下 `String` 的定义:

```java
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
```

`String` 类被 `final` 关键字修饰,表示**不可继承 `String` 类**

`String` 类的数据存储于 `char[]` 数组,这个数组被 `final` 关键字修饰,表示 **`String` 对象不可被更改**

为什么 Java 要这样设计?

1**保证 String 对象安全性**。避免 String 被篡改。

2**保证 hash 值不会频繁变更**

3**可以实现字符串常量池**。通常有两种创建字符串对象的方式,一种是通过字符串常量的方式创建,如 `String str="abc";` 另一种是字符串变量通过 new 形式的创建,如 `String str = new String("abc")`。

使用第一种方式创建字符串对象时,JVM 首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。

`String str = new String("abc")` 这种方式,首先在编译类文件时,`"abc"` 常量字符串将会放入到常量结构中,在类加载时,`"abc"` 将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 `String` 的构造函数,同时引用常量池中的 `"abc"` 字符串,在堆内存中创建一个 `String` 对象;最后,str 将引用 `String` 对象。

## String 的优化

### 字符串拼接

如果需要使用**字符串拼接,应该优先考虑 `StringBuilder` 或 `StringBuffer`(线程安全) 的 `append` 方法替代使用 `+` 号**。

【示例】错误示例

```
String str= "ab" + "cd" + "ef";
```

程序会先生成 ab 对象,再生成 abcd 对象,最后生成 abcdef 对象。

即使使用 `+` 号作为字符串的拼接,也一样可以被编译器优化成 `StringBuilder` 的方式。但再细致些,你会发现在编译器优化的代码中,每次循环都会生成一个新的 `StringBuilder` 实例,同样也会降低系统的性能。

### 如何使用 String.intern 节省内存

在每次赋值的时候使用 `String` 的 `intern` 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。

在字符串常量中,默认会将对象放入常量池;在字符串变量中,对象是会创建在堆内存中,同时也会在常量池中创建一个字符串对象,复制到堆内存对象中,并返回堆内存对象引用。

如果调用 intern 方法,会去查看字符串常量池中是否有等于该对象的字符串,如果没有,就在常量池中新增该对象,并返回该对象引用;如果有,就返回常量池中的字符串引用。堆内存中原有的对象由于没有引用指向它,将会通过垃圾回收器回收。

【示例】

```java
public class SharedLocation {

private String city;
private String region;
private String countryCode;
}

SharedLocation sharedLocation = new SharedLocation();
sharedLocation.setCity(messageInfo.getCity().intern()); sharedLocation.setCountryCode(messageInfo.getRegion().intern());
sharedLocation.setRegion(messageInfo.getCountryCode().intern());
```



## 参考资料

- [《Java 编程思想(Thinking in java)》](https://item.jd.com/10058164.html)
- [《Java 核心技术 卷 I 基础知识》](https://item.jd.com/12759308.html)
- [Java基本数据类型和引用类型](https://juejin.im/post/59cd71835188255d3448faf6)
- [深入剖析 Java 中的装箱和拆箱](https://www.cnblogs.com/dolphin0520/p/3780005.html)
11 changes: 11 additions & 0 deletions docs/concurrent/java-atomic-class.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [三、引用类型](#三引用类型)
- [四、数组类型](#四数组类型)
- [五、属性更新器类型](#五属性更新器类型)
- [六、LongAddr](#六longaddr)
- [参考资料](#参考资料)

<!-- /TOC -->
Expand Down Expand Up @@ -435,6 +436,16 @@ public class AtomicReferenceFieldUpdaterDemo {
}
```

## 六、LongAddr

在 JDK1.8 中,Java 提供了一个新的原子类 LongAdder。LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好,代价就是会消耗更多的内存空间。

LongAdder 内部由一个 base 变量和一个 cell[] 数组组成。当只有一个写线程,没有竞争的情况下,LongAdder 会直接使用 base 变量作为原子操作变量,通过 CAS 操作修改变量;当有多个写线程竞争的情况下,除了占用 base 变量的一个写线程之外,其它各个线程会将修改的变量写入到自己的槽 cell[] 数组中,最终结果可通过以下公式计算得出:

$$value = base + \sum_{i=0}^ncell[i]$$

我们可以发现,LongAdder 在操作后的返回值只是一个近似准确的数值,但是 LongAdder 最终返回的是一个准确的数值, 所以在一些对实时性要求比较高的场景下,LongAdder 并不能取代 AtomicInteger 或 AtomicLong。

## 参考资料

- [《Java 并发编程实战》](https://item.jd.com/10922250.html)
Expand Down
51 changes: 34 additions & 17 deletions docs/concurrent/java-concurrent-basic-mechanism.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,22 +243,36 @@ public class SynchronizedDemo3 implements Runnable {

`synchronized` 同步块是互斥的,即已进入的线程执行完成前,会阻塞其他试图进入的线程。

#### 锁的机制
#### 同步代码块

`synchronized` 在修饰同步代码块时,是由 `monitorenter``monitorexit` 指令来实现同步的。进入 `monitorenter` 指令后,线程将持有 `Monitor` 对象,退出 `monitorenter` 指令后,线程将释放该 `Monitor` 对象。

#### 同步方法

`synchronized` 修饰同步方法时,会设置一个 `ACC_SYNCHRONIZED` 标志。当方法调用时,调用指令将会检查该方法是否被设置 `ACC_SYNCHRONIZED` 访问标志。如果设置了该标志,执行线程将先持有 `Monitor` 对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该 `Mointor` 对象,当方法执行完成后,再释放该 `Monitor` 对象。

锁具备以下两种特性:
#### Monitor

- **互斥性**:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。
- **可见性**:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作从而引起不一致。
每个对象实例都会有一个 `Monitor``Monitor` 可以和对象一起创建、销毁。`Monitor` 是由 `ObjectMonitor` 实现,而 `ObjectMonitor` 是由 C++ 的 `ObjectMonitor.hpp` 文件实现。

#### 锁类型
当多个线程同时访问一段同步代码时,多个线程会先被存放在 EntryList 集合中,处于 block 状态的线程,都会被加入到该列表。接下来当线程获取到对象的 Monitor 时,Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex。

- **对象锁** - 在 Java 中,**每个对象都会有一个 `monitor` 对象,这个对象其实就是 Java 对象的锁**,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。
- **类锁** - 在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。
如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入 WaitSet 集合中,等待下一次被唤醒。如果当前线程顺利执行完方法,也将释放 Mutex。

### synchronized 的优化

> **Java 1.6 以后,`synchronized` 做了大量的优化,其性能已经与 `Lock``ReadWriteLock` 基本上持平**
#### Java 对象头

在 JDK1.6 JVM 中,对象实例在堆内存中被分为了三个部分:对象头、实例数据和对齐填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组长度三部分组成。

Mark Word 记录了对象和锁有关的信息。Mark Word 在 64 位 JVM 中的长度是 64bit,我们可以一起看下 64 位 JVM 的存储结构是怎么样的。如下图所示:

![](http://dunwu.test.upcdn.net/snap/20200629191250.png)

锁升级功能主要依赖于 Mark Word 中的锁标志位和释放偏向锁标志位,`synchronized` 同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。

Java 1.6 引入了偏向锁和轻量级锁,从而让 `synchronized` 拥有了四个状态:

- **无锁状态(unlocked)**
Expand All @@ -268,14 +282,9 @@ Java 1.6 引入了偏向锁和轻量级锁,从而让 `synchronized` 拥有了

当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现。

当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作(compare and swap),
在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不
涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会
被一个线程锁定,使用偏斜锁可以降低无竞争开销。
当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。

如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏向
锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重
试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏向锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。

#### 偏向锁

Expand All @@ -291,11 +300,17 @@ Java 1.6 引入了偏向锁和轻量级锁,从而让 `synchronized` 拥有了

![](http://dunwu.test.upcdn.net/snap/20200604105248.png)

#### 锁消除
#### 锁消除 / 锁粗化

除了锁升级优化,Java 还使用了编译器对锁进行优化。

**(1)锁消除**

**锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除**

锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。
JIT 编译器在动态编译同步块的时候,借助了一种被称为逃逸分析的技术,来判断同步块使用的锁对象是否只能够被一个线程访问,而没有被发布到其它线程。

确认是的话,那么 JIT 编译器在编译这个同步块的时候不会生成 synchronized 所表示的锁的申请与释放的机器码,即消除了锁的使用。在 Java7 之后的版本就不需要手动配置了,该操作可以自动实现。

对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:

Expand All @@ -319,7 +334,9 @@ public static String concatString(String s1, String s2, String s3) {

每个 `append()` 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 `concatString()` 方法内部。也就是说,sb 的所有引用永远不会逃逸到 `concatString()` 方法之外,其他线程无法访问到它,因此可以进行消除。

#### 锁粗化
**(2)锁粗化**

锁粗化同理,就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。

如果**一系列的连续操作都对同一个对象反复加锁和解锁**,频繁的加锁操作就会导致性能损耗。

Expand Down
10 changes: 10 additions & 0 deletions docs/concurrent/java-concurrent-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,16 @@ J.U.C 包中提供的并发容器命名一般分为三类:
- `CopyOnWrite*` - 读写分离。读操作时不加锁,写操作时通过在副本上加锁保证并发安全,空间开销较大。
- `Blocking*` - 内部实现一般是基于锁,提供阻塞队列的能力。

### 并发场景下的 Map

如果对数据有强一致要求,则需使用 Hashtable;在大部分场景通常都是弱一致性的情况下,使用 ConcurrentHashMap 即可;如果数据量在千万级别,且存在大量增删改操作,则可以考虑使用 ConcurrentSkipListMap。

### 并发场景下的 List

读多写少用 `CopyOnWriteArrayList`

写多读少用 `ConcurrentLinkedQueue` ,但由于是无界的,要有容量限制,避免无限膨胀,导致内存溢出。

## 三、ConcurrentHashMap

> `ConcurrentHashMap` 是线程安全的 `HashMap` ,用于替代 `Hashtable`
Expand Down
29 changes: 17 additions & 12 deletions docs/concurrent/java-lock.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,23 @@ public final boolean releaseShared(int arg)

### AQS 的原理

> ASQ 原理要点:
>
> - AQS 使用一个整型的 `volatile` 变量来 **维护同步状态**。状态的意义由子类赋予。
> - AQS 维护了一个 FIFO 的双链表,用来存储获取锁失败的线程。
>
> AQS 围绕同步状态提供两种基本操作“获取”和“释放”,并提供一系列判断和处理方法,简单说几点:
>
> - state 是独占的,还是共享的;
>
> - state 被获取后,其他线程需要等待;
>
> - state 被释放后,唤醒等待线程;
>
> - 线程等不及时,如何退出等待。
>
> 至于线程是否可以获得 state,如何释放 state,就不是 AQS 关心的了,要由子类具体实现。
#### AQS 的数据结构

阅读 AQS 的源码,可以发现:AQS 继承自 `AbstractOwnableSynchronize`
Expand Down Expand Up @@ -274,18 +291,6 @@ static final class Node {
- `PROPAGATE(-3)` - 此状态表示:下一个 `acquireShared` 应无条件传播。
- 0 - 非以上状态。

AQS 围绕 state 提供两种基本操作“获取”和“释放”,并将阻塞的等待线程存入双链表中,并提供一系列判断和处理方法,简单说几点:

- state 是独占的,还是共享的;

- state 被获取后,其他线程需要等待;

- state 被释放后,唤醒等待线程;

- 线程等不及时,如何退出等待。

至于线程是否可以获得 state,如何释放 state,就不是 AQS 关心的了,要由子类具体实现。

#### 独占锁的获取和释放

##### 获取独占锁
Expand Down
4 changes: 2 additions & 2 deletions docs/concurrent/java-memory-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,14 @@ Java 实现多线程可见性的方式有:
- `volatile` 关键字会禁止指令重排序。
- `synchronized` 关键字通过互斥保证同一时刻只允许一条线程操作。

### 先行发生原则(Happens-Before
### Happens-Before

> JMM 为程序中所有的操作定义了一个偏序关系,称之为 **`先行发生原则(Happens-Before)`**
>
> 先行发生原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作间是否可能存在冲突的所有问题。
- **程序次序规则** - 一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- **管程锁定规则** - 一个 `unLock` 操作先行发生于后面对同一个锁的 `lock` 操作。
- **锁定规则** - 一个 `unLock` 操作先行发生于后面对同一个锁的 `lock` 操作。
- **volatile 变量规则** - 对一个 `volatile` 变量的写操作先行发生于后面对这个变量的读操作。
- **线程启动规则** - `Thread` 对象的 `start()` 方法先行发生于此线程的每个一个动作。
- **线程终止规则** - 线程中所有的操作都先行发生于线程的终止检测,我们可以通过 `Thread.join()` 方法结束、Thread.isAlive() 的返回值手段检测到线程已经终止执行。
Expand Down
12 changes: 12 additions & 0 deletions docs/concurrent/java-thread-pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
- [newFixedThreadPool](#newfixedthreadpool)
- [newCachedThreadPool](#newcachedthreadpool)
- [newScheduleThreadPool](#newschedulethreadpool)
- [五、线程池优化](#五线程池优化)
- [计算线程数量](#计算线程数量)
- [参考资料](#参考资料)

<!-- /TOC -->
Expand Down Expand Up @@ -451,6 +453,16 @@ public class ScheduledThreadPoolDemo {
}
```

## 五、线程池优化

### 计算线程数量

一般多线程执行的任务类型可以分为 CPU 密集型和 I/O 密集型,根据不同的任务类型,我们计算线程数的方法也不一样。

**CPU 密集型任务:**这种任务消耗的主要是 CPU 资源,可以将线程数设置为 NCPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

**I/O 密集型任务:**这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

## 参考资料

- [《Java 并发编程实战》](https://item.jd.com/10922250.html)
Expand Down
Loading

0 comments on commit a5d1ae4

Please sign in to comment.