0%


IDEA整合LeetCode的插件,有了这个插件,可以在IDEA本地编辑代码并且运行提交,还能关联自己的账号,简直实用之极。看网上介绍的都不太详细,我来写个清楚点的。插件如图:

一:下载插件

点击intelij Idea->Preferences->Plugins:

搜索leetcode下载就行了。如果你的搜不到,可以尝试重新打开Setting重新搜,还没有的话,可以去官网插件库下载,然后导入就可以了。链接:https://plugins.jetbrains.com/plugin/12132-leetcode-editor

二:配置

安装完成之后,点击IdealiJ idea->Preference->Tools->leetcode plugin,

也可以点击右下角的leetcode图标

配置界面如图:

注意:上图中TempFilePath对应的文件夹一定要是你此项目模块源码的位置 。我的新建一个项目的意思是,像我那样重新建一个名为“LeetCode”的项目,然后选择其src目录,评论区红色无效文件可能就是这个原因。

关于下面几个参数的定义,官方给的是:

  • Custom code template: 开启使用自定义模板,否则使用默认生成格式

  • CodeFileName: 生成文件的名称,默认为题目标题

  • CodeTemplate: 生成题目代码的内容,默认为题目描述和题目代码

  • TemplateConstant: 模板常用变量

  • ${question.title}:题目标题,例如:两数之和

  • ${question.titleSlug}:题目标记,例如:two-sum

  • ${question.frontendQuestionId}:题目编号,例如:1

  • ${question.content}:题目描述内容

  • ${question.code}:题目代码部分

  • $!velocityTool.camelCaseName(str):一个函数,用来将字符串转化为驼峰样式

CodeFileName这个里面填的就是以后自动生成类的类名,使用我的这个配置刚好可以

1
P$!{question.frontendQuestionId}$!velocityTool.camelCaseName(${question.titleSlug})

CodeTemplate就是自动生成的代码格式,对于有强迫症的人来说,这个自动生成的格式就非常重要了,不然看着心里就烦。其中main()方法是用来debug的。我的配置(如果复制过去格式不对,请手动改成我这样的,空行也不要删):

1
2
3
4
5
6
7
8
9
10
package leetcode.editor.cn;
${question.content}
public class $!velocityTool.camelCaseName(${question.titleSlug}){undefined
public
static
void main(String[] args) {undefined
Solution solution = new $!velocityTool.camelCaseName(${question.titleSlug})().new Solution();
}
${question.code}
}

就这样自动生成的代码是这样的,个人觉得还可以:

注意:

在生成的自定义代码中包含两行关键信息:

  • leetcode submit region begin(Prohibit modification and deletion):提交到leetcode进行验证的代码开始标记

  • leetcode submit region end(Prohibit modification and deletion):提交到leetcode进行验证的代码结束标记

这两行标记标示了提交到leetcode服务器进行验证的代码范围,在此范围内只允许有出现与题目解答相关的内容,出现其他内容可能导致leetcode验证不通过。

除了此范围内,其他区域是可以任意填写的,内容不会提交到leetcode,可以增加一些可以本地调试的内容,例如:import java.util.Arrays;

所以,这两行内容是不能被删除和修改的,否则将识别不到提交的内容。

补充:

如图中的文档注释中的类,没有快捷键可以一次性取消,如果一行一行删又太费事 ,我们可以用这个方法。

光标放在这里,按下Alt+鼠标左键,就可以对多行进行删除,简单方便。

三:使用

点击左下角的按钮,然后点击上面的小地球进行联网登录,登陆成功就是图中的画面了。双击题目,就会自动创建类

写完代码,右键

如图,就可以运行测试和提交了,在下面的Event Log可以查看运行情况

spark 的启动流程?

shell端的启动流程

以一个常规的wordcount 程序spark启动命令为例,spark在shell端的启动流程如下

  1. 首先,用户在shell端提交spark提交命令,一个常见的workcount提交命令如下
1
2
3
4
5
./spark-submit --master yarn-client \
--class com.example.spark.WordCount \
--executor-memory 1G \
--total-executor-cores 2 \
/opt/wordcount.jar hdfs://hacluster/aa/hello.tx

