1、并发的基本概念:同时拥有两个或者多个线程,如果程序在单核处理器上运行,多个线程将交替地换入或者换出内存,这些线程是同时存在的,每个线程都处于执行过程中的某个状态。如果允许在多核处理器上,此时程序中的每个线程都将分配到一个处理器核上,因此可以同时运行。并发,多个线程操作相同的资源,保证线程安全,合理利用资源。

2、高并发的概念:高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。

  高并发,服务能同时处理很多请求,提高程序性能。主要是指系统运行过程中,短时间内,遇到大量操作请求的情况,主要发生在系统集中收到大量请求,比如12306的抢票,天猫双十一的活动,这种情况的发生就会导致系统在这段时间内执行大量的操作,例如对资源的请求,数据库的操作等等。

3、并发编程与线程安全:线程安全就是代码所在的进程有多个线程在同时执行,而这些线程k可能会运行同一段代码,如果每次运行结果和单线程运行结果一致,而且其他变量的值也和预期是一样的,我们就认为这是线程安全的,就是并发环境下得到我们期望的正确的结果。线程不安全不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据出现脏数据,也可能在计算的时候出现错误。

4、并发模拟的几种方式。

  第一种:Postman,Http请求模拟工具。

  第二种:Apache Bench(简称AB),Apache附带的工具,测试网站性能。

  第三种:JMeter,Apache组织开发的压力测试工具。

  第四种:Semaphore、CountDownLatch等等代码进行并发模拟测试。

4.1、Postman,Http请求模拟工具,测试如下所示:

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程

点击Run以后,设置完毕参数开始执行。

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java开发_02

执行完的效果如下所示:

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程_03

4.2、Apache Bench(简称AB),Apache附带的工具,测试网站性能。AB是一个命令行的工具,输入命令就可以进行测试,对发起负载的本机要求很低,根据ab命令可以创建很多的并发访问线程,模拟多个访问者同时对同一个url地址进行访问,因此可以用来测试目标服务器的负载压力。

AB指定命令发送请求以后,可以得到每秒产生的字节数、每次处理请求的时间、每秒处理请求的数目等等统计数据。

安装Apache服务器,官网下载地址:​​https://www.apachelounge.com/download/​

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程_04Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java开发_05转存失败重新上传取消Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java开发_06

在D:\biehl\ApacheBench\Apache24\bin目录下面找到ab.exe。-n是本次测试的总数,-c是指定本次并发数。

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程_07

4.3、JMeter,Apache组织开发的压力测试工具。官网地址:​​https://jmeter.apache.org/​

在D:\biehl\JMeter\apache-jmeter-5.2.1\bin目录下面执行jmeter.bat脚本文件。

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程_08

4.4、Semaphore、CountDownLatch等等代码进行并发模拟测试。

1 package com.bie.concurrency.test;
2
3 import java.util.concurrent.CountDownLatch;
4 import java.util.concurrent.ExecutorService;
5 import java.util.concurrent.Executors;
6 import java.util.concurrent.Semaphore;
7
8 import com.bie.concurrency.annoations.NotThreadSafe;
9
10 import lombok.extern.slf4j.Slf4j;
11
12 /**
13 *
14 *
15 * @Title: CountDownLatchTest.java
16 * @Package com.bie.concurrency.test
17 * @Description: TODO
18 * @author biehl
19 * @date 2020年1月2日
20 * @version V1.0
21 *
22 * 并发模拟测试的程序。
23 *
24 * 1、CountDownLatch计数器向下减的闭锁类。该类可以阻塞线程,并保证线程在满足某种特定的条件下继续执行。
25 * CountDownLatch比较适合我们保证线程执行完之后再继续其他的处理。
26 *
27 * 2、Semaphore信号量,实现的功能是可以阻塞进程并且控制同一时间的请求的并发量。
28 * Semaphore更适合控制同时并发的线程数。
29 *
30 * 3、CountDownLatch、Semaphore配合线程池一起使用。
31 *
32 */
33 @Slf4j
34 @NotThreadSafe // 由于每次结果不一致,所以是线程不安全的类。不要使用此程序进行并发测试。
35 public class ConcurrencyTest {
36
37 public static int clientTotal = 5000;// 1000个请求,请求总数
38
39 public static int threadTotal = 200;// 允许同时并发执行的线程数目
40
41 public static int count = 0;// 计数的值
42
43 // 自增计数器
44 private static void add() {
45 count++;
46 }
47
48 public static void main(String[] args) {
49 // 定义线程池
50 ExecutorService executorService = Executors.newCachedThreadPool();
51 // 定义信号量,信号量里面需要定义允许并发的数量
52 final Semaphore semaphore = new Semaphore(threadTotal);
53 // 定义计数器闭锁,希望所有请求完以后统计计数结果,将计数结果放入
54 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
55 // 放入请求操作
56 for (int i = 0; i < clientTotal; i++) {
57 // 所有请求放入到线程池结果中
58 executorService.execute(() -> {
59 // 在线程池执行的时候引入了信号量,信号量每次做acquire()操作的时候就是判断当前进程是否允许被执行。
60 // 如果达到了一定并发数的时候,add方法可能会临时被阻塞掉。当acquire()可以返回值的时候,add方法可以被执行。
61 // add方法执行完毕以后,释放当前进程,此时信号量就已经引入完毕了。
62 // 在引入信号量的基础上引入闭锁机制。countDownLatch
63 try {
64 // 执行核心执行方法之前引入信号量,信号量每次允许执行之前需要调用方法acquire()。
65 semaphore.acquire();
66 // 核心执行方法。
67 add();
68 // 核心执行方法执行完成以后,需要释放当前进程,释放信号量。
69 semaphore.release();
70 } catch (InterruptedException e) {
71 e.printStackTrace();
72 }
73 // try-catch是一次执行系统的操作,执行完毕以后调用一下闭锁。
74 // 每次执行完毕以后countDownLatch里面对应的计算值减一。
75 // 执行countDown()方法计数器减一。
76 countDownLatch.countDown();
77 });
78 }
79 // 这个方法可以保证之前的countDownLatch必须减为0,减为0的前提就是所有的进程必须执行完毕。
80 try {
81 // 调用await()方法当前进程进入等待状态。
82 countDownLatch.await();
83 } catch (InterruptedException e) {
84 e.printStackTrace();
85 }
86 // 通常,线程池执行完毕以后,线程池不再使用,记得关闭线程池
87 executorService.shutdown();
88 // 如果我们希望在所有线程执行完毕以后打印当前计数的值。只需要log.info之前执行上一步即可countDownLatch.await();。
89 log.info("count:{}", count);
90
91 }
92
93 }

5、线程安全性。

  线程安全性的定义,当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

6、线程安全性主要体现在三个方面原子性、可见性、有序性。

  a、原子性,提供了互斥访问,同一时刻只能有一个线程来对它进行操作。

  b、可见性,一个线程对主内存的修改可以及时的被其他线程观察到。

  c、有序性,一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

7、线程安全性的原子性的底层代码理解,如下所示。

