当前位置: 首页 > wzjs >正文

jsp网站开发简单代码深圳seo优化电话

jsp网站开发简单代码,深圳seo优化电话,昆山网站建设ikelv,h5手机模板网站一、引言 想象一下这样的场景:你的程序在单线程环境下完美运行,但在多线程条件下却时而崩溃、时而产生错误结果,甚至出现"只在生产环境发生"的诡异问题。这些难以复现、难以调试的问题,往往源于并发编程中的三大核心挑…

一、引言

想象一下这样的场景:你的程序在单线程环境下完美运行,但在多线程条件下却时而崩溃、时而产生错误结果,甚至出现"只在生产环境发生"的诡异问题。这些难以复现、难以调试的问题,往往源于并发编程中的三大核心挑战:可见性原子性有序性问题。本文将深入探讨并发编程中的这三大核心问题,揭示它们背后的原理,并提供实用的解决方案。

二、缓存导致的可见性问题

在单核时代,所有的线程都是在一颗 CPU 上执行,CPU 缓存与内存的数据一致性容易解决。因为所有线程都是操作同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来 说一定是可见的。例如在下面的图中,线程 A 和线程 B 都是操作同一个 CPU 里面的缓 存,所以线程 A 更新了变量 X 的值,那么线程 B 之后再访问变量 X,得到的一定是 X 的最新值(线程 A 写过的值)。

一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性

多核时代,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易 解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。比如下图中,线程 A 操作的是 CPU-1 上的缓存,而线程 B 操作的是 CPU-2 上的缓存,很明 显,这个时候线程 A 对变量X 的操作对于线程 B 而言就不具备可见性了。

通过上图我们可以清晰的看见在多核时代,我们不同的线程操作同一变量的时候都会先加载到自己缓存里面去,但是每个线程从缓存都是私有的,这就导致了我们的可见性问题。

三、线程切换带来的原子性问题

由于 IO 太慢,早期的操作系统就发明了多进程,即便在单核的 CPU 上我们也可以一边听 着歌,一边写 Bug,这个就是多进程的功劳。

Java 并发程序都是基于多线程的,自然也会涉及到任务切换,也许你想不到,任务切换竟 然也是并发编程里诡异 Bug 的源头之一。任务切换的时机大多数是在时间片结束的时候, 我们现在基本都使用高级语言编程,高级语言里一条语句往往需要多条 CPU 指令完成,例 如上面代码中的count += 1,至少需要三条 CPU 指令。

  • 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
  • 指令 2:之后,在寄存器中执行 +1 操作;
  • 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

操作系统做任务切换,可以发生在任何一条CPU 指令执行完,是的,是 CPU 指令,而不 是高级语言里的一条语句。对于上面的三条指令来说,我们假设 count=0,如果线程 A 在 指令 1 执行完后做线程切换,线程 A 和线程 B 按照下图的序列执行,那么我们会发现两个 线程都执行了 count+=1 的操作,但是得到的结果不是我们期望的 2,而是 1。

我们潜意识里面觉得 count+=1 这个操作是一个不可分割的整体,就像一个原子一样,线 程的切换可以发生在 count+=1 之前,也可以发生在 count+=1 之后,但就是不会发生在 中间。我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方。

四、编译优化带来的有序性问题

有一个经典的案例就是利用双重检查创建单例对象,例如下面的代码:在获取实 例 getInstance() 的方法中,我们首先判断 instance 是否为空,如果为空,则锁定 Singleton.class 并再次检查 instance 是否为空,如果还为空则创建 Singleton 的一个实例。

public class Singleton {static Singleton instance;static Singleton getInstance(){if (instance == null) {synchronized(Singleton.class) {if (instance == null)instance = new Singleton();}}return instance;}
} 

假设有两个线程 A、B 同时调用 getInstance() 方法,他们会同时发现 instance == null ,于是同时对 Singleton.class 加锁,此时 JVM 保证只有一个线程能够加锁成功(假 设是线程 A),另外一个线程则会处于等待状态(假设是线程 B);线程 A 会创建一个 Singleton 实例,之后释放锁,锁释放后,线程 B 被唤醒,线程 B 再次尝试加锁,此时是可以加锁成功的,加锁成功后,线程 B 检查 instance == null 时会发现,已经创建过 Singleton 实例了,所以线程 B 不会再创建一个 Singleton 实例。

理想很完美,但是现实是残酷的,实际上这个 getInstance() 方法并不完美。问题出在 哪里呢?出在 new 操作上,我们以为的 new 操作应该是:

  1. 分配一块内存 M;
  2. 在内存 M 上初始化 Singleton 对象;
  3. 然后 M 的地址赋值给 instance 变量;

但是实际上优化后的执行路径却是这样的:

  1. 分配一块内存 M;
  2. 将 M 的地址赋值给 instance 变量;
  3. 最后在内存 M 上初始化 Singleton 对象;

我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方 法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。

五、可见性和有序性问题解决方案

通过上面的介绍,我们已经知道了,导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接的办法就是禁用缓存和编译优化,但是这样问题虽然解决了,我们程序的性能可就堪忧了。

