一、并发的基本概念:同时拥有两个或者多个线程,若是程序在单核处理器上运行,多个线程将交替地换入或者换出内存,这些线程是同时存在的,每一个线程都处于执行过程当中的某个状态。若是容许在多核处理器上,此时程序中的每一个线程都将分配到一个处理器核上,所以能够同时运行。并发,多个线程操做相同的资源,保证线程安全,合理利用资源。html
二、高并发的概念:高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它一般是指,经过设计保证系统可以同时并行处理不少请求。
高并发,服务能同时处理不少请求,提升程序性能。主要是指系统运行过程当中,短期内,遇到大量操做请求的状况,主要发生在系统集中收到大量请求,好比12306的抢票,天猫双十一的活动,这种状况的发生就会致使系统在这段时间内执行大量的操做,例如对资源的请求,数据库的操做等等。java
三、并发编程与线程安全:线程安全就是代码所在的进程有多个线程在同时执行,而这些线程k可能会运行同一段代码,若是每次运行结果和单线程运行结果一致,并且其余变量的值也和预期是同样的,咱们就认为这是线程安全的,就是并发环境下获得咱们指望的正确的结果。线程不安全不提供数据访问保护,有可能出现多个线程前后更改数据形成所获得的数据出现脏数据,也可能在计算的时候出现错误。算法
四、并发模拟的几种方式。数据库
第一种:Postman,Http请求模拟工具。
第二种:Apache Bench(简称AB),Apache附带的工具,测试网站性能。
第三种:JMeter,Apache组织开发的压力测试工具。
第四种:Semaphore、CountDownLatch等等代码进行并发模拟测试。
apache
4.一、Postman,Http请求模拟工具,测试以下所示:编程
点击Run之后,设置完毕参数开始执行。数组
执行完的效果以下所示:缓存
4.二、Apache Bench(简称AB),Apache附带的工具,测试网站性能。AB是一个命令行的工具,输入命令就能够进行测试,对发起负载的本机要求很低,根据ab命令能够建立不少的并发访问线程,模拟多个访问者同时对同一个url地址进行访问,所以能够用来测试目标服务器的负载压力。安全
AB指定命令发送请求之后,能够获得每秒产生的字节数、每次处理请求的时间、每秒处理请求的数目等等统计数据。bash
安装Apache服务器,官网下载地址:https://www.apachelounge.com/download/
在D:\biehl\ApacheBench\Apache24\bin目录下面找到ab.exe。-n是本次测试的总数,-c是指定本次并发数。
4.三、JMeter,Apache组织开发的压力测试工具。官网地址:https://jmeter.apache.org/
在D:\biehl\JMeter\apache-jmeter-5.2.1\bin目录下面执行jmeter.bat脚本文件。
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 * 一、CountDownLatch计数器向下减的闭锁类。该类能够阻塞线程,并保证线程在知足某种特定的条件下继续执行。 25 * CountDownLatch比较适合咱们保证线程执行完以后再继续其余的处理。 26 * 27 * 二、Semaphore信号量,实现的功能是能够阻塞进程而且控制同一时间的请求的并发量。 28 * Semaphore更适合控制同时并发的线程数。 29 * 30 * 三、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 }
五、线程安全性。
线程安全性的定义,当多个线程访问某个类时,无论运行时环境采用何种调度方式或者这些进程将如何交替执行,而且在主调代码中不须要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
六、线程安全性主要体如今三个方面原子性、可见性、有序性。
a、原子性,提供了互斥访问,同一时刻只能有一个线程来对它进行操做。
b、可见性,一个线程对主内存的修改能够及时的被其余线程观察到。
c、有序性,一个线程观察其余线程中的指令执行顺序,因为指令重排序的存在,该观察结果通常杂乱无序。
七、线程安全性的原子性的底层代码理解,以下所示。
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 // 里面的三个参数对象下面方法的var一、var二、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是二、var5从底层传递的二、最后一个参数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.一、线程安全性的原子性的使用,以下所示:
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 * 一、CountDownLatch计数器向下减的闭锁类。该类能够阻塞线程,并保证线程在知足某种特定的条件下继续执行。 26 * CountDownLatch比较适合咱们保证线程执行完以后再继续其余的处理。 27 * 28 * 二、Semaphore信号量,实现的功能是能够阻塞进程而且控制同一时间的请求的并发量。 Semaphore更适合控制同时并发的线程数。 29 * 30 * 三、CountDownLatch、Semaphore配合线程池一块儿使用。 31 * 32 * 四、jdk提供了Atomic包,来实现原子性,Atomic包里面提供了不少AtomicXXX类,他们都是经过CAS来完成原子性的。 33 * 34 * 五、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.二、线程安全性的原子性的使用,以下所示:
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 * 一、CountDownLatch计数器向下减的闭锁类。该类能够阻塞线程,并保证线程在知足某种特定的条件下继续执行。 27 * CountDownLatch比较适合咱们保证线程执行完以后再继续其余的处理。 28 * 29 * 二、Semaphore信号量,实现的功能是能够阻塞进程而且控制同一时间的请求的并发量。 Semaphore更适合控制同时并发的线程数。 30 * 31 * 三、CountDownLatch、Semaphore配合线程池一块儿使用。 32 * 33 * 四、jdk提供了Atomic包,来实现原子性,Atomic包里面提供了不少AtomicXXX类,他们都是经过CAS来完成原子性的。 34 * 35 * 五、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.三、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 * 一、CountDownLatch计数器向下减的闭锁类。该类能够阻塞线程,并保证线程在知足某种特定的条件下继续执行。 26 * CountDownLatch比较适合咱们保证线程执行完以后再继续其余的处理。 27 * 28 * 二、Semaphore信号量,实现的功能是能够阻塞进程而且控制同一时间的请求的并发量。 Semaphore更适合控制同时并发的线程数。 29 * 30 * 三、CountDownLatch、Semaphore配合线程池一块儿使用。 31 * 32 * 四、jdk提供了Atomic包,来实现原子性,Atomic包里面提供了不少AtomicXXX类,他们都是经过CAS来完成原子性的。 33 * 34 * 五、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.四、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.五、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.六、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 * 一、AtomicStampReference,解决CAS的ABA问题。compareAndSet该方法。s 24 * 25 * 1.一、ABA问题就是CAS在操做的时候,其余线程将变量的值A修改为了B,又改会了A。 26 * 本线程使用指望值A与当前变量进行比较的时候,发现A变量没有改变。 27 * 因而CAS就将A值进行了交换操做。其实此时该值已经被其余线程改变过了,这与设计思想是不符合的。 28 * 29 * 1.二、ABA问题解决思路是每次变量更新的时候,把变量的版本号加一,那么以前将变量的值A修改为了B,又改会了A,版本号修改了三次。 30 * 此时,只要某一个变量被线程修改了,该变量对应的版本号就会发生递增变化,从而解决了ABA问题。 31 * 32 * 二、AtomicLongArray,维护的是一个数组。这个数组能够选择性的更新某一个索引对应的值,也是进行原子性操做的,相比于AtomicLong,AtomicLongArray会多一个索引值去更新。 33 * 34 * 三、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 }
八、原子性提供了互斥访问,同一时刻,只能有一个线程来对它进行操做。同一时刻只能有一个线程来对它进行操做,除了atomic包里面的类,还有锁,jdk提供锁主要分两种。特别注意,volatile是不具有原子性的。
1)、一种是synchronized(依赖JVM)。是java的关键字,主要依赖jvm来实现锁机制。所以在这个关键字做用对象的做用范围内都是同一时刻只能有一个线程能够进行操做的,切记是做用对象的做用范围内。
2)、另一种锁是jdk提供的代码层面的锁Lock(Lock是接口)。依赖特殊的CPU指令,代码实现,ReentrantLock。
3)、synchronized是java中的一个关键字,是一种同步锁,修饰的对象主要有四种。
第一种,修饰代码块,被修饰的代码称为同步语句块,做用范围是大括号括起来的代码,做用对象是调用的对象。
第二种,修饰方法,被修饰的方法称为同步方法,做用范围是整个方法,做用对象是调用这个方法的对象。
第三种,修饰静态方法,做为范围是整个静态方法,做用的对象是这个类的全部对象。
第四种,修饰类,做用范围是synchronized后面括号括起来的部分,做用对象是这个类的全部对象。
8.一、第一种,修饰代码块,被修饰的代码称为同步语句块,做用范围是大括号括起来的代码,做用对象是调用的对象。第二种,修饰方法,被修饰的方法称为同步方法,做用范围是整个方法,做用对象是调用这个方法的对象。
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 * 一、若是一个方法内部是完整的同步代码块,那么它和用synchronized修饰的方法是等同的。 19 * 由于整个实际中须要执行的代码都是被synchronized修饰的。 20 * 21 * 二、若是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.二、第三种,修饰静态方法,做为范围是整个静态方法,做用的对象是这个类的全部对象。第四种,修饰类,做用范围是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 * 一、一个方法里面若是全部须要执行的代码部分都是被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 }
九、可见性是一个线程对主内存的修改,能够及时的被其余线程观察到,提及可见性,何时会致使不可见呢?致使共享变量在线程间不可见的缘由,主要有下面三个方面。对于可见性,JVM提供了synchronized、volatile。
1)、方面1、线程交叉执行。
2)、方面2、重排序结合线程交叉执行。
3)、方面3、共享变量更新后的值没有在工做内存与主内存间及时更新。
十、对于可见性,JVM(java内存模型)提供了synchronized、volatile。可见性,JVM提供的synchronized。JMM关于synchronized的两条规定,以下所示:
1)、线程解锁前,必须把共享变量的最新值刷新到主内存。
2)、线程加锁时候,将清空工做内存中共享变量的值,从而使用共享变量时须要从主内存中从新读取最新的值(注意,加锁和解锁是同一把锁)。
注意:在原子性里面,synchronized的四种修饰方法,修饰方法前两条是针对于调用对象的,对于不一样对象,锁的范围是不同的,此时,若是不是同一把锁,互相以前是不影响的。正是由于有了synchronized的可见性,解决了咱们以前见到的原子性,所以咱们在作线程安全同步的时候,咱们只要使用synchronized进行修饰以后,咱们的变量能够放心的进行使用。
十一、可见性,volatile,经过加入内存屏障和禁止重排序优化来实现可见性的。
1)、对volatile变量写操做的时候,会在写操做后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存中。
2)、对volatile变量读操做的时候,会在读操做前加入一条load屏障指令,从主内存中读取共享变量。
注意:这两点,通俗的说,volatile变量在每次线程访问的时候,都强迫从主内存中读取该变量的值,而当该变量发生变化的时候,又会强迫线程将最新的值刷新到主内存中,这样的话,任什么时候候不一样的线程总能看到该变量的最新值。
特别注意,volatile是不具有原子性的。因此volatile是不适合计算场景的。那么volatile适合什么场景呢,使用volatile必须具有两个条件,第一个是对变量的写操做不依赖于当前值,第二个是该变量没有包含在具备其余变量不变的式子中。因此volatile很适合状态标记量。另一个使用场景就是doubleCheck即检查两次场景。
十二、volatile读操做,写操做插入内存屏障和禁止重排序的示意图。
1)、volatile写操做,插入Store屏障的示意图。对遇到volatile写操做时,首先会在volatile写以前插入一个StoreStore屏障(其做用是禁止上面的普通写和下面的volatile写重排序),以后会在volatile写插入一个StoreLoad屏障(其做用是防止上面的volatile写和下面可能有的volatile读/写重排序)。
1三、volatile读操做,插入Load屏障的示意图。对遇到volatile读操做时,会插入Load屏障,首先是插入一个LoadLoad屏障(其做用是禁止下面全部普通操做和上面的volatile读重排序),接下来插入LoadStore屏障(其做用是禁止下面全部的写操做和上面的volatile读重排序)。全部这些都是在CPU指令级别进行操做的,所以当使用volatile的时候已经具有了当前所说的这些规范。
1四、线程安全性里面的有序性,Java内存模型中,容许编译器和处理器对指令进行重排序,可是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。在Java中可使用volatile保证必定的有序性,另外也可使用synchronized和lock保证必定的有序性,很显然synchronized和lock保证每一个时刻是有一个线程执行同步代码,至关因而让线程顺序执行同步代码,天然就保证了有序性。另外呢,Java内存模型具有先天的有序性,即不须要任何手段保证有序性,这个一般被称为happen-before原则,若是两个操做的执行顺序没法从happen-before原则推导出来,他们就不能保证他们有序性了,虚拟机能够随意对他们进行重排序了。
1五、有序性,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原则推导出来,就不能保证他们的有序性,虚拟机就能够随意的对他们进行重排序。
1六、线程安全性的总结。
1)、原子性,主要是提供了互斥访问,同一时刻只能有一个线程ji进行操做。原子性里面须要注意Atomic包、CAS算法、synchronized、Locl锁。
2)、可见性,是指一个线程对主内存的修改,能够及时的被其余线程观察到。在可见性里面,须要注意synchronized、volatile关键字。
3)、有序性,主要介绍了happens-before原则,一个线程观察q其余线程中指令执行顺序,因为指令重排序的存在,这个观察结果通常都会杂乱无序的。若是两个操做的执行次序,没法从happens-before原则推导出来,就不能保证他们的有序性,虚拟机就能够随意的对他们进行重排序。
1七、CPU的多级缓存。左侧的图展现的最简单的高速缓存的配置,数据的读取和存储都通过高速缓存的,CPU核心与高速缓存之间是有一条特殊的快速通道,在这个简化的图里面,主存与告诉缓存都链接在系统总线上,这条总线同时也用于其余组件的通讯。右侧的图展现的是,在高速缓存出现后不久,系统变得更加复杂,高速缓存与主存之间的速度差别被拉大,直到加入了另外一级的缓存,新加入的这一缓存比第一缓存更大,可是更慢,因为加大一级缓存的作饭从经济上考虑是行不通的,因此有了二级缓存,甚至有的系统出现了三级缓存。
1八、为何须要CPU cache缓存呢?
答:CPU的频率太快了,快到主存跟不上,这样在处理器时钟周期内,CPU经常须要等待主存,浪费资源,因此cache的出现,是为了缓解CPU和主存之间速度的不匹配问题(注意,结构如是,cpu -> cache缓存 -> memory主存)。
1九、CPU cache缓存有什么意思呢,缓存的容量远远小于主存的,所以出现缓存不被命中的几率在所不免,既然缓存不能包含CPU所须要的全部数据,那么缓存的存在到底有什么意义呢?
1)、时间局部性,若是某个数据被访问,那么在不久的未来它极可能被再次访问。
2)、空间局部性,若是某个数据被访问,那么与它相邻的数据很快也可能被访问的。
20、CPU多级缓存的缓存一致性(MESI,MESI协议是基于Invalidate的高速缓存一致性协议,而且是支持回写高速缓存的最经常使用协议之一)。参考http://www.javashuo.com/article/p-qgwgeljq-r.html。
多核CPU的状况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。MESI协议用于保证多个CPU cache之间缓存共享数据的一致。MESI是指4中状态的首字母,定义了每一个Cache line(缓存行,缓存存储数据的单元)的4个状态,可用2个bit表示。CPU对cache的四种操做可能会出现不一致的状态,所以缓存控制器监听到本地操做和远程操做的时候,须要对Cache line作出必定的修改,从而保证数据在多个缓存之间流转的一致性。
2一、MESI状态转换图,以下所示:
local read、local write、remote read、remote write四种操做,以下所示:
MESI协议的Cache line数据状态有四种,引发数据状态转换的cpu cache操做也是有四种的。若是要深入理解MESI协议,要深入理解16种转换的状况,状态之间的相互转换关系,以下所示:
在一个典型的多核系统中,每个核都会有本身的缓存,来共享主存总线,每一个响应的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状态的缓存不须要使用总线事务。
2二、CPU多级缓存,乱序执行优化。
答:CPU多级缓存,乱序执行优化。处理器为了提升运算速度而作出违背代码原有顺序的优化。可是计算过程,在正常状况下,不会对结果形成影响的。在单核时代,处理器保证作出的优化不会致使执行的结果远离预期目标,可是在多核环境下,并不是如此,多核时代,同时有多个核执行指令,每一个核的指令均可能被乱序,另外,处理器还引入了L1,L2等缓存机制,每一个核都有本身的缓存,这就致使了逻辑次序后写入内存的未必真的最后写入,最终致使了一个问题,若是咱们不作任何防御措施,处理器最终获得的结果和逻辑获得的结果大不相同。
2三、Java虚拟机提供了Java内存模型(Java Memory Model,简称JMM)。
答:了解了CPU的缓存一致性、乱序执行优化,在多核多并发下须要额外作不少操做的,才能保证程序执行符合咱们的预期。
为了屏蔽各类硬件和操做系统内存的访问差别,以实现Java程序在各类平台下都能达到一致的并发效果,Java虚拟机提供了Java内存模型(Java Memory Model,简称JMM)。JMM是一种规范,规范了Java虚拟机与计算机内存如何协同工做的,规定了一个线程如何和什么时候能够看到其余线程修改事后的共享变量的值,以及必须时如何同步的访问共享变量。
JVM内存分片的两个概念,Heap堆、Stack栈。
1)、Heap堆,java里面的堆是运行时的数据区,堆是由垃圾回收负责的,堆的优点是能够动态的分配内存大小,生存期也没必要事先告诉编译器,由于它是在运行时动态分配内存的,java的垃圾收集器会自动搜索再也不使用数据,可是也有缺点,缺点就是因为须要在运行时动态分配内存,所以它的存取速度相对慢一些。
2)、Stack栈,栈的优点是存取速度比堆要快,仅次于计算机里面的寄存器,栈里面的数据是能够共享的,可是它的缺点存在栈中的数据大小与生存期必须是肯定的,缺少一些灵活性,栈中主要存在一些基本类型的变量。Java内存模型要求调用栈和本地变量存放在线程栈上,对象存放在堆上。
3)、一个本地变量多是指向一个对象的引用,这种状况下,引用这个本地变量是存放在线程栈上的,可是对象自己是存放在堆上的。
4)、一个对象可能包含方法methodOne()、methodTwo(),这些方法可能包含本地变量,local variable一、local variable2,这些本地变量仍然是存放在线程栈上的,即便这些方法所属的对象存储在堆上。
5)、一个对象的成员变量可能会随着这个对象自身存放在堆上,无论这个成员变量是原始类型仍是引用类型。
6)、静态成员变量跟随着类的定义一块儿存放在堆上,存放在堆上的对象能够被所持久对这个对象引用的线程访问。
7)、若是Thead存放了Object的引用,是能够访问Object的,当一个线程能够访问一个对象的时候,此线程也能够访问这个对象的成员变量,当了两个线程同时访问一个对象的同一个方法,两个线程会都访问该对象的成员变量,可是每一个线程都拥有该对象成员变量的私有拷贝。如Thead Stack同时调用了Object3对象的methodOne()方法。
2四、计算机硬件架构简单的图示。以下所示:
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须要将结果回写到主存的时候,它会将内部寄存器里面的值刷新到缓存中,而后在某个时间点将值刷新到主存中。
2五、Java内存模型与硬件内存架构之间的一些关联。
Java内存模型与硬件内存架构是存在一些差别的,硬件内存架构是没有区分线程栈、堆、堆。对于硬件内存架构全部线程栈、堆都分布在主内存中,部分线程栈和堆可能会出如今CPU缓存中,和CPU内部的寄存器里面。
2六、Java内存模型抽象结构图。
线程和主内存之间的抽象关系。线程之间的共享变量存储在主内存中,每一个线程都有一个私有的本地内存,本地内存是Java内存模型的一个抽象概念,并非真实存在的,它涵盖了缓存、写缓冲区、寄存器以及其余的硬件和编译器的优化。本地内存中存储了该线程以读或者写共享变量拷贝的一个副本,好比若是线程A要使用主内存中共享变量,先拷贝主内存中一个共享变量副本,放到本身的本地内存中,从耕地的层次来讲,主内存就是硬件的内存,是为了获取更好的运行速度,虚拟机和硬件内存可能会让工做内存优先存储于寄存器和高速缓存中。Java内存模型中的线程中的工做内存是CPU的寄存器和高速缓存的一个抽象的描述,而JVM的静态存储模型(即JVM内存模型)只是一种对内存的物理划分而已,只局限于内存,并且只局限于JVM的内存。如今线程之间通讯必需要通过主内存,若是线程A和线程B之间要进行通讯,那么必须通过两个步骤,第一步,线程A将本地的内存A中的更新过的共享变量刷新到主内存中去,线程B去主内存中读取线程A已经更新过的共享变量。
2七、Java内存模型,同步的八种操做、以及Java内存模型的同步规则。
八种操做的概念解释,以下所示:
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操做)。
2八、并发的优点与风险。
做者:别先生
博客园:https://www.cnblogs.com/biehongli/
若是您想及时获得我的撰写文章以及著做的消息推送,能够扫描上方二维码,关注我的公众号哦。