Skip to content

Commit f7f578f

Browse files
committed
并发编程
1 parent 2f540c9 commit f7f578f

4 files changed

Lines changed: 259 additions & 10 deletions

File tree

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@
4545
- [设计模式](notes/JavaArchitecture/06%20设计模式.md)
4646
- Web框架
4747
- [Spring](notes/JavaWeb/Spring.md)
48-
- Mybatis
48+
- SpringMVC
49+
- MyBatis
4950
- 系统架构
5051

5152

@@ -85,8 +86,6 @@
8586

8687

8788

88-
89-
9089
## 一些话
9190

9291
  都说好记性不如烂笔头,定期的学习和整理必然对学习巩固有所帮助,在这里主要通过索引的方式对 Java 研发工程师的技术栈做一个系统的整理,方便随时巩固和应对随时到来的面试。在学习这条路上难免会有很多盲点和学不完的知识,不要把大脑当成硬盘,也不要做高速运转的 CPU,而修行自己的大脑成为一个搜索引擎,学会分析解决问题。

notes/JavaArchitecture/03 Java 并发编程.md

Lines changed: 255 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 前言
22

3-
在本文将总结多线程并发编程中的常见面试题,主要核心线程生命周期、线程通信、并发包部分。主要分成“并发编程”“面试指南”两部分,在面试指南中将讨论并发相关面经。
3+
在本文将总结多线程并发编程中的常见面试题,主要核心线程生命周期、线程通信、并发包部分。主要分成 “并发编程”“面试指南” 两部分,在面试指南中将讨论并发相关面经。
44

55

66

