Skip to content

Commit 1a880f7

Browse files
修正
1 parent 8c56ec8 commit 1a880f7

File tree

20 files changed

+106
-57
lines changed

20 files changed

+106
-57
lines changed

04.彻底理解synchronized/java关键字---synchronized.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public class SynchronizedDemo implements Runnable {
2727
```
2828
开启了10个线程,每个线程都累加了1000000次,如果结果正确的话自然而然总数就应该是10 * 1000000 = 10000000。可就运行多次结果都不是这个数,而且每次运行结果都不一样。这是为什么了?有什么解决方案了?这就是我们今天要聊的事情。
2929

30-
在上一篇博文中我们已经了解了[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)的一些知识,并且已经知道出现线程安全的主要来源于JMM的设计,主要集中在主内存和线程的工作内存而导致的**内存可见性问题**,以及**重排序导致的问题**,进一步知道了**happens-before规则**。线程运行时拥有自己的栈空间,会在自己的栈空间运行,如果多线程间没有共享的数据也就是说多线程间并没有协作完成一件事情,那么,多线程就不能发挥优势,不能带来巨大的价值。那么共享数据的线程安全问题怎样处理?很自然而然的想法就是每一个线程依次去读写这个共享变量,这样就不会有任何数据安全的问题,因为每个线程所操作的都是当前最新的版本数据。那么,在java关键字synchronized就具有使每个线程依次排队操作共享变量的功能。很显然,这种同步机制效率很低,但synchronized是其他并发容器实现的基础,对它的理解也会大大提升对并发编程的感觉,从功利的角度来说,这也是面试高频的考点。好了,下面,就来具体说说这个关键字。
30+
在上一篇博文中我们已经了解了[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)
31+
的一些知识,并且已经知道出现线程安全的主要来源于JMM的设计,主要集中在主内存和线程的工作内存而导致的**内存可见性问题**,以及**重排序导致的问题**,进一步知道了**happens-before规则**。线程运行时拥有自己的栈空间,会在自己的栈空间运行,如果多线程间没有共享的数据也就是说多线程间并没有协作完成一件事情,那么,多线程就不能发挥优势,不能带来巨大的价值。那么共享数据的线程安全问题怎样处理?很自然而然的想法就是每一个线程依次去读写这个共享变量,这样就不会有任何数据安全的问题,因为每个线程所操作的都是当前最新的版本数据。那么,在java关键字synchronized就具有使每个线程依次排队操作共享变量的功能。很显然,这种同步机制效率很低,但synchronized是其他并发容器实现的基础,对它的理解也会大大提升对并发编程的感觉,从功利的角度来说,这也是面试高频的考点。好了,下面,就来具体说说这个关键字。
3132
# 2. synchronized实现原理 #
3233
在java代码中使用synchronized可是使用在代码块和方法中,根据Synchronized用的位置可以有这些使用场景:
3334

@@ -70,7 +71,8 @@ public class SynchronizedDemo {
7071

7172

7273
## 2.2 synchronized的happens-before关系 ##
73-
在上一篇文章中讨论过[happens-before](https://juejin.im/post/5ae6d309518825673123fd0e)规则,抱着学以致用的原则我们现在来看一看Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。继续来看代码:
74+
在上一篇文章中讨论过[happens-before](https://juejin.im/post/5ae6d309518825673123fd0e)
75+
规则,抱着学以致用的原则我们现在来看一看Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。继续来看代码:
7476
```java
7577
public class MonitorDemo {
7678
private int a = 0;
@@ -120,7 +122,8 @@ public class MonitorDemo {
120122

121123
## 3.1 CAS操作 ##
122124
### 3.1.1 什么是CAS? ###
123-
使用锁时,线程获取锁是一种**悲观锁策略**,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种**乐观锁策略**,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用**CAS(compare and swap)**又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。
125+
使用锁时,线程获取锁是一种**悲观锁策略**,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种**乐观锁策略**,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用**CAS(compare and swap)**
126+
又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。
124127
### 3.1.2 CAS的操作过程 ###
125128
CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:**V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值**。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程
126129

05.彻底理解volatile/java关键字---volatile.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# 1. volatile简介 #
22

3-
在上一篇文章中我们深入理解了java关键字[synchronized](https://juejin.im/post/5ae6dc04f265da0ba351d3ff),我们知道在java中还有一大神器就是关键volatile,可以说是和synchronized各领风骚,其中奥妙,我们来共同探讨下。
3+
在上一篇文章中我们深入理解了java关键字[synchronized](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)
4+
,我们知道在java中还有一大神器就是关键volatile,可以说是和synchronized各领风骚,其中奥妙,我们来共同探讨下。
45

5-
通过上一篇的文章我们了解到synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁。而volatile就可以说是java虚拟机提供的最轻量级的同步机制。但它同时不容易被正确理解,也至于在并发编程中很多程序员遇到线程安全的问题就会使用synchronized。[Java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中?这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。
6+
通过上一篇的文章我们了解到synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁。而volatile就可以说是java虚拟机提供的最轻量级的同步机制。但它同时不容易被正确理解,也至于在并发编程中很多程序员遇到线程安全的问题就会使用synchronized。[Java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)
7+
告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中?这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。
68

79
现在我们有了一个大概的印象就是:**被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。**
810

@@ -29,7 +31,9 @@ instance = new Instancce() //instance是volatile变量
2931

3032
经过上面的分析,我们已经知道了volatile变量可以通过**缓存一致性协议**保证每个线程都能获得最新值,即满足数据的“可见性”。我们继续延续上一篇分析问题的方式(我一直认为思考问题的方式是属于自己,也才是最重要的,也在不断培养这方面的能力),我一直将并发分析的切入点分为**两个核心,三大性质**。两大核心:JMM内存模型(主内存和工作内存)以及happens-before;三条性质:原子性,可见性,有序性(关于三大性质的总结在以后得文章会和大家共同探讨)。废话不多说,先来看两个核心之一:volatile的happens-before关系。
3133

32-
在六条[happens-before规则](https://juejin.im/post/5ae6d309518825673123fd0e)中有一条是:**volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。**下面我们结合具体的代码,我们利用这条规则推导下:
34+
在六条[happens-before规则](https://juejin.im/post/5ae6d309518825673123fd0e)
35+
中有一条是:**volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。**
36+
下面我们结合具体的代码,我们利用这条规则推导下:
3337
```java
3438
public class VolatileExample {
3539
private int a = 0;
@@ -76,6 +80,7 @@ JMM内存屏障分为四类见下图,
7680
![内存屏障分类表](http://upload-images.jianshu.io/upload_images/2615789-27cf04634cbdf284.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/680)
7781

7882

83+
7984
java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表:
8085

8186

06.你以为你真的了解final吗?/java关键字--final.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,8 @@ JDK中提供的八个包装类和String类都是不可变类,我们来看看St
126126
可以看出String的value就是final修饰的,上述其他几条性质也是吻合的。
127127

128128
# 4. 多线程中你真的了解final吗? #
129-
上面我们聊的final使用,应该属于**Java基础层面**的,当理解这些后我们就真的算是掌握了final吗?有考虑过final在多线程并发的情况吗?在[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)中我们知道java内存模型为了能让处理器和编译器底层发挥他们的最大优势,对底层的约束就很少,也就是说针对底层来说java内存模型就是一弱内存数据模型。同时,处理器和编译为了性能优化会对指令序列有**编译器和处理器重排序**。那么,在多线程情况下,final会进行怎样的重排序?会导致线程安全的问题吗?下面,就来看看final的重排序。
129+
上面我们聊的final使用,应该属于**Java基础层面**的,当理解这些后我们就真的算是掌握了final吗?有考虑过final在多线程并发的情况吗?在[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)
130+
中我们知道java内存模型为了能让处理器和编译器底层发挥他们的最大优势,对底层的约束就很少,也就是说针对底层来说java内存模型就是一弱内存数据模型。同时,处理器和编译为了性能优化会对指令序列有**编译器和处理器重排序**。那么,在多线程情况下,final会进行怎样的重排序?会导致线程安全的问题吗?下面,就来看看final的重排序。
130131

131132
## 4.1 final域重排序规则 ##
132133

@@ -162,7 +163,8 @@ public class FinalDemo {
162163
写final域的重排序规则**禁止对final域的写重排序到构造函数之外**,这个规则的实现主要包含了两个方面:
163164

164165
1. JMM禁止编译器把final域的写重排序到构造函数之外;
165-
2. 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障(关于内存屏障可以看[这篇文章](https://juejin.im/post/5ae6d309518825673123fd0e))。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。
166+
2. 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障(关于内存屏障可以看[这篇文章](https://juejin.im/post/5ae6d309518825673123fd0e)
167+
)。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。
166168

167169
我们再来分析writer方法,虽然只有一行代码,但实际上做了两件事情:
168170

07.三大性质总结:原子性、可见性以及有序性/三大性质总结:原子性、可见性以及有序性.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# 1. 三大性质简介 #
2-
在并发编程中分析线程安全的问题时往往需要切入点,那就是**两大核心**:JMM抽象内存模型以及happens-before规则(在[这篇文章](https://juejin.im/post/5ae6d309518825673123fd0e)中已经经过了),三条性质:**原子性,有序性和可见性**。关于[synchronized](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)[volatile](https://juejin.im/post/5ae9b41b518825670b33e6c4)已经讨论过了,就想着将并发编程中这两大神器在 **原子性,有序性和可见性**上做一个比较,当然这也是面试中的高频考点,值得注意。
2+
在并发编程中分析线程安全的问题时往往需要切入点,那就是**两大核心**:JMM抽象内存模型以及happens-before规则(在[这篇文章](https://juejin.im/post/5ae6d309518825673123fd0e)
3+
中已经经过了),三条性质:**原子性,有序性和可见性**。关于[synchronized](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)
4+
[volatile](https://juejin.im/post/5ae9b41b518825670b33e6c4)
5+
已经讨论过了,就想着将并发编程中这两大神器在 **原子性,有序性和可见性**上做一个比较,当然这也是面试中的高频考点,值得注意。
36

47
# 2. 原子性 #
58
原子性是指**一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉**。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。我们先来看看哪些是原子操作,哪些不是原子操作,有一个直观的印象:
@@ -9,7 +12,8 @@ a++; //2
912
int b=a; //3
1013
a = a+1; //4
1114
```
12-
上面这四个语句中只**有第1个语句是原子操作**,将10赋值给线程工作内存的变量a,而语句2(a++),实际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3.将计算后的值再赋值给变量a,而这三个操作无法构成原子操作。对语句3,4的分析同理可得这两条语句不具备原子性。当然,[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)中定义了8中操作都是原子的,不可再分的。
15+
上面这四个语句中只**有第1个语句是原子操作**,将10赋值给线程工作内存的变量a,而语句2(a++),实际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3.将计算后的值再赋值给变量a,而这三个操作无法构成原子操作。对语句3,4的分析同理可得这两条语句不具备原子性。当然,[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)
16+
中定义了8中操作都是原子的,不可再分的。
1317

1418
1. lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
1519
2. unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
@@ -99,7 +103,9 @@ instance = new Singleton();
99103
如果2和3进行了重排序的话,线程B进行判断if(instance==null)时就会为true,而实际上这个instance并没有初始化成功,显而易见对线程B来说之后的操作就会是错得。而**用volatile修饰**的话就可以禁止2和3操作重排序,从而避免这种情况。**volatile包含禁止指令重排序的语义,其具有有序性**
100104
# 4. 可见性 #
101105

102-
可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。通过之前对[synchronzed](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,**synchronized具有可见性**。同样的在[volatile分析中](https://juejin.im/post/5ae9b41b518825670b33e6c4),会通过在指令中添加**lock指令**,以实现内存可见性。因此, **volatile具有可见性**
106+
可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。通过之前对[synchronzed](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)
107+
内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,**synchronized具有可见性**。同样的在[volatile分析中](https://juejin.im/post/5ae9b41b518825670b33e6c4)
108+
,会通过在指令中添加**lock指令**,以实现内存可见性。因此, **volatile具有可见性**
103109

104110
# 5. 总结 #
105111
通过这篇文章,主要是比较了synchronized和volatile在三条性质:原子性,可见性,以及有序性的情况,归纳如下:

09.深入理解AbstractQueuedSynchronizer(AQS)/深入理解AbstractQueuedSynchronizer(AQS).md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# 1. AQS简介
22

3-
[上一篇文章](https://juejin.im/post/5aeb055b6fb9a07abf725c8c)中我们对lock和AbstractQueuedSynchronizer(AQS)有了初步的认识。在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义,AQS则实现了对**同步状态的管理,以及对阻塞线程进行排队,等待通知**等等一些底层的实现处理。AQS的核心也包括了这些方面:**同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现**,而这些实际上则是AQS提供出来的模板方法,归纳整理如下:
3+
[上一篇文章](https://juejin.im/post/5aeb055b6fb9a07abf725c8c)
4+
中我们对lock和AbstractQueuedSynchronizer(AQS)有了初步的认识。在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义,AQS则实现了对**同步状态的管理,以及对阻塞线程进行排队,等待通知**等等一些底层的实现处理。AQS的核心也包括了这些方面:**同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现**,而这些实际上则是AQS提供出来的模板方法,归纳整理如下:
45

56
**独占式锁:**
67

@@ -100,7 +101,8 @@ private transient volatile Node tail;
100101

101102
## 3.1 独占锁的获取(acquire方法)
102103

103-
我们继续通过看源码和debug的方式来看,还是以上面的demo为例,调用lock()方法是获取独占式锁,获取失败就将当前线程加入同步队列,成功则线程执行。而lock()方法实际上会调用AQS的**acquire()**方法,源码如下
104+
我们继续通过看源码和debug的方式来看,还是以上面的demo为例,调用lock()方法是获取独占式锁,获取失败就将当前线程加入同步队列,成功则线程执行。而lock()方法实际上会调用AQS的**acquire()**
105+
方法,源码如下
104106

105107
```java
106108
public final void acquire(int arg){

0 commit comments

Comments
 (0)