一切福田,不離方寸,從心而覓,感無不通。

Category Archives: Java

spring boot服务docker镜像优化之旅

缘由 我们在使用spring boot开发的服务中,一般会选择打包成单体的fatjar来发布服务,这在传统的部署方式下是非常方便的,但是当我们选择使用docker这种容器化的方式来部署应用的时候,却有一点点的不便之处,因为这个单体的jar一般都比较大,每次镜像push到仓库和从仓库拉取都需要比较长的时间。 原因是什么了? docker的一大特色就是镜像的存储是分层的,参考下面这张官图 我们在Dockerfile中的每一个指令会对应到镜像的每一层,docker在更新镜像时,只会推送变更过的层,当它计算出来这一层的摘要和之前的版本一致时,会复用上一次打包镜像时的缓存,会极大的提高打包镜像以及镜像push/pull操作的速度。 那么问题来了,当我们springboot打包出来的单体jar的时候,每次编译这个jar都会发生变化,对应的存储层也会发生变化,push和pull操作时都需要重新推送,而且这个jar一般都不小,一个典型的应用会在100M左右,对应用部署和发布的速度会有比较大的影响。 稍作思考,很容易就能发现这个肥大的jar文件里面,大部分其实都是固定不变的各种依赖库,我们真正每次编译会变化的业务代码部分其实很小很小,可能也就只有几百KB,只要能将这两部分分离,变成docker镜像中的两层,一定能极大的提升镜像发布的速度 牛刀小试 首先拿来动手尝试的是一个springboot admin的项目,项目的结构是这样的: 使用最常见的打包方式:

会生成一个32M的jar文件,优化之前的Dockerfile非常简单:

  可以看到这种方式在构建v2版本的镜像时,会重新copy整个完整的jar 如果要拆开这个单体的jar,有两种方式,一是修改mvn打包的配置,将lib包放在独立的文件夹下,在这里我们考虑到项目众多,尽量减少修改,选择了在Docker打包镜像时,解压打包出来的jar包,将其中的内容分开来copy,修改后的Dockerfile如下:

  看看修改后的效果: 在copy lib目录时,是直接using cache的。来看看push的时候效果对比 首先是优化前的push: 可以看到在push v2的时候还是会push一个33MB的层,虽然其实我们一行代码没有修改。 然后是优化后的: 可以看到这一次仅仅只推送了一个13KB的层,推送的速度快了非常多,同理也可以想象的到,我们在拉取镜像更新版本时速度会快很多。 路遇荆棘 在针对springboot-admin这个最简单的项目的优化取得很好的效果之后,就开始准备照搬到其他的项目中,没想到同样的方式怎么折腾都无效,分离之后的lib目录依然会每次需要全量重新push。出问题的项目结构大概是这样的: 一个常见的多模块mvn项目,有common,domain,rest-client,rest-server 这4个子模块,其中rest-server会依赖common和domain这两个子模块,打包出来的jar是在rest-server这个模块中。 究竟是什么鬼了? 终得正果 苦苦思索一番之后,lib目录既然不能复用上一次的cache,那一定是因为里面的内容有变化,遂将jar包解压,进到lib目录,真凶果然在此: 项目自身的3个子模块在每次编译的时候也会做为jar包放到lib目录下,这3个jar包每次编译都会有变化,所以导致这一层的cache失效。 找到问题之后,解决的思路就很简单了,将这种jar单独copy到一个目录下即可,修改后的Dockerfile如下:

  这样修改之后效果就和上面单模块的项目一样了,至此,基本完成了springboot项目的docker镜像优化,在jenkins的流水线上可以将原来镜像push的时间从1分钟以上优化到10s左右 未来之路 在整个优化的过程中,发现springboot2.3 M1版本已经有针对性的优化方案,增加了LAYERED_JAR的打包格式,未来可期。 具体可参考下文: www.jdon.com/53738 注: 本文中举例的两个项目案例,可在github上找到:github.com/yishh/sprin… 作者:thor_lee 链接:https://juejin.cn/post/6844904119338008583 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

龙生   18 Jun 2021
View Details

docker springboot项目镜像优化