@@ -1761,15 +1761,15 @@ executorService.execute(() -> {
17611761

17621762
synchronized 和 ReentrantLock。
17631763

1764+
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
17641765

1766+
互斥同步属于一种**悲观的并发策略**,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
17651767

1766-
#### 2. 非阻塞同步
17671768

1768-
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
17691769

1770-
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
1770+
#### 2. 非阻塞同步
17711771

1772-
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
1772+
随着硬件指令集的发展,我们可以使用基于冲突检测的**乐观并发策略**:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。
17731773

17741774
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。
17751775

@@ -1864,10 +1864,250 @@ public static void main(String[] args) {
18641864

18651865
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
18661866

1867-
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的“**一个请求对应一个服务器线程**”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
1867+
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的 **一个请求对应一个服务器线程**”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
18681868

18691869
可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
18701870

1871+
**示例用法**
1872+
1873+
先通过下面这个实例来理解ThreadLocal的用法。先声明一个ThreadLocal对象,存储布尔类型的数值。然后分别在主线程中、Thread1、Thread2中为ThreadLocal对象设置不同的数值:
1874+
1875+
```java
1876+
public class ThreadLocalDemo {
1877+
public static void main(String[] args) {
1878+
1879+
// 声明 ThreadLocal对象
1880+
ThreadLocal<Boolean> mThreadLocal = new ThreadLocal<Boolean>();
1881+
1882+
// 在主线程、子线程1、子线程2中去设置访问它的值
1883+
mThreadLocal.set(true);
1884+
1885+
System.out.println("Main " + mThreadLocal.get());
1886+
1887+
new Thread("Thread#1"){
1888+
@Override
1889+
public void run() {
1890+
mThreadLocal.set(false);
1891+
System.out.println("Thread#1 " + mThreadLocal.get());
1892+
}
1893+
}.start();
1894+
1895+
new Thread("Thread#2"){
1896+
@Override
1897+
public void run() {
1898+
System.out.println("Thread#2 " + mThreadLocal.get());
1899+
}
1900+
}.start();
1901+
}
1902+
}
1903+
```
1904+
1905+
打印的结果输出如下所示:
1906+
1907+
```
1908+
MainThread true
1909+
Thread#1 false
1910+
Thread#2 null
1911+
```
1912+
1913+
可以看见,在不同线程对同一个ThreadLocal对象设置数值,在不同的线程中取出来的值不一样。接下来就分析一下源码,看看其内部结构。
1914+
1915+
**结构概览**
1916+
1917+
<div align="center"><img src="assets/006dXScfgy1fj7s01fjqpj30ng0jbabn.jpg" width="500"/></div><br/>
1918+
1919+
清晰的看到一个线程 Thread 中存在一个 ThreadLocalMap,ThreadLocalMap 中的 key 对应 ThreadLocal,在此处可见 Map 可以存储多个 key 即(ThreadLocal)。另外 Value 就对应着在 ThreadLocal 中存储的 Value。
1920+
1921+
因此总结出:每个 Thread 中都具备一个 ThreadLocalMap,而 ThreadLocalMap 可以存储以 ThreadLocal 为key的键值对。这里解释了为什么每个线程访问同一个 ThreadLocal,得到的确是不同的数值。如果此处你觉得有点突兀,接下来看源码分析!
1922+
1923+
**源码分析**
1924+
1925+
###### **ThreadLocal#set**
1926+
1927+
```
1928+
public void set(T value) {
1929+
// 获取当前线程对象
1930+
Thread t = Thread.currentThread();
1931+
// 根据当前线程的对象获取其内部Map
1932+
ThreadLocalMap map = getMap(t);
1933+
// 注释1
1934+
if (map != null)
1935+
map.set(this, value);
1936+
else
1937+
createMap(t, value);
1938+
}
1939+
```
1940+
1941+
如上所示,大部分解释已经在代码中做出,注意注释1处,得到map对象之后,用的`this`作为key,this在这里代表的是当前线程的ThreadLocal对象。 另外就是第二句根据getMap获取一个ThreadLocalMap,其中getMap中传入了参数t(当前线程对象),这样就能够获取每个线程的`ThreadLocal`了。
1942+
1943+
继续跟进到ThreadLocalMap中查看set方法:
1944+
1945+
###### **ThreadLocalMap**
1946+
1947+
ThreadLocalMap是ThreadLocal的一个内部类,在分析其set方法之前,查看一下其类结构和成员变量。
1948+
1949+
```
1950+
static class ThreadLocalMap {
1951+
// Entry类继承了WeakReference<ThreadLocal<?>>,即每个Entry对象都有一个ThreadLocal的弱引用
1952+
//(作为key),这是为了防止内存泄露。一旦线程结束,key变为一个不可达的对象,这个Entry就可以被GC了。
1953+
static class Entry extends WeakReference<ThreadLocal<?>> {
1954+
/** The value associated with this ThreadLocal. */
1955+
Object value;
1956+
Entry(ThreadLocal<?> k, Object v) {
1957+
super(k);
1958+
value = v;
1959+
}
1960+
}
1961+
// ThreadLocalMap 的初始容量,必须为2的倍数
1962+
private static final int INITIAL_CAPACITY = 16;
1963+
1964+
// resized时候需要的table
1965+
private Entry[] table;
1966+
1967+
// table中的entry个数
1968+
private int size = 0;
1969+
1970+
// 扩容数值
1971+
private int threshold; // Default to 0
1972+
```
1973+
1974+
一起看一下其常用的构造函数:
1975+
1976+
```
1977+
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
1978+
table = new Entry[INITIAL_CAPACITY];
1979+
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
1980+
table[i] = new Entry(firstKey, firstValue);
1981+
size = 1;
1982+
setThreshold(INITIAL_CAPACITY);
1983+
}
1984+
```
1985+
1986+
构造函数的第一个参数就是本ThreadLocal实例(this),第二个参数就是要保存的线程本地变量。构造函数首先创建一个长度为16的Entry数组,然后计算出firstKey对应的哈希值,然后存储到table中,并设置size和threshold。
1987+
1988+
注意一个细节,计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现(和HashMap中的思路相同)。正是因为这种算法,我们要求size必须是2的指数,因为这可以使得hash发生冲突的次数减小。
1989+
1990+
###### **ThreadLocalMap#set**
1991+
1992+
ThreadLocal中put函数最终调用了ThreadLocalMap中的set函数,跟进去看一看:
1993+
1994+
```
1995+
private void set(ThreadLocal<?> key, Object value) {
1996+
Entry[] tab = table;
1997+
int len = tab.length;
1998+
int i = key.threadLocalHashCode & (len-1);
1999+
2000+
for (Entry e = tab[i];
2001+
e != null;
2002+
// 冲突了
2003+
e = tab[i = nextIndex(i, len)]) {
2004+
ThreadLocal<?> k = e.get();
2005+
2006+
if (k == key) {
2007+
e.value = value;
2008+
return;
2009+
}
2010+
2011+
if (k == null) {
2012+
replaceStaleEntry(key, value, i);
2013+
return;
2014+
}
2015+
}
2016+
2017+
tab[i] = new Entry(key, value);
2018+
int sz = ++size;
2019+
if (!cleanSomeSlots(i, sz) && sz >= threshold)
2020+
rehash();
2021+
}
2022+
```
2023+
2024+
在上述代码中如果Entry在存放过程中冲突了,调用nextIndex来处理,如下所示。是否还记得hashmap中对待冲突的处理?这里好像是另一种套路:只要i的数值小于len,就加1取值,官方术语称为:线性探测法。
2025+
2026+
```
2027+
private static int nextIndex(int i, int len) {
2028+
return ((i + 1 < len) ? i + 1 : 0);
2029+
}
2030+
```
2031+
2032+
以上步骤ok了之后,再次关注一下源码中的cleanSomeSlots,该函数主要的作用就是清理无用的entry,具体细节就不扣了:
2033+
2034+
```
2035+
private boolean cleanSomeSlots(int i, int n) {
2036+
boolean removed = false;
2037+
Entry[] tab = table;
2038+
int len = tab.length;
2039+
do {
2040+
i = nextIndex(i, len);
2041+
Entry e = tab[i];
2042+
if (e != null && e.get() == null) {
2043+
n = len;
2044+
removed = true;
2045+
i = expungeStaleEntry(i);
2046+
}
2047+
} while ( (n >>>= 1) != 0);
2048+
return removed;
2049+
}
2050+
```
2051+
2052+
###### **ThreadLocal#get**
2053+
2054+
看完了set函数,肯定是要关注Get的,源码如下所示:
2055+
2056+
```
2057+
public T get() {
2058+
// 获取Thread对象t
2059+
Thread t = Thread.currentThread();
2060+
// 获取t中的map
2061+
ThreadLocalMap map = getMap(t);
2062+
if (map != null) {
2063+
ThreadLocalMap.Entry e = map.getEntry(this);
2064+
if (e != null) {
2065+
@SuppressWarnings("unchecked")
2066+
T result = (T)e.value;
2067+
return result;
2068+
}
2069+
}
2070+
return setInitialValue();
2071+
}
2072+
```
2073+
2074+
如果map为null,就返回setInitialValue()这个方法,跟进这个方法看一下:
2075+
2076+
```
2077+
private T setInitialValue() {
2078+
T value = initialValue();
2079+
Thread t = Thread.currentThread();
2080+
ThreadLocalMap map = getMap(t);
2081+
if (map != null)
2082+
map.set(this, value);
2083+
else
2084+
createMap(t, value);
2085+
return value;
2086+
}
2087+
```
2088+
2089+
最后返回的是value,而value来自`initialValue()`,进入这个源码中查看:
2090+
2091+
```
2092+
protected T initialValue() {
2093+
return null;
2094+
}
2095+
```
2096+
2097+
原来如此,如果不设置ThreadLocal的数值,默认就是null,来自于此。
2098+
2099+
Ok,整体上关于的ThreadLocal内容就这么多了,还有一些细节没有讲述到,慢慢补充和优化。
2100+
2101+
2102+
2103+
2104+
2105+
2106+
2107+
--------------------------------------------
2108+
2109+
2110+
18712111
对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。
18722112

18732113
```java
@@ -1966,6 +2206,15 @@ ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因
19662206

19672207

19682208

2209+
2210+
2211+
参考资料:
2212+
2213+
- [深入理解 Java 之 ThreadLocal 工作原理](https://allenwu.itscoder.com/threadlocal-source)
2214+
2215+
2216+
2217+
19692218
## 12. 锁优化
19702219

19712220
这里的锁优化主要是指虚拟机对 synchronized 的优化。

notes/JavaArchitecture/05 Java 虚拟机.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1038,7 +1038,8 @@ https://www.jianshu.com/p/25e94a1399a0
10381038

10391039
## 9. 了解过JVM调优没,基本思路是什么
10401040

1041-
1041+
美团技术:从实际案例聊聊Java应用的GC优化 -
1042+
https://tech.meituan.com/jvm_optimize.html
10421043

10431044

10441045

67.3 KB
Loading

0 commit comments

Comments
 (0)