Java并发编程:01 - 并发问题的引入以及初体验(理解Java内存模型)

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)

https://www.cnblogs.com/yanlong300/p/8986041.html

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 并发的优势与风险

在这里插入图片描述

并发编程是博主的学习记录,权当学习笔记,这里的记录有可能不精准甚至不准确,因此需要借阅请慎重!!!
深知并发的水很深,因此如果文章中有差错,还望指出更正,谢谢!!!
讨论:for.houyu@foxmail.com

展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 精致技术 设计师: CSDN官方博客
应支付0元
点击重新获取
扫码支付

支付成功即可阅读