-
ROW_NUMBER() OVER(…) 虽然写法很复杂,但这确实只是一个普通函数(就像字符串转数字这样的函数),可以得出一个值 这个函数不会改变数据条数,它的作用是给每个数据记录增加一个字段,这个字段值就是函数得到的值 函数虽然不会改变查询结果条数,但会改变结果的顺序。会按照某个字段a(这个字段会重复)分组,函数的值就是在每个分组内部的排序值(1,2,3,4.。。) 比如:一群人在排队买东西。会自动以家庭为单位分成一段一段的“小组”,然后又在家庭内部按照年龄大小做了排序(比如年龄大人在前面,小孩跟在大人后面),这个函数值就是这个家庭内部排序值 按照上面的比喻,这个函数就是ROW_NUMBER() over (PARTITION BY 家庭编号 order by 年龄) 函数完整的样子:ROW_NUMBER() OVER(PARTITION BY… ORDER BY…)。为什么要很啰嗦的再加一个ROW_NUMBER()? 因为前面这个ROW_NUMBER()会进一步控制函数的特性,后面会讲解【不过我个人认为这个前缀确实很啰嗦。让我设计的话,我会去掉它。因为对于大部分人可能就只会用最普通的函数使用方式,不写时就默认为ROW_NUMBER()多好】 类比一个完整的查询 select *, ROW_NUMBER() over (PARTITION BY 家庭编号 order by 年龄 desc) as “家庭地位” from 人员表 这句话除了得到了所有人员的信息之外,还额外得到了一个字段"家庭地位"(这里就默认:年龄越大,家庭地位越高) 理解了函数的含义和用法,然后我们就可以利用函数,比如,只获取所有家庭地位最高的人,那么就可以在我们再包一层,得到: select * from( select *, ROW_NUMBER() over (PARTITION BY 家庭编号 order by 年龄 desc) as “家庭地位” from 人员表 ) t where t.家庭地位=1 引申 这样就可以解决类似:表T中 字段a会重复,但我们查询的结果又不想要重复的数据,并且要求只要其中最新的那一条 select * from( select *, ROW_NUMBER() over (PARTITION BY a order by create_time desc) as index from T ) t where t.index=1 或者topN问题:分组后,把每组前5个找出来 select * from( select *, ROW_NUMBER() over (PARTITION BY a order by create_time desc) as index from T ) t where t.index < 6 ROW_NUMBER() 这是一个控制 OVER函数特性的参数,而且不能省,over前面必须有一个。但并不是只有ROW_NUMBER()这一种。 假设在一共家庭中,有四个孩子,其中两个是双胞胎,年龄分别是5岁,7岁,7岁,9岁。 两个7岁的双胞胎 谁大谁小是很难说。理论上可以讲,两个孩子年龄是相同的。那么排队时,谁是老大谁是老二呢。 ROW_NUMBER():简单粗暴的做了自己的判断,哪条数据在前面,哪个就是老大(谁先生出来谁就是老大)。即便他们年龄一样(over函数值一样),它给这几个孩子定的家庭地位分别为4,3,2,1 DENSE_RENK():显得更公正一点:既然定好了按年龄排序,那么年龄相同,地位就是相同。它给四个孩子定的家庭地位分别为:3,2,2,1 RENK():比较特殊,它觉得那个5岁的孩子的家庭地位不应该是第3位。因为家里明明有4个孩子,它是最小的,两个并列第二之后,下一个应该是4(所以直接把3给跳过了)。所以它给的家庭地位分别是4,2,2,1 名称 下面这些都是这个函数的名称: 窗口函数,分析函数,分区函数,一般称为窗口函数(window function)。 一般关系型数据库都支持。由于属于比较高级的函数,都是在数据库不断完善的过程中增加的。比如mysql就是8.x之后才有。 和聚合函数对比 聚合函数:sum(), avg() 等统计函数 配合 group by 称为聚合函数 得出的是分组后 每一组等统计数据,改变了数据条数 如果想保持数据原有的样子,则需要使用窗口函数 窗口函数前面除了前面介绍的常用的三种,还可以使用sum() avg()等统计函数,变成: sum() over(partition by ...) x 这里得到的x,就是对这个组内的求和,组内每一条数据都得到一个相同的值 进阶-和排序函数对比 虽然前面说窗口函数的核心是partition by,但实际上partition by也可以去掉。 也就是说:xxx() over(…) 这个函数非常灵活。 其中partition by也可去掉,但此时必须要有order by。 变成:xxx() over(order by …) xxx 此时窗口函数 的窗口就只有一个了:所有数据都在同一个窗口里。 只剩下排序功能,比如 dense_renk() over(over by 成绩) 名次 常见作用:给班级学生成绩排名,成绩一样的,名次就一样 ———————————————— 原文链接:https://blog.csdn.net/yunduanyou/article/details/122583303
-
1.窗口函数 (1)partition by窗口函数 和 group by分组的区别: partition by关键字是分析性函数的一部分,它和聚合函数(如group by)不同的地方在于它能返回一个分组中的多条记录,而聚合函数一般只有一条反映统计值的记录。 partition by用于给结果集分组,如果没有指定那么它把整个结果集作为一个分组。 partition by与group by不同之处在于前者返回的是分组里的每一条数据,并且可以对分组数据进行排序操作。后者只能返回聚合之后的组的数据统计值的记录。 partition by相比较于group by,能够在保留全部数据的基础上,只对其中某些字段做分组排序(类似excel中的操作),而group by则只保留参与分组的字段和聚合函数的结果; 简单来说窗口函数对部分数据进行排序、计算等操作,group by对一组值进行聚合,即窗口函数是每一行都会保留,group by是从多行浓缩为少数行。 (2)窗口函数基本语法 <窗口函数> over ( partition by<用于分组的列名> order by <用于排序的列名>) (3)窗口函数 专用窗口函数: rank(), dense_rank(), row_number() 聚合函数 : sum(), max(), min(), count(), avg() 等 2.窗口函数的使用 2.1 over函数的写法: over(partition by type order by price desc) --先对 type 中相同的进行分区,在 type 中相同的情况下对 price 进行排序 2.2 专用窗口函数 rank() 和 row_number() 以及 dense_rank() SELECT *,rank() over(partition by type order by price desc) as mm from commodity; SELECT *,row_number() over(partition by type order by price desc) as mm from commodity; SELECT *,dense_rank() over(partition by type order by price desc) as mm from commodity; 从以上结果来看: rank()函数:如果存在并列名次的行,会占用下一个名次的位置,比如苹果的组内排名 1,2,3,4, 但是由于有两个是并列的,所以显示的排名是 1,1,3,4 ,其中 2 的位置还是被占用了 row_number()函数:不考虑并列的情况,此函数即使遇到了price 相同的情况,还是会默认排出一个先后来 dense_rank()函数:如果存在并列名次的行,不会占用下一个名次的位置,例如图片的最后显示的是 1,1,2,3 2.3 聚合函数作为窗口函数 (1) sum() SELECT *,sum(price) over(partition by type order by price) as sum from commodity; 在进行求和的时候是这样的,当前行的 sum 值是组内当前行与其组内当前行之前所有行的和,例如红色圈出来不的数据,橙子第一行是 6 ,第二行是 两行的和 6 +8 = 14,同样的红色圈出来的 苹果的也是同样的道理。需要注意的是当在排序的时候出现相同的时候,同样的都是 12 或者 同样的都是 5 无法进行区分,所以在计算的时候会把两个或多个值都加进去,这样也就是 橙色圈出来的部分了 从 8 --> 8+10 = 18 --> 18+12+12 = 42 -->18+12+12 = 42 ,大概就是这个意思,下文会告诉大家如何解决这种问题 (rows between unbounded preceding and current row) 我们来多看几种排序的结果是否符合上面的描述: -- order by type SELECT *,sum(price) over(partition by type order by type) as sum from commodity; -- order by position SELECT *,sum(price) over(partition by type order by position) as sum from commodity; -- order by id SELECT *,sum(price) over(partition by type order by id) as sum from commodity; (2) max(), min(), avg(), count() SELECT *,sum(price) over(partition by type order by price) as sum, max(price) over(partition by type order by price) as max, min(price) over(partition by type order by price) as min, avg(price) over(partition by type order by price) as avg, count(price) over(partition by type order by price) as count from commodity; 我们可以看的到, 不管是sum(), avg() 还是min(), max(), count() 他们在窗口函数中,都是对自身记录以及位于自身记录之前的数据进行聚合,求和、求平均、最小值、最大值等。所以,聚合函数作为窗口函数的时候可以在每一行的数据里直观的看到,截止到本行数据统计数据是多少,也可以看出每一行数据对整体的影响。(注意 : 数据重复的除外,有点特殊)也就是说 sum(), max(), min(), avg(), count() 都是类似的。 2.4 rows 与 range rows是物理窗口,即根据order by 子句排序后,取的前N行及后N行的数据计算(与当前行的值无关,只与排序后的行号相关) range是逻辑窗口,是指定当前行对应值的范围取值,列数不固定,只要行值在范围内,对应列都包含在内 通俗点来讲就是说:rows 取的时候是取当前行的前几行以及后几行,包括当前行在内一起进行计算的;而 range 不受行的限制,他跟当前行的值有关,当前行的值减去几,加上几,这个范围内的值都是要进行计算的数据,具体例子如下所示: --在当前行往前1行,往后2行,一共4行范围内进行计算 rows between 1 preceding and 2 following --在当前行的数值往前1个数值,往后2个数值,进行计算,范围不一定,因为可能会出现重复值 range between 1 preceding and 2 following --rows SELECT *,sum(price) over(partition by type order by price rows between 1 preceding and 2 following) as sum from commodity where type = '苹果'; 第一行 8 ,前一行没有,后两行是 10,12 --> 8 + 10 + 12 = 30 第二行是 10 ,前一行 8,后两行 12,12 --> 8 + 10 + 12 + 12 = 42 第三行是 12 ,前一行 10,后两行 12 --> 10 + 12 + 12 = 34 第四行是 12 ,前一行 12,后两行没有 --> 12 + 12 = 24 --range SELECT *,sum(price) over(partition by type order by price range between 1 preceding and 2 following) as sum from commodity where type = '苹果'; 第一行 8 ,往前一个数值 8-1 = 7,往后两个数值 8+2 = 10 --> 7 <= price <= 10 --> 8 + 10 = 18 第二行 10 ,往前一个数值 10-1 = 9,往后两个数值 10+2 = 12 --> 9 <= price <= 12 --> 10 + 12 + 12 = 34 第三行 12 ,往前一个数值 12-1 = 11,往后两个数值 12+2 = 14 --> 11 <= price <= 14 --> 12 + 12 = 24 第四行 12 ,往前一个数值 12-1 = 11,往后两个数值 12+2 = 14 --> 11 <= price <= 14 --> 12 + 12 = 24 2.5 unbound 和 current row --在当前行往前1行,往后2行,一共4行范围内进行计算 rows between 1 preceding and 2 following --在当前行的数值往前1个数值,往后2个数值,进行计算,范围不一定,因为可能会出现重复值 range between 1 preceding and 2 following between … and … 后面的数字可以随着需求进行替换,当然也可以使用 unbound 和 current row ; 其中 unbounded 表示不做限制,current row 表示当前行 --按照分组内全部行求和,不做任何限制 rows between unbounded preceding and unbounded following --从分组内排序的起始行到当前行 rows between unbounded preceding and current row --按照分组内全部行求和,不做任何限制 range between unbounded preceding and unbounded following --从分组内排序的起始行的值到当前行的值 range between unbounded preceding and current row --rows between unbounded preceding and unbounded following SELECT *,sum(price) over(partition by type order by price rows between unbounded preceding and unbounded following) as sum from commodity where type = '苹果'; --rows between unbounded preceding and current row SELECT *,sum(price) over(partition by type order by price rows between unbounded preceding and current row) as sum from commodity where type = '苹果'; --range between unbounded preceding and unbounded following SELECT *,sum(price) over(partition by type order by price range between unbounded preceding and unbounded following) as sum from commodity where type = '苹果'; --range between unbounded preceding and current row SELECT *,sum(price) over(partition by type order by price range between unbounded preceding and current row) as sum from commodity where type = '苹果'; 2.6 first_value(), last_valus(), lag(), lead() first_value(字段) over(partition by … order by …) 求分组后的第一个值 last_value(字段) over(partition by … order by …) 求分组后的最后一个值 SELECT *,first_value(price) over(partition by type order by price) as mm from commodity; SELECT *,last_value(price) over(partition by type order by price) as mm from commodity; lag(expresstion,<offset>,<default>) over(partition by … order by …) 取出分组后前n行数据 lead(expresstion,<offset>,<default>) over(partition by … order by …) 取出分组后后n行数据 --取分组后的前两行数据/后两行数据, 默认值设置为 0 SELECT *,lag(price,2,0) over(partition by type order by price) as mm from commodity; SELECT *,lead(price,2,0) over(partition by type order by price) as mm from commodity; SELECT *,lag(price,1,0) over(partition by type order by price) as lag,lead(price,1,0) over(partition by type order by price) as lead from commodity; --第一个参数:要取的字段 --第二个参数:取排序后的第几条记录 --第三个参数:缺省值,如果后面的记录取不到值就默认取值第三个参数的值,注意参数的类型要与第一个参数所取字段的类型一致哦,话默认为空 注:具体的sql输出结果下文放置了建表语句,可以执行一下,自己体验体验!!! 2.7 preceding 和 following Hive函数, preceding:向前 following:向后,这两个窗口函数不仅可以实现滑窗求和(指定rows范围)或者指定范围内数据求和(指定range范围),也可以用来计算移动平均值: SELECT *,sum(price) over(partition by type order by price) as sum,avg(price) over(partition by type order by price) as avg,avg(price) over(partition by type order by price rows 2 preceding) as avg2 from commodity where type = '苹果'; 1 3.参考文献 SQL高级功能:窗口函数、存储过程及经典排名问题、topN问题等 分区函数Partition By的用法 SQL:聚合类窗口函数的preceding和following参数用法 4.建表语句 -- ---------------------------- -- Table structure for commodity -- ---------------------------- DROP TABLE IF EXISTS "public"."commodity"; CREATE TABLE "public"."commodity" ( "id" varchar(50) COLLATE "pg_catalog"."default" NOT NULL, "position" varchar(50) COLLATE "pg_catalog"."default", "type" varchar(50) COLLATE "pg_catalog"."default", "price" numeric(10,2) ) ; COMMENT ON COLUMN "public"."commodity"."id" IS '主键'; COMMENT ON COLUMN "public"."commodity"."position" IS '位置(商品放置的货架)'; COMMENT ON COLUMN "public"."commodity"."type" IS '类型'; COMMENT ON COLUMN "public"."commodity"."price" IS '价格'; -- ---------------------------- -- Records of commodity -- ---------------------------- INSERT INTO "public"."commodity" VALUES ('1', '1-001', '苹果', 8.00); INSERT INTO "public"."commodity" VALUES ('2', '2-002', '苹果', 10.00); INSERT INTO "public"."commodity" VALUES ('3', '3-003', '苹果', 12.00); INSERT INTO "public"."commodity" VALUES ('6', '1-001', '橘子', 5.00); INSERT INTO "public"."commodity" VALUES ('7', '1-001', '橙子', 6.00); INSERT INTO "public"."commodity" VALUES ('8', '3-003', '橙子', 8.00); INSERT INTO "public"."commodity" VALUES ('10', '2-002', '菠萝', 10.00); INSERT INTO "public"."commodity" VALUES ('9', '2-002', '香蕉', 5.00); INSERT INTO "public"."commodity" VALUES ('4', '1-001', '苹果', 12.00); INSERT INTO "public"."commodity" VALUES ('5', '1-001', '香蕉', 5.00); -- ---------------------------- -- Primary Key structure for table commodity -- ---------------------------- ALTER TABLE "public"."commodity" ADD CONSTRAINT "commodity_pkey" PRIMARY KEY ("id"); ———————————————— 原文链接:https://blog.csdn.net/weixin_44711823/article/details/135966741
-
group by 必须与聚合函数一起使用,最终使用的结果是将多行变成一行,默认取第一行,可能丢失数据partition by 也是分组,不过不会丢失数据,只是把数据做分组1. group by是分组函数,partition by是分析函数(然后像sum()等是聚合函数);2. 在执行顺序上,以下是常用sql关键字的优先级from > where > group by > having > order by而partition by应用在以上关键字之后,实际上就是在执行完select之后,在所得结果集之上进行partition。3.partition by相比较于group by,能够在保留全部数据的基础上,只对其中某些字段做分组排序(类似excel中的操作),而group by则只保留参与分组的字段和聚合函数的结果(类似excel中的pivot)。partition bygroup by4.如果在partition结果上聚合,千万注意聚合函数是逐条累计运行结果的!而在group by后的结果集上使用聚合函数,会作用在分组下的所有记录上。数据如下SQLselect a.cc,a.item,sum(a.num)from table_temp agroup by a.cc,a.itemResult111条记录经group by后为10条,其中cc='cn' and item='8.1.1'对应的两条记录的num汇总成值3.SQL2select a.cc,a.num, min(a.num) over (partition by a.cc order by a.num asc) as amountfrom table_temp agroup by a.cc,a.num;select a.cc,a.num, min(a.num) over (partition by a.cc order by a.num desc) as amountfrom table_temp agroup by a.cc,a.num;Result2两个sql的唯一区别在于a.num的排序上,但从结果红框中的数据对比可以看到amount值并不相同,且第二个结果集amount并不都是最小值1。在这里就是要注意将聚合函数用在partition后的结果集上时,聚合函数是逐条累积计算值的!其实partition by常同row_number() over一起使用,select a.*, row_number() over (partition by a.cc,a.item order by a.num desc) as seqfrom table_temp a两个sql的唯一区别在于a.num的排序上,但从结果红框中的数据对比可以看到amount值并不相同,且第二个结果集amount并不都是最小值1。在这里就是要注意将聚合函数用在partition后的结果集上时,聚合函数是逐条累积计算值的!SQL中只要用到聚合函数就一定要用到group by 吗?答:看情况1、当只做聚集函数查询时候,就不需要进行分组了。2、当聚集函数和非聚集函数出现在一起时,需要将非聚集函数进行group by举例来说:情况一:不需要使用Group by 进行分组,因为其中没有非聚合字段,所以不用Group by 也可以。SELECT SUM(bonus) FROM person情况二:SELECT SUM(bonus),gender FROM person GROUP BY gender由于gender是非聚合字段,Group by 后才可以正常执行。原文链接:https://blog.csdn.net/weixin_44547599/article/details/88764558
-
1.讲下对HashMap的认识 HashMap 存储的是键值对 key - value,key 具有唯一性,采用了链地址法来处理哈希冲突。当往 HashMap 中添加元素时,会计算 key 的 hash 值取余得出元素在数组中的的存放位置。 HashMap底层的数据结构在 JDK1.8 中有了较大的变化,1.8之前采用数组加链表的数据结构,1.8采用数组加链表加红黑树的数据结构。 HashMap 是线程不安全的,线程安全可以使用 HashTable 和 ConcurrentHashMap 。 在 1.8 版本的中 hash() 和 resize( ) 方法也有了很大的改变,提升了性能。 键和值都可存放null,键只能存放一个null,键为null时存放入table[0]。 2.HashMap的一些参数 //HashMap的默认初始长度16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //HashMap的最大长度2的30次幂 static final int MAXIMUM_CAPACITY = 1 << 30; //HashMap的默认加载因子0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; //HashMap链表升级成红黑树的临界值 static final int TREEIFY_THRESHOLD = 8; //HashMap红黑树退化成链表的临界值 static final int UNTREEIFY_THRESHOLD = 6; //HashMap链表升级成红黑树第二个条件:HashMap数组(桶)的长度大于等于64 static final int MIN_TREEIFY_CAPACITY = 64; //HashMap底层Node桶的数组 transient Node<K,V>[] table; //扩容阈值,当你的hashmap中的元素个数超过这个阈值,便会发生扩容 //threshold = capacity * loadFactor int threshold; 3.为什么HashMap的长度必须是2的n次幂? 在计算存入结点下标时,会利用 key 的 hsah 值进行取余操作,而计算机计算时,并没有取余等运算,会将取余转化为其他运算。 当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n,就可以用位运算代替取余运算,计算更加高效。 4.HashMap 为什么在获取 hash 值时要进行位运算 换种问法:能不能直接使用key的hashcode值计算下标存储? static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 如果使用直接使用hashCode对数组大小取余,那么相当于参与运算的只有hashCode的低位,高位是没有起到任何作用的,所以我们的思路就是让 hashCode取值出的高位也参与运算,进一步降低hash碰撞的概率,使得数据分布更平均,我们把这样的操作称为扰动。 (h >>> 16)是无符号右移16位的运算,右边补0,得到 hashCode 的高16位。 (h = key.hashCode()) ^ (h >>> 16) 把 hashCode 和它的高16位进行异或运算,可以使得到的 hash 值更加散列,尽可能减少哈希冲突,提升性能。 而这么来看 hashCode 被散列 (异或) 的是低16位,而 HashMap 数组长度一般不会超过2的16次幂,那么高16位在大多数情况是用不到的,所以只需要拿 key 的 HashCode 和它的低16位做异或即可利用高位的hash值,降低哈希碰撞概率也使数据分布更加均匀。 5.HashMap在JDK1.7和JDK1.8中有哪些不同? HashMap的底层实现 在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。 JDK1.8主要解决或优化了以下问题: resize 扩容和 计算hash 优化 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。 6.HashMap的put方法的具体流程? 源码 HashMap是懒加载,只有在第一次put时才会创建数组。 总结 ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容; ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals; ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值 对,否则转向⑤; ⑤.遍历table[i],并记录遍历长度,如果遍历过程中发现key值相同的,则直接覆盖value,没有相同的key则在链表尾部插入结点,插入后判断该链表长度是否大等于8,大等于则考虑树化,如果数组的元素个数小于64,则只是将数组resize,大等于才树化该链表; ⑥.插入成功后,判断数组中的键值对数量size是否超过了阈值threshold,如果超过,进行扩容。 7.HashMap 的 get 方法的具体流程? public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //Node数组不为空,数组长度大于0,数组对应下标的Node不为空 if ((tab = table) != null && (n = tab.length) > 0 && //也是通过 hash & (length - 1) 来替代 hash % length 的 (first = tab[(n - 1) & hash]) != null) { //先和第一个结点比,hash值相等且key不为空,key的第一个结点的key的对象地址和值均相等 //则返回第一个结点 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //如果key和第一个结点不匹配,则看.next是否为空,不为null则继续,为空则返回null if ((e = first.next) != null) { //如果此时是红黑树的结构,则进行处理getTreeNode()方法搜索key if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); //是链表结构的话就一个一个遍历,直到找到key对应的结点, //或者e的下一个结点为null退出循环 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } 总结 首先根据 hash 方法获取到 key 的 hash 值 然后通过 hash & (length - 1) 的方式获取到 key 所对应的Node数组下标 ( length对应数组长度 ) 首先判断此结点是否为空,是否就是要找的值,是则返回空,否则判断第二个结点是否为空,是则返回空,不是则判断此时数据结构是链表还是红黑树 链表结构进行顺序遍历查找操作,每次用 == 符号 和 equals( ) 方法来判断 key 是否相同,满足条件则直接返回该结点。链表遍历完都没有找到则返回空。 红黑树结构执行相应的 getTreeNode( ) 查找操作。 8.HashMap的扩容操作是怎么实现的? 不管是JDK1.7或者JDK1.8 当put方法执行的时候,如果table为空,则执行resize()方法扩容。默认长度为16。 JDK1.7扩容 条件:发生扩容的条件必须同时满足两点 当前存储的数量大于等于阈值 发生hash碰撞 因为上面这两个条件,所以存在下面这些情况 就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。 当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(这时元素个数小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。 特点:先扩容,再添加(扩容使用的头插法) 缺点:头插法会使链表发生反转,多线程环境下可能会死循环 扩容之后对table的调整: table容量变为2倍,所有的元素下标需要重新计算,newIndex = hash (扰动后) & (newLength - 1) JDK1.8扩容 条件: 当前存储的数量大于等于阈值 当某个链表长度>=8,但是数组存储的结点数size() < 64时 特点:先插后判断是否需要扩容(扩容时是尾插法) 缺点:多线程下,1.8会有数据覆盖 举例: 线程A:往index插,index此时为空,可以插入,但是此时线程A被挂起 线程B:此时,对index写入数据,A恢复后,就把B数据覆盖了 扩容之后对table的调整: table容量变为2倍,但是不需要像之前一样计算下标,只需要将hash值和旧数组长度相与即可确定位置。 如果 Node 桶的数据结构是链表会生成 low 和 high 两条链表,是红黑树则生成 low 和 high 两颗红黑树 依靠 (hash & oldCap) == 0 判断 Node 中的每个结点归属于 low 还是 high。 把 low 插入到 新数组中 当前数组下标的位置,把 high 链表插入到 新数组中 [当前数组下标 + 旧数组长度] 的位置 如果生成的 low,high 树中元素个数小于等于6退化成链表再插入到新数组的相应下标的位置 9.HashMap 在扩容时为什么通过位运算 (e.hash & oldCap) 得到下标? 从下图中我们可以看出,计算下标通过(n - 1) & hash,旧table的长度为16,hash值只与低四位有关,扩容后,table长度为32(两倍),此时只与低五位有关。 所以此时后几位的结果相同,前后两者之间的差别就差在了第五位上。 同时,扩容的时候会有 low 和 high 两条链表或红黑树来记录原来下标的数据和原来下标 + 旧table下标的数据。 如果第五位 b 是 0,那么只要看低四位 (也就是原来的下标);如果第五位是 1,只要把低四位的二进制数 + 1 0 0 0 0 ,就可以得到新数组下标。前面的部分刚好是原来的下标,后一部分就是旧table的长度 。那么我们就得出来了为什么把 low 插入扩容后 新数组[原来坐标] 的位置,把 high 插入扩容后 新数组[当前坐标 + 旧数组长度] 的位置。 那为什么根据 (e.hash & oldCap) == 0 来做判断条件呢?是因为旧数组的长度 length 的二进制数的第五位刚好是 1,hash & length 就可以计算 hash 值的第五位是 0 还是 1,就可以区别是在哪个位置上。 10.链表升级成红黑树的条件 链表长度大于8时才会考虑升级成红黑树,是有一个条件是 HashMap 的 Node 数组长度大于等于64(不满足则会进行一次扩容替代升级)。 11.红黑树退化成链表的条件 扩容 resize( ) 时,红黑树拆分成的 树的结点数小于等于临界值6个,则退化成链表。 删除元素 remove( ) 时,在 removeTreeNode( ) 方法会检查红黑树是否满足退化条件,与结点数无关。如果红黑树根 root 为空,或者 root 的左子树/右子树为空,root.left.left 根的左子树的左子树为空,都会发生红黑树退化成链表。 12.HashMap是怎么解决哈希冲突的? 使用链地址法(使用散列表)来链接拥有相同下标的数据; 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均; 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快; 13.HaspMap的初始化时数组长度和加载因子的约束范围 可以看到如果初始化数组长度 initialCapacity 小于 0 的话会跑出 IllegalArgumentException 的异常,initialCapacity 大于 MAXIMUM_CAPACITY 即 2 的 30 次幂的时候最大长度也只会固定在 MAXIMUM_CAPACITY ,在扩容的时候,如果数组的长度大等于MAXIMUM_CAPACITY,会将阈值设置为Integer.MAX_VALUE。 加载因子小于等于0时,或者加载因子是NaN时 (NaN 实际上就是 Not a Number的简称) 会抛出 IllegalArgumentException 的异常。 ———————————————— 原文链接:https://blog.csdn.net/qq_49217297/article/details/126304736
-
一、概括 1.1 介绍 Kafka 消息延迟和时序性 Kafka 消息延迟和时序性对于大多数实时数据流应用程序至关重要。本章将深入介绍这两个核心概念,它们是了解 Kafka 数据流处理的关键要素。 1.1.1 什么是 Kafka 消息延迟? Kafka 消息延迟是指消息从生产者发送到消息被消费者接收之间的时间差。这是一个关键的概念,因为它直接影响到数据流应用程序的实时性和性能。在理想情况下,消息应该以最小的延迟被传递,但在实际情况中,延迟可能会受到多种因素的影响。 消息延迟的因素包括: 网络延迟:消息必须通过网络传输到 Kafka 集群,然后再传输到消费者。网络延迟可能会受到网络拓扑、带宽和路由等因素的影响。 硬件性能:Kafka 集群的硬件性能,包括磁盘、内存和 CPU 的速度,会影响消息的写入和读取速度。 Kafka 内部处理:Kafka 集群的内部处理能力也是一个关键因素。消息必须经过分区、日志段和复制等处理步骤,这可能会引入一些处理延迟。 1.1.2 为什么消息延迟很重要? 消息延迟之所以如此重要,是因为它直接关系到实时数据处理应用程序的可靠性和实时性。在一些应用中,如金融交易处理,甚至毫秒级的延迟都可能导致交易失败或不一致。在监控和日志处理应用中,过高的延迟可能导致数据不准确或失去了时序性。 管理和优化 Kafka 消息延迟是确保应用程序在高负载下仍能快速响应的关键因素。不仅需要了解延迟的来源,还需要采取相应的优化策略。 1.1.3 什么是 Kafka 消息时序性? Kafka 消息时序性是指消息按照它们发送的顺序被接收。这意味着如果消息 A 在消息 B 之前发送,那么消息 A 应该在消息 B 之前被消费。保持消息的时序性对于需要按照时间顺序处理的应用程序至关重要。 维护消息时序性是 Kafka 的一个强大特性。在 Kafka 中,每个分区都可以保证消息的时序性,因为每个分区内的消息是有序的。然而,在多个分区的情况下,时序性可能会受到消费者处理速度不一致的影响,因此需要采取一些策略来维护全局的消息时序性。 1.1.4 消息延迟和时序性的关系 消息延迟和消息时序性之间存在密切的关系。如果消息延迟过大,可能会导致消息失去时序性,因为一条晚到的消息可能会在一条早到的消息之前被处理。因此,了解如何管理消息延迟也包括了维护消息时序性。 在接下来的章节中,我们将深入探讨如何管理和优化 Kafka 消息延迟,以及如何维护消息时序性,以满足实时数据处理应用程序的需求。 1.2 延迟的来源 为了有效地管理和优化 Kafka 消息延迟,我们需要深入了解延迟可能来自哪些方面。下面是一些常见的延迟来源: 1.2.1 Kafka 内部延迟 Kafka 内部延迟是指与 Kafka 内部组件和分区分配相关的延迟。这些因素可能会影响消息在 Kafka 内部的分发、复制和再平衡。 分区分布不均:如果分区分布不均匀,某些分区可能会变得拥挤,而其他分区可能会滞后,导致消息传递延迟。 复制延迟:在 Kafka 中,消息通常会进行复制以确保冗余。复制延迟是指主题的所有副本都能复制消息所需的时间。 再平衡延迟:当 Kafka 集群发生再平衡时,消息的重新分配和复制可能导致消息传递延迟。 二、衡量和监控消息延迟 在本节中,我们将深入探讨如何度量和监控 Kafka 消息延迟,这将帮助你更好地了解问题并采取相应的措施来提高延迟性能。 2.1 延迟的度量 为了有效地管理 Kafka 消息延迟,首先需要能够度量它。下面是一些常见的延迟度量方式: 2.1.1 生产者到 Kafka 延迟 这是指消息从生产者发送到 Kafka 集群之间的延迟。为了度量这一延迟,你可以采取以下方法: 记录发送时间戳:在生产者端,记录每条消息的发送时间戳。一旦消息成功写入 Kafka,记录接收时间戳。然后,通过将这两个时间戳相减,你可以获得消息的生产者到 Kafka 的延迟。 以下是如何记录发送和接收时间戳的代码示例: // 记录消息发送时间戳 long sendTimestamp = System.currentTimeMillis(); ProducerRecord<String, String> record = new ProducerRecord<>("my_topic", "key", "value"); producer.send(record, (metadata, exception) -> { if (exception == null) { long receiveTimestamp = System.currentTimeMillis(); long producerToKafkaLatency = receiveTimestamp - sendTimestamp; System.out.println("生产者到 Kafka 延迟:" + producerToKafkaLatency + " 毫秒"); } else { System.err.println("消息发送失败: " + exception.getMessage()); } }); 1 2 3 4 5 6 7 8 9 10 11 12 2.1.2 Kafka 内部延迟 Kafka 内部延迟是指消息在 Kafka 集群内部传递的延迟。你可以使用 Kafka 内置度量来度量它,包括: Log End-to-End Latency:这是度量消息从生产者发送到消费者接收的总延迟。它包括了网络传输、分区复制、再平衡等各个环节的时间。 以下是一个示例: // 创建 Kafka 消费者 Properties consumerProps = new Properties(); consumerProps.put("bootstrap.servers", "kafka-broker:9092"); consumerProps.put("group.id", "my-group"); consumerProps.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); consumerProps.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps); // 订阅主题 consumer.subscribe(Collections.singletonList("my_topic")); while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<String, String> record : records) { long endToEndLatency = record.timestamp() - record.timestampType().createTimestamp(); System.out.println("Log End-to-End 延迟:" + endToEndLatency + " 毫秒"); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 2.1.3 消费者处理延迟 消费者处理延迟是指消息从 Kafka 接收到被消费者实际处理的时间。为了度量这一延迟,你可以采取以下方法: 记录消费时间戳:在消费者端,记录每条消息的接收时间戳和处理时间戳。通过计算这两个时间戳的差值,你可以得到消息的消费者处理延迟。 以下是如何记录消费时间戳的代码示例: KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps); consumer.subscribe(Collections.singletonList("my_topic")); while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<String, String> record : records) { long receiveTimestamp = System.currentTimeMillis(); long consumerProcessingLatency = receiveTimestamp - record.timestamp(); System.out.println("消费者处理延迟:" + consumerProcessingLatency + " 毫秒"); } } 1 2 3 4 5 6 7 8 9 10 11 2.2 监控和度量工具 在度量和监控 Kafka 消息延迟时,使用适当的工具和系统是至关重要的。下面是一些工具和步骤,帮助你有效地监控 Kafka 消息延迟,包括代码示例: 2.2.1 Kafka 内置度量 Kafka 提供了内置度量,可通过多种方式来监控。以下是一些示例,演示如何通过 Kafka 的 JMX 界面访问这些度量: 使用 JConsole 直接连接到 Kafka Broker: 启动 Kafka Broker。 打开 JConsole(Java 监控与管理控制台)。 在 JConsole 中选择 Kafka Broker 进程。 导航到 “kafka.server” 和 “kafka.consumer”,以查看各种度量。 使用 Jolokia(Kafka JMX HTTP Bridge): 启用 Jolokia 作为 Kafka Broker 的 JMX HTTP Bridge。 使用 Web 浏览器或 HTTP 请求访问 Jolokia 接口来获取度量数据。例如,使用 cURL 进行 HTTP GET 请求: curl http://localhost:8778/jolokia/read/kafka.server:name=BrokerTopicMetrics/TotalFetchRequestsPerSec 1 这将返回有关 Kafka Broker 主题度量的信息。 2.2.2 第三方监控工具 除了 Kafka 内置度量,你还可以使用第三方监控工具,如 Prometheus 和 Grafana,来收集、可视化和警报度量数据。以下是一些步骤: 配置 Prometheus: 部署和配置 Prometheus 服务器。 创建用于监控 Kafka 的 Prometheus 配置文件,定义抓取度量数据的频率和目标。 启动 Prometheus 服务器。 设置 Grafana 仪表板: 部署和配置 Grafana 服务器。 在 Grafana 中创建仪表板,使用 Prometheus 作为数据源。 添加度量查询,配置警报规则和可视化图表。 可视化 Kafka 延迟数据: 在 Grafana 仪表板中,你可以设置不同的图表来可视化 Kafka 延迟数据,例如生产者到 Kafka 延迟、消费者处理延迟等。通过设置警报规则,你还可以及时收到通知,以便采取行动。 2.2.3 配置和使用监控工具 为了配置和使用监控工具,你需要执行以下步骤: 定义度量指标:确定你要度量的关键度量指标,如生产者到 Kafka 延迟、消费者处理延迟等。 设置警报规则:为了快速响应问题,设置警报规则,以便在度量数据超出预定阈值时接收通知。 创建可视化仪表板:使用监控工具(如 Grafana)创建可视化仪表板,以集中展示度量数据并实时监测延迟情况。可配置的图表和仪表板有助于更好地理解数据趋势。 以上步骤和工具将帮助你更好地度量和监控 Kafka 消息延迟,以及及时采取行动来维护系统的性能和可靠性。 三、降低消息延迟 既然我们了解了 Kafka 消息延迟的来源以及如何度量和监控它,让我们继续探讨如何降低消息延迟。以下是一些有效的实践方法,可以帮助你减少 Kafka 消息延迟: 3.1 优化 Kafka 配置 3.1.1 Producer 和 Consumer 参数 生产者参数示例: # 生产者参数示例 acks=all compression.type=snappy linger.ms=20 max.in.flight.requests.per.connection=1 1 2 3 4 5 acks 设置为 all,以确保生产者等待来自所有分区副本的确认。这提高了可靠性,但可能增加了延迟。 compression.type 使用 Snappy 压缩消息,减小了网络传输延迟。 linger.ms 设置为 20 毫秒,以允许生产者在发送消息之前等待更多消息。这有助于减少短暂的消息发送延迟。 max.in.flight.requests.per.connection 设置为 1,以确保在收到分区副本的确认之前不会发送新的消息。 消费者参数示例: # 消费者参数示例 max.poll.records=500 fetch.min.bytes=1 fetch.max.wait.ms=100 enable.auto.commit=false 1 2 3 4 5 max.poll.records 设置为 500,以一次性拉取多条消息,提高吞吐量。 fetch.min.bytes 设置为 1,以确保即使没有足够数据,也立即拉取消息。 fetch.max.wait.ms 设置为 100 毫秒,以限制拉取消息的等待时间。 enable.auto.commit 禁用自动提交位移,以确保精确控制消息的确认。 3.1.2 Broker 参数 优化 Kafka broker 参数可以提高整体性能。以下是示例: # Kafka Broker 参数示例 num.network.threads=3 num.io.threads=8 log.segment.bytes=1073741824 log.retention.check.interval.ms=300000 1 2 3 4 5 num.network.threads 和 num.io.threads 设置为适当的值,以充分利用硬件资源。 log.segment.bytes 设置为 1 GB,以充分利用磁盘性能。 log.retention.check.interval.ms 设置为 300,000 毫秒,以降低清理日志段的频率。 3.1.3 Topic 参数 优化每个主题的参数以满足应用程序需求也很重要。以下是示例: # 创建 Kafka 主题并设置参数示例 kafka-topics.sh --create --topic my_topic --partitions 8 --replication-factor 2 --config cleanup.policy=compact 1 2 --partitions 8 设置分区数量为 8,以提高并行性。 --replication-factor 2 设置复制因子为 2,以提高可靠性。 --config cleanup.policy=compact 设置清理策略为压缩策略,以减小数据保留成本。 通过适当配置这些参数,你可以有效地优化 Kafka 配置以降低消息延迟并提高性能。请根据你的应用程序需求和硬件资源进行调整。 3.2 编写高效的生产者和消费者 最后,编写高效的 Kafka 生产者和消费者代码对于降低延迟至关重要。以下是一些最佳实践: 3.2.1 生产者最佳实践 使用异步发送:将多个消息批量发送,而不是逐条发送。这可以减少网络通信的次数,提高吞吐量。 使用 Kafka 生产者的缓冲机制:充分利用 Kafka 生产者的缓冲功能,以减少网络通信次数。 使用分区键:通过选择合适的分区键,确保数据均匀分布在不同的分区上,从而提高并行性。 3.2.2 消费者最佳实践 使用多线程消费:启用多个消费者线程,以便并行处理消息。这可以提高处理能力和降低延迟。 调整消费者参数:调整消费者参数,如 fetch.min.bytes 和 fetch.max.wait.ms,以平衡吞吐量和延迟。 使用消息批处理:将一批消息一起处理,以减小处理开销。 3.2.3 数据序列化 选择高效的数据序列化格式对于降低数据传输和存储开销很重要。以下是一些建议的格式: Avro:Apache Avro 是一种数据序列化框架,具有高度压缩和高性能的特点。它适用于大规模数据处理。 Protocol Buffers:Google Protocol Buffers(ProtoBuf)是一种轻量级的二进制数据格式,具有出色的性能和紧凑的数据表示。 四、Kafka 消息时序性 消息时序性是大多数实时数据流应用程序的核心要求。在本节中,我们将深入探讨消息时序性的概念、为何它如此重要以及如何保障消息时序性。 4.1 什么是消息时序性? 消息时序性是指消息按照它们发送的顺序被接收和处理的特性。在 Kafka 中,每个分区内的消息是有序的,这意味着消息以它们被生产者发送的顺序排列。然而,跨越多个分区的消息需要额外的工作来保持它们的时序性。 4.1.1 为何消息时序性重要? 消息时序性对于许多应用程序至关重要,特别是需要按照时间顺序处理数据的应用。以下是一些应用领域,消息时序性非常关键: 金融领域:在金融交易中,确保交易按照它们发生的确切顺序进行处理至关重要。任何失去时序性的交易可能会导致不一致性或错误的交易。 日志记录:在日志记录和监控应用程序中,事件的时序性对于分析和排查问题非常关键。失去事件的时序性可能会导致混淆和数据不准确。 电商应用:在线商店的订单处理需要确保订单的创建、支付和发货等步骤按照正确的顺序进行,以避免订单混乱和不准确。 4.2 保障消息时序性 在分布式系统中,保障消息时序性可能会面临一些挑战,特别是在跨越多个分区的情况下。以下是一些策略和最佳实践,可帮助你确保消息时序性: 4.2.1 分区和消息排序 使用合适的分区策略对消息进行排序,以确保相关的消息被发送到同一个分区。这样可以维护消息在单个分区内的顺序性。对于需要按照特定键排序的消息,可以使用自定义分区器来实现。 以下是如何使用合适的分区策略对消息进行排序的代码示例: // 自定义分区器,确保相关消息基于特定键被发送到同一个分区 public class CustomPartitioner implements Partitioner { @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { // 在此处根据 key 的某种规则计算分区编号 // 例如,可以使用哈希函数或其他方法 int numPartitions = cluster.partitionsForTopic(topic).size(); return Math.abs(key.hashCode()) % numPartitions; } @Override public void close() { // 可选的资源清理 } @Override public void configure(Map<String, ?> configs) { // 可选的配置 } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 4.2.2 数据一致性 确保生产者发送的消息是有序的。这可能需要在应用程序层面实施,包括对消息进行缓冲、排序和合并,以确保它们按照正确的顺序发送到 Kafka。 以下是如何确保数据一致性的代码示例: // 生产者端的消息排序 ProducerRecord<String, String> record1 = new ProducerRecord<>("my-topic", "key1", "message1"); ProducerRecord<String, String> record2 = new ProducerRecord<>("my-topic", "key2", "message2"); // 发送消息 producer.send(record1); producer.send(record2); // 消费者端保证消息按照键排序 ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<String, String> record : records) { // 处理消息,确保按照键的顺序进行 } 1 2 3 4 5 6 7 8 9 10 11 12 13 4.2.3 消费者并行性 在消费者端,使用适当的线程和分区分配来确保消息以正确的顺序处理。这可能涉及消费者线程数量的管理以及确保每个线程只处理一个分区,以避免顺序混乱。 以下是如何确保消费者并行性的代码示例: // 创建具有多个消费者线程的 Kafka 消费者 Properties consumerProps = new Properties(); consumerProps.put("bootstrap.servers", "kafka-broker:9092"); consumerProps.put("group.id", "my-group"); consumerProps.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); consumerProps.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); // 创建 Kafka 消费者 KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps); // 订阅主题 consumer.subscribe(Collections.singletonList("my-topic")); // 创建多个消费者线程 int numThreads = 3; for (int i = 0; i < numThreads; i++) { Runnable consumerThread = new ConsumerThread(consumer); new Thread(consumerThread).start(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 五、总结 在本篇技术博客中,我们深入探讨了 Kafka 消息延迟和时序性的重要性以及如何度量、监控和降低消息延迟。我们还讨论了消息时序性的挑战和如何确保消息时序性。对于构建实时数据流应用程序的开发人员来说,深入理解这些概念是至关重要的。通过合理配置 Kafka、优化网络和硬件、编写高效的生产者和消费者代码,以及维护消息时序性,你可以构建出高性能和可靠的数据流系统。 无论你的应用是金融交易、监控、日志记录还是其他领域,这些建议和最佳实践都将帮助你更好地处理 Kafka 消息延迟和时序性的挑战,确保数据的可靠性和一致性。 六、从零开始学架构:照着做,你也能成为架构师 1、内容介绍 京东购买链接:从零开始学架构:照着做,你也能成为架构师 本书的内容主要包含以下几部分: 架构设计基础,包括架构设计相关概念、历史、原则、基本方法,让架构设计不再神秘; 架构设计流程,通过一个虚拟的案例,描述了一个通用的架构设计流程,让架构设计不再依赖天才的创作,而是有章可循; 架构设计专题:包括高性能架构设计、高可用架构设计、可扩展架构设计,这些模式可以直接参考和应用; 架构设计实战,包括重构、开源方案引入、架构发展路径、互联网架构模板等 ———————————————— 原文链接:https://blog.csdn.net/guorui_java/article/details/135060020
-
提起SimpleDateFormat类,想必做过Java开发的童鞋都不会感到陌生。没错,它就是Java中提供的日期时间的转化类。这里,为什么说SimpleDateFormat类有线程安全问题呢?有些小伙伴可能会提出疑问:我们生产环境上一直在使用SimpleDateFormat类来解析和格式化日期和时间类型的数据,一直都没有问题啊!我的回答是:没错,那是因为你们的系统达不到SimpleDateFormat类出现问题的并发量,也就是说你们的系统没啥负载!接下来,我们就一起看下在高并发下SimpleDateFormat类为何会出现安全问题,以及如何解决SimpleDateFormat类的安全问题。重现SimpleDateFormat类的线程安全问题为了重现SimpleDateFormat类的线程安全问题,一种比较简单的方式就是使用线程池结合Java并发包中的CountDownLatch类和Semaphore类来重现线程安全问题。有关CountDownLatch类和Semaphore类的具体用法和底层原理与源码解析在【高并发专题】后文会深度分析。这里,大家只需要知道CountDownLatch类可以使一个线程等待其他线程各自执行完毕后再执行。而Semaphore类可以理解为一个计数信号量,必须由获取它的线程释放,经常用来限制访问某些资源的线程数量,例如限流等。好了,先来看下重现SimpleDateFormat类的线程安全问题的代码,如下所示。package io.binghe.concurrent.lab06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 测试SimpleDateFormat的线程不安全问题 */ public class SimpleDateFormatTest01 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; //SimpleDateFormat对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }可以看到,在SimpleDateFormatTest01类中,首先定义了两个常量,一个是程序执行的总次数,一个是同时运行的线程数量。程序中结合线程池和CountDownLatch类与Semaphore类来模拟高并发的业务场景。其中,有关日期转化的代码只有如下一行。simpleDateFormat.parse("2020-01-01");当程序捕获到异常时,打印相关的信息,并退出整个程序的运行。当程序正确运行后,会打印“所有线程格式化日期成功”。运行程序输出的结果信息如下所示。Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-2" 线程:pool-1-thread-7 格式化日期失败 线程:pool-1-thread-9 格式化日期失败 线程:pool-1-thread-10 格式化日期失败 Exception in thread "pool-1-thread-3" Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-6" 线程:pool-1-thread-15 格式化日期失败 线程:pool-1-thread-21 格式化日期失败 Exception in thread "pool-1-thread-23" 线程:pool-1-thread-16 格式化日期失败 线程:pool-1-thread-11 格式化日期失败 java.lang.ArrayIndexOutOfBoundsException 线程:pool-1-thread-27 格式化日期失败 at java.lang.System.arraycopy(Native Method) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:597) at java.lang.StringBuffer.append(StringBuffer.java:367) at java.text.DigitList.getLong(DigitList.java:191)线程:pool-1-thread-25 格式化日期失败 at java.text.DecimalFormat.parse(DecimalFormat.java:2084) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) 线程:pool-1-thread-14 格式化日期失败 at java.text.DateFormat.parse(DateFormat.java:364) at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47) 线程:pool-1-thread-13 格式化日期失败 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) java.lang.NumberFormatException: For input string: "" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) 线程:pool-1-thread-20 格式化日期失败 at java.lang.Long.parseLong(Long.java:601) at java.lang.Long.parseLong(Long.java:631) at java.text.DigitList.getLong(DigitList.java:195) at java.text.DecimalFormat.parse(DecimalFormat.java:2084) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) java.lang.NumberFormatException: For input string: "" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Long.parseLong(Long.java:601) at java.lang.Long.parseLong(Long.java:631) at java.text.DigitList.getLong(DigitList.java:195) at java.text.DecimalFormat.parse(DecimalFormat.java:2084) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) Process finished with exit code 1说明,在高并发下使用SimpleDateFormat类格式化日期时抛出了异常,SimpleDateFormat类不是线程安全的!!!接下来,我们就看下,SimpleDateFormat类为何不是线程安全的。SimpleDateFormat类为何不是线程安全的?那么,接下来,我们就一起来看看真正引起SimpleDateFormat类线程不安全的根本原因。通过查看SimpleDateFormat类的源码,我们得知:SimpleDateFormat是继承自DateFormat类,DateFormat类中维护了一个全局的Calendar变量,如下所示。/** * The {@link Calendar} instance used for calculating the date-time fields * and the instant of time. This field is used for both formatting and * parsing. * * <p>Subclasses should initialize this field to a {@link Calendar} * appropriate for the {@link Locale} associated with this * <code>DateFormat</code>. * @serial */ protected Calendar calendar;从注释可以看出,这个Calendar对象既用于格式化也用于解析日期时间。接下来,我们再查看parse()方法接近最后的部分。@Override public Date parse(String text, ParsePosition pos){ ################此处省略N行代码################## Date parsedDate; try { parsedDate = calb.establish(calendar).getTime(); // If the year value is ambiguous, // then the two-digit year == the default start year if (ambiguousYear[0]) { if (parsedDate.before(defaultCenturyStart)) { parsedDate = calb.addYear(100).establish(calendar).getTime(); } } } // An IllegalArgumentException will be thrown by Calendar.getTime() // if any fields are out of range, e.g., MONTH == 17. catch (IllegalArgumentException e) { pos.errorIndex = start; pos.index = oldStart; return null; } return parsedDate; }可见,最后的返回值是通过调用CalendarBuilder.establish()方法获得的,而这个方法的参数正好就是前面的Calendar对象。接下来,我们再来看看CalendarBuilder.establish()方法,如下所示。Calendar establish(Calendar cal) { boolean weekDate = isSet(WEEK_YEAR) && field[WEEK_YEAR] > field[YEAR]; if (weekDate && !cal.isWeekDateSupported()) { // Use YEAR instead if (!isSet(YEAR)) { set(YEAR, field[MAX_FIELD + WEEK_YEAR]); } weekDate = false; } cal.clear(); // Set the fields from the min stamp to the max stamp so that // the field resolution works in the Calendar. for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) { for (int index = 0; index <= maxFieldIndex; index++) { if (field[index] == stamp) { cal.set(index, field[MAX_FIELD + index]); break; } } } if (weekDate) { int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1; int dayOfWeek = isSet(DAY_OF_WEEK) ? field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek(); if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) { if (dayOfWeek >= 8) { dayOfWeek--; weekOfYear += dayOfWeek / 7; dayOfWeek = (dayOfWeek % 7) + 1; } else { while (dayOfWeek <= 0) { dayOfWeek += 7; weekOfYear--; } } dayOfWeek = toCalendarDayOfWeek(dayOfWeek); } cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek); } return cal; }在CalendarBuilder.establish()方法中先后调用了cal.clear()与cal.set(),也就是先清除cal对象中设置的值,再重新设置新的值。由于Calendar内部并没有线程安全机制,并且这两个操作也都不是原子性的,所以当多个线程同时操作一个SimpleDateFormat时就会引起cal的值混乱。类似地, format()方法也存在同样的问题。因此, SimpleDateFormat类不是线程安全的根本原因是:DateFormat类中的Calendar对象被多线程共享,而Calendar对象本身不支持线程安全。那么,得知了SimpleDateFormat类不是线程安全的,以及造成SimpleDateFormat类不是线程安全的原因,那么如何解决这个问题呢?接下来,我们就一起探讨下如何解决SimpleDateFormat类在高并发场景下的线程安全问题。解决SimpleDateFormat类的线程安全问题解决SimpleDateFormat类在高并发场景下的线程安全问题可以有多种方式,这里,就列举几个常用的方式供参考,大家也可以在评论区给出更多的解决方案。1.局部变量法最简单的一种方式就是将SimpleDateFormat类对象定义成局部变量,如下所示的代码,将SimpleDateFormat类对象定义在parse(String)方法的上面,即可解决问题。package io.binghe.concurrent.lab06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 局部变量法解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest02 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }此时运行修改后的程序,输出结果如下所示。所有线程格式化日期成功至于在高并发场景下使用局部变量为何能解决线程的安全问题,会在【JVM专题】的JVM内存模式相关内容中深入剖析,这里不做过多的介绍了。当然,这种方式在高并发下会创建大量的SimpleDateFormat类对象,影响程序的性能,所以,这种方式在实际生产环境不太被推荐。2.synchronized锁方式将SimpleDateFormat类对象定义成全局静态变量,此时所有线程共享SimpleDateFormat类对象,此时在调用格式化时间的方法时,对SimpleDateFormat对象进行同步即可,代码如下所示。package io.binghe.concurrent.lab06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过Synchronized锁解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest03 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; //SimpleDateFormat对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { synchronized (simpleDateFormat){ simpleDateFormat.parse("2020-01-01"); } } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }此时,解决问题的关键代码如下所示。synchronized (simpleDateFormat){ simpleDateFormat.parse("2020-01-01"); }运行程序,输出结果如下所示。所有线程格式化日期成功需要注意的是,虽然这种方式能够解决SimpleDateFormat类的线程安全问题,但是由于在程序的执行过程中,为SimpleDateFormat类对象加上了synchronized锁,导致同一时刻只能有一个线程执行parse(String)方法。此时,会影响程序的执行性能,在要求高并发的生产环境下,此种方式也是不太推荐使用的。3.Lock锁方式Lock锁方式与synchronized锁方式实现原理相同,都是在高并发下通过JVM的锁机制来保证程序的线程安全。通过Lock锁方式解决问题的代码如下所示。package io.binghe.concurrent.lab06; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author binghe * @version 1.0.0 * @description 通过Lock锁解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest04 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; //SimpleDateFormat对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); //Lock对象 private static Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { lock.lock(); simpleDateFormat.parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }finally { lock.unlock(); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }通过代码可以得知,首先,定义了一个Lock类型的全局静态变量作为加锁和释放锁的句柄。然后在simpleDateFormat.parse(String)代码之前通过lock.lock()加锁。这里需要注意的一点是:为防止程序抛出异常而导致锁不能被释放,一定要将释放锁的操作放到finally代码块中,如下所示。finally { lock.unlock(); }运行程序,输出结果如下所示。所有线程格式化日期成功此种方式同样会影响高并发场景下的性能,不太建议在高并发的生产环境使用。4.ThreadLocal方式使用ThreadLocal存储每个线程拥有的SimpleDateFormat对象的副本,能够有效的避免多线程造成的线程安全问题,使用ThreadLocal解决线程安全问题的代码如下所示。package io.binghe.concurrent.lab06; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest05 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){ @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { threadLocal.get().parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }通过代码可以得知,将每个线程使用的SimpleDateFormat副本保存在ThreadLocal中,各个线程在使用时互不干扰,从而解决了线程安全问题。运行程序,输出结果如下所示。所有线程格式化日期成功此种方式运行效率比较高,推荐在高并发业务场景的生产环境使用。另外,使用ThreadLocal也可以写成如下形式的代码,效果是一样的。package io.binghe.concurrent.lab06; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过ThreadLocal解决SimpleDateFormat类的线程安全问题 */ public class SimpleDateFormatTest06 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); private static DateFormat getDateFormat(){ DateFormat dateFormat = threadLocal.get(); if(dateFormat == null){ dateFormat = new SimpleDateFormat("yyyy-MM-dd"); threadLocal.set(dateFormat); } return dateFormat; } public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { getDateFormat().parse("2020-01-01"); } catch (ParseException e) { System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); }catch (NumberFormatException e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }5.DateTimeFormatter方式DateTimeFormatter是Java8提供的新的日期时间API中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。代码如下所示。package io.binghe.concurrent.lab06; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过DateTimeFormatter类解决线程安全问题 */ public class SimpleDateFormatTest07 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { LocalDate.parse("2020-01-01", formatter); }catch (Exception e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }可以看到,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用DateTimeFormatter类来处理日期的格式化操作。运行程序,输出结果如下所示。所有线程格式化日期成功使用DateTimeFormatter类来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。6.joda-time方式joda-time是第三方处理日期时间格式化的类库,是线程安全的。如果使用joda-time来处理日期和时间的格式化,则需要引入第三方类库。这里,以Maven为例,如下所示引入joda-time库。<dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9.9</version> </dependency>引入joda-time库后,实现的程序代码如下所示。package io.binghe.concurrent.lab06; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; /** * @author binghe * @version 1.0.0 * @description 通过DateTimeFormatter类解决线程安全问题 */ public class SimpleDateFormatTest08 { //执行总次数 private static final int EXECUTE_COUNT = 1000; //同时运行的线程数量 private static final int THREAD_COUNT = 20; private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < EXECUTE_COUNT; i++){ executorService.execute(() -> { try { semaphore.acquire(); try { DateTime.parse("2020-01-01", dateTimeFormatter).toDate(); }catch (Exception e){ System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); System.exit(1); } semaphore.release(); } catch (InterruptedException e) { System.out.println("信号量发生错误"); e.printStackTrace(); System.exit(1); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); System.out.println("所有线程格式化日期成功"); } }这里,需要注意的是:DateTime类是org.joda.time包下的类,DateTimeFormat类和DateTimeFormatter类都是org.joda.time.format包下的类,如下所示。import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter;运行程序,输出结果如下所示。所有线程格式化日期成功使用joda-time库来处理日期的格式化操作运行效率比较高,推荐在高并发业务场景的生产环境使用。解决SimpleDateFormat类的线程安全问题的方案总结综上所示:在解决解决SimpleDateFormat类的线程安全问题的几种方案中,局部变量法由于线程每次执行格式化时间时,都会创建SimpleDateFormat类的对象,这会导致创建大量的SimpleDateFormat对象,浪费运行空间和消耗服务器的性能,因为JVM创建和销毁对象是要耗费性能的。所以,不推荐在高并发要求的生产环境使用。synchronized锁方式和Lock锁方式在处理问题的本质上是一致的,通过加锁的方式,使同一时刻只能有一个线程执行格式化日期和时间的操作。这种方式虽然减少了SimpleDateFormat对象的创建,但是由于同步锁的存在,导致性能下降,所以,不推荐在高并发要求的生产环境使用。ThreadLocal通过保存各个线程的SimpleDateFormat类对象的副本,使每个线程在运行时,各自使用自身绑定的SimpleDateFormat对象,互不干扰,执行性能比较高,推荐在高并发的生产环境使用。DateTimeFormatter是Java 8中提供的处理日期和时间的类,DateTimeFormatter类本身就是线程安全的,经压测,DateTimeFormatter类处理日期和时间的性能效果还不错(后文单独写一篇关于高并发下性能压测的文章)。所以,推荐在高并发场景下的生产环境使用。joda-time是第三方处理日期和时间的类库,线程安全,性能经过高并发的考验,推荐在高并发场景下的生产环境使用。来自:https://zhuanlan.zhihu.com/p/395751163
-
Java中主要有四种类型的线程池,它们分别是:可缓存线程池:通过Executors.newCachedThreadPool()创建,这种线程池会根据需要创建新线程,但同时会重用空闲的线程。如果线程池中的线程超过60秒未被使用,则会被终止并移除,这样可以避免资源浪费。固定线程池:通过Executors.newFixedThreadPool(int nThreads)创建,这种线程池的特点是核心线程数和最大线程数相同,适用于执行长期任务且任务数量固定的情况。定时线程池:通过Executors.newScheduledThreadPool(int corePoolSize)创建,适用于需要周期性执行任务的场景,如定时任务、定时扫描等。单线程化线程池:通过Executors.newSingleThreadExecutor()创建,这种线程池只有一个工作线程,适用于需要保证任务按顺序执行的场景。这些线程池都是ExecutorService接口的实现类,它们各自有不同的特点和适用场景。在实际开发中,选择合适的线程池类型可以提高程序的性能和响应速度。
-
在Java中,跨线程的数据同步问题可以通过以下几种方式来解决:使用synchronized关键字:通过在方法或代码块上添加synchronized关键字,可以确保同一时间只有一个线程能够访问共享资源。例如:public class SynchronizedExample { private int count = 0; public synchronized void increment() { count++; } public synchronized void decrement() { count--; } public synchronized int getCount() { return count; } }使用ReentrantLock类:ReentrantLock是一个可重入的互斥锁,它提供了与synchronized相同的基本行为和语义,但具有更高的灵活性。例如:import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockExample { private final ReentrantLock lock = new ReentrantLock(); private int count = 0; public void increment() { lock.lock(); try { count++; } finally { lock.unlock(); } } public void decrement() { lock.lock(); try { count--; } finally { lock.unlock(); } } public int getCount() { lock.lock(); try { return count; } finally { lock.unlock(); } } }使用Semaphore类:Semaphore是一个计数信号量,可以用来控制同时访问某个资源的线程数量。例如:import java.util.concurrent.Semaphore; public class SemaphoreExample { private final Semaphore semaphore = new Semaphore(1); private int count = 0; public void increment() throws InterruptedException { semaphore.acquire(); try { count++; } finally { semaphore.release(); } } public void decrement() throws InterruptedException { semaphore.acquire(); try { count--; } finally { semaphore.release(); } } public int getCount() throws InterruptedException { semaphore.acquire(); try { return count; } finally { semaphore.release(); } } }使用Atomic类:Atomic类提供了一种无锁的方式来实现线程安全的操作。例如:import java.util.concurrent.atomic.AtomicInteger; public class AtomicExample { private AtomicInteger count = new AtomicInteger(0); public void increment() { count.incrementAndGet(); } public void decrement() { count.decrementAndGet(); } public int getCount() { return count.get(); } }以上四种方法都可以解决Java中跨线程的数据同步问题,具体选择哪种方法取决于实际需求和场景。
-
在Java中,对Long类型的变量进行判空时,可以使用Objects.isNull()方法和!= null两种方式。它们的区别如下:Objects.isNull()方法:这是Java 8引入的一个工具类方法,用于判断一个对象是否为null。它接受一个参数,如果参数为null,则返回true,否则返回false。这个方法主要用于处理包装类型(如Long、Integer等)的null值判断。import java.util.Objects; public class Main { public static void main(String[] args) { Long num = null; if (Objects.isNull(num)) { System.out.println("num is null"); } else { System.out.println("num is not null"); } } }!= null:这是Java中常见的判空方式,用于判断一个对象是否为null。如果对象不等于null,则表示对象不为空。这种方式适用于所有类型的对象,包括基本类型和引用类型。public class Main { public static void main(String[] args) { Long num = null; if (num != null) { System.out.println("num is not null"); } else { System.out.println("num is null"); } } }总结:Objects.isNull()方法主要用于处理包装类型的null值判断,而!= null方式适用于所有类型的对象。在实际使用中,可以根据需要选择合适的判空方式。
-
大家好,三月的合集又来了,本次涵盖了java,mysql,spirngboot,oracle,nginx,webpack,css,python,mongoDB,devops,golang诸多内容供大家学习。 1.Python中数据解压缩的技巧分享【转】 https://bbs.huaweicloud.com/forum/thread-0274147063634001023-1-1.html 2.CSS如何设置背景模糊周边有白色光晕(解决方案)【转】 https://bbs.huaweicloud.com/forum/thread-02121147063476589021-1-1.html 3.CSS实现渐变式圆点加载动画【转】 https://bbs.huaweicloud.com/forum/thread-0274147063231589021-1-1.html 4. Nginx access.log日志详解及统计分析小结【转】 https://bbs.huaweicloud.com/forum/thread-0274147062581229019-1-1.html 5.Webpack部署本地服务器的方法【转】 https://bbs.huaweicloud.com/forum/thread-02110147062400691022-1-1.html 6.Nginx漏洞整改实现限制IP访问&隐藏nginx版本信息【转】 https://bbs.huaweicloud.com/forum/thread-0273147062364276013-1-1.html 7.Nginx加固的几种方式(控制超时时间&限制客户端下载速度&并发连接数)【转】 https://bbs.huaweicloud.com/forum/thread-02109147062305049017-1-1.html 8.Nginx配置http和https的实现步骤【转】 https://bbs.huaweicloud.com/forum/thread-0274147061484998017-1-1.html 9.MongoDB内存过高问题分析及解决【转】 https://bbs.huaweicloud.com/forum/thread-0276147061392824019-1-1.html 10.Oracle数据库中字符串截取最全方法总结【转】 https://bbs.huaweicloud.com/forum/thread-0240147061327119025-1-1.html 11.MySQL数据库如何克隆(带脚本)【转】 https://bbs.huaweicloud.com/forum/thread-0294147061178556014-1-1.html 12.mysql5.6建立索引报错1709问题及解决【转】 https://bbs.huaweicloud.com/forum/thread-0240147061135949024-1-1.html 13.修改Mysql索引长度限制解决767 byte限制问题【转】 https://bbs.huaweicloud.com/forum/thread-0240147060706517023-1-1.html 14.SQL实现模糊查询的四种方法小结【转】 https://bbs.huaweicloud.com/forum/thread-02121147060465820020-1-1.html 15.MySql查询中按多个字段排序的方法【转】 https://bbs.huaweicloud.com/forum/thread-0240147060411844022-1-1.html 16. Devops-01-devops 是什么?【转】 https://bbs.huaweicloud.com/forum/thread-02127146828359305034-1-1.html 17.使用 Java 在Excel中创建下拉列表【转】 https://bbs.huaweicloud.com/forum/thread-0297146827870477028-1-1.html 18.管理与控制平面设计 https://bbs.huaweicloud.com/forum/thread-0239146030318575010-1-1.html 19.cisco https://bbs.huaweicloud.com/forum/thread-0292146030276181005-1-1.html 20. NSX-V整体架构 https://bbs.huaweicloud.com/forum/thread-02127146028999196007-1-1.html 21.从NVP到NSX https://bbs.huaweicloud.com/forum/thread-0279146028887825006-1-1.html 22.【监控】spring actuator源码速读-转载 https://bbs.huaweicloud.com/forum/thread-0239145954118788007-1-1.html 23.SpringCloud-RabbitMQ消息模型-转载 https://bbs.huaweicloud.com/forum/thread-0282145954036229004-1-1.html 24.【Golang入门教程】Go语言变量的声明-转载 https://bbs.huaweicloud.com/forum/thread-0279145953985911003-1-1.html 25. Spring Boot 3核心技术与最佳实践-转载 https://bbs.huaweicloud.com/forum/thread-0239145953918959006-1-1.html
-
java22发布了,你们公司现在都在用哪个版本开发呢?
-
1-IDEA的日常快捷键 第1组:通用型 说明 快捷键 复制代码-copy ctrl + c 粘贴-paste ctrl + v 剪切-cut ctrl + x 撤销-undo ctrl + z 反撤销-redo ctrl + shift + z 保存-save all ctrl + s 全选-select all ctrl + a 第2组:提高编写速度(上) 说明 快捷键 智能提示-edit alt + enter 提示代码模板-insert live template ctrl+j 使用xx块环绕-surround with ... ctrl+alt+t 调出生成getter/setter/构造器等结构-generate ... alt+insert 自动生成返回值变量-introduce variable ... ctrl+alt+v 复制指定行的代码-duplicate line or selection ctrl+d 删除指定行的代码-delete line ctrl+y 切换到下一行代码空位-start new line shift + enter 切换到上一行代码空位-start new line before current ctrl +alt+ enter 向上移动代码-move statement up ctrl+shift+↑ 向下移动代码-move statement down ctrl+shift+↓ 向上移动一行-move line up alt+shift+↑ 向下移动一行-move line down alt+shift+↓ 方法的形参列表提醒-parameter info ctrl+p 第3组:提高编写速度(下) 说明 快捷键 批量修改指定的变量名、方法名、类名等-rename shift+f6 抽取代码重构方法-extract method ... ctrl+alt+m 重写父类的方法-override methods ... ctrl+o 实现接口的方法-implements methods ... ctrl+i 选中的结构的大小写的切换-toggle case ctrl+shift+u 批量导包-optimize imports ctrl+alt+o 第4组:类结构、查找和查看源码 说明 快捷键 如何查看源码-go to class... ctrl + 选中指定的结构 或 ctrl+n 显示当前类结构,支持搜索指定的方法、属性等-file structure ctrl+f12 退回到前一个编辑的页面-back ctrl+alt+← 进入到下一个编辑的页面-forward ctrl+alt+→ 打开的类文件之间切换-select previous/next tab alt+←/→ 光标选中指定的类,查看继承树结构-Type Hierarchy ctrl+h 查看方法文档-quick documentation ctrl+q 类的UML关系图-show uml popup ctrl+alt+u 定位某行-go to line/column ctrl+g 回溯变量或方法的来源-go to implementation(s) ctrl+alt+b 折叠方法实现-collapse all ctrl+shift+ - 展开方法实现-expand all ctrl+shift+ + 第5组:查找、替换与关闭 说明 快捷键 查找指定的结构 ctlr+f 快速查找:选中的Word快速定位到下一个-find next ctrl+l 查找与替换-replace ctrl+r 直接定位到当前行的首位-move caret to line start home 直接定位到当前行的末位 -move caret to line end end 查询当前元素在当前文件中的引用,然后按 F3 可以选择 ctrl+f7 全项目搜索文本-find in path ... ctrl+shift+f 关闭当前窗口-close ctrl+f4 第6组:调整格式 说明 快捷键 格式化代码-reformat code ctrl+alt+l 使用单行注释-comment with line comment ctrl + / 使用/取消多行注释-comment with block comment ctrl + shift + / 选中数行,整体往后移动-tab tab 选中数行,整体往前移动-prev tab shift + tab 2-Debug快捷键 说明 快捷键 单步调试(不进入函数内部)- step over F8 单步调试(进入函数内部)- step into F7 强制单步调试(进入函数内部) - force step into alt+shift+f7 选择要进入的函数 - smart step into shift + F7 跳出函数 - step out shift + F8 运行到断点 - run to cursor alt + F9 继续执行,进入下一个断点或执行完程序 - resume program F9 停止 - stop Ctrl+F2 查看断点 - view breakpoints Ctrl+Shift+F8 关闭 - close Ctrl+F4 ———————————————— 原文链接:https://blog.csdn.net/m0_59281987/article/details/137051712
-
Idea拉取Jar包,报Unable to import maven project: See logs for details 查看日志信息No implementation for org.apache.maven.model.path.PathTranslator was bound 解决办法: IDEA执行Maven报错 Unable to import maven project: See logs for details Help -> Show Log in Explorer 查看详细的错误日志 1) No implementation for org.apache.maven.model.path.PathTranslator was bound. while locating org.apache.maven.model.path.PathTranslator for field at org.apache.maven.model.interpolation.AbstractStringBasedModelInterpolator.pathTranslator(Unknown Source) at org.codehaus.plexus.DefaultPlexusContainer$1.configure(DefaultPlexusContainer.java:350) 2) No implementation for org.apache.maven.model.path.UrlNormalizer was bound. while locating org.apache.maven.model.path.UrlNormalizer for field at org.apache.maven.model.interpolation.AbstractStringBasedModelInterpolator.urlNormalizer(Unknown Source) at org.codehaus.plexus.DefaultPlexusContainer$1.configure(DefaultPlexusContainer.java:350) 解决办法 Maven与IDEA版本不一致导致: 当前:Maven-3.6.3 IDEA2019.01 修改Maven版本为:3.5.4 https://mirrors.tuna.tsinghua.edu.cn/apache/maven/maven-3/3.5.4/binaries/ 最后:修改maven在IDEA中的版本配置即可解决。 ———————————————— 原文链接:https://blog.csdn.net/qq_31016939/article/details/137032201
-
VM options这项需要我们自己填写,我填的是VM options : -Xms256m -Xmx256m -XX:PermSize=32M -XX:MaxPermSize=128m 每一项以空格隔开 参数说明 -Xms256m:设置JVM初始堆内存为256m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。 -Xmx256m:设置JVM最大堆内存为256m。 -Xss128k:设置每个线程的栈大小。JDK5.0以后每个线程栈大小为1M,之前每个线程栈大小为256K。应当根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。需要注意的是:当这个值被设置的较大(例如>2MB)时将会在很大程度上降低系统的性能。 -Xmn2g:设置年轻代大小为2G。在整个堆内存大小确定的情况下,增大年轻代将会减小年老代,反之亦然。此值关系到JVM垃圾回收,对系统性能影响较大,官方推荐配置为整个堆大小的3/8。 -XX:NewSize=1024m:设置年轻代初始值为1024M。 -XX:MaxNewSize=1024m:设置年轻代最大值为1024M。 -XX:PermSize=256m:设置持久代初始值为256M。 -XX:MaxPermSize=256m:设置持久代最大值为256M。 -XX:NewRatio=4:设置年轻代(包括1个Eden和2个Survivor区)与年老代的比值。表示年轻代比年老代为1:4。 -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的比值。表示2个Survivor区(JVM堆内存年轻代中默认有2个大小相等的Survivor区)与1个Eden区的比值为2:4,即1个Survivor区占整个年轻代大小的1/6。 -XX:MaxTenuringThreshold=7:表示一个对象如果在Survivor区(救助空间)移动了7次还没有被垃圾回收就进入年老代。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代,对于需要大量常驻内存的应用,这样做可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代存活时间,增加对象在年轻代被垃圾回收的概率,减少Full GC的频率,这样做可以在某种程度上提高服务稳定性。 标准参数,所有JVM都必须支持这些参数的功能,而且向后兼容;例如: -client——设置JVM使用Client模式,特点是启动速度比较快,但运行时性能和内存管理效率不高,通常用于客户端应用程序或开发调试;在32位环境下直接运行Java程序默认启用该模式。 -server——设置JVM使Server模式,特点是启动速度比较慢,但运行时性能和内存管理效率很高,适用于生产环境。在具有64位能力的JDK环境下默认启用该模式。 非标准参数(-X),默认JVM实现这些参数的功能,但是并不保证所有JVM实现都满足,且不保证向后兼容; 非稳定参数(-XX),此类参数各个JVM实现会有所不同,将来可能会不被支持,需要慎重使用; ———————————————— 原文链接:https://blog.csdn.net/style_L_/article/details/129079538
-
流程如下: 打开 IntelliJ IDEA: 启动你的 IntelliJ IDEA。 打开新的项目或窗口: 可以打开一个新的项目,或者在当前项目中打开一个新的窗口。 配置新的运行/调试配置: 在 IntelliJ IDEA 的工具栏中,找到并点击右侧的编辑配置按钮(位于运行/调试配置的旁边,通常是一个小下拉箭头)。 选择 Edit Configurations: 在弹出的菜单中,选择 "Edit Configurations"。 添加新的配置: 在打开的配置窗口左上角,点击加号按钮或复制按钮,以添加一个新的配置。 选择 Application 配置类型: 在弹出的菜单中,选择 "Application" 配置类型。 配置新的运行/调试配置: 在右侧的配置窗口中,配置以下参数: Name(名称): 为你的配置起一个名称,以便区分。 Main class(主类): 指定你的 Java 主类。 VM options(虚拟机选项): 在这里输入你想要配置的虚拟机选项,比如 -Duser.language=en。 Program arguments(程序参数): 如果你的程序需要参数,可以在这里添加。 保存配置: 确保点击窗口底部的 "OK" 按钮来保存你的配置。 运行新的配置: 在 IntelliJ IDEA 的工具栏中选择你刚刚配置的运行/调试配置,点击运行按钮。 通过以上步骤,你就可以在 IntelliJ IDEA 中同时运行多个不同的 JVM 虚拟机实例。你可以通过创建多个配置,每个配置对应一个虚拟机实例,方便地管理和切换。这在需要模拟多个环境或进行并发调试时非常有用。 手把手教学 1.看看自己的项目 2.可能开始的时候啥也没有,就点Run Configuration Type 3.再点击Edit Configurations... 4.点击+号添加SpringBoot 5.主类选择一下,一般就一个,点他选了就行。 6.然后点击Modify Options 选择添加add VM Options 7.点击apply 再OK就有了!!! ———————————————— 原文链接:https://blog.csdn.net/m0_62645012/article/details/134628159
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签