遇到的问题: 公司java项目按老的方式打包出来一个fat jar,100MB, 推送到腾讯云镜像仓库很慢,8分钟。。。走的是公网,专线暂时还没配置好 以前是内网harbor,速度还不明显。 归根究底,一次推送100MB是个不合理的事情 思路: 了解spring boot打包,期望将依赖的libs 和 业务代码拆分 优化dockerfile,充分利用缓存   解决问题: 先修改spring-boot-maven-plugin,只打包业务代码。网上有些是配置exclude,我试了,恶心到了。。那么多包挨个找

  新增maven-dependency-plugin,将依赖移到libs目录下

  优化dockerfile 参考 https://medium.com/@nieldw/caching-maven-dependencies-in-a-docker-build-dca6ca7ad612 ,牛逼

  搞定。最终代码变化每次推送也就1MB多 启动命令 java -Dloader.path="libs/" -jar app.jar 作者:小猋_a8f1 链接:https://www.jianshu.com/p/32456eea0488 来源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

龙生   18 Jun 2021
View Details

Guava 快速入门(一)

Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如:集合 [collections] 、缓存 [caching] 、原生类型支持 [primitives support] 、并发库 [concurrency libraries] 、通用注解 [common annotations] 、字符串处理 [string processing] 、I/O 等等。   Guava 是Java的工具集,提供了一些常用的便利的操作工具类,减少因为 空指针、异步操作等引起的问题BUG,提高开发效率。 本文主要介绍了Guava常用的工具方法,快速入门Guava。 1、基本工具(Base utils) 1. Optional null 值出现在代码中,有如下缺点: 语义模糊,引起歧义。例如,Map.get(key)返回Null时,可能表示map中的值是null,亦或map中没有key对应的值。 在应用层面可能造成混乱,出现令人意外的错误。 为了尽量避免程序中的null值,guava提供了Optional对数据进行封装。如果值为空则立即抛出异常,并且提供了Absent和Present两个子类分别表示值缺失和值存在的情形,来增强null的语义。 常用方法如下: isPresent():如果Optional包含非null的引用(引用存在),返回true get() :如果Optional为NULL将触发异常

or(defaultvalue) :包含的引用缺失(null),返回默认的值,否则返回本身 orNull():包含的引用缺失,返回null asSet():如果引用存在,返回只有单一元素的集合;若为NULl返回空集合 2. 先决条件 Preconditions Preconditions 提供了判断条件是否合法的静态方法,如果不符合要求会抛出异常。类似断言。 方法声明(不包括额外参数) 描述 检查失败时抛出的异常 checkArgument(boolean) 检查boolean是否为true,用来检查传递给方法的参数 IllegalArgumentException checkNotNull(T) 检查value是否为null,该方法直接返回value,因此可以内嵌使用checkNotNull NullPointerException checkState(boolean) 用来检查对象的某些状态。 IllegalStateException checkElementIndex(int index, int size) 检查index作为索引值对某个列表、字符串或数组是否有效。index>=0 && index<size IndexOutOfBoundsException checkPositionIndex(int index, int size) 检查index作为位置值对某个列表、字符串或数组是否有效。index>=0 && index<=size IndexOutOfBoundsException checkPositionIndexes(int start, int end, int size) 检查[start, end]表示的位置范围对某个列表、字符串或数组是否有效 IndexOutOfBoundsException 每个判断方法都有三个多态方法: 没有额外参数:抛出的异常中没有错误消息; 有一个Object对象作为额外参数:抛出的异常使用Object.toString() 作为错误消息; 有一个String对象作为额外参数,并且有一组任意数量的附加Object对象:这个变种处理异常消息的方式有点类似printf,但考虑GWT的兼容性和效率,只支持%s指示符。例如:

  3. […]

龙生   26 May 2021
View Details

想学Google Guava看这篇就够了

