面试题19解析-线程池(下)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 本文阅读大概需要15分钟。

在多线程开发中,经常需要多线程公用一个变量、对象或资源,我相信大多数开发者可以使用synchronized、volatile、ConcurrentHashMap等一些线程同步控制方式来开发多线程应用,Doug Lea大师的确为我们开发了很多Java并发控制工具,使得多线程的变得容易,但是如果我们仅仅只会用,而不明白其中原理,往往同步控制,写出的程序性能低下,遇到问题,并发程序难于调试,打击信心,进度拖延。因此,对于原理的理解,才是一个程序员能够立足于快速技术迭代的IT潮流下的资本,是真正的内功。扯远了,话题转回来,在讨论Java并发程序的共享问题之前,我们不得不先来看看Java的内存模型——JMM(Java Memory Module)。


一Java内存模型(JMM)


Java内存模型(JMM)是Java多线程共享变量的控制机制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。这里可能就有疑问了,既然是共享变量,一个线程对于共享变量的更改对于其他线程不应该是立即可见的吗?之所以有这样的疑问,是因为不了解JMM下多线程对于共享变量操作的规则。JMM共享变量操作结构图如下:


image.png


在JMM模型中,线程的运行都拥有各自线程的内存存储区域,抽象为线程的本地内存(Local Memory),注意这个本地内存概念是一个抽象概念,并不是真的就是计算机内存,他指的是线程运行状态下,存储变量的所有位置(计算机内存、缓存等)。通过上图可以看到,线程的共享变量是保存在主存中的,线程的本地内存在线程初始化过程中只是保存了共享变量的副本,并在运行过程中操作的是该副本变量,并不是主存中的变量。图中,线程1与线程2的本地内存都存储了共享变量B的副本,但它们却不是同一变量,也就是说两个线程对各自副本变量的操作对于另一线程来说是不可见的,而保证共享变量B对于两个线程的可见性的唯一方法,就是需要将变量副本与主存中的共享变量进行同步操作,而至于何时将副本刷入主存就是由JMM规则控制的。


二volatile线程可见性


在Java中关键字volatile控制了多线程变量可见性,当线程操作对volatile变量进行写操作时,JMM会将该线程的本地内存中的副本变量刷新到主存中去;当线程对volatile变量进行读操作时,线程先将本地内存共享变量设置为无效,线程就会从主存中读取共享变量的值了。JMM下volatile共享变量的操作就变成了下图模型:


image.png


从实质来说,共享变量的副本就是主存中变量的缓存,与软件开发系统中的缓存原理相同,在缓存失效时,缓存系统便会从持久存储中读取最新的变量值,当缓存值修改后,缓存系统会在合适时机将新值同步回持久存储中。JMM之所以在线程本地内存中使用副本,也是为了使线程对变量访问更快。从计算机体系的角度来看,线程的本地内存则很多时候是保存在CPU的缓存中的,CPU缓存速度远快于系统内存(因为CPU访问内存需要通过主板的总线系统,而CPU缓存就在CPU的内部,处理器可以直接访问,因此更快),所以说java中volatile的规则的是底层CPU缓存与内存之间进行数据同步的操作来实现的。


这里针对网络中有些对volatile内存语义解释为:“volatile变量会促使线程直接操作主存中的变量”的说法,这里进行辩证说明一下:本作者认为该说法不严密,在处理器与内存之间没有任何缓存介质时,该命题成立。如果处理器直接操作内存,需要获取BUS总线锁,在BUS总线锁被占用的过程中,其他处理器无法对内存进行任何操作,是一种低效的内存控制技术,随着处理器的发展,CPU增加了缓存系统,只有CPU缓存失效或缓存未命中时,处理器才会从内存读取数据,不同的处理器都担保了处理器缓存与内存变量之间的同步的机制,因此这里只可以认为是“等效”于直接操作主存变量,并不一定是直接操作。


对于volatile例子:我们经常通过主线程与子线程之间通过一个布尔变量isRunning来控制子线程退出的时机,在JVM -server运行模式下,如果isRunning没有声明为volatile变量,子线程会一直在私有堆栈中读取isRunning变量,从而导致子线程一直无法退出。在《Effective JAVA》中,该现象被称为“活性失败”。这个例子,也说明了volatile使用的重要性,有些程序员会错误认为,isRunning变量迟早会被同步,的确,两个变量迟早会被同步,认为没有必要用volatile修饰,但是却错误估计了该情况下同步需要的时间有可能与线程的生命周期相同。


有些Java初学者,一直分不清可见性与锁的区别。锁控制的是代码块的同步,控制的是指令的执行顺序,而可见性指的是处理器确保共享变量在不同线程之间的可见,可见性不能保证共享变量操作的原子性,两者解决的问题不同,面对的对象也不同。例如共享volatile变量i++操作,volatile可以确保变量i的值多线程可见,但却不能保证i++是原子操作,因为i++实际上也是由三个原子操作组成:read i、inc、 write i,在多线程环境下,依然可能产生对变量i的脏读,脏写现象。因此只有读者明白JMM可见性产生的原因,JMM共享变量的结构,才能理解并发编程中可见性的作用。


那么对于变量的原子操作是不是必须加锁?不一定,这里建议读者参看AtomicInteger类的实现,其使用无锁CAS操作确保了volatile变量的原子操作。


三为什么CPU需要缓存


CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存。缓存大小是CPU的重要指标之一,而且缓存的结构和大小对CPU速度的影响非常大,CPU内缓存的运行频率极高,一般是和处理器同频运作,工作效率远远大于系统内存和硬盘。实际工作时,CPU往往需要重复读取同样的数据块,而缓存容量的增大,可以大幅度提升CPU内部读取数据的命中率,而不用再到内存或者硬盘上寻找,以此提高系统性能。但是从CPU芯片面积和成本的因素来考虑,缓存都很小。


image.png


按照数据读取顺序和与CPU结合的紧密程度,CPU缓存又可以分为一级缓存,二级缓存,部分高端CPU还具有三级缓存,每一级缓存中所储存的全部数据都是下一级缓存的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。一般来说,每级缓存的命中率大概都在80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取,由此可见一级缓存是整个CPU缓存架构中最为重要的部分。


既然缓存是好动西,那为什么不把数据都放入缓存?造价问题,CPU缓存造价远高于内存,这和内存造价远高于硬盘一个道理。

相关文章
|
19天前
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
98 0
|
2天前
|
缓存 前端开发 JavaScript
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
13 1
|
2月前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
115 29
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
6天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
8 1
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
334 37
|
2月前
|
缓存 Java 应用服务中间件
Java虚拟线程探究与性能解析
本文主要介绍了阿里云在Java-虚拟-线程任务中的新进展和技术细节。
|
2月前
|
缓存 Android开发 开发者
Android RecycleView 深度解析与面试题梳理
本文详细介绍了Android开发中高效且功能强大的`RecyclerView`,包括其架构概览、工作流程及滑动优化机制,并解析了常见的面试题。通过理解`RecyclerView`的核心组件及其优化技巧,帮助开发者提升应用性能并应对技术面试。
65 8
|
2月前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
63 8
|
26天前
|
安全 Java 数据库连接
Python多线程编程:竞争问题的解析与应对策略
Python多线程编程:竞争问题的解析与应对策略
14 0
|
26天前
|
安全 Java 数据库连接
Python多线程编程:竞争问题的解析与应对策略【2】
Python多线程编程:竞争问题的解析与应对策略【2】
12 0

推荐镜像

更多