该命令表示像yarn提交一个spark程序进行运行,在shell端的执行流程如下。

  1. spark-submit 比较简单,直接将shell后面的参数转交给spark-class,并告诉spark-class以后运行的java类为 org.apache.spark.deploy.SparkSubmit。
1
exec "${SPARK_HOME}"/bin/spark-class org.apache.spark.deploy.SparkSubmit "$@"
  1. 此时程序在spark-class执行
  • spark-class会对环境变量做一些简单的查找和配置,如设置SPARK_HOME,SPARK_JARS_DIR,JAVA_HOME等
  • 调用build_command方法获取最终要执行的java命令,该命令会调用org.apache.spark.launcher.Main

build_command的代码如下

1
2
3
4
build_command() {
"$RUNNER" -Xmx128m -cp "$LAUNCH_CLASSPATH" org.apache.spark.launcher.Main "$@"
printf "%d\0" $?
}

该代码将shell后面的参数传入org.apache.spark.launcher.Main,获取执行结果

  1. org.apache.spark.launcher.Main,该类根据传入的参数,拼装classpath,jar等参数,返回最终要执行的java命令给spark-class
  • 输入:shell后面自带的参数,如
    –class com.example.spark.WordCount
    –executor-memory 1G
  • 输出我们看到的最终执行的java命令,如
    1
    2
    3
    4
    /opt/java/bin/java -cp /opt/spark/conf/:/opt/spark/jars/* \
    -Xmx1g org.apache.spark.deploy.SparkSubmit --master yarn \
    --class com.example.spark.WordCount --executor-memory 1G \
    --total-executor-cores 2 /opt/wordcount.jar hdfs://hacluster/aa/hello.tx
  • 执行过程
    Main类通过一个标准的建造者模式,传入参数,构建AbstractCommandBuilder
    1
    2
    3
    4
    AbstractCommandBuilder builder;
    if (className.equals("org.apache.spark.deploy.SparkSubmit")) {
    try {
    builder = new SparkSubmitCommandBuilder(args);
    最后传入env返回cmdList,通过对应系统的prepareCommand输出,由spark-class shell接收回参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    List<String> cmd = builder.buildCommand(env);
    if (printLaunchCommand) {
    System.err.println("Spark Command: " + join(" ", cmd));
    System.err.println("========================================");
    }

    if (isWindows()) {
    System.out.println(prepareWindowsCommand(cmd, env));
    } else {
    // In bash, use NULL as the arg separator since it cannot be used in an argument.
    List<String> bashCmd = prepareBashCommand(cmd, env);
    for (String c : bashCmd) {
    System.out.print(c);
    System.out.print('\0');
    }
    }
  1. spark-class 执行最终的启动命令
    1
    2
    CMD=("${CMD[@]:0:$LAST}")
    exec "${CMD[@]}"

至此,在shell端的代码执行完毕,spark程序进入真正运行的java端代码

背景

当前数仓表的生命周期策略数据生命周期管理规范,除个别表外,无条件对所有表采用93天快照策略,数据重复存储,造成存储压力大。

现状:共xxx张表,占用xxx张空间,平均每张xxx存储。

基于合理利用存储,控制存储无序扩展,节约公司存储成本,需要该表快照策略,由快照策略改为拉链策略。

说明

本方案所说拉链,指的是替换快照方式日粒度的存储拉链,有别于基于业务流水数据设计的拉链。

方案比较

快照vs拉链

简洁版

比较项 快照 拉链
写入 无需加工 需要加工
使用 无成本 理解成本高
存储 126份副本 2~5份副本
计算 去重&排序
适用 所有 有主键&数据变更时间

明细版

比较项 快照 拉链
写入 快照直接按数据日期写入,简单易懂 拉链需要结合当天最新数据和历史拉链数据加工出最新数据,略微复杂
使用 快照关联历史直接按数据日期取多版本,简单易懂 拉链需要根据业务日期和开始时间及结束时间比较,确定唯一版本,使用成本高
存储 快照为93天周期+每月最后一天,以3年为例,共计126份副本 拉链只有在数据变更才会重复存储,根据数据量变更频繁程度,预估2~5份副本
计算 无计算 需要根据id和创建时间和结束时间进行去重和排序,进而计算出开始时间和结束时间
适用 所有 需要有主键&数据变更时间

总结

快照方案简单粗暴有效,拉链方案复杂优雅有效。

加工和使用成本上,可以认为拉链是快照方案的2~5倍

存储成本上,可以任务拉链是快照方案的 1/30 之一

前提条件

并非所有的表都可以做成拉链表来存储历史记录,业务表必须满足一下几点方可设计拉链

条件 解释 现状
有唯一主键id 业务库必须有唯一主键id用于标识同一条记录
这样才能对同一条记录进行版本区分
开发规范里有明确必须有主键id
不排除个别表
有业务数据变更时间 如果数据字段发生变更,然而数据时间不进行变更会导致拉链表无法确认时间 开发规范里有明确edit_time需要自动更新
历史老表存在不更新现象,需要在mysql表结构中确认是否自动更新

设计

统一规范

1)无穷大的结束时间,统一采用 29991231 表示
2)无穷小的开始时间,统一采用 20000101表示
3)拉链表统一新增三个字段
之所以加上data前置,为了防止字段重名

字段名 解释
data_start_date 数据生效开始日期
data_end_date 数据生效结束日期
data_is_active 数据是否当前有效
4)拉链表分区统一使用三个字段进行分区
其中data_start_year和data_end_year是为了加快使用效率
dayid是为了获取昨天版本
字段名 解释
———– ———–
data_start_year 数据开始年份
data_end_year 数据结束年份
dayid 数据日期

5)data_start_date和data_end_date采用闭区间设计
6)拉链表的生命周期为2,即保留两份副本
7) 命名统一采用 xxx_fds命名
8)当前拉链表只针对dwd进行设计

设计举例

mysql 举例表结构

表名:test_a

字段 解释 其他
id 主键id pk
test_name 测试名称
create_time 创建时间
edit_time 编辑时间

mysql 举例表数据

20210701数据

日期 id test_name create_time edit_time
20210701 1 what’s your name 20210701 20210701
20210701 2 what’s your age 20210701 20210701
2021072 1 what’s wrong 20210701 20210702
20210702 2 what’s your age 20210701 20210701
20210710 1 whattttttttttt 20210701 20210710
20210710 2 what’s your age 20210701 20210701

拉链表代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
set hive.exec.dynamic.partition=true;
set hive.exec.dynamic.partition.mode=nostrick;

-- 通用字段
set hivevar:com_columns=id,test_name,create_time,edit_time;

-- 原始dwd表名称
set hivevar:dwd_table_name=dwd_test_a_d;

-- fds全量拉量表名称
set hivevar:fds_table_name=dws_test_a_fds;

-- 主键id,分区表id会重复,注意
set hivevar:pk_id=id;



create table if not exists ${fds_table_name}
(
data_start_date string comment '开始日期'
,data_end_date string comment '结束日期'
,id bigint COMMENT '自增主键'
,test_name string COMMENT '测试名称'
,create_time string COMMENT '表记录创建时间'
,edit_time string COMMENT '表记录修改时间')
partitioned by (dayid string comment '数据日期',data_is_active tinyint comment '数据是否当前最新版本'
,data_start_year string comment '起始年',data_end_year string comment '结束年')
stored as orc;





--删除旧分区,防止脏数据无法删除
ALTER TABLE ${fds_table_name} DROP if exists PARTITION (dayid='$v_date');

--去重后的全量数据
with full_data as (
select dayid
,${com_columns} --字符串会被替换,需要转义
from
(select dayid
,${com_columns}
from ${dwd_table_name} --获取当天最新数据
where dayid='$v_date'
and substr(create_time,1,8)<='$v_date'

union all

select data_start_date as dayid
,\${com_columns}
from \${fds_table_name} -- 从历史拉链表获取历史所有数据
where substr(create_time,1,8)<='$v_date'
and dayid=date_format(date_sub(to_date('$v_date','yyyyMMdd'),1),'yyyyMMdd')
) a
group by dayid,\${com_columns}
)
--清洗数据,使数据合规,我们做的是天粒度的拉链,同一个主键id一天取最后一条记录
--之所以要清洗,理论上同一个id一天是会存在多条记录的,比如记录在1号03分变更,然后在1号10分抽取,然后在1号19:08分变更,然后在2号10分抽取
,legal_data as (
select *
from
(select *
,row_number() over(partition by ${pk_id},substr(edit_time,1,8) order by edit_time desc,dayid desc) as data_rank -- 加入dayid排序,为修改值,但是eidt未改变的值,取最新的数据
from full_data
) a
where data_rank=1 --同一个主键id一天取最后一条记录
)
--日粒度的拉链
,date_data_ds as (
select if(lag_edit_date is null,substr(create_time,1,8),substr(edit_time,1,8)) as data_start_date --如果上一条编辑时间为空,说明是第一条,采用创建日期作为开始日期,不然采用本条编辑时间作为开始时间
,case when lead_edit_date is null and dayid='$v_date' then '29991231' --如果下一条编辑时间为空,且该条记录分区时间为最新分区,说明是最后一条,采用默认29991231作为结束时间,不然采用下一条编辑时间作为结束时间
when lead_edit_date is null and dayid<'$v_date' then if(substr(edit_time,1,8)>=dayid,substr(edit_time,1,8),dayid) -- 如果下一条编辑时间为空,该表记录分区不是最新分区,说明该记录在最新分区中已被删除,该记录的修改时间和分区时间取最大值。 该判断为兼容记录物理删除骚操作,取最大值为防止dwd重刷导致的数据错乱
else lead_edit_date -- 下一条记录存在,采用下一条编辑时间作为结束时间
end as data_end_date
,if(lead_edit_date is null and dayid='$v_date',1,0) as data_is_active --没有下一条,说明是最新的一条
,\${com_columns}
from
(select *
,lead(date_format(date_add(to_date(substr(edit_time,1,8),'yyyyMMdd'),-1),'yyyyMMdd'),1,null) over (partition by ${pk_id} order by create_time,edit_time) as lead_edit_date --下一条记录的编辑日期
,lag(substr(edit_time,1,8),1,null) over (partition by ${pk_id} order by create_time,edit_time) as lag_edit_date --上一条记录的编辑时间,由于开始时间和结束时间是区间包含,edit是作为结束时间的闭区间,因此需要上一条日期+1作为开始时间
--,last_value(edit_time) over (partition by id order by create_time,edit_time) as last_edit_time --分组内最后一条记录编辑日期
from legal_data
) a

)


insert overwrite table ${fds_table_name} partition (dayid,data_is_active,data_start_year,data_end_year)
select data_start_date
,data_end_date
,${com_columns}
,dayid
,data_is_active
,data_start_year
,data_end_year
from
(select data_start_date
,data_end_date
,data_is_active
,${com_columns}
,substr(data_start_date,1,4) as data_start_year
,substr(data_end_date,1,4) as data_end_year
,'$v_date' as dayid
from date_data_ds
) a
;

拉链表数据效果

|数据日期| data_start_date| data_end_date| data_is_active| id| test_name| create_time| edit_time| data_start_year| data_end_year|
|—–| —–| —–| —–| —–| —–| —–| —–| —–| —–|
|20210701| 20210701| 29991231| 1| 1| what’s your name| 20210701| 20210701| 2021| 2999|
|20210701|20210701 |29991231 |1 |2 |what’s your age| 20210701| 20210701| 2021| 2999|
|20210702| 20210701| 20210701| 0| 1| what’s your name| 20210701| 20210701| 2021| 2021|
|20210702|20210702| 29991231| 1| 1| what’s wrong| 20210701| 20210702| 2021| 2999|
|20210702|20210701| 29991231| 1| 2| what’s your age| 20210701| 20210701| 2021| 2999|
|20210710| 20210701| 20210701| 0| 1| what’s your name| 20210701| 20210701| 2021| 2021|
|20210710|20210702| 20210709| 0| 1| what’s wrong| 20210701| 20210702| 2021| 2021|
|20210710|20210710| 29991231| 1| 1| whattttttttttt| 20210701| 20210710 |2021| 2999|
|20210710|20210701 |29991231 |1| 2| what’s your age| 20210701| 20210701| 2021| 2999|

拉链表的使用

获取当前最新数据

1)优先使用最新表而非快照表

2)一定要用快照: sql select * from dwd_test_a_fds where dayid='cur_time' and data_is_active=1

获取某一天的状态

1
2
3
4
5
6
select * from dwd_test_a_fds
where dayid='cur_time'
and data_start_year<=substr('$v_date',1,4) --索引查询
and data_end_year>=substr('$v_date',1,4)
and data_start_date<='$v_date'
and data_end_date>='$v_date

根据业务字段字段进行匹配

1
2
3
4
5
6
7
8
select a.*
from
(select * from test_test where dayid='$v_date') a
left join
(select * from dwd_test_a_fds where dayid='cur_time') b
on a.biz_id=b.id
and substr(a.biz_time,1,8)>=b.data_start_time
and substr(a.biz_time,1,8)<=b.data_end_time

效果

待测试,暂定dwd_order_shop_full_d 表

流程

变量

成员变量与局部变量的区别?

  • 语法形式 :从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  • 存储方式 :从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  • 生存时间 :从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。
  • 默认值 :从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

转载

本文转自基础概念与常识

java下的JUC是什么?

是 java jdk 包java.util.concurrent的简称,jdk将并发多线程相关的类都放到该包下。

Atomic 原子类介绍

Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

所以,所谓原子类说简单点就是具有原子/原子操作特征的类。

并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic下,如下图所示。

JUC原子类概览

根据操作的数据类型,可以将 JUC 包中的原子类分为 4 类

基本类型

使用原子的方式更新基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

数组类型

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整型数组原子类
  • AtomicLongArray:长整型数组原子类
  • AtomicReferenceArray:引用类型数组原子类

引用类型

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
  • AtomicMarkableReference :原子更新带有标记位的引用类型

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

CAS ABA 问题

  • 描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功。
  • 例子描述(可能不太合适,但好理解): 年初,现金为零,然后通过正常劳动赚了三百万,之后正常消费了(比如买房子)三百万。年末,虽然现金零收入(可能变成其他形式了),但是赚了钱是事实,还是得交税的!
  • 代码例子(以AtomicInteger为例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDefectDemo {
public static void main(String[] args) {
defectOfABA();
}

static void defectOfABA() {
final AtomicInteger atomicInteger = new AtomicInteger(1);

Thread coreThread = new Thread(
() -> {
final int currentValue = atomicInteger.get();
System.out.println(Thread.currentThread().getName() + " ------ currentValue=" + currentValue);

// 这段目的:模拟处理其他业务花费的时间
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}

boolean casResult = atomicInteger.compareAndSet(1, 2);
System.out.println(Thread.currentThread().getName()
+ " ------ currentValue=" + currentValue
+ ", finalValue=" + atomicInteger.get()
+ ", compareAndSet Result=" + casResult);
}
);
coreThread.start();

// 这段目的:为了让 coreThread 线程先跑起来
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

Thread amateurThread = new Thread(
() -> {
int currentValue = atomicInteger.get();
boolean casResult = atomicInteger.compareAndSet(1, 2);
System.out.println(Thread.currentThread().getName()
+ " ------ currentValue=" + currentValue
+ ", finalValue=" + atomicInteger.get()
+ ", compareAndSet Result=" + casResult);

currentValue = atomicInteger.get();
casResult = atomicInteger.compareAndSet(2, 1);
System.out.println(Thread.currentThread().getName()
+ " ------ currentValue=" + currentValue
+ ", finalValue=" + atomicInteger.get()
+ ", compareAndSet Result=" + casResult);
}
);
amateurThread.start();
}
}

输出内容如下:

1
2
3
4
Thread-0 ------ currentValue=1
Thread-1 ------ currentValue=1, finalValue=2, compareAndSet Result=true
Thread-1 ------ currentValue=2, finalValue=1, compareAndSet Result=true
Thread-0 ------ currentValue=1, finalValue=2, compareAndSet Result=true

下面我们来详细介绍一下这些原子类。

基本类型原子类

基本类型原子类介绍

使用原子的方式更新基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean :布尔型原子类

上面三个类提供的方法几乎相同,所以我们这里以 AtomicInteger 为例子来介绍。

AtomicInteger 类常用方法

1
2
3
4
5
6
7
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

AtomicInteger 常见方法使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {

public static void main(String[] args) {
// TODO Auto-generated method stub
int temvalue = 0;
AtomicInteger i = new AtomicInteger(0);
temvalue = i.getAndSet(3);
System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:0; i:3
temvalue = i.getAndIncrement();
System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:3; i:4
temvalue = i.getAndAdd(5);
System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:4; i:9
}

}

基本数据类型原子类的优势

通过一个简单例子带大家看一下基本数据类型原子类的优势

① 多线程环境不使用原子类保证线程安全(基本数据类型)

1
2
3
4
5
6
7
8
9
10
11
class Test {
private volatile int count = 0;
//若要线程安全执行执行count++,需要加锁
public synchronized void increment() {
count++;
}

public int getCount() {
return count;
}
}

② 多线程环境使用原子类保证线程安全(基本数据类型)

1
2
3
4
5
6
7
8
9
10
11
12
class Test2 {
private AtomicInteger count = new AtomicInteger();

public void increment() {
count.incrementAndGet();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public int getCount() {
return count.get();
}
}

AtomicInteger 线程安全原理简单分析

AtomicInteger 类的部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。

CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

数组类型原子类

数组类型原子类介绍

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray :引用类型数组原子类

上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。

AtomicIntegerArray 类常用方法

1
2
3
4
5
6
7
public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

AtomicIntegerArray 常见方法使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayTest {

public static void main(String[] args) {
// TODO Auto-generated method stub
int temvalue = 0;
int[] nums = { 1, 2, 3, 4, 5, 6 };
AtomicIntegerArray i = new AtomicIntegerArray(nums);
for (int j = 0; j < nums.length; j++) {
System.out.println(i.get(j));
}
temvalue = i.getAndSet(0, 2);
System.out.println("temvalue:" + temvalue + "; i:" + i);
temvalue = i.getAndIncrement(0);
System.out.println("temvalue:" + temvalue + "; i:" + i);
temvalue = i.getAndAdd(0, 5);
System.out.println("temvalue:" + temvalue + "; i:" + i);
}

}

引用类型原子类

引用类型原子类介绍

基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
  • AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

上面三个类提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。

AtomicReference 类使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {

public static void main(String[] args) {
AtomicReference<Person> ar = new AtomicReference<Person>();
Person person = new Person("SnailClimb", 22);
ar.set(person);
Person updatePerson = new Person("Daisy", 20);
ar.compareAndSet(person, updatePerson);

System.out.println(ar.get().getName());
System.out.println(ar.get().getAge());
}
}

class Person {
private String name;
private int age;

public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

}

上述代码首先创建了一个 Person 对象,然后把 Person 对象设置进 AtomicReference 对象中,然后调用 compareAndSet 方法,该方法就是通过 CAS 操作设置 ar。如果 ar 的值为 person 的话,则将其设置为 updatePerson。实现原理与 AtomicInteger 类中的 compareAndSet 方法相同。运行上面的代码后的输出结果如下:

1
2
Daisy
20

AtomicStampedReference 类使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceDemo {
public static void main(String[] args) {
// 实例化、取当前值和 stamp 值
final Integer initialRef = 0, initialStamp = 0;
final AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(initialRef, initialStamp);
System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp());

// compare and set
final Integer newReference = 666, newStamp = 999;
final boolean casResult = asr.compareAndSet(initialRef, newReference, initialStamp, newStamp);
System.out.println("currentValue=" + asr.getReference()
+ ", currentStamp=" + asr.getStamp()
+ ", casResult=" + casResult);

// 获取当前的值和当前的 stamp 值
int[] arr = new int[1];
final Integer currentValue = asr.get(arr);
final int currentStamp = arr[0];
System.out.println("currentValue=" + currentValue + ", currentStamp=" + currentStamp);

// 单独设置 stamp 值
final boolean attemptStampResult = asr.attemptStamp(newReference, 88);
System.out.println("currentValue=" + asr.getReference()
+ ", currentStamp=" + asr.getStamp()
+ ", attemptStampResult=" + attemptStampResult);

// 重新设置当前值和 stamp 值
asr.set(initialRef, initialStamp);
System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp());

// [不推荐使用,除非搞清楚注释的意思了] weak compare and set
// 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191]
// 但是注释上写着 "May fail spuriously and does not provide ordering guarantees,
// so is only rarely an appropriate alternative to compareAndSet."
// todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发
final boolean wCasResult = asr.weakCompareAndSet(initialRef, newReference, initialStamp, newStamp);
System.out.println("currentValue=" + asr.getReference()
+ ", currentStamp=" + asr.getStamp()
+ ", wCasResult=" + wCasResult);
}
}

输出结果如下:

1
2
3
4
5
6
currentValue=0, currentStamp=0
currentValue=666, currentStamp=999, casResult=true
currentValue=666, currentStamp=999
currentValue=666, currentStamp=88, attemptStampResult=true
currentValue=0, currentStamp=0
currentValue=666, currentStamp=999, wCasResult=true

AtomicMarkableReference 类使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.concurrent.atomic.AtomicMarkableReference;

public class AtomicMarkableReferenceDemo {
public static void main(String[] args) {
// 实例化、取当前值和 mark 值
final Boolean initialRef = null, initialMark = false;
final AtomicMarkableReference<Boolean> amr = new AtomicMarkableReference<>(initialRef, initialMark);
System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked());

// compare and set
final Boolean newReference1 = true, newMark1 = true;
final boolean casResult = amr.compareAndSet(initialRef, newReference1, initialMark, newMark1);
System.out.println("currentValue=" + amr.getReference()
+ ", currentMark=" + amr.isMarked()
+ ", casResult=" + casResult);

// 获取当前的值和当前的 mark 值
boolean[] arr = new boolean[1];
final Boolean currentValue = amr.get(arr);
final boolean currentMark = arr[0];
System.out.println("currentValue=" + currentValue + ", currentMark=" + currentMark);

// 单独设置 mark 值
final boolean attemptMarkResult = amr.attemptMark(newReference1, false);
System.out.println("currentValue=" + amr.getReference()
+ ", currentMark=" + amr.isMarked()
+ ", attemptMarkResult=" + attemptMarkResult);

// 重新设置当前值和 mark 值
amr.set(initialRef, initialMark);
System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked());

// [不推荐使用,除非搞清楚注释的意思了] weak compare and set
// 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191]
// 但是注释上写着 "May fail spuriously and does not provide ordering guarantees,
// so is only rarely an appropriate alternative to compareAndSet."
// todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发
final boolean wCasResult = amr.weakCompareAndSet(initialRef, newReference1, initialMark, newMark1);
System.out.println("currentValue=" + amr.getReference()
+ ", currentMark=" + amr.isMarked()
+ ", wCasResult=" + wCasResult);
}
}

输出结果如下:

1
2
3
4
5
6
currentValue=null, currentMark=false
currentValue=true, currentMark=true, casResult=true
currentValue=true, currentMark=true
currentValue=true, currentMark=false, attemptMarkResult=true
currentValue=null, currentMark=false
currentValue=true, currentMark=true, wCasResult=true

对象的属性修改类型原子类

对象的属性修改类型原子类介绍

如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类。

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新长整形字段的更新器
  • AtomicReferenceFieldUpdater :原子更新引用类型里的字段的更新器

要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符。

上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerFieldUpdater为例子来介绍。

AtomicIntegerFieldUpdater 类使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

public class AtomicIntegerFieldUpdaterTest {
public static void main(String[] args) {
AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

User user = new User("Java", 22);
System.out.println(a.getAndIncrement(user));// 22
System.out.println(a.get(user));// 23
}
}

class User {
private String name;
public volatile int age;

public User(String name, int age) {
super();
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

}

输出结果:

1
2
22
23

能不能给我简单介绍一下 AtomicInteger 类的原理 ?

转载

本文转自Java 并发常见面试题总结(下)