Guava是谷歌提供的一个核心Java类库,其中包括新的集合类型、不可变集合、图库,以及用于并发、I/O、Hash、缓存、字符串等的 实用工具。它在谷歌中的大多数Java项目中被广泛使用,也被许多其他公司广泛使用,熟练掌握这些工具类能帮助我们快速的处理日常开发中的一些问题,比如,不可变集合、集合的转换、字符串处理、本地缓存等 最近一段时间,我把Guava中常用到的工具类学了一遍,感觉有些工具类还是挺有用的,至少它帮你封装了很多功能,让你在处理一些逻辑的时候,不用太关注细节,把Guava的工具类直接拿来用就好了。下面我就介绍一下Guava中最常用的一些工具: 1、Guava不可变集合 不可变集合就是集合创建之后元素是不可改变的,主要用途如下: 不可变对象提供给别人使用时是安全的,因为不可变,所有人都无法进行修改,只能读 支持多个线程调用,不存在竞争的问题,天然支持多线程 不可变集合节省内存空间,因为不可变,集合空间在创建时就已经确定好了,不用考虑扩容等问题,内存利用率高 不可变集合可用于常量 Guava针对常用的集合类型List、Set、Map等都提供了不可变类型的集合 详细使用方法,可查看另一篇博客《Guava系列之不可变集合》 2、Guava新的集合类型 Guava提供了几种新的集合类型,补充了JDK中的集合类型 比如我们要统计List中某个元素出现的次数,如果使用JDK中的list就需要使用循环遍历进行统计,但使用了Guava的Multiset就可以直接统计出来元素出现的次数 再比如,我们要通过Map中的key查找value,通过value来查找值,也就是需要一个双向Map,如果使用JDK中的Map,我们需要维护两个Map,一个从key映射到value,另外一个从value映射到key,而且不管是新增还是修改Map中的元素,都要保持两个Map同步修改,维护成本太高了,使用Guava的BiMap可以通过一个Map轻松解决这个问题 更多新集合类型请查看《Guava系列之新的集合类型》 3、Guava超实用的集合工具类 JDK中集合的操作已经提供了很多工具类,比如基本的集合交集、并集、差集这些常用的操作,Guava中提供的工具类是对JDK的补充,在Guava中提供了静态的创建集合的方法,还有集合的很多操作,比如笛卡尔集、list反转、排列组合、Set转Map、Map的各种过滤等 新集合工具类的详细使用,请查看《Guava系列之超实用的集合工具类》 4、Guava本地缓存Cache Guava中的缓存是本地缓存的实现,与ConcurrentMap相似,但不完全一样。最基本的区别就是,ConcurrentMap会一直保存添加进去的元素,除非你主动remove掉。而Guava Cache为了限制内存的使用,通常都会设置自动回收 Guava Cache的使用场景: 以空间换取时间,就是你愿意用内存的消耗来换取读取性能的提升 你已经预测到某些数据会被频繁的查询 缓存中存放的数据不会超过内存空间 Guava Cache的详细使用方法,可查看《Guava系列之Cache》 5、Guava强大的String工具类 String是我们平时开发工作当中使用最频繁的类型, Guava提供了字符串的连接、分隔等操作,特别是字符串的匹配,那是相当强大,比如提取出字符串中的字母、数字、特殊字符等,可以从指定字符串中提取、删除、替换等操作 举个例子,提取“er 3j6o 3k ,)$ wt@ wr4576je ow3453535345irjew jwfel ” 字符串的字母,直接可以调用现成的方法 再比如,你需要将上述字符串中的数字全部移除或替换成其他字符,都有现成的方法,使用起来非常方便,只要你使用好了这些工具类, 可以大大提升你对字符串的处理效率 具体详细用法,请查看《Guava系列之强大的String工具类》 6、Guava限流RateLimiter 在互联网高并发场景下,限流是用来保证系统稳定性的一种手段,当系统遭遇瞬时流量激增时,可能会由于系统资源耗尽导致宕机。而限流可以把一小部分流量拒绝掉,保证大部分流量可以正常访问,从而保证系统只接收承受范围以内的请求,多余的请求给拒绝掉 我们常用的限流算法有:漏桶算法、令牌桶算法 Guava中的限流使用的是令牌桶算法,RateLimiter提供了两种限流实现 平滑突发限流(SmoothBursty) 平滑预热限流(SmoothWarmingUp) Guava RateLimiter的详细用法,请查看《Guava系列之限流RateLimiter》 7、Guava发布/订阅EventBus EventBus是Guava中实现的用于发布/订阅模式的事件处理组件,它是设计模式中观察者模式的优雅实现 EventBus是消息总线,它会根据消息的类型发送到指定的消息订阅者,当有消息没有订阅者接收时,会将消息发送给DeadEvent 关于EventBus的详细用法,请查看《Guava系列之EventBus》 以上是对最近学习Guava类库的一个总结,它包括了我们平常开发中最常用的一些组件工具类,熟练掌握这些工具类的使用方法,必然会让你的工作如虎添翼~   from:https://www.pianshen.com/article/57281487560/