1 public static AtomicInteger count = new AtomicInteger(0);// 计数的值,count的值是在工作内存中的,而var5就是主内存的值,可以进行参考学习。
2 // 自增操作
3 count.incrementAndGet();
4
5 // 调用AtomicInteger.incrementAndGet()方法。
6 public final int incrementAndGet() {
7 // this是调用的值,如上面定义的count变量
8 // 里面的三个参数对象下面方法的var1、var2、var4
9 return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
10 }
11
12 // 调用Unsafe的getAndAddInt();方法
13 // var1是传递的值,比如自己定义的count
14 // var2是当前的值,比如当前值是2
15 // var4是1,比如当前值是1
16 public final int getAndAddInt(Object var1, long var2, int var4) {
17 // 定义变量var5
18 int var5;
19 // do循环
20 do {
21 // var5是调用底层方法获取到的值,调用底层方法得到底层当前的值。
22 // 此时,如果没有其他线程处理var1的时候,正常返回的值应该是2。
23 var5 = this.getIntVolatile(var1, var2);
24 // 此时,传递到compareAndSwapInt的参数是count对象、var2是2、var5从底层传递的2、最后一个参数var5 + var4从底层传递的值加上1。
25 // 这个方法希望打到的目的是对于var1这个count对象,如果当前的var2的值和底层的这个var5的值一致,把它count更新成var5 + var4从底层传递的值加上1。
26 // 如果执行此处更新操作的时候,把它count更新成var5 + var4从底层传递的值加上1的时候可能被其他线程修改,因此这里判断如果当前值var2和期望值var5相同的话,就允许var5 + var4这个加1操作的。
27 // 否则,重新取出var5,比如是3,然后var2重新从var1中取出,比如是3,再次进行判断。此时var2等于var5,那么此时最后一个参数var5 + var4等于4。
28 // 核心原理,当前对象var1的值var2,去和底层的var5的值进行对比,如果当前的值var2和底层的值var5相等,就执行var5+var4操作,否则就一直进行循环操作。
29 } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
30 // 返回底层的值var5
31 return var5;
32 }

7.1、线程安全性的原子性的使用,如下所示:

  atomic包里面AtomicInteger类,调用了Unsafe类实现自增操作。this.compareAndSwapInt()方法核心就是CAS的核心。CAS实现的原理是拿当前的对象和底层里面的值进行对比,如果当前对象的值和底层的值一致的时候才执行对应的加一操作。

1 package com.bie.concurrency.atomic;
2
3 import java.util.concurrent.CountDownLatch;
4 import java.util.concurrent.ExecutorService;
5 import java.util.concurrent.Executors;
6 import java.util.concurrent.Semaphore;
7 import java.util.concurrent.atomic.AtomicInteger;
8
9 import com.bie.concurrency.annoations.NotThreadSafe;
10
11 import lombok.extern.slf4j.Slf4j;
12
13 /**
14 *
15 *
16 * @Title: CountDownLatchTest.java
17 * @Package com.bie.concurrency.test
18 * @Description: TODO
19 * @author biehl
20 * @date 2020年1月2日
21 * @version V1.0
22 *
23 * 并发模拟测试的程序。
24 *
25 * 1、CountDownLatch计数器向下减的闭锁类。该类可以阻塞线程,并保证线程在满足某种特定的条件下继续执行。
26 * CountDownLatch比较适合我们保证线程执行完之后再继续其他的处理。
27 *
28 * 2、Semaphore信号量,实现的功能是可以阻塞进程并且控制同一时间的请求的并发量。 Semaphore更适合控制同时并发的线程数。
29 *
30 * 3、CountDownLatch、Semaphore配合线程池一起使用。
31 *
32 * 4、jdk提供了Atomic包,来实现原子性,Atomic包里面提供了很多AtomicXXX类,他们都是通过CAS来完成原子性的。
33 *
34 * 5、atomic包里面AtomicInteger类,调用了Unsafe类实现自增操作。
35 * unsafe.getAndAddInt(this, valueOffset, 1) + 1;
36 * this.compareAndSwapInt()方法核心就是CAS的核心。CAS实现的原理是拿当前的对象和底层里面的值进行对比,如果当前对象的值和底层的值一致的时候才执行对应的加一操作。
37 *
38 */
39 @Slf4j
40 @ThreadSafe // 由于每次结果一致,所以是线程安全的类。可以使用此程序进行并发测试。
41 public class ConcurrencyAtomicExample1 {
42
43 public static int clientTotal = 5000;// 5000个请求,请求总数
44
45 public static int threadTotal = 200;// 允许同时并发执行的线程数目
46
47 // int基本数据类型对应的atomic包里面的类是AtomicInteger类型的。
48 // 初始化值为0
49 public static AtomicInteger count = new AtomicInteger(0);// 计数的值
50
51 // 自增计数器
52 private static void add() {
53 // 自增操作调用的方法,类比++i
54 count.incrementAndGet();
55 // 或者调用下面的方法,类比i++
56 // count.getAndIncrement();
57 }
58
59 public static void main(String[] args) {
60 // 定义线程池
61 ExecutorService executorService = Executors.newCachedThreadPool();
62 // 定义信号量,信号量里面需要定义允许并发的数量
63 final Semaphore semaphore = new Semaphore(threadTotal);
64 // 定义计数器闭锁,希望所有请求完以后统计计数结果,将计数结果放入
65 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
66 // 放入请求操作
67 for (int i = 0; i < clientTotal; i++) {
68 // 所有请求放入到线程池结果中
69 executorService.execute(() -> {
70 // 在线程池执行的时候引入了信号量,信号量每次做acquire()操作的时候就是判断当前进程是否允许被执行。
71 // 如果达到了一定并发数的时候,add方法可能会临时被阻塞掉。当acquire()可以返回值的时候,add方法可以被执行。
72 // add方法执行完毕以后,释放当前进程,此时信号量就已经引入完毕了。
73 // 在引入信号量的基础上引入闭锁机制。countDownLatch
74 try {
75 // 执行核心执行方法之前引入信号量,信号量每次允许执行之前需要调用方法acquire()。
76 semaphore.acquire();
77 // 核心执行方法。
78 add();
79 // 核心执行方法执行完成以后,需要释放当前进程,释放信号量。
80 semaphore.release();
81 } catch (InterruptedException e) {
82 e.printStackTrace();
83 }
84 // try-catch是一次执行系统的操作,执行完毕以后调用一下闭锁。
85 // 每次执行完毕以后countDownLatch里面对应的计算值减一。
86 // 执行countDown()方法计数器减一。
87 countDownLatch.countDown();
88 });
89 }
90 // 这个方法可以保证之前的countDownLatch必须减为0,减为0的前提就是所有的进程必须执行完毕。
91 try {
92 // 调用await()方法当前进程进入等待状态。
93 countDownLatch.await();
94 } catch (InterruptedException e) {
95 e.printStackTrace();
96 }
97 // 通常,线程池执行完毕以后,线程池不再使用,记得关闭线程池
98 executorService.shutdown();
99 // 如果我们希望在所有线程执行完毕以后打印当前计数的值。只需要log.info之前执行上一步即可countDownLatch.await();。
100 log.info("count:{}", count.get());
101
102 }
103
104 }

7.2、线程安全性的原子性的使用,如下所示:

  atomic包里面AtomicLong类,调用了Unsafe类实现自增操作。jdk1.8新增了LongAddder类比AtomicLong类。this.compareAndSwapLong()方法核心就是CAS的核心。CAS实现的原理是拿当前的对象和底层里面的值进行对比,如果当前对象的值和底层的值一致的时候才执行对应的加一操作。

