文章目录
- 01 基本概念
- 02 并发问题的引入
- 03 思考问题
- 04 解决问题
- 05 出现并发问题的分析
- 05.01 CPU多级缓存 - 缓存一致性(MESI)
- 05.02 CPU多级缓存 - 乱序执行优化
- 06 Java内存模型(Java Memory Model, JMM)
- 07 Java内存模型 - 抽象结构图
- 08 Java内存模型 - 同步八种操作(很重要)
- 09 Java内存模型 - 同步规则(很重要)
- 10 并发的优势与风险
01 基本概念
并发 : 多个线程操作相同资源,保证线程安全,合理使用资源
高并发 : 服务器能同事处理很多请求,提高程序性能
02 并发问题的引入
模拟一个并发场景,同时5000个客户端请求,并发数量是200,统计一下一共有多少个客户端?理想情况下是5000,但是出现并发问题后,每次的结果都会 <= 5000
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
import java.util.function.Consumer;
/**
* @author houyu
* @createTime 2019/10/27 16:43
*/
@Slf4j
public class Test1 {
public static int i = 0;
public static void main(String[] args) {
testConcurrent(5000, 200, index -> i++);
System.out.println("i = " + i);
}
/**
* testConcurrent(5000, 200, index -> i++);
* @param clientCount 客户端数量
* @param threadCount 并发数量
* @param consumer 回调接口 回调一个index, 也就是每一个客户端下标
*/
public static void testConcurrent(int clientCount, int threadCount, Consumer<Integer> consumer) {
ExecutorService threadPool = new ThreadPoolExecutor(threadCount, threadCount, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), r -> {
Thread thread = new Thread(r);
thread.setDaemon(false);
return thread;
});
CountDownLatch runLatch = new CountDownLatch(1);
CountDownLatch countDownLatch = new CountDownLatch(clientCount);
for(int i = 0; i < clientCount; i++) {
int finalI = i + 1;
threadPool.execute(() -> {
try {
runLatch.await();
consumer.accept(finalI);
} catch(Exception e) {
log.warn("execute has Exception", e);
} finally {
countDownLatch.countDown();
}
});
}
try {
// 开始执行任务
runLatch.countDown();
// 等待任务执行完毕
countDownLatch.await();
} catch(InterruptedException e) {
log.warn("await has Exception", e);
} finally {
threadPool.shutdown();
}
}
}
控制台输入
i = 4899
03 思考问题
- 每次运行输出的结果都不太一致,有时候是4999,有时候4800+
- 如果并发数量设置为1,输出的结构每次都是5000
// int clientCount, int threadCount, Consumer<Integer> consumer
testConcurrent(5000, 1, index -> i++);
04 解决问题
这里有很多解决方案,这里使用一种比较常用的比较简单的方法(实际开发中不是推荐写法)
public static void main(String[] args) {
testConcurrent(5000, 200, index -> {
synchronized (Test1.class) {
i++;
}
});
System.out.println("i = " + i);
}
05 出现并发问题的分析
05.01 CPU多级缓存 - 缓存一致性(MESI)
05.02 CPU多级缓存 - 乱序执行优化
处理器为提高运算速度而做出违背代码原有顺序的优化,如果在多核多线程场景下就会出现问题
06 Java内存模型(Java Memory Model, JMM)
Heap 堆,动态分配大小(存储速度相对于Stack慢),由垃圾回收机制回收处理
Stack 栈,固定分配大小(速度仅次于计算机寄存器速度),主要存储Java基本类型变量如 int long…可以存储对象的引用
说明:如果多个线程都访问一个对象,那么如果访问对象的成员变量,那么是对成员变量的私有拷贝
07 Java内存模型 - 抽象结构图
说明:Java 线程之间需要通信,需要依赖主内存,因此回顾一下刚才初体验的问题,我们不难发现,i = 0存储于主内存,每个线程需要操作 i 的时候,都会进行拷贝一份共享变量副本,然后处理完成之后回写到主内存中,那么问题就出现在这里啦,由于CPU是切片执行的,假设线程A拷贝共享变量 i 进行 + 1 操作完成后(尚未回写到主内存),CPU执行权给了线程B,线程B同样拷贝一份共享变量副本 i = 0, 进行 + 1 操作,线程B操作完成之后把 i = 1回写到共享内存中,等到线程A获取CPU执行权之后,同样对把 i = 1回写到共享内存中,所以实质上是+2的,但是最终的结果是+1,因此每次输出的结果都是 <=5000
08 Java内存模型 - 同步八种操作(很重要)
- lock (锁定):作用于主内存的变量,把一个变量标示为一条线程锁定状态
- read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load(载入):作用于工作内存的变量,把read操作从主内存中得到的值放入工作内存的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值赋值给工作内存的变量
- store(存储):作用于工作内存的变量,把工作内存中的一个变量值传到到主内存中,以便随后的write操作
- write(写入):作用于主内存的变量,它把store操作的工作内存中的一个变量值传送到主内存的变量中
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
09 Java内存模型 - 同步规则(很重要)
- 如果要把一个变量从主内存中复制到工作内存,就需要按顺序地执行 read 和 load 操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行 store 和 write 操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行
- 不允许 read 和 load、store 和 write 操作之一单独出现,即 read 和 load、store和 write 是有顺序的,但是不一定是原子性的
- 不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须同步到主内存中
- 不允许一个线程无原因地(没有发生过任何 assign 赋值操作)把数据从工作内存同步回主内存中,一个新的变量只能在主内存中诞生
- 不允许在工作内存中直接使用一个未被初始化( load 或 assign )的变量。即就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作
- 一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。lock 和 unlock 必须成对出现
- 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值
- 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 这个被其他线程锁定的变量
- 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 wite 操作)
10 并发的优势与风险
并发编程是博主的学习记录,权当学习笔记,这里的记录有可能不精准甚至不准确,因此需要借阅请慎重!!!