龙生   26 May 2021
View Details

Google guava工具类的介绍和使用

概述 工具类 就是封装平常用的方法,不需要你重复造轮子,节省开发人员时间,提高工作效率。谷歌作为大公司,当然会从日常的工作中提取中很多高效率的方法出来。所以就诞生了guava。 guava的优点: 高效设计良好的API,被Google的开发者设计,实现和使用 遵循高效的java语法实践 使代码更刻度,简洁,简单 节约时间,资源,提高生产力 Guava工程包含了若干被Google的 Java项目广泛依赖 的核心库,例如: 集合 [collections] 缓存 [caching] 原生类型支持 [primitives support] 并发库 [concurrency libraries] 通用注解 [common annotations] 字符串处理 [string processing] I/O 等等。 使用 引入gradle依赖(引入Jar包)

  1.集合的创建

  创建不可变集合 先理解什么是immutable(不可变)对象 在多线程操作下,是线程安全的 所有不可变集合会比可变集合更有效的利用资源 中途不可改变

  这声明了一个不可变的List集合,List中有数据1,2,3,4。类中的 操作集合的方法(譬如add, set, sort, replace等)都被声明过期,并且抛出异常。 而没用guava之前是需要声明并且加各种包裹集合才能实现这个功能

  当我们需要一个map中包含key为String类型,value为List类型的时候,以前我们是这样写的

  而现在

  其他的黑科技集合

  2.将集合转换为特定规则的字符串 以前我们将list转换为特定规则的字符串是这样写的:

  把map集合转换为特定规则的字符串

  3.将String转换为特定的集合

  如果

  guava还可以使用 omitEmptyStrings().trimResults() 去除空串与空格

  将String转换为map

  4.guava还支持多个字符切割,或者特定的正则分隔

  关于字符串的操作 都是在Splitter这个类上进行的

  […]

龙生   26 May 2021
View Details

Apache Commons 工具集使用简介

转自:http://www.codeceo.com/article/apache-commons-tools.html Apache Commons包含了很多开源的工具,用于解决平时编程经常会遇到的问题,减少重复劳动。我选了一些比较常用的项目做简单介绍。文中用了很多网上现成的东西,我只是做了一个汇总整理。 一、Commons BeanUtils http://jakarta.apache.org/commons/beanutils/index.html 说明:针对Bean的一个工具集。由于Bean往往是有一堆get和set组成,所以BeanUtils也是在此基础上进行一些包装。 使用示例:功能有很多,网站上有详细介绍。一个比较常用的功能是Bean Copy,也就是copy bean的属性。如果做分层架构开发的话就会用到,比如从PO(Persistent Object)拷贝数据到VO(Value Object)。 传统方法如下: //得到TeacherForm TeacherForm teacherForm=(TeacherForm)form; //构造Teacher对象 Teacher teacher=new Teacher(); //赋值 teacher.setName(teacherForm.getName()); teacher.setAge(teacherForm.getAge()); teacher.setGender(teacherForm.getGender()); teacher.setMajor(teacherForm.getMajor()); teacher.setDepartment(teacherForm.getDepartment()); //持久化Teacher对象到数据库 HibernateDAO= ; HibernateDAO.save(teacher); 使用BeanUtils后,代码就大大改观了,如下所示: //得到TeacherForm TeacherForm teacherForm=(TeacherForm)form; //构造Teacher对象 Teacher teacher=new Teacher(); //赋值 BeanUtils.copyProperties(teacher,teacherForm); //持久化Teacher对象到数据库 HibernateDAO= ; HibernateDAO.save(teacher); 二、Commons CLI http://jakarta.apache.org/commons/cli/index.html 说明:这是一个处理命令的工具。比如main方法输入的string[]需要解析。你可以预先定义好参数的规则,然后就可以调用CLI来解析。 使用示例: // create Options object Options options = new Options(); // add t option, option is the command parameter, false indicates that // this parameter is not required. options.addOption(“t”, false, “display current time”); options.addOption("c", true, "country code"); CommandLineParser parser = new PosixParser(); CommandLine cmd […]

龙生   26 May 2021
View Details

什么是雪花ID?