1 package com.bie.concurrency.atomic;
2
3 import java.util.concurrent.CountDownLatch;
4 import java.util.concurrent.ExecutorService;
5 import java.util.concurrent.Executors;
6 import java.util.concurrent.Semaphore;
7 import java.util.concurrent.atomic.AtomicInteger;
8 import java.util.concurrent.atomic.AtomicLong;
9
10 import com.bie.concurrency.annoations.ThreadSafe;
11
12 import lombok.extern.slf4j.Slf4j;
13
14 /**
15 *
16 *
17 * @Title: CountDownLatchTest.java
18 * @Package com.bie.concurrency.test
19 * @Description: TODO
20 * @author biehl
21 * @date 2020年1月2日
22 * @version V1.0
23 *
24 * 并发模拟测试的程序。
25 *
26 * 1、CountDownLatch计数器向下减的闭锁类。该类可以阻塞线程,并保证线程在满足某种特定的条件下继续执行。
27 * CountDownLatch比较适合我们保证线程执行完之后再继续其他的处理。
28 *
29 * 2、Semaphore信号量,实现的功能是可以阻塞进程并且控制同一时间的请求的并发量。 Semaphore更适合控制同时并发的线程数。
30 *
31 * 3、CountDownLatch、Semaphore配合线程池一起使用。
32 *
33 * 4、jdk提供了Atomic包,来实现原子性,Atomic包里面提供了很多AtomicXXX类,他们都是通过CAS来完成原子性的。
34 *
35 * 5、atomic包里面AtomicLong类,调用了Unsafe类实现自增操作。jdk1.8新增了LongAddder类比AtomicLong类。
36 *
37 * unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
38 * this.compareAndSwapLong()方法核心就是CAS的核心。CAS实现的原理是拿当前的对象和底层里面的值进行对比,如果当前对象的值和底层的值一致的时候才执行对应的加一操作。
39 *
40 */
41 @Slf4j
42 @ThreadSafe // 由于每次结果一致,所以是线程安全的类。可以使用此程序进行并发测试。
43 public class ConcurrencyAtomicExample2 {
44
45 public static int clientTotal = 5000;// 5000个请求,请求总数
46
47 public static int threadTotal = 200;// 允许同时并发执行的线程数目
48
49 // int基本数据类型对应的atomic包里面的类是AtomicInteger类型的。
50 // 初始化值为0
51 public static AtomicLong count = new AtomicLong(0);// 计数的值
52
53 // 自增计数器
54 private static void add() {
55 // 自增操作调用的方法,类比++i
56 count.incrementAndGet();
57 // 或者调用下面的方法,类比i++
58 // count.getAndIncrement();
59 }
60
61 public static void main(String[] args) {
62 // 定义线程池
63 ExecutorService executorService = Executors.newCachedThreadPool();
64 // 定义信号量,信号量里面需要定义允许并发的数量
65 final Semaphore semaphore = new Semaphore(threadTotal);
66 // 定义计数器闭锁,希望所有请求完以后统计计数结果,将计数结果放入
67 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
68 // 放入请求操作
69 for (int i = 0; i < clientTotal; i++) {
70 // 所有请求放入到线程池结果中
71 executorService.execute(() -> {
72 // 在线程池执行的时候引入了信号量,信号量每次做acquire()操作的时候就是判断当前进程是否允许被执行。
73 // 如果达到了一定并发数的时候,add方法可能会临时被阻塞掉。当acquire()可以返回值的时候,add方法可以被执行。
74 // add方法执行完毕以后,释放当前进程,此时信号量就已经引入完毕了。
75 // 在引入信号量的基础上引入闭锁机制。countDownLatch
76 try {
77 // 执行核心执行方法之前引入信号量,信号量每次允许执行之前需要调用方法acquire()。
78 semaphore.acquire();
79 // 核心执行方法。
80 add();
81 // 核心执行方法执行完成以后,需要释放当前进程,释放信号量。
82 semaphore.release();
83 } catch (InterruptedException e) {
84 e.printStackTrace();
85 }
86 // try-catch是一次执行系统的操作,执行完毕以后调用一下闭锁。
87 // 每次执行完毕以后countDownLatch里面对应的计算值减一。
88 // 执行countDown()方法计数器减一。
89 countDownLatch.countDown();
90 });
91 }
92 // 这个方法可以保证之前的countDownLatch必须减为0,减为0的前提就是所有的进程必须执行完毕。
93 try {
94 // 调用await()方法当前进程进入等待状态。
95 countDownLatch.await();
96 } catch (InterruptedException e) {
97 e.printStackTrace();
98 }
99 // 通常,线程池执行完毕以后,线程池不再使用,记得关闭线程池
100 executorService.shutdown();
101 // 如果我们希望在所有线程执行完毕以后打印当前计数的值。只需要log.info之前执行上一步即可countDownLatch.await();。
102 log.info("count:{}", count.get());
103
104 }
105
106 }

7.3、LongAdder类和AtomicLong类。jdk1.8新增了LongAddder,新增的类,肯定是有优点的。

  1)、AtomicInteger(CAS的实现原理)实现原理在死循环内里面不断进行循环修改目标值,在竞争不激烈的时候,修改成功的概率很高,但是在竞争激烈的时候,修改失败的机率很高,修改失败以后进行循环操作,直到修改成功,是十分影响性能。对于普通类型的long,double变量,jvm允许将64位的读操作或者写操作拆分成2个32位的操作。

  2)、LongAddder类的优点,核心是将热点数据分离,可以将AtomicLong内部核心数据value分离成一个数组,每个线程访问的时候,通过hash等算法,映射到其中一个数字进行计数,最终的计数结果则为这个数组的求和累加,其中热点数据value会被分离成多个单元的sell,每个sell独自维护内部的值,当前对象实际的值由所有sell累加合成,这样的话,热点就进行了有效的分离并提高了并行度,这样一来LongAddder相当于是在AtomicLong的基础上将单点的更新压力分散到各个节点上,在低并发的时候,通过对bash的直接更新可以很好的保证和Atomic的性能基本一致,而在高并发的时候,则通过分散提高了性能。

  3)、LongAddder类的缺点,统计的时候,如果有并发更新,可能会导致统计的数据出现误差。

  4)、实际使用中,在处理高并发计算的s时候,我们可以优先使用LongAdder类,而不是继续使用AtomicLong。当然了,在线程竞争很低的情况下进行计数,使用Atomic还是更简单,更直接一些,并且效果会更高一些。其他的情况下,比如序列号生成,这种情况下需要准确的数据,全局唯一的AtomicLong才是正确的选择,此时不适合使用LongAdder类。