我们的Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及 Happens-Before 规则。

5.1 volatile关键字

例如,我们声明一个 volatile 变量 volatile int x = 0,它表达的是:告诉编译器, 对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。

例如下面的示例代码,假设线程 A 执行 writer() 方法,按照 volatile 语义,会把变量 “v=true” 写入内存;假设线程 B 执行 reader() 方法,同样按照 volatile 语义,线程 B 会从内存中读取变量 v,如果线程 B 看到 “v == true” 时,那么线程 B 看到的变量 x 是 多少呢?

class VolatileExample {int x = 0;volatile boolean v = false;public void writer() {x = 42;v = true;}public void reader() {if (v == true) {// 这里 x 会是多少呢?}}
}

直觉上看,应该是 42,那实际应该是多少呢?这个要看 Java 的版本,如果在低于 1.5 版 本上运行,x 可能是 42,也有可能是 0;如果在 1.5 以上的版本上运行,x 就是等于 42。

5.2 Happens-Before 规则

Happens-Before 关系并不完全等同于时间上的先后顺序,而是可见性的保证:如果操作 A happens-before 操作 B,那么 A 对共享变量的修改对 B 是可见的。

5.2.1 程序顺序规则
  • 在同一个线程中,按照程序代码的顺序,前面的操作 happens-before 后面的操作

  • 这只是针对单个线程内的执行,且允许编译器和处理器进行指令重排序(只要不影响单线程执行结果)

5.2.2 锁规则
  • 对一个锁的解锁 happens-before 于随后对这个锁的加锁

synchronized (lock) {// 线程A在这里的修改
} // 解锁// 其他线程
synchronized (lock) { // 加锁// 能看到线程A的所有修改
}
5.2.3 volatile变量规则
  • 对一个volatile变量的写操作 happens-before 于后续对这个volatile变量的读操作

volatile boolean flag = false;// 线程A
flag = true; // 写操作// 线程B
if (flag) { // 读操作// 保证能看到flag=true
}
5.2.4 线程启动规则
  • 线程A启动线程B,那么线程A在启动线程B之前的操作 happens-before 线程B中的任何操作

int x = 10;Thread t = new Thread(() -> {// 这里能看到x=10
});
x = 20;
t.start();
5.2.5 线程终止规则
  • 线程A等待线程B终止(通过Thread.join()),那么线程B中的所有操作 happens-before 线程A从join()返回

Thread t = new Thread(() -> {// 一些操作
});
t.start();
t.join();
// 这里能看到线程t中的所有操作结果
5.2.6 传递性规则
  • 如果A happens-before B,且B happens-before C,那么A happens-before C

5.3 Happens-Before 的实际意义

  1. 解决可见性问题:确保一个线程的修改对其他线程可见

  2. 限制指令重排序:编译器/处理器不能优化掉具有happens-before关系的操作

  3. 简化并发编程:开发者可以基于这些规则推理多线程程序的行为

六、小结

并发编程的复杂性主要来源于 可见性、原子性、有序性 问题,理解它们的成因和解决方案是写出正确、高效多线程代码的关键。Java 提供了 synchronizedvolatileAtomic 类等机制来应对这些问题,但正确使用它们需要深入理解内存模型和并发原理。

http://www.dtcms.com/wzjs/229627.html

相关文章:

  • 经营性网站备案申请网络推广十大平台
  • 食堂承包技术支持 东莞网站建设百度top排行榜
  • 一家专门做特产的网站南宁百度seo排名优化软件
  • 烟台装修公司网站建设搜索引擎是什么
  • 洞口做网站多少钱网站关键词免费优化
  • 温州哪里可以做企业网站seo网页优化工具
  • 高端网站创建市场营销公司
  • 做网站一定要后台嘛东莞推广平台有哪些
  • 怎么做好营销型网站站长统计ios
  • 网站做优化需要哪些后台信息站长之家网站查询
  • 郑州网站建设哪家公司便宜谷歌浏览器下载手机版安卓官网
  • 网站频道规划网站快速排名推广软件
  • 北仑区生态湿地建设管委会网站公司网站开发费用
  • 武汉北京网站建设品牌营销策略论文
  • 网站建设相关新闻优化设计高中
  • 母婴用品购物网站制作市场营销活动策划方案
  • 公司网站页面百度高级搜索入口
  • 网站建设 翰臣科技公司网站seo关键词优化技巧
  • 外贸公司如何接单seo排名快速刷
  • 传媒公司 网站开发环球资源网官方网站
  • 网站已经备案更换主机郑州聚商网络科技有限公司
  • 龙岩做网站百度推广渠道代理
  • 免费营销型网站模版安徽seo人员
  • 企业网站建站费用学计算机哪个培训机构好
  • 你愿不愿意做我女朋友网站长沙官网seo收费
  • 外贸网站空间哪个好深圳宝安seo外包
  • 做阿里巴巴网站口碑超级优化大师下载
  • 网站建设顺德全网网站快速排名推广软件
  • 设计网站banner图片seo网站推广全程实例
  • 中信建设证券有限责任公司企业网站seo方案