为什么使用雪花ID 在以前的项目中,最常见的两种主键类型是自增Id和UUID,在比较这两种ID之前首先要搞明白一个问题,就是为什么主键有序比无序查询效率要快,因为自增Id和UUID之间最大的不同点就在于有序性。 我们都知道,当我们定义了主键时,数据库会选择表的主键作为聚集索引(B+Tree),mysql 在底层是以数据页为单位来存储数据的。 也就是说如果主键为自增 id 的话,mysql 在写满一个数据页的时候,直接申请另一个新数据页接着写就可以了。如果一个数据页存满了,mysql 就会去申请一个新的数据页来存储数据。如果主键是UUID,为了确保索引有序,mysql 就需要将每次插入的数据都放到合适的位置上。这就造成了页分裂,这个大量移动数据的过程是会严重影响插入效率的。 一句话总结就是,InnoDB表的数据写入顺序能和B+树索引的叶子节点顺序一致的话,这时候存取效率是最高的。 但是为什么很多情况又不用自增id作为主键呢? 容易导致主键重复。比如导入旧数据时,线上又有新的数据新增,这时就有可能在导入时发生主键重复的异常。为了避免导入数据时出现主键重复的情况,要选择在应用停业后导入旧数据,导入完成后再启动应用。显然这样会造成不必要的麻烦。而UUID作为主键就不用担心这种情况。 不利于数据库的扩展。当采用自增id时,分库分表也会有主键重复的问题。UUID则不用担心这种问题。 那么问题就来了,自增id会担心主键重复,UUID不能保证有序性,有没有一种ID既是有序的,又是唯一的呢? 当然有,就是雪花ID。 什么是雪花ID snowflake是Twitter开源的分布式ID生成算法,结果是64bit的Long类型的ID,有着全局唯一和有序递增的特点。 最高位是符号位,因为生成的 ID 总是正数,始终为0,不可用。 41位的时间序列,精确到毫秒级,41位的长度可以使用69年。时间位还有一个很重要的作用是可以根据时间进行排序。 10位的机器标识,10位的长度最多支持部署1024个节点。 12位的计数序列号,序列号即一系列的自增ID,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号。 缺点也是有的,就是强依赖机器时钟,如果机器上时钟回拨,有可能会导致主键重复的问题。 Java实现雪花ID 下面是用Java实现雪花ID的代码,供大家参考一下。

main方法,测试结果如下:

总结 在大部分公司的开发项目中里,雪花ID是主流的ID生成策略,除了自己实现之外,目前市场上也有很多开源的实现,比如: 美团开源的Leaf 百度开源的UidGenerator 有兴趣的可以自行观摩一下,那么这篇文章就写到这里了,感谢大家的阅读。 作者:java技术爱好者 链接:https://juejin.cn/post/6965510420387856398 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

龙生   25 May 2021
View Details

java 各种数据类型的互相转换

StringBuilder转化为String

整型数组转化为字符串

字符串转化为整形数组

字符串转化为字符数组

字符数组转化为字符串

字符数组转化为整型数组

整型数组转化为字符数组

整型数转化为字符串

字符串转化为整型数

  java类型转换 Integer String Long Float Double Date 1如何将字串 String 转换成整数 int? A. 有两个方法:

2 如何将整数 int 转换成字串 String ? A. 有叁种方法:

JAVA中常用数据类型转换函数 虽然都能在JAVA API中找到,整理一下做个备份。

数据类型 基本类型有以下四种:   int长度数据类型有:byte(8bits)、short(16bits)、int(32bits)、long(64bits)、 float长度数据类型有:单精度(32bits float)、双精度(64bits double) boolean类型变量的取值有:ture、false char数据类型有:unicode字符,16位 对应的类类型:Integer、Float、Boolean、Character、Double、Short、Byte、Long 转换原则 从低精度向高精度转换 byte 、short、int、long、float、double、char 注:两个char型运算时,自动转换为int型;当char与别的类型运算时,也会先自动转换为int型的,再做其它类型的自动转换 基本类型向类类型转换 正向转换:通过类包装器来new出一个新的类类型的变量 Integer a= new Integer(2); 反向转换:通过类包装器来转换 int b=a.intValue(); 类类型向字符串转换 正向转换:因为每个类都是object类的子类,而所有的object类都有一个toString()函数,所以通过toString()函数来转换即可 反向转换:通过类包装器new出一个新的类类型的变量 eg1: int i=Integer.valueOf(“123”).intValue() 说明:上例是将一个字符串转化成一个Integer对象,然后再调用这个对象的intValue()方法返回其对应的int数值。 eg2: float f=Float.valueOf(“123”).floatValue() 说明:上例是将一个字符串转化成一个Float对象,然后再调用这个对象的floatValue()方法返回其对应的float数值。 eg3: boolean b=Boolean.valueOf(“123”).booleanValue() 说明:上例是将一个字符串转化成一个Boolean对象,然后再调用这个对象的booleanValue()方法返回其对应的boolean数值。 eg4:double d=Double.valueOf(“123”).doublue() […]