1 package com.bie.concurrency.atomic;
2
3 import java.util.concurrent.CountDownLatch;
4 import java.util.concurrent.ExecutorService;
5 import java.util.concurrent.Executors;
6 import java.util.concurrent.Semaphore;
7 import java.util.concurrent.atomic.LongAdder;
8
9 import com.bie.concurrency.annoations.ThreadSafe;
10
11 import lombok.extern.slf4j.Slf4j;
12
13 /**
14 *
15 *
16 * @Title: CountDownLatchTest.java
17 * @Package com.bie.concurrency.test
18 * @Description: TODO
19 * @author biehl
20 * @date 2020年1月2日
21 * @version V1.0
22 *
23 * 并发模拟测试的程序。
24 *
25 * 1、CountDownLatch计数器向下减的闭锁类。该类可以阻塞线程,并保证线程在满足某种特定的条件下继续执行。
26 * CountDownLatch比较适合我们保证线程执行完之后再继续其他的处理。
27 *
28 * 2、Semaphore信号量,实现的功能是可以阻塞进程并且控制同一时间的请求的并发量。 Semaphore更适合控制同时并发的线程数。
29 *
30 * 3、CountDownLatch、Semaphore配合线程池一起使用。
31 *
32 * 4、jdk提供了Atomic包,来实现原子性,Atomic包里面提供了很多AtomicXXX类,他们都是通过CAS来完成原子性的。
33 *
34 * 5、jdk1.8新增了LongAddder,类比AtomicLong类。
35 *
36 * AtomicInteger(CAS的实现原理)实现原理在死循环内里面不断进行循环修改目标值,直到修改成功,影响性能。
37 *
38 * LongAddder类的优点,核心是将热点数据分离。
39 *
40 * LongAddder类的缺点,统计的时候,如果有并发更新,会出现误差。
41 *
42 */
43 @Slf4j
44 @ThreadSafe // 由于每次结果一致,所以是线程安全的类。可以使用此程序进行并发测试。
45 public class ConcurrencyAtomicExample3 {
46
47 public static int clientTotal = 5000;// 5000个请求,请求总数
48
49 public static int threadTotal = 200;// 允许同时并发执行的线程数目
50
51 // int基本数据类型对应的atomic包里面的类是AtomicInteger类型的。
52 // 初始化值为0
53 public static LongAdder count = new LongAdder();// 计数的值,LongAdder默认值是0。
54
55 // 自增计数器
56 private static void add() {
57 // 自增操作调用的方法,类比++i
58 count.increment();
59 }
60
61 public static void main(String[] args) {
62 // 定义线程池
63 ExecutorService executorService = Executors.newCachedThreadPool();
64 // 定义信号量,信号量里面需要定义允许并发的数量
65 final Semaphore semaphore = new Semaphore(threadTotal);
66 // 定义计数器闭锁,希望所有请求完以后统计计数结果,将计数结果放入
67 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
68 // 放入请求操作
69 for (int i = 0; i < clientTotal; i++) {
70 // 所有请求放入到线程池结果中
71 executorService.execute(() -> {
72 // 在线程池执行的时候引入了信号量,信号量每次做acquire()操作的时候就是判断当前进程是否允许被执行。
73 // 如果达到了一定并发数的时候,add方法可能会临时被阻塞掉。当acquire()可以返回值的时候,add方法可以被执行。
74 // add方法执行完毕以后,释放当前进程,此时信号量就已经引入完毕了。
75 // 在引入信号量的基础上引入闭锁机制。countDownLatch
76 try {
77 // 执行核心执行方法之前引入信号量,信号量每次允许执行之前需要调用方法acquire()。
78 semaphore.acquire();
79 // 核心执行方法。
80 add();
81 // 核心执行方法执行完成以后,需要释放当前进程,释放信号量。
82 semaphore.release();
83 } catch (InterruptedException e) {
84 e.printStackTrace();
85 }
86 // try-catch是一次执行系统的操作,执行完毕以后调用一下闭锁。
87 // 每次执行完毕以后countDownLatch里面对应的计算值减一。
88 // 执行countDown()方法计数器减一。
89 countDownLatch.countDown();
90 });
91 }
92 // 这个方法可以保证之前的countDownLatch必须减为0,减为0的前提就是所有的进程必须执行完毕。
93 try {
94 // 调用await()方法当前进程进入等待状态。
95 countDownLatch.await();
96 } catch (InterruptedException e) {
97 e.printStackTrace();
98 }
99 // 通常,线程池执行完毕以后,线程池不再使用,记得关闭线程池
100 executorService.shutdown();
101 // 如果我们希望在所有线程执行完毕以后打印当前计数的值。只需要log.info之前执行上一步即可countDownLatch.await();。
102 log.info("count:{}", count);
103
104 }
105
106 }

7.4、AtomicReference类提供了一个可以原子读写的对象引用变量。原子意味着尝试更改相同AtomicReference的多个线程(例如,使用比较和交换操作)不会使AtomicReference最终达到不一致的状态。AtomicReference甚至有一个先进的compareAndSet()方法,它可以将引用与预期值(引用)进行比较,如果它们相等,则在AtomicReference对象内设置一个新的引用。

1 package com.bie.concurrency.atomic;
2
3 import java.util.concurrent.atomic.AtomicReference;
4
5 import com.bie.concurrency.annoations.ThreadSafe;
6
7 import lombok.extern.slf4j.Slf4j;
8
9 /**
10 *
11 *
12 * @Title: CountDownLatchTest.java
13 * @Package com.bie.concurrency.test
14 * @Description: TODO
15 * @author biehl
16 * @date 2020年1月2日
17 * @version V1.0
18 *
19 * AtomicReference类提供了一个可以原子读写的对象引用变量。
20 *
21 * 原子意味着尝试更改相同AtomicReference的多个线程(例如,使用比较和交换操作)不会使AtomicReference最终达到不一致的状态。
22 *
23 * AtomicReference甚至有一个先进的compareAndSet()方法,它可以将引用与预期值(引用)进行比较,如果它们相等,则在AtomicReference对象内设置一个新的引用。
24 *
25 *
26 */
27 @Slf4j
28 @ThreadSafe // 由于每次结果一致,所以是线程安全的类。可以使用此程序进行并发测试。
29 public class ConcurrencyAtomicExample4 {
30
31 // 默认值0
32 //
33 private static AtomicReference<Integer> count = new AtomicReference<Integer>(0);
34
35 public static void main(String[] args) {
36 count.compareAndSet(0, 2); // count = 2
37 count.compareAndSet(0, 1); // 不执行,因为此时参数一不是0哦,其他类比一样的。
38 count.compareAndSet(1, 3); // 不执行
39 count.compareAndSet(2, 4); // count = 4
40 count.compareAndSet(3, 5); // 不执行
41 log.info("count: {}", count.get());
42 }
43
44 }

7.5、AtomicIntegerFieldUpdater核心是原子性的去更新某一个类的实例,指定的某一个字段。字段必须通过volatile修饰的。

1 package com.bie.concurrency.atomic;
2
3 import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
4 import java.util.concurrent.atomic.AtomicReference;
5
6 import com.bie.concurrency.annoations.ThreadSafe;
7
8 import lombok.Getter;
9 import lombok.extern.slf4j.Slf4j;
10
11 /**
12 *
13 *
14 * @Title: CountDownLatchTest.java
15 * @Package com.bie.concurrency.test
16 * @Description: TODO
17 * @author biehl
18 * @date 2020年1月2日
19 * @version V1.0
20 *
21 * AtomicIntegerFieldUpdater核心是原子性的去更新某一个类的实例,指定的某一个字段。字段必须通过volatile修饰的。
22 *
23 */
24 @Slf4j
25 @ThreadSafe // 由于每次结果一致,所以是线程安全的类。可以使用此程序进行并发测试。
26 public class ConcurrencyAtomicExample5 {
27
28 // 需要自己定义一个字段名称的变量,必须使用volatile关键字进行修饰。
29 @Getter
30 private volatile int count = 100;
31
32 // ConcurrencyAtomicExample5是更新的对象
33 // 参数1是ConcurrencyAtomicExample5类对象的class
34 // 参数2是对于的字段名称
35 private static AtomicIntegerFieldUpdater<ConcurrencyAtomicExample5> updater = AtomicIntegerFieldUpdater
36 .newUpdater(ConcurrencyAtomicExample5.class, "count");
37
38 // 定义一个实例,里面包含了上面定义的字段count,其值是100.
39 // private static ConcurrencyAtomicExample5 concurrencyAtomicExample5 = new
40 // ConcurrencyAtomicExample5();
41
42 public static void main(String[] args) {
43 ConcurrencyAtomicExample5 concurrencyAtomicExample5 = new ConcurrencyAtomicExample5();
44
45 // 如果concurrencyAtomicExample5实例里面的值是100,就更新为120
46 if (updater.compareAndSet(concurrencyAtomicExample5, 100, 120)) {
47 log.info("update success 1 : {} ", concurrencyAtomicExample5.getCount());
48 }
49
50 if (updater.compareAndSet(concurrencyAtomicExample5, 100, 120)) {
51 log.info("update success 2 : {} ", concurrencyAtomicExample5.getCount());
52 } else {
53 log.info("update failed : {} ", concurrencyAtomicExample5.getCount());
54 }
55 }
56
57 }

