Java高并发–CPU多级缓存与Java内存模型

释放双眼,带上耳机,听听看~!

Java高并发–CPU多级缓存与Java内存模型

主要是学习慕课网实战视频《Java并发编程入门与高并发面试》的笔记

CPU多级缓存

为什么需要CPU缓存:CPU的频率太快,以至于主存跟不上,这样在处理器时钟周期内,CPU常常需要等待主存,浪费了资源。所有缓存的出现是为了缓解CPU和主存之间速度不匹配的问题——将运算所需数据复制到缓存中,使得运算能快速进行;当运算结束后再将缓存同步回内存中,这样处理器无需等待缓慢的内存读写。

Java高并发--CPU多级缓存与Java内存模型

缓存并非存储了所有的数据,那么它存在的意义是什么?

  • 时间局部性:如果某个数据被访问,那么它在不久的将来有可能被再次访问
  • 空间局部性:如果某个数据被访问,那么与它相邻的数据很快可能被访问

Java内存模型

下图中是JVM中堆和栈的关系,在不同的线程中,可能有多个变量,它们指向的是堆上的同一个对象,这些变量都是该对象的“私有拷贝”。私有表示仅在当前线程可访问,拷贝是说这是该对象的一个引用。

Java高并发--CPU多级缓存与Java内存模型

线程A和线程B之间如果要通信的话,必须经历以下两个步骤:

  • 线程A将本地内存(工作内存)中的变量刷新到主内存中
  • 主内存将变量复制到本地内存B中,使得线程B在读取变量时更快

Java高并发--CPU多级缓存与Java内存模型

Java内存模型中,所有的变量都存储在主内存中,每条线程还有自己的工作内存(与高速缓存类比),线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量;不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需通过主内存完成

如果将Java内存模型和Java堆、栈比较,主内存对应Java堆中的对象实例部分,工作内存对应虚拟机栈中的部分

主内存和工作内存之间需要交互,Java内存模型中有8种原子操作:

  • lock:作用于主内存变量,将其标识为线程独占。
  • unlock:作用于主内存变量,将其从锁定状态释放,释放后才可被其他线程锁定。
  • read:作用于主内存的变量,将一个变量从主内存中传输到工作内存中,以便随后的load动作使用。
  • load:作用于工作内存中的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use:作用于工作内存的变量,把工作内存中的一个变量的值传递给执行引擎,当虚拟机需要使用到变量的值的字节码指令时会执行这个操作。
  • assign:作用于工作内存的变量,把一个从执行引擎接受到的值赋给工作内存中的变量。当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store:作用于工作内存中的变量,把工作内存中的一个变量的值传送到主内存中,以便之后write操作使用。
  • write:作用于主内存中的变量,把store操作从工作内存中得到的变量值放入主内存的变量中。

如果一个变量从主内存复制到工作内存,必须先执行read然后执行load操作(read和load之间允许插入其他操作,只要保证这个顺序即可);如果要把变量从工作内存同步回主内存中,需要先执行store操作然后执行write操作(store和write之间允许插入其他操作,只要保证这个顺序即可)。

Java高并发--CPU多级缓存与Java内存模型

最后来看一个线程不安全的例子


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
1package com.shy.concurrency;
2
3import com.shy.concurrency.annotations.NotThreadSafe;
4import lombok.extern.slf4j.Slf4j;
5
6import java.util.concurrent.CountDownLatch;
7import java.util.concurrent.ExecutorService;
8import java.util.concurrent.Executors;
9import java.util.concurrent.Semaphore;
10
11/**
12 * @author Haiyu
13 * @date 2018/12/16 16:08
14 */
15@Slf4j
16@NotThreadSafe
17public class ConcurrencyTest {
18    // 请求总数
19    public static int requestTotal = 5000;
20    // 并发量,同时进入临界区的线程数量
21    public static int concurrentTotal = 20;
22    // 计数器
23    public static int count = 0;
24
25    @NotThreadSafe
26    private static void add() {
27        count++;
28    }
29
30    public static void main(String[] args) throws InterruptedException {
31        ExecutorService executorService = Executors.newCachedThreadPool();
32        // 信号量,控制并发数
33        final Semaphore semaphore = new Semaphore(concurrentTotal);
34        // 倒计数器,在这里用来阻塞主线程直到计数器为0
35        final CountDownLatch countDownLatch = new CountDownLatch(requestTotal);
36        // requestTotal次请求,每一次请求自增1,按理说最后count的值是requestTotal
37        // 但是在并发下,多个线程同时执行了add()使得多次自增值只增加了1,导致最后的结果比requestTotal小
38        for (int i = 0; i < requestTotal; i++) {
39            executorService.execute(() -> {
40                try {
41                    semaphore.acquire();
42                    // 最多可同时concurrentTotal个线程同时执行
43                    add();
44                    semaphore.release();
45                } catch (InterruptedException e) {
46                    log.error("exception", e);
47                }
48                // 每自增一次,计数器减1
49                countDownLatch.countDown();
50            });
51        }
52        // 主线程在countdownLatch上等待,等计数器为0后才能执行
53        countDownLatch.await();
54        log.info("count:{}", count);
55        executorService.shutdown();
56    }
57}
58
59

总共发起5000次请求(5000个线程),每次请求对count变量做自增操作。显然在并发下,主线程等5000个线程执行完毕后count一般是小于5000的。原因如下:

以两个线程为例,它们在同一时刻从主内存中读取count的值并装载到各自的工作内存中,此时count的值是一样的,假设都是10。此时线程A先自增,将自增后的值更新到工作内存,最后刷回主内存,count变成了11;而在线程B也进行同样的自增操作,注意之前线程B已经读取过count的值了,此时在B的工作内存中的count还是等于10的,接着B也更新count,最后刷回主内存中,count变成11。也就是说明明执行了两次自增,最后count只增大了1。因此在并发下,多次add可能只会有一次自增。

semaphore信号量用于控制并发量,即同时进入临界区的操作同一个共享资源的线程数。


1
2
3
4
1semaphore.acquire(); // 可以认为是获得锁
2// other code
3semaphore.release(); // 可以认为是释放了锁
4

semaphore.acquire()和semaphore.release()之间的代码可以认为是临界区,这里指定了可以同时20个线程进入临界区,换种说法就是并发量是20。

如果将semaphore允许的并发量改成1,那么就相当于任意时刻只能有一个线程执行add操作,5000个线程井然有序的按照先后顺序执行add,不存在同时执行的情况,这种情况下最后的结果总是5000,某种意义上变成了串行。

解决上面的线程不安全问题,除了可以将semaphore的并发量控制为1;还可以使用重入锁,synchronized关键字,原子变量AtomicInteger等。

转载于:https://www.cnblogs.com/sun-haiyu/p/10741307.html

给TA打赏
共{{data.count}}人
人已打赏
安全经验

Google AdSense 全面解析(申请+操作+作弊+忠告)

2021-10-11 16:36:11

安全经验

安全咨询服务

2022-1-12 14:11:49

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索