龙生   25 May 2021
View Details

java类型转换常见的错误

类型转换虽然很简单,但是还是有些小细节要多注意。 String转化为int:

  String转化为Integer,可以如下所示。 也适用于int转化为Integer:

  注意:不管是使用Integer.parseInt(),还是使用Integer.valueOf()将字符串转换成数字, 如果是非数字的字符串,会报错:NumberFormatException: For input string: "" 另外,Integer类取值和 int 类型取值一致,取值范围是从-2147483648 至 2147483647(-231至 231-1) ,包括-2147483648 和 2147483647。 如果超过了这个范围,也会报错。比如Integer.valueOf("2147483648"),超过了Integer范围。因此会报错: For input string: "2147483648" 更安全的做法是,使用apache包的NumberUtils,如下: 注意:NumberUtils只处理整数,不能用来处理小数。

  String转BigDecimal:

  String转double :

  Double转化为int:

  其他类型转String:

  注意:当String.valueOf()的参数obj为null时,返回值是字符串"null"!!而不是null。 如果希望obj为null时,返回"",可以使用apache-commons-lang的包,如下所示:

  如果希望obj为null时,返回null,如下: ObjectUtils.toString(object,nullStr),第二参数nullStr表示,当object为null时,方法返回的值。

  Integer转double: 使用doubleValue()方法,或者 (double)强制转换。

  其他类型转Double:

  比较小数是否相等。 比较Double是否相等。比较BigDecimal是否相等。 如下所示:

  比较Double类型的大小:

  比较double类型的大小: 除了用BigDemical的compare()方法,可以直接用Double.doubleToLongBits()的结果值用==,>,<进行比较

  from:https://www.cnblogs.com/expiator/p/12602446.html

龙生   25 May 2021
View Details

Java并发编程:volatile关键字解析

volatile这个关键字可能很多朋友都听说过,或许也都用过。在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果。在Java 5之后,volatile关键字才得以重获生机。 volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情。由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用volatile关键字的场景。 以下是本文的目录大纲: 一.内存模型的相关概念 二.并发编程中的三个概念 三.Java内存模型 四..深入剖析volatile关键字 五.使用volatile关键字的场景 若有不正之处请多多谅解,并欢迎批评指正。 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/dolphin0520/p/3920373.html 一.内存模型的相关概念 大家都知道,计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。 也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。举个简单的例子,比如下面的这段代码: 1 i = i + 1; 当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。 这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题了。在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的)。本文我们以多核CPU为例。 比如同时有2个线程执行这段代码,假如初始时i的值为0,那么我们希望两个线程执行完之后i的值变为2。但是事实会是这样吗? 可能存在下面一种情况:初始时,两个线程分别读取i的值存入各自所在的CPU的高速缓存当中,然后线程1进行加1操作,然后把i的最新值1写入到内存。此时线程2的高速缓存当中i的值还是0,进行加1操作之后,i的值为1,然后线程2把i的值写入内存。 最终结果i的值是1,而不是2。这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。 也就是说,如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。 为了解决缓存不一致性问题,通常来说有以下2种解决方法: 1)通过在总线加LOCK#锁的方式 2)通过缓存一致性协议 这2种方式都是硬件层面上提供的方式。 在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。 但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。 所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。 二.并发编程中的三个概念 在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念: 1.原子性 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。 一个很经典的例子就是银行账户转账问题: 比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。 试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。 所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。 同样地反映到并发编程中会出现什么结果呢? 举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果? 1 i = 9; 假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。 那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。 2.可见性 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 举个简单的例子,看下面这段代码: 1 2 3 4 5 6 //线程1执行的代码 int i = 0; i = 10; //线程2执行的代码 j = i; 假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。 此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10. 这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。 3.有序性 有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码: 1 2 […]

龙生   19 May 2021
View Details
1 31 32 33 63