7.6、AtomicBoolean演示了某段代码只会执行一次,不会出现重复的情况。

1 package com.bie.concurrency.atomic;
2
3 import java.util.concurrent.CountDownLatch;
4 import java.util.concurrent.ExecutorService;
5 import java.util.concurrent.Executors;
6 import java.util.concurrent.Semaphore;
7 import java.util.concurrent.atomic.AtomicBoolean;
8
9 import com.bie.concurrency.annoations.ThreadSafe;
10
11 import lombok.extern.slf4j.Slf4j;
12
13 /**
14 *
15 *
16 * @Title: CountDownLatchTest.java
17 * @Package com.bie.concurrency.test
18 * @Description: TODO
19 * @author biehl
20 * @date 2020年1月2日
21 * @version V1.0
22 *
23 * 1、AtomicStampReference,解决CAS的ABA问题。compareAndSet该方法。s
24 *
25 * 1.1、ABA问题就是CAS在操作的时候,其他线程将变量的值A修改成了B,又改会了A。
26 * 本线程使用期望值A与当前变量进行比较的时候,发现A变量没有改变。
27 * 于是CAS就将A值进行了交换操作。其实此时该值已经被其他线程改变过了,这与设计思想是不符合的。
28 *
29 * 1.2、ABA问题解决思路是每次变量更新的时候,把变量的版本号加一,那么之前将变量的值A修改成了B,又改会了A,版本号修改了三次。
30 * 此时,只要某一个变量被线程修改了,该变量对应的版本号就会发生递增变化,从而解决了ABA问题。
31 *
32 * 2、AtomicLongArray,维护的是一个数组。这个数组可以选择性的更新某一个索引对应的值,也是进行原子性操作的,相比于AtomicLong,AtomicLongArray会多一个索引值去更新。
33 *
34 * 3、AtomicBoolean演示了某段代码只会执行一次,不会出现重复的情况。
35 *
36 */
37 @Slf4j
38 @ThreadSafe // 由于每次结果一致,所以是线程安全的类。可以使用此程序进行并发测试。
39 public class ConcurrencyAtomicExample6 {
40
41 public static int clientTotal = 5000;// 5000个请求,请求总数
42
43 public static int threadTotal = 200;// 允许同时并发执行的线程数目
44
45 public static AtomicBoolean isHappened = new AtomicBoolean();//
46
47 // 原子性操作,false变成true只会执行一次。剩下的4999次都没有执行。
48 private static void test() {
49 // 如果当前值是false,将其变成true。
50 if (isHappened.compareAndSet(false, true)) {
51 log.info("execute");
52 }
53 }
54
55 public static void main(String[] args) {
56 // 定义线程池
57 ExecutorService executorService = Executors.newCachedThreadPool();
58 // 定义信号量,信号量里面需要定义允许并发的数量
59 final Semaphore semaphore = new Semaphore(threadTotal);
60 // 定义计数器闭锁,希望所有请求完以后统计计数结果,将计数结果放入
61 final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
62 // 放入请求操作
63 for (int i = 0; i < clientTotal; i++) {
64 // 所有请求放入到线程池结果中
65 executorService.execute(() -> {
66 // 在线程池执行的时候引入了信号量,信号量每次做acquire()操作的时候就是判断当前进程是否允许被执行。
67 // 如果达到了一定并发数的时候,add方法可能会临时被阻塞掉。当acquire()可以返回值的时候,add方法可以被执行。
68 // add方法执行完毕以后,释放当前进程,此时信号量就已经引入完毕了。
69 // 在引入信号量的基础上引入闭锁机制。countDownLatch
70 try {
71 // 执行核心执行方法之前引入信号量,信号量每次允许执行之前需要调用方法acquire()。
72 semaphore.acquire();
73 // 核心执行方法。
74 test();
75 // 核心执行方法执行完成以后,需要释放当前进程,释放信号量。
76 semaphore.release();
77 } catch (InterruptedException e) {
78 e.printStackTrace();
79 }
80 // try-catch是一次执行系统的操作,执行完毕以后调用一下闭锁。
81 // 每次执行完毕以后countDownLatch里面对应的计算值减一。
82 // 执行countDown()方法计数器减一。
83 countDownLatch.countDown();
84 });
85 }
86 // 这个方法可以保证之前的countDownLatch必须减为0,减为0的前提就是所有的进程必须执行完毕。
87 try {
88 // 调用await()方法当前进程进入等待状态。
89 countDownLatch.await();
90 } catch (InterruptedException e) {
91 e.printStackTrace();
92 }
93 // 通常,线程池执行完毕以后,线程池不再使用,记得关闭线程池
94 executorService.shutdown();
95 // 如果我们希望在所有线程执行完毕以后打印当前计数的值。只需要log.info之前执行上一步即可countDownLatch.await();。
96 log.info("isHappened:{}", isHappened.get());
97 }
98
99 }

8、原子性提供了互斥访问,同一时刻,只能有一个线程来对它进行操作。同一时刻只能有一个线程来对它进行操作,除了atomic包里面的类,还有锁,jdk提供锁主要分两种。特别注意,volatile是不具备原子性的。

  1)、一种是synchronized(依赖JVM)。是java的关键字,主要依赖jvm来实现锁机制。因此在这个关键字作用对象的作用范围内都是同一时刻只能有一个线程可以进行操作的,切记是作用对象的作用范围内。

  2)、另外一种锁是jdk提供的代码层面的锁Lock(Lock是接口)。依赖特殊的CPU指令,代码实现,ReentrantLock。

  3)、synchronized是java中的一个关键字,是一种同步锁,修饰的对象主要有四种。

    第一种,修饰代码块,被修饰的代码称为同步语句块,作用范围是大括号括起来的代码,作用对象是调用的对象。

    第二种,修饰方法,被修饰的方法称为同步方法,作用范围是整个方法,作用对象是调用这个方法的对象。

    第三种,修饰静态方法,作为范围是整个静态方法,作用的对象是这个类的所有对象。

    第四种,修饰类,作用范围是synchronized后面括号括起来的部分,作用对象是这个类的所有对象。

8.1、第一种,修饰代码块,被修饰的代码称为同步语句块,作用范围是大括号括起来的代码,作用对象是调用的对象。第二种,修饰方法,被修饰的方法称为同步方法,作用范围是整个方法,作用对象是调用这个方法的对象。

