学习笔记
快来分享你的内容吧~
- 07-15 11:20·后端开发
- 02-14 17:57·Java后端
- 02-14 00:28·Java后端
- 02-09 13:33·Java后端
- 02-07 09:17·Java后端
- 2025-11-21·Java后端查看全文学习笔记 我们直接看破坏占有且等待条件的代码 public Class Allocator{ private static final Allocator INSTANCE = new Allocator(); public static Allocator getInstanc...加油鸭:你的代码分析非常透彻!从破坏占有等待到优化忙等待,逻辑清晰且深入,这种钻研精神值得点赞!110分享
- 2025-11-12·Java后端查看全文学习笔记 java如何解决可见性和有序性?我们都知道了,可见性和有序性的问题就是CPU缓存问题以及编译优化导致的执行重排序问题,那么怎么解决可见性问题和有序性的问题也很明了了,把CPU缓存以及编译优化全禁用了不就行了?确实,禁用确实性,但是我们的性能就糟糕了。所以我们不能盲目的禁用,要按需的禁用。 ...加油鸭:深入浅出讲得真清楚!把happens-before规则分析得这么透彻,你一定是下功夫钻研过的。这样的技术分享超有价值!110分享
- 2025-11-10·Java后端查看全文学习笔记 在单核时代,多个线程在一个CPU上执行,每个线程都是对一个CPU里的这个缓存进行操作,一个线程对这个缓存进行的操作,另一个线程一定是可见的,因为它们操作的是同一片缓存。 一个线程对共享变量的修改,另一个线程能立刻看到,这就是可见性。 但在多核时代,多个线程在多个CPU上执行,每个线程可能操...加油鸭:讲得太清晰了!把多线程的可见性问题用这么生动的例子说明,真的让人一下就能理解本质,感谢分享这么硬核的知识点!130分享
Java异步处理实例
### 采用的实现流程: ` 用户发起异步批量申请请求 ---> Service创建任务对象存入任务池中,并存入redis中 ---> Service方法中通过TaskId异步调用执行器 ---> 执行器中通过TaskId去redis中读取任务信息 ---> 执行器执行后更新redis任务状态`  这里需要注意一点,也是我在写这块内容的时候发现的,也是异步处理的关键点,这个返回给用户TaskId并不是在执行完任务之后返回给用户的,这个TaskId是在创建存入Redis后直接返回给用户的,至于任务异步处理都是在后台自己执行的。 这时候就要问了,那我怎么知道怎么这个任务是否完成了,我们需要再写一个接口去查看redis中任务完成情况。 采用redis作为缓存,异步执行缓存内任务实现异步操作 ### 具体项目实施: 模块异步配置文件: ```java @Configuration @EnableAsync public class AsyncConfig { /** * 批量审核异步线程池 * <p> * Bean 名称 {@code AuditBatchExecutor},供批量提交、批量审批的 * {@code @Async("AuditBatchExecutor")} 方法使用。 * </p> * * @return 线程池执行器 */ @Bean("AuditBatchExecutor") public Executor AuditBatchExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 1. 核心线程数:常驻工作线程 executor.setCorePoolSize(2); // 2. 最大线程数:高峰时可扩容 executor.setMaxPoolSize(5); // 3. 队列容量:等待执行的任务上限 executor.setQueueCapacity(100); // 4. 线程名前缀:便于日志区分异步线程 executor.setThreadNamePrefix("cbs-audit-batch-"); // 5. 队列满时由调用线程执行,避免任务直接丢弃 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } } ``` 存入Redis操作: 构建一个任务对象 ``` java AuditBatchTask task = new AuditBatchTask(); // 创建对象AuditBatchTask需要的参数 // ... // 调用异步批量的函数方法,创建任务并且存入Redis中 batchTaskStore.createProcessingTask(task); // 执行异步处理,通过TaskId,这个是一个执行器创建的对象 batchAsyncExecutor.execute(taskId, request); // 这里可以直接返回给前端,任务Id,处理状态、任务处理条数,这些都根据自己实际情况来 return ... ``` 方法createProcessingTask()实现: ``` java @Override public void createProcessingTask(CbsPaymentAuditBatchTask task) { // Constants.BATCH_TASK_REDIS_TTL_HOURS为设定的常量过期时间 redisCache.set(buildSubmitKey(task.getTaskId()), task, Constants.BATCH_TASK_REDIS_TTL_HOURS, TimeUnit.HOURS); } ``` 异步执行器中batchAsyncExecutor.execute(taskId, request)方法实现: ``` java //lazy 不加这个会出现Resource循环 @Autowired @Lazy // 这个就根据你的具体 private PaymentAuditService PaymentAuditService; @Autowired private AuditBatchTaskStore batchTaskStore; // 异步处理注解 根据我之前学习注解的经验,这个就是一个说明执行器方法在class中的名字 @Async("cbsAuditBatchExecutor") public void execute(String taskId, AuditBatchSubmitRequest request) { // 去redis中获取task任务,判断任务是否存在 AuditBatchTask task = batchTaskStore.getTask(taskId); if (task == null) { return; } // 2. 调用 try { // 具体执行,这个根据具体情况执行 AuditBatchTaskStatusVO statusVO = AuditService.processBatchSubmit(request); // 创建更新后的任务对象,AuditConstants.BATCH_TASK_STATUS_COMPLETED我设定的常量 task.setTaskStatus(AuditConstants.BATCH_TASK_STATUS_COMPLETED); task.setProcessedCount(statusVO.getProcessedCount()); task.setSuccessCount(statusVO.getSuccessCount()); task.setFailCount(statusVO.getFailCount()); task.setResults(statusVO.getResults()); task.setFinishTime(LocalDateTime.now()); batchTaskStore.updateTask(task); } catch (Exception e) { task.setTaskStatus(CbsAuditConstants.BATCH_TASK_STATUS_FAILED); task.setErrorMessage(e.getMessage()); task.setFinishTime(LocalDateTime.now()); batchTaskStore.updateTask(task); } } ```
关于内存池的学习
## 关于volatile 这个是一个类型修饰符,用于内存级别的修饰,我对它的理解为: 在用这个修饰符之后,更新的内容可以及时被其他线程看到,但是这并非线程安全。 为什么这么说呢,在书本《深入理解Java虚拟机》中有描述,在一个多线程的轮流给总数 +1 的情况,10个线程每个都++100次,按理论来说最后总数应该是10*100=1000,但是具体执行后会返现总数会变化,但是一直不回到达1000,造成一个问题的原因是,被volatile修饰变量(计算总数),虽然可以被及时看到,但是另一个线程使用的还是旧的数据,就导致线程A在执行的时候只能保证执行的那一刻数据是对的,但是在之后的 +1 的时候,数据已经被另外的线程B或者C执行过了,这时候拿到的数据已经是老的数据了,所以修改的还是老数据。在这里就可以这个volatile并非线程安全。 在一个线程执行 +1 操作的时候有一下步骤,读取count count++ 写回count++ ,这时候线程A和线程B都是有操作的,这个数据就可以遇到 少加的情况。
vue+ElementUI
总结一句话:用哪个粘哪个。 不知道效果就一点点删除,删着删着你就知道他是干什么的了。 ## 使用方法 只要根据vue-router的知识,理清楚页面的实现逻辑,使用elementUI就很简单 1. 安装依赖 ```npm i element-ui -S``` 2. 创建组件,组件可以放在两类文件夹中 , 直接复制你看好的组件(elementUI组件) - view 用于存放样式类的组件 - components 用于存放功能类的组件  3. 创建路由文件(导入插件,注册插件,配置和导出插件,将引入的ElementUI组件配置到路由中)  4. 在index.js中填写对应的信息,可以直接复制,elemntUI已经提供  5. 在App.vue中直接使用  单词:新学5+复习5 周三作业已补完
HTTPS页面请求HTTP接口失败?一文讲透Mixed Content
> 前端开发中,有一个高频踩坑场景:前端绑定域名后,HTTP 协议下请求后端 IP 接口一切正常,可一旦开启 SSL 切换为 HTTPS,接口就直接请求失败,控制台报出红色错误 —— 这不是代码 Bug,而是浏览器的安全限制,核心原因就是「Mixed Content(混合内容)」。 ## 一、先明确核心概念:Mixed Content(混合内容) 首先厘清两个基础认知,避免混淆: 1. HTTPS 的本质 HTTPS = HTTP + TLS(Transport Layer Security,传输层安全协议),相当于给 HTTP 传输的数据加了一层加密外套,防止数据被窃听、篡改,保障访问安全;而 HTTP 是明文传输,无任何加密保护。我们常说的「开启 SSL」,本质就是启用 HTTPS 加密。 2. Mixed Content(混合内容)的定义 当 HTTPS 加密页面 中,请求了 HTTP 明文资源 时,就属于 Mixed Content(混合内容)。 浏览器的安全逻辑:HTTPS 页面代表用户处于安全的加密环境,若此时请求 HTTP 明文接口,传输的数据会暴露在风险中,可能被抓包、篡改。因此,浏览器会强制拦截此类请求,这是无法通过后端配置绕过的硬规则。 浏览器控制台的标准报错: ```js Mixed Content: The page at 'https://yourdomain.com' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://your-server-ip:port/api'. This request has been blocked; the content must be served over HTTPS. ``` ## 二、重点解答:HTTPS 页面能否请求 HTTP 接口? 结论:对于接口请求(fetch/axios/XHR),现代浏览器 100% 拦截,无生产可用的例外场景。 浏览器将 Mixed Content 分为两类,仅被动内容有极特殊情况: - 被动内容(Passive Mixed Content):如图片、CSS、字体,部分浏览器可能放行,但会标红警告,无实际生产意义; - 主动内容(Active Mixed Content):如接口请求、JS 脚本、XHR/fetch,所有现代浏览器(Chrome/Edge/Firefox 等)一律强制拦截,这也是我们踩坑的核心场景。 补充:手动关闭浏览器混合内容拦截(仅测试用),无法推广给普通用户,生产环境完全不可用。 ## 三、最简单、一劳永逸的解决方案(Nginx 代理) 核心思路:让前端和接口「同域名、同 HTTPS」,彻底规避 Mixed Content 和跨域,无需修改后端代码。 ### 最终实现效果 前端:https://yourdomain.com (HTTPS 加密,原域名); 接口:https://yourdomain.com/api (同域名、同 HTTPS,通过 Nginx 代理到后端)。 ### Nginx 核心配置(复制即用,替换实际信息) ```Nginx server { listen 443 ssl; server_name [yourdomain.com](https://yourdomain.com); # 你的前端域名 # SSL 证书配置(复用前端证书,无需额外申请) ssl_certificate /your-ssl-path/yourdomain.pem; ssl_certificate_key /your-ssl-path/yourdomain.key; # 前端静态资源配置(无需部署前端可删除) location / { root /your-frontend-path; index index.html; } #核心:接口代理,转发 /api 请求到后端 HTTP 服务 location /api { proxy_pass http://your-server-ip:your-server-port; # 后端 IP + 端口 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } # 可选:80 端口自动跳转 HTTPS,优化用户体验 server { listen 80; server_name [yourdomain.com](https://yourdomain.com); return 301 https://hostrequest_uri; } ``` ### 前端代码修改(1 行搞定) 原写法(硬编码 HTTP 协议和 IP 地址,触发 Mixed Content 拦截): ```js fetch('http://your-server-ip:your-server-port/api/xxx') ``` 修改后(使用相对路径,自动继承页面的 HTTPS 协议和域名,规避混合内容): ```js fetch('/api/xxx') ``` ## 四、终极避坑口诀(记牢不踩坑) 1. HTTPS 页面只能请求 HTTPS 接口,否则触发 Mixed Content 拦截; 2. HTTP 页面可请求 HTTP/HTTPS 接口,无 Mixed Content 限制; 3. 跨域可通过后端 CORS 解决,Mixed Content 只能统一 HTTPS 协议; 4. 接口类主动内容,HTTPS 页面请求 HTTP 接口必失败,无例外。 来源于 https://blog.itrf.cn/posts/ceca/
自定义事件
# 基础语法 v-bind(缩写 :):绑定DOM 元素的属性 / 特性,关联 “数据→视图”,核心是 “传数据”; v-on(缩写 @):绑定DOM 元素的事件,关联 “视图→逻辑”,核心是 “响应用户操作”; 二者是 Vue 模板中最基础的两个指令,分别处理 “数据渲染” 和 “交互逻辑”,是 Vue 响应式开发的核心基础。 # 逻辑图  前端通过el属性和Vue对象建立联系 前端通过插槽(slot)和 组件建立联系 但是我们想要在组件中删除Vue对象的数据是做不到的。要想实现需要借助自定义事件这个中间桥梁  # 达到效果 让子组件的消息模版可以删除vue对象中的数据。原来是只有父组件的消息模版可以删除vue对象中的数据 # 自定义事件的实现方法 1. 在vue对象中创建删除方法 2. 在组件中定义自定义事件方法 (使用 this.$emit(事件名)) 3. 在前端通过@事件名(因为他也是一个方法所以使用@) 和 组件进行绑定, 通过 @事件名="vue对象的方法名称" 和vue对象建立联系  
vue 插槽slot学习
## 形象类别 可以在凹槽中配置各种东西  ## 使用方法 1. 创建vue对象 2. 创建多个组件,其中在创建插槽所使用的组件时,要在定义模版内容的时候格外注意 通过使用 \ 进行换行 3. 在组件标签中定义绑定关系和调用关系 下图所示为数据的传递方向  实际上只要理解了数据是如何传递的, 绑定关系是如何建立的 , 是怎么调用数据的,理解插槽就会更加的简单,他无非就是多了一个模版写法而已。下面来介绍一下这三种方式是如何实现的。 1. vue对象里面通过data属性创建了数据源,用于后期视图进行展示 2. 定义的组件件对象,通过props属性和vue对象中data中的数据建立联系。当这层联系建立后,数据源中的数据就可以 以props作为媒介,将数据传输给组件对象的template属性 3. 组件对象的template有了数据后,就可以通过组件标签进行数据展示了。 4. 数据流向:根实例 data(vm) → 父组件模板绑定(<todo> 内部的绑定,父传子的 props 绑定关系)→ 子组件 props(todo-title/todo-items) → 子组件模板展示  注意:最顶层的 “父” 是 Vue 根实例(var vm = new Vue({ el: "#app" ... })),它对应的模板就是 <div id="app"> 里的所有内容 slot="xxx" 只是给这两个子组件 “标记归属的插槽位置”,不改变 props 绑定的父传子本质。 这就是昨天学习的内容。 单词和vue学习都已完成并补交。 开心,我会变得越来越好! 加油!
学习笔记 我们直接看破坏占有且等待条件的代码 public Class Allocator{ private static final Allocator INSTANCE = new Allocator(); public static Allocator getInstance() { return INSTANCE; } private Allocator() { als = new ArrayList<>(); } private List<Object> als; synchronized boolean apply(Object from,Object to){ if(als.constains(from)||als.constains(to)){ returan false }else{ als.add(from); als.add(to); } return true; } synchronized void free(Object from,Object to){ als.remove(from); als.remove(to); } } public Class Account{ //这里假设actr是个单例, private Allocator actr; = Allocator.getInstance; private int balance; void transfer(Account actr,int amt){ while(!actr.apply(from,to)); try{ synchronize(this){ synchronized(from){ if(this.balance>amt){ this.balance-=amt; from.balance+=amt; } } } }finally{ atcr.free(this,from); } } } 这段代码的执行流程:用户A向用户B转账时,会先去让Allocator去拿锁A和锁B的使用权,Allocator调用apply去拿锁A和锁B的使用权时,还得先获取自己的锁,然后再去拿锁A和锁B的使用权,如果Allocator没同时拿到两个锁的使用权,用户A就会使用while再次去让Allocator去拿锁的使用权,直到Allocator同时拿到锁A和锁B的使用权时。用户A再向下执行,去获取锁A和锁B,此时其他线程是无法去抢到锁A和锁B的,因为其他线程也要向A或B进行转账,那么它们也要让Allocator去拿锁A或锁B的使用权,但是此时锁的使用权已经被A拿到了,所以它们没有使用权的情况下,不会去抢夺锁。拿到锁A和锁B后,用户A执行完转账流程,就将两个锁释放,然后再把锁A和锁B的使用权释放。 以上这段代码有个缺点,就是再忙等待,线程在调用transfer时,会不停的去调用apply方法,一直在忙,一直在调用CPU,但是却没有进展,这就是忙等待。 我们还可以优化一下: public Class Allocator{ private static final Allocator INSTANCE = new Allocator(); public static Allocator getInstance() { return INSTANCE; } private Allocator() { als = new ArrayList<>(); } private List<Object> als; synchronized void apply(Object from,Object to){ while(als.constains(from)||als.constains(to)){ try{ wait(); }catch(Exception e){ } als.add(from); als.add(to); } } synchronized void free(Object from,Object to){ als.remove(from); als.remove(to); notifyAll(); } } public Class Account{ //这里假设actr是个单例, private Allocator actr; = Allocator.getInstance; private int balance; void transfer(Account actr,int amt){ actr.apply(from,to); try{ synchronize(this){ synchronized(from){ if(this.balance>amt){ this.balance-=amt; from.balance+=amt; } } } }finally{ atcr.free(this,from); } } } 这是修改后的方法 将Allocator里的apply方法改为了void,if变为while,并且在里面用了wait,然后free也使用了notifyAll而Account也不用while去调用apply方法了,而是直接调用apply,只调用一次。我们来详细讲讲 首先wait和notifyAll是个什么? 1.它和notifyAll以及notify得在synchronized临界区里面调用,否则jvm会报错 2.执行wait之后,会释放当前持有的锁,然后进入等待队列。 3.notifyAll会通知唤醒所有等待队列中的线程,而notify则是随机通知唤醒一个线程 4.等待队列中的线程被唤醒后,会在当前wait的代码行被唤醒,继续执行代码逻辑。 我们来执行一遍代码的逻辑,例如线程T1执行账户A向账户B转账。账户A调用transfer方法,然后去获取锁,执行一次apply方法,apply里去执行while方法,去获取锁A和锁B的使用权,如果当前无法同时获取锁A和锁B的使用权,那就执行wait方法,进入等待队列,随后等待被唤醒,线程T1被唤醒后,继续往下执行while里的apply,然后又要重新去获取锁,,但是发现又没有使用权,于是继续睡(这也是为什么要把if改为while,因为线程被唤醒!=线程拿到使用权,因为线程被唤醒之后,要重新去抢apply方法的锁)。直到拿到两个锁的使用权,然后去获取两个锁,执行转账逻辑,最后释放两个锁,然后把两个锁的使用权也释放掉,然后执行notifyAll方法唤醒等待队列里的线程。
学习笔记 java如何解决可见性和有序性?我们都知道了,可见性和有序性的问题就是CPU缓存问题以及编译优化导致的执行重排序问题,那么怎么解决可见性问题和有序性的问题也很明了了,把CPU缓存以及编译优化全禁用了不就行了?确实,禁用确实性,但是我们的性能就糟糕了。所以我们不能盲目的禁用,要按需的禁用。 java自然提供了方法,让我们得以按需禁用,比如说volatile。volatile在C语言中也有,它的原始的意义就是禁用CPU缓存。但当我们秉着volatile禁用CPU缓存的这个认知去学习代码的时候,也会遇到一些困惑,比如说下列代码: class Example { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 这里x会是多少呢? x=? } } } 线程A执行writer方法,将v的改动写入内存,然后线程b执行reader方法,从内存中获取到v==true,但是在再去读取x的值时,x的值是多少呢?x没有使用volatile声明,它是不是就没有禁用缓存呢?这两个疑问,我先回答第一个,第二个疑问后续会解答的。x的值无非就两种,要么是42,要么是0,这取决于java的版本,如果java在1.5之前,那么即可能是42,也可能是0,如果java在1.5之后呢,就是42了。 为什么?因为java1.5之后对volatile进行了增强,使用了happens-before规则。什么是happens-before规则?简单来说,A happens-before B,意思就是A的操作对于B的操作是可见的。对于我们程序员来说,我们需要知道的happens-before规则,共有六项,下面就来详细讲解,也会去解答上面代码的volatile疑问。 第一项规则:程序的顺序性规则。 这个很好理解。在单线程的前提下,按照程序的执行顺序,前面执行的操作,对于后面执行的操作是可见的。这基本就符合我们编写代码时的直觉了,毕竟在单线程情况下不用考虑那么多的。 第二项规则:volatile规则。 假如一个变量v被volatile声明了 ,那么对变量A的操作就会禁用内存,那么就会实现线程A对v的操作 happens-before 线程B对v的操作,就好像上述代码中的一样,线程A调用方法writer,修改了变量v的值,线程B调用reader方法去读取变量v的值时,是可见的,线程B能读取到v最新的值。到这,可能还是有小伙伴疑惑,那这跟上面代码的x有什么关系吗?到底为什么线程B能读到变量x的最新值42?别急,我们看第三项规则。 第三项规则:传递性 简单来说,A happens-before B 且 B happens-before C,那么A happends-before C成立。 直接使用上面代码的例子说明。根据第一项规则,对于线程A来说,它执行writer方法时,先执行操作x=42,然后执行操作v=true,那么线程A执行x=42这一操作对于线程A去执行v=true这一操作时,是可见的,即线程A执行x=42 happens-before 线程A执行v=true.然后线程B执行reader方法,根据第二项规则,v使用volatile声明,所以线程A执行v=true这一操作对于线程B去进行读取v的操作时,是可见的,即 线程A执行v=true happens-before 线程B读取v。最后,根据第三项规则,线程A执行x=42这一操作,线程B也能看到!也就是说线程A执行x=42 happens-before 线程B操作x。现在来回答“x没有使用volatile声明,它是不是就没有禁用缓存呢?”这个疑问,x其实并没有被禁用缓存,那么它怎么被可见的呢?是强制将缓存中x的值刷新到内存里了。 第四项规则:管程中锁的规则 什么是管程?管程是一种同步原语,synchronized是在java对管程的实现,在这个阶段,我们先把管程当成synchronized即可。 这个规则也符合我们的直觉,理解起来应该不难。如下列的例子 synchronized (this) { // x是共享变量,初始值=10 if (this.x < 12) { this.x = 12; } } 线程A拿到锁之后,对x进行了读取并修改,然后自动释放锁,线程B又拿到了这个锁,然后读取x时,能看到线程A对x的修改,也就是说它能读到x=12。这就是规则4,符合我们的直觉,不难理解。 第五项规则:线程的start规则 主线程要使用start去开启一个子线程去执行任务,在start之前,主线程对变量的操作,子线程是可见的。比如说: Thread B = new Thread(()->{ // 主线程调用B.start()之前 // 所有对共享变量的修改,此处皆可见 // 此例中,var==77 }); // 此处对共享变量var修改 var = 77; // 主线程启动子线程 B.start(); 第六项规则:线程的join规则 子线程在执行任务时,主线程使用join方法等待子线程的返回,如果join方法成功返回,那么在join方法后,主线程能看到子线程对共享变量的操作,如: Thread B = new Thread(()->{ //此处可以读取到主线程对var的操作 var == 77; // 此处对共享变量var修改 var = 66; }); // 例如此处对共享变量修改, // 则这个修改结果对线程B可见 var =77; // 主线程启动子线程 B.start(); B.join() // 子线程所有对共享变量的修改 // 在主线程调用B.join()之后皆可见 // 此处主线程可以读取到子线程对var的操作 var == 66;
学习笔记 因为IO操作执行慢,我们就发明了多进程。这是怎么回事呢?在使用一个CPU时,当一个进程执行IO操作时,这个进程就会标记自己为休眠状态,此时就会执行任务切换(也可以说进程切换),将CPU的使用权交给其他进程,待IO操作执行完毕之后,操作系统会把这个进程唤醒,这个进程就有机会重新获得CPU使用权了,这就是多进程。多进程使得我们可以一边听歌,一边写代码。 但是,多个进程之间是不共享内存空间的,所以当我们进行任务切换(也可以说进程切换)时,需要切换内存映射地址,成本高。 于是我们又有了多线程,同一个进程的多个线程是共享内存空间的,所以使用线程去做任务,线程进行切换的时候,不需要切换内存映射地址,成本就低,所以我们现在用的基本都是多线程,任务切换也指的是线程切换。 又但是,线程切换回导致原子性的并发问题。我们现在基本上都是使用高级编程语言,它的一句语法可能是由多个CPU指令组成的,比如说count++,它是由三个CPU指令去执行的,CPU指令1:先将count变量从内存加载到缓存再加载到CPU寄存器。CPU指令2:然后寄存器去执行count+1操作。CPU指令3:最后将结果写进内存(也有可能写入缓存,因为缓存机制的存在)。 那么问题来了,我们的原子性是基于CPU指令的,CPU保证的原子操作是CPU指令级别的,也就是说,它只能保证CPU指令1或者指令2或者指令3能够完整的运行。又换句话说,线程切换是可以发生在执行完CPU指令1之后,执行CPU指令2之前的,这就是原子性问题了,它也会导致count加了两次,但是结果为1; 线程A执行指令1,从内存中读取到count=0。此时线程切换到线程B,线程B也读取到count=0,但是完整的执行了三个指令操作,返回给内存count=1。然后线程又切换回A,A继续执行操作,但是它任然是对count=0执行后面两条指令的操作,最后得到的结果也是count=1,返回给内存。但是我们期望的结果是2而不是1;
学习笔记 在单核时代,多个线程在一个CPU上执行,每个线程都是对一个CPU里的这个缓存进行操作,一个线程对这个缓存进行的操作,另一个线程一定是可见的,因为它们操作的是同一片缓存。 一个线程对共享变量的修改,另一个线程能立刻看到,这就是可见性。 但在多核时代,多个线程在多个CPU上执行,每个线程可能操作着不同的CPU里的缓存。CPU去修改变量时,先从内存里加载变量到寄存器里进行操作,然后写进它的缓存里,再找个时机写进内存中。每个CPU都有各自的缓存,不同的线程操作不同的缓存时,看不到另一个线程操作的缓存,这就是可见性问题。 比如说,两个线程同时执行count++的操作,线程A使用CPU-1,线程B使用CPU-2,两个线程同时在内存里加载了count=0到不同的缓存中,然后执行了+1操作,此时两个缓存中的count都是=1,如果此时再同时写进内存,那么count的值就是=1,明明加了两次。 线程A和线程B都在各自CPU的缓存里自己搞自己的,互不可见,如果线程A和线程B都各自在自己的缓存中执行一万次count++的操作,那最后得到的count的结果不是20000,而是10000-20000之间(为什么是10000-20000之间?因为两个线程中,可能有一个线程会从内存里读到的count的值是另一个线程操作了n次后返回给内存的),可是,我明明对count进行20000次的+1操作。这就是可见性问题导致的。
