|
1 | 1 | # 前言 |
2 | 2 |
|
3 | | -在本文将总结多线程并发编程中的常见面试题,主要核心线程生命周期、线程通信、并发包部分。主要分成“并发编程”和“面试指南”两部分,在面试指南中将讨论并发相关面经。 |
| 3 | +在本文将总结多线程并发编程中的常见面试题,主要核心线程生命周期、线程通信、并发包部分。主要分成 “并发编程” 和 “面试指南” 两部分,在面试指南中将讨论并发相关面经。 |
4 | 4 |
|
5 | 5 |
|
6 | 6 |
|
@@ -1761,15 +1761,15 @@ executorService.execute(() -> { |
1761 | 1761 |
|
1762 | 1762 | synchronized 和 ReentrantLock。 |
1763 | 1763 |
|
| 1764 | +互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。 |
1764 | 1765 |
|
| 1766 | +互斥同步属于一种**悲观的并发策略**,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。 |
1765 | 1767 |
|
1766 | | -#### 2. 非阻塞同步 |
1767 | 1768 |
|
1768 | | -互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。 |
1769 | 1769 |
|
1770 | | -互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。 |
| 1770 | +#### 2. 非阻塞同步 |
1771 | 1771 |
|
1772 | | -随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。 |
| 1772 | +随着硬件指令集的发展,我们可以使用基于冲突检测的**乐观并发策略**:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。 |
1773 | 1773 |
|
1774 | 1774 | 乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。 |
1775 | 1775 |
|
@@ -1864,10 +1864,250 @@ public static void main(String[] args) { |
1864 | 1864 |
|
1865 | 1865 | 如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。 |
1866 | 1866 |
|
1867 | | -符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的“**一个请求对应一个服务器线程**”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。 |
| 1867 | +符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完,其中最重要的一个应用实例就是经典 Web 交互模型中的 “**一个请求对应一个服务器线程**”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。 |
1868 | 1868 |
|
1869 | 1869 | 可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。 |
1870 | 1870 |
|
| 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 | + |
1871 | 2111 | 对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。 |
1872 | 2112 |
|
1873 | 2113 | ```java |
@@ -1966,6 +2206,15 @@ ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因 |
1966 | 2206 |
|
1967 | 2207 |
|
1968 | 2208 |
|
| 2209 | + |
| 2210 | + |
| 2211 | +参考资料: |
| 2212 | + |
| 2213 | +- [深入理解 Java 之 ThreadLocal 工作原理](https://allenwu.itscoder.com/threadlocal-source) |
| 2214 | + |
| 2215 | + |
| 2216 | + |
| 2217 | + |
1969 | 2218 | ## 12. 锁优化 |
1970 | 2219 |
|
1971 | 2220 | 这里的锁优化主要是指虚拟机对 synchronized 的优化。 |
|
0 commit comments