1 package com.bie.concurrency.example.sync;
2
3 import java.util.concurrent.ExecutorService;
4 import java.util.concurrent.Executors;
5
6 import lombok.extern.slf4j.Slf4j;
7
8 /**
9 *
10 *
11 * @Title: SynchronizedExample1.java
12 * @Package com.bie.concurrency.example.sync
13 * @Description: TODO
14 * @author biehl
15 * @date 2020年1月3日
16 * @version V1.0
17 *
18 * 1、如果一个方法内部是完整的同步代码块,那么它和用synchronized修饰的方法是等同的。
19 * 因为整个实际中需要执行的代码都是被synchronized修饰的。
20 *
21 * 2、如果SynchronizedExample1是父类,子类继承了该类,如果调用codeMethod方法,是带不上synchronized的。
22 * 因为synchronized不属于方法声明的一部分,是不能继承的。如果子类也需要使用synchronized,需要自己显示声明的。
23 */
24 @Slf4j
25 public class SynchronizedExample1 {
26
27 // synchronized修饰代码块
28 // 第一种,修饰代码块,被修饰的代码称为同步语句块,作用范围是大括号括起来的代码,作用对象是调用的对象。
29 // 对于同步代码块,作用于的是当前对象,对于不同调用对象是互相不影响的。
30 public void codeBlock(int j) {
31 // 作用范围是大括号括起来的代码
32 synchronized (this) {
33 for (int i = 0; i < 10; i++) {
34 log.info("codeBlock {} - {} ", j, i);
35 }
36 }
37 }
38
39 // synchronized修饰一个方法。
40 // 第二种,修饰方法,被修饰的方法称为同步方法,作用范围是整个方法,作用对象是调用这个方法的对象。
41 // 修饰方法,被修饰的方法称为同步方法。
42 // 作用范围是整个方法。
43 // 对于synchronized修饰方法,作用于调用对象的,对于不同调用对象是互相不影响的。
44 public synchronized void codeMethod(int j) {
45 for (int i = 0; i < 10; i++) {
46 log.info("codeMethod {} - {} ", j, i);
47 }
48 }
49
50 public static void main(String[] args) {
51 SynchronizedExample1 example1 = new SynchronizedExample1();
52 SynchronizedExample1 example2 = new SynchronizedExample1();
53 // 声明一个线程池
54 ExecutorService executorService = Executors.newCachedThreadPool();
55 // 开启进程去执行这个方法。
56 executorService.execute(() -> {
57 // 第一种,修饰代码块,被修饰的代码称为同步语句块,作用范围是大括号括起来的代码,作用对象是调用的对象。
58 // example1.codeBlock(1);
59
60 example1.codeMethod(1);
61 });
62
63 // 开启进程去执行这个方法。
64 executorService.execute(() -> {
65 // 第二种,修饰方法,被修饰的方法称为同步方法,作用范围是整个方法,作用对象是调用这个方法的对象。
66 // example1.codeBlock(2);
67
68 // example1.codeMethod(2);
69
70 // example2.codeBlock(2);
71
72 example2.codeMethod(2);
73 });
74
75 }
76
77 }

8.2、第三种,修饰静态方法,作为范围是整个静态方法,作用的对象是这个类的所有对象。第四种,修饰类,作用范围是synchronized后面括号括起来的部分,作用对象是这个类的所有对象。

1 package com.bie.concurrency.example.sync;
2
3 import java.util.concurrent.ExecutorService;
4 import java.util.concurrent.Executors;
5
6 import lombok.extern.slf4j.Slf4j;
7
8 /**
9 *
10 *
11 * @Title: SynchronizedExample1.java
12 * @Package com.bie.concurrency.example.sync
13 * @Description: TODO
14 * @author biehl
15 * @date 2020年1月3日
16 * @version V1.0
17 *
18 * 1、一个方法里面如果所有需要执行的代码部分都是被synchronized修饰的一个类来包围的时候,
19 * 那么它和synchronized修饰的静态方法的表现是一致的。
20 */
21 @Slf4j
22 public class SynchronizedExample2 {
23
24 // 第四种,修饰类,作用范围是synchronized后面括号括起来的部分,作用对象是这个类的所有对象。
25 public void codeClass(int j) {
26 synchronized (SynchronizedExample2.class) {
27 for (int i = 0; i < 10; i++) {
28 log.info("codeBlock {} - {} ", j, i);
29 }
30 }
31 }
32
33 // 第三种,修饰静态方法,作为范围是整个静态方法,作用的对象是这个类的所有对象。
34 // 使用不同的类来调用静态方法,调用被synchronized修饰的静态方法的时候,同一个时间只允许一个线程可以被调用执行。
35 // 使用synchronized修饰静态方法,所有类之间都是原子性操作,同一个时间只允许一个线程可以被调用执行。
36 public static synchronized void codeStaticMethod(int j) {
37 for (int i = 0; i < 10; i++) {
38 log.info("codeMethod {} - {} ", j, i);
39 }
40 }
41
42 @SuppressWarnings("static-access")
43 public static void main(String[] args) {
44 SynchronizedExample2 example1 = new SynchronizedExample2();
45 SynchronizedExample2 example2 = new SynchronizedExample2();
46 // 声明一个线程池
47 ExecutorService executorService = Executors.newCachedThreadPool();
48 // 开启进程去执行这个方法。
49 executorService.execute(() -> {
50 // example1.codeStaticMethod(1);
51
52 example1.codeClass(1);
53 });
54
55 // 开启进程去执行这个方法。
56 executorService.execute(() -> {
57 // example1.codeStaticMethod(2);
58
59 example2.codeClass(1);
60 });
61
62 }
63
64 }

9、可见性是一个线程对主内存的修改,可以及时的被其他线程观察到,说起可见性,什么时候会导致不可见呢?导致共享变量在线程间不可见的原因,主要有下面三个方面。对于可见性,JVM提供了synchronized、volatile。

  1)、方面一、线程交叉执行。

  2)、方面二、重排序结合线程交叉执行。

  3)、方面三、共享变量更新后的值没有在工作内存与主内存间及时更新。

10、对于可见性,JVM(java内存模型)提供了synchronized、volatile。可见性,JVM提供的synchronized。JMM关于synchronized的两条规定,如下所示:

  1)、线程解锁前,必须把共享变量的最新值刷新到主内存。

  2)、线程加锁时候,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意,加锁和解锁是同一把锁)。

  注意:在原子性里面,synchronized的四种修饰方法,修饰方法前两条是针对于调用对象的,对于不同对象,锁的范围是不一样的,此时,如果不是同一把锁,互相之前是不影响的。正是因为有了synchronized的可见性,解决了我们之前见到的原子性,因此我们在做线程安全同步的时候,我们只要使用synchronized进行修饰之后,我们的变量可以放心的进行使用。

11、可见性,volatile,通过加入内存屏障和禁止重排序优化来实现可见性的。

  1)、对volatile变量写操作的时候,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存中。

  2)、对volatile变量读操作的时候,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。

  注意:这两点,通俗的说,volatile变量在每次线程访问的时候,都强迫从主内存中读取该变量的值,而当该变量发生变化的时候,又会强迫线程将最新的值刷新到主内存中,这样的话,任何时候不同的线程总能看到该变量的最新值。

  特别注意,volatile是不具备原子性的。所以volatile是不适合计算场景的。那么volatile适合什么场景呢,使用volatile必须具备两个条件,第一个是对变量的写操作不依赖于当前值,第二个是该变量没有包含在具有其他变量不变的式子中。所以volatile很适合状态标记量。另外一个使用场景就是doubleCheck即检查两次场景。

12、volatile读操作,写操作插入内存屏障和禁止重排序的示意图。

  1)、volatile写操作,插入Store屏障的示意图。对遇到volatile写操作时,首先会在volatile写之前插入一个StoreStore屏障(其作用是禁止上面的普通写和下面的volatile写重排序),之后会在volatile写插入一个StoreLoad屏障(其作用是防止上面的volatile写和下面可能有的volatile读/写重排序)。

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java开发_09

13、volatile读操作,插入Load屏障的示意图。对遇到volatile读操作时,会插入Load屏障,首先是插入一个LoadLoad屏障(其作用是禁止下面所有普通操作和上面的volatile读重排序),接下来插入LoadStore屏障(其作用是禁止下面所有的写操作和上面的volatile读重排序)。所有这些都是在CPU指令级别进行操作的,因此当使用volatile的时候已经具备了当前所说的这些规范。

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java开发_10

14、线程安全性里面的有序性,Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。在Java中可以使用volatile保证一定的有序性,另外也可以使用synchronized和lock保证一定的有序性,很显然synchronized和lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。另外呢,Java内存模型具备先天的有序性,即不需要任何手段保证有序性,这个通常被称为happen-before原则,如果两个操作的执行顺序无法从happen-before原则推导出来,他们就不能保证他们有序性了,虚拟机可以随意对他们进行重排序了。

15、有序性,happens-before原则即先行发生原则,八条原则,如下所示:

  1)、第一条:程序次序规则,一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。一段程序代码的执行,在单个线程中,看起来是有序的,虽然这条规则中提到书写在前面的操作先行发生于书写在后面的操作,这个应该是程序看起来,执行的顺序是按照代码的顺序执行的,因为虚拟机可能会对程序代码进行指令重排序,虽然进行了重排序,但是最终执行的结果是与程序顺序执行的结果是一致的,只会对不存在数据依赖的指令进行重排序,因此在单线程中程序执行看起来是有序的。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但是无法保证程序在多线程中执行的正确性。

  2)、第二条:锁定规则,一个UnLock操作先行发生于后面对同一个锁的lock操作。也就是说无论在单线程中还是多线程中,同一个锁如果处于被锁定的状态,那么必须先对锁进行释放操作,后面才能继续进行lock操作。

  3)、第三条:volatile变量规则,对一个变量的写操作先行发生于后面对这个变量的读操作。如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

  4)、第四条:传递规则,如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。

  5)、第五条:线程启动规则,Thread对象的start()方法先行发生于此线程的每一个动作。一个Thread对象必须先执行start()方法才能做其他的操作。

  6)、第六条:线程中断规则,对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。必须执行了interrupt()方法才可以被检测到中断事件的发生。

  7)、第七条:线程终结规则,线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。

  8)、第八条:对象终结规则,一个对象的初始化完成先行发生于他的finalize()方法的开始。

  注意:如果两个操作的执行次序,无法从happens-before原则推导出来,就不能保证他们的有序性,虚拟机就可以随意的对他们进行重排序。

16、线程安全性的总结。

  1)、原子性,主要是提供了互斥访问,同一时刻只能有一个线程ji进行操作。原子性里面需要注意Atomic包、CAS算法、synchronized、Locl锁。

  2)、可见性,是指一个线程对主内存的修改,可以及时的被其他线程观察到。在可见性里面,需要注意synchronized、volatile关键字。

  3)、有序性,主要介绍了happens-before原则,一个线程观察q其他线程中指令执行顺序,由于指令重排序的存在,这个观察结果一般都会杂乱无序的。如果两个操作的执行次序,无法从happens-before原则推导出来,就不能保证他们的有序性,虚拟机就可以随意的对他们进行重排序。

 


 

17、CPU的多级缓存。左侧的图展示的最简单的高速缓存的配置,数据的读取和存储都经过高速缓存的,CPU核心与高速缓存之间是有一条特殊的快速通道,在这个简化的图里面,主存与告诉缓存都连接在系统总线上,这条总线同时也用于其他组件的通信。右侧的图展示的是,在高速缓存出现后不久,系统变得更加复杂,高速缓存与主存之间的速度差异被拉大,直到加入了另一级的缓存,新加入的这一缓存比第一缓存更大,但是更慢,由于加大一级缓存的做饭从经济上考虑是行不通的,所以有了二级缓存,甚至有的系统出现了三级缓存。

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程_11

18、为什么需要CPU cache缓存呢?

  答:CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费资源,所以cache的出现,是为了缓解CPU和主存之间速度的不匹配问题(注意,结构如是,cpu -> cache缓存 -> memory主存)。

19、CPU cache缓存有什么意思呢,缓存的容量远远小于主存的,因此出现缓存不被命中的概率在所难免,既然缓存不能包含CPU所需要的所有数据,那么缓存的存在到底有什么意义呢?

  1)、时间局部性,如果某个数据被访问,那么在不久的将来它很可能被再次访问。

  2)、空间局部性,如果某个数据被访问,那么与它相邻的数据很快也可能被访问的。

20、CPU多级缓存的缓存一致性(MESI,MESI协议是基于Invalidate的高速缓存一致性协议,并且是支持回写高速缓存的最常用协议之一)。参考​​https://www.cnblogs.com/yanlong300/p/8986041.html​​。

  多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。MESI协议用于保证多个CPU cache之间缓存共享数据的一致。MESI是指4中状态的首字母,定义了每个Cache line(缓存行,缓存存储数据的单元)的4个状态,可用2个bit表示。CPU对cache的四种操作可能会出现不一致的状态,因此缓存控制器监听到本地操作和远程操作的时候,需要对Cache line做出一定的修改,从而保证数据在多个缓存之间流转的一致性。

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程_12

21、MESI状态转换图,如下所示:

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程_13

local read、local write、remote read、remote write四种操作,如下所示:

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java开发_14

MESI协议的Cache line数据状态有四种,引起数据状态转换的cpu cache操作也是有四种的。如果要深刻理解MESI协议,要深刻理解16种转换的情况,状态之间的相互转换关系,如下所示:

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程_15

  在一个典型的多核系统中,每一个核都会有自己的缓存,来共享主存总线,每个响应的cpu会发出读写请求,而缓存的目的是减少CPU读写共享主存的次数,一个缓存除了在invalid状态之外,都可以满足CPU的读请求,一个写请求,只有该缓存行在M状态或者E状态的时候,才可以被执行,如果当前状态是处于S状态的时候,必须先将缓存中的缓存行变成无效的状态,这个操作通常作用于广播的方式来完成,这个时候既不允许不同的CPU来修改同一个缓存行,即使修改该缓存行不同的位置数据也是不允许的,这里主要解决缓存一致性的问题。一个处于M状态的缓存行必须时刻监听所有试图读该缓存行相对主存的操作,这种操作必须在缓存将该缓存行写回到主存,并将状态变成S状态之前被延迟执行。一个处于S状态的缓存行也必须监听其他缓存使该缓存行无效,或者独享该缓存行的请求并将缓存行变成I无效状态。一个处于E状态的缓存行要监听其他缓存读缓存中该缓存行的操作,一旦有该缓存行的操作,那么他需要变成S状态。所以对于M和E状态,它们的数据总是精确的,它们在和缓存行真正状态是一致的,而S状态可能是非一致的,如果缓存将处于S状态的缓存行作废了,另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将缓存行升迁为E状态,这是因为其他缓存不会广播他们作废掉该缓存行的通知,同样,由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy的值变成invalid状态,而修改E状态的缓存不需要使用总线事务。 

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程_16

22、CPU多级缓存,乱序执行优化。

  答:CPU多级缓存,乱序执行优化。处理器为了提高运算速度而做出违背代码原有顺序的优化。但是计算过程,在正常情况下,不会对结果造成影响的。在单核时代,处理器保证做出的优化不会导致执行的结果远离预期目标,但是在多核环境下,并非如此,多核时代,同时有多个核执行指令,每个核的指令都可能被乱序,另外,处理器还引入了L1,L2等缓存机制,每个核都有自己的缓存,这就导致了逻辑次序后写入内存的未必真的最后写入,最终导致了一个问题,如果我们不做任何防护措施,处理器最终得到的结果和逻辑得到的结果大不相同。

23、Java虚拟机提供了Java内存模型(Java Memory Model,简称JMM)。

  答:了解了CPU的缓存一致性、乱序执行优化,在多核多并发下需要额外做很多操作的,才能保证程序执行符合我们的预期。

    为了屏蔽各种硬件和操作系统内存的访问差异,以实现Java程序在各种平台下都能达到一致的并发效果,Java虚拟机提供了Java内存模型(Java Memory Model,简称JMM)。JMM是一种规范,规范了Java虚拟机与计算机内存如何协同工作的,规定了一个线程如何和何时可以看到其他线程修改过后的共享变量的值,以及必须时如何同步的访问共享变量。

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程_17

JVM内存分片的两个概念,Heap堆、Stack栈。

  1)、Heap堆,java里面的堆是运行时的数据区,堆是由垃圾回收负责的,堆的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,java的垃圾收集器会自动搜索不再使用数据,但是也有缺点,缺点就是由于需要在运行时动态分配内存,因此它的存取速度相对慢一些。

  2)、Stack栈,栈的优势是存取速度比堆要快,仅次于计算机里面的寄存器,栈里面的数据是可以共享的,但是它的缺点存在栈中的数据大小与生存期必须是确定的,缺乏一些灵活性,栈中主要存在一些基本类型的变量。Java内存模型要求调用栈和本地变量存放在线程栈上,对象存放在堆上。

  3)、一个本地变量可能是指向一个对象的引用,这种情况下,引用这个本地变量是存放在线程栈上的,但是对象本身是存放在堆上的。

  4)、一个对象可能包含方法methodOne()、methodTwo(),这些方法可能包含本地变量,local variable1、local variable2,这些本地变量仍然是存放在线程栈上的,即使这些方法所属的对象存储在堆上。

  5)、一个对象的成员变量可能会随着这个对象自身存放在堆上,不管这个成员变量是原始类型还是引用类型。

  6)、静态成员变量跟随着类的定义一起存放在堆上,存放在堆上的对象可以被所持久对这个对象引用的线程访问。

  7)、如果Thead存放了Object的引用,是可以访问Object的,当一个线程可以访问一个对象的时候,此线程也可以访问这个对象的成员变量,当了两个线程同时访问一个对象的同一个方法,两个线程会都访问该对象的成员变量,但是每个线程都拥有该对象成员变量的私有拷贝。如Thead Stack同时调用了Object3对象的methodOne()方法。

24、计算机硬件架构简单的图示。如下所示:

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java开发_18

1)、CPU简介,现在的计算机通常有多个CPU,其中一些CPU还有多核,在有2个或者多个CPU的计算机上,同时运行多个线程是非常有可能的,而且每个CPU在某一个时刻运行一个线程是肯定没有问题的,这就意味着,你的Java程序是多线程的,在你的java程序中,每个CPU上一个线程可能是并发执行的。

2)、CPU寄存器,CPU Registers,每个CPU都包含一系列的寄存器,他们是CPU内存的基础,CPU在寄存器上执行操作的速度远远大于在主存上执行的速度,这是因为CPU访问寄存器的速度远大于主存。

3)、CPU高速缓存,CPU Cache Memory,由于计算机的存储设备与处理器的预算速度之间有几个数量级的差距,现在的计算机都加入了读写速度尽可能接近处理器运算速度的高级缓存,来作为内存与处理器之间的缓冲,将运算需要使用到的数据复制到缓存中,让运算可以快速的进行,当运算结束后,再从缓存同步到内存之中,这样处理器就不用等待缓存的内存读写了,CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还是要慢一点的,每个CPU都有一个CPU的缓存层,一个CPU还有多层缓存,在某一时刻,一个或者多个缓存行,可能被读到缓存,可能在被刷新回主存,同一时间点可能有多个操作在这里面。

4)、内存,RAM-MAIN Memory,一个计算机还包含一个主存,所有的CPU都可以访问主存,主存通常比CPU中的缓存大的多。

5)、运作原理,通常情况下,当一个CPU需要读取主存的时候呢,它会将主存的部分读取到CPU缓存中,可能会将缓存中的部分内存读取到CPU内部的寄存器里面,然后再寄存器里面执行操作,当CPU需要将结果回写到主存的时候,它会将内部寄存器里面的值刷新到缓存中,然后在某个时间点将值刷新到主存中。

25、Java内存模型与硬件内存架构之间的一些关联。

  Java内存模型与硬件内存架构是存在一些差异的,硬件内存架构是没有区分线程栈、堆、堆。对于硬件内存架构所有线程栈、堆都分布在主内存中,部分线程栈和堆可能会出现在CPU缓存中,和CPU内部的寄存器里面。

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java开发_19

26、Java内存模型抽象结构图。

  线程和主内存之间的抽象关系。线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存是Java内存模型的一个抽象概念,并不是真实存在的,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器的优化。本地内存中存储了该线程以读或者写共享变量拷贝的一个副本,比如如果线程A要使用主内存中共享变量,先拷贝主内存中一个共享变量副本,放到自己的本地内存中,从耕地的层次来说,主内存就是硬件的内存,是为了获取更好的运行速度,虚拟机和硬件内存可能会让工作内存优先存储于寄存器和高速缓存中。Java内存模型中的线程中的工作内存是CPU的寄存器和高速缓存的一个抽象的描述,而JVM的静态存储模型(即JVM内存模型)只是一种对内存的物理划分而已,只局限于内存,而且只局限于JVM的内存。现在线程之间通信必须要经过主内存,如果线程A和线程B之间要进行通信,那么必须经过两个步骤,第一步,线程A将本地的内存A中的更新过的共享变量刷新到主内存中去,线程B去主内存中读取线程A已经更新过的共享变量。

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java开发_20

27、Java内存模型,同步的八种操作、以及Java内存模型的同步规则。

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程_21

八种操作的概念解释,如下所示:

  1)、lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占变量。lock对应着unlock。

  2)、unlock(解锁):作用于主内存中的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

  3)、read(读取):作用于主内存的变量,它把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。

  4)、load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量的副本中。

  5)、use(使用):作用于工作内存的变量,把工作内存中的一个变量的值传给执行引擎。每当虚拟机遇到一个使用到变量的指令时都会使用该指令。

  6)、assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  7)、store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。

  8)、write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

Java内存模型的同步规则,如下所示:

  1)、如果要把一个变量从主内存中复制到工作内存,就需要按照寻地执行read和Load操作,如果把变量从工作内存中同步回内存中,就要按照顺序地执行store和write操作。但是Java内存模型只要求上述操作必须按照顺序执行,而没有保证必须是连续执行。

  2)、不允许read和load、store和write操作之一单独出现。因为它们其实是一个连贯的动作,读取和写回。以上两个操作必须按照顺序执行,只有read完了才可以load,只有store完了才可以write,但是没有保证必须是连续执行,read和load、store和write之间是可以插入其他指令的。

  3)、不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须把变化同步到主内存中。

  4)、不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。必须有assign操作,才可以从工作内存同步回主内存中。

  5)、一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。

  6)、一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。

  7)、如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load和assign操作初始化变量的值。

  8)、如果一个变量实现没有被Lock操作锁定,则不允许对它执行unlock操作。也不允许去unlock一个被其他线程锁定的变量。

  9)、对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

28、并发的优势与风险。Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java教程_22

 

Java并发编程与高并发之线程安全性(原子性、可见性、有序性)_Java开发_23

作者:别先生​

如果您想及时得到个人撰写文章以及著作的消息推送,可以扫描上方二维码,关注个人公众号哦。