ThreadLocal的线性探测

参考:
https://www.liaoxuefeng.com/wiki/1252599548343744/1306581251653666
http://songkun.me/2019/10/26/2019-10-26-java-threadlocal-hash-clash/
https://juejin.im/post/5d43e415e51d4561db5e39ed

避免ThreadLocal的内存泄露

参考上述资料,ThreadLocal是存在线程自己的变量,只不过用threadLocalMap把这个线程所有的ThreadLocal变量都集中存储了,key用的是ThreadLocal对象,value用的是对象T,也就是实际的数据。
由于key用的是弱引用,因此使用ThreadLocal时,使用结束时一定要进行remove(),否则会导致内存泄露。

这里的内存泄露有3重含义:

局部ThreadLocal变量

这个数据语义上已经不用了,而且语法上ThreadLocal变量也已经离开作用域,也回收了,key变成了null;但由于ThreadLocal设计实现上的缺陷,ThreadLocalMap中的key变成null,value却不是null,依然强引用了数据,所以不能自动回收,需要程序员手动处理:

1
2
3
4
5
6
try {
threadLocalUser.set(user);
...
} finally {
threadLocalUser.remove();
}

static ThreadLocal变量

这个数据语义上已经不用了,但语法上这个ThreadLocal是static,没有离开作用域,
因此理论上还能访问到,设计上的生命周期太过于长了。 这种属于设计缺陷。也需要程序员手动处理,代码同上,也是调用remove()方法。

搭配线程池的ThreadLocal变量

这个数据本意上已经不用了, 如果不是线程池的线程,如果只是上述两种情况,也只会内存泄露一段时间,等到线程销毁的时候,就会释放相应的threadlocalMap和内存。
但如果搭配线程池使用,这里的ThreadLocalMap和ThreadLocal变量都会继续使用,永不释放内存。
因此程序语义变为不同任务都共享这个threadLocal变量,很可能有语义错误。
保险起见,也是同上,调用remove()方法即可。

remove方法背后: ThreadLocalMap的实现机制

remove方法的语义很简单,就是将ThreadLocalMap的对应key/value都删除。
实际底层实现却并不简单,涉及到性能考虑=>线性探测法=>删除时rehash的连锁反应,导致实现较为特殊。

斐波那契哈希

ThreadLocalMap希望尽可能提高性能,因此使用斐波那契哈希,使用魔法数0x61c88647,为每一个ThreadLocal变量涉及了尽可能均匀、理论无碰撞的完美哈希,同时辅助以rehash扩容,来保证碰撞和线性探测死循环尽可能不发生。

线性探测与删除

ThreadLocalMap处理哈希冲突时使用的是线性探测法,因此删除key的时候不能直接简单把entry置为null; 它采用的方法是把后续每个不为null的entry进行rehash, 放在合适的位置,保证不会因为删除导致线性探测失效中断。这里会把整个哈希表的数组看作循环队列,一直rehash到null的entry,因为当前的entry一定是null,而且负载因子不会太高,而且是单线程,因此一定不会死循环,有终止条件。

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
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// 删除对应位置的entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

Entry e;
int i;

// rehash过程,直到entry为null
for (i = nextIndex(staleSlot, len);(e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// k为空,证明已经被垃圾回收了
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
// 判断当前元素是否处于"真正"应该待的位置
if (h != i) {
tab[i] = null;
// 线性探测
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

综上, 理论上每次删除都会进行局部链的rehash,但是由于斐波那契哈希设计上是绝对均匀,因此这个”局部链”的长度理论上是非常短甚至是0.
此外, 除了这种rehash的方法,还可以对entry进行delete标记来确保线性探测不会中断。

长连接相关

WHAT: 什么是长连接

长连接: TCP, 默认不断开,用keep alive心跳包保活;
持久连接: HTTP 1.1+ keep alive, 默认断开。
短连接: 普通HTTP

WHY: 长连接有什么好处? 为什么要使用长连接

  1. 不同域名可以复用一个长连接; (HTTP不行)
  2. 不依赖DNS,节省DNS开销、避免DNS劫持;(HTTP不行)
  3. 减少握手时间开销;
  4. 可以push消息;

HOW: 如何实现一个好的长连接? (难点何在)

难点1 长连接断开

导致断开的原因:

  1. 各个环节的超时时间不匹配; – 动态探测合适的超时时间;
  2. 后台服务; – 或者服务主备切换
  3. app后台进程挂了; – app保活课题
  4. 切换网络;

难点2 机器资源

1亿长连接:
10w / 台 => 1000台机器
200w / 台 => 50台机器

20%活跃度=> 总共需要2kw连接;
2000w => 10台~200台;

连接限制:

  1. 网卡限制: 网卡带宽、每秒通信量;
  2. 内核限制: 每个进程最大打开文件句柄数; (NR_OPEN =100w)
  3. 内存限制: TCP的发送、输出缓冲;

假设一台机器抗200w个连接,则网卡需要:
200w1KB/s = 2000MB/s = 16 Gps
假设平均20%活跃,则
0.2 = 3.2 Gps
依此类推内存。

难点3 惊群效应

cpu: 100%
thundering herd: 唤醒一群进程,只有一个进程工作,其他的又继续睡;反复这个流程。

多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应。

实现要点

  1. 处理好上述长连接断开的4个原因,保证服务质量;
  2. 进行断开重连: 监控+重连。
  3. 架构容灾: (1) 多地部署接入点;
    (2) 长连接失败则降级到udp\http;

分布式限流: redis+令牌桶

限流一般有漏桶和令牌桶两种实现,详情网上很多资料。
一般都会选用令牌桶算法,比较灵活,可以支持预热、容忍一定突发流量、预支一部分流量的弹性。

单进程限流

单个jvm限流有现成的库,谷歌的guava库提供了RateLimiter,底层实现上有两个,一个是能容忍突发的实现,一个是能提供预热功能的实现;通过create时提供不同的参数来获得。

实现原理

朴素解法

如果按照令牌桶算法朴素的定义来实现的话,一个很自然的思路就是增加一个定时线程、进程,然后不断生成新的token;

朴素解法的缺点

重度依赖这个定时线程,如果定时程序挂了,所有工作线程就被卡住了,风险较大。
如果给定时线程加监控,又会需要监控的监控,那就变成俄罗斯套娃了。

guava库中的解法

需要访问者多输入一个参数: 当前时钟。
lazy eval: 每次取token的时候才计算当前”应该”有多少token。
基于时钟来计算: 当前时钟下, 过去了多久没有生成token,应该生成多少新的token。
核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time.
*/
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
storedPermits = min(maxPermits,
storedPermits
+ (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros());
nextFreeTicketMicros = nowMicros;
}
}

guava库中解法的优点

开销更低: 去掉了定时线程的开销;
健壮性更高: 每个线程都可以承担生成新token的任务;

guava库中解法的缺点

每个线程的时钟得对齐。但这一点在单进程场景下很好保证。

使用

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
import com.google.common.util.concurrent.RateLimiter;
private static void run1() {
RateLimiter limiter = RateLimiter.create(5);// 令牌桶
while (true) {
System.out.println("get 1 tokens: " + limiter.acquire() + "s"
);

}
}

/**
* 3秒预热期: 预热期内限频较严格: 1.3s , 0.9s , 0.6s // 线性提速
* 预热期后: 正式达到1秒2个的额定速度.
*/
private static void run2() {// 预热测试
RateLimiter r = RateLimiter.create(2, 3, TimeUnit.SECONDS);
while (true) {
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("end");
}
}

private static void run3() {// 突发测试
RateLimiter r = RateLimiter.create(5);
while (true) {
System.out.println("get 5 tokens: " + r.acquire(5) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");// 每次都负责还债
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("end");
}
}

分布式限流

如果是多实例的情况下,下游处理能力有上限时(例如物料有限),需要对整体有一个限流。
可以参考guava的实现,实现一个基于redis的,有一定弹性的令牌桶实现:
(由于是借鉴guava的实现,所以有相同的依赖: 所有进程、机器的时钟对齐)

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
public void tokenLimitDemo() {
DefaultRedisScript<Long> lua = new DefaultRedisScript<>();
lua.setResultType(Long.class);
lua.setScriptSource(new ResourceScriptSource(new
ClassPathResource("tokenLimiter.lua")));
List<String> keys = new ArrayList<>();
keys.add("test_ip");
for (int i = 0; i < 100; i++) {
Long res = (Long) redisTemplate.execute(lua, keys
, "1"
, String.valueOf(System.currentTimeMillis())
);
if (res != 1) {
try {
System.out.println("waiting");
Thread.sleep(100);
i--;
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(res);
}
}
}

相应的lua脚本:

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
-- 令牌桶算法:
-- 1. 校验输入:
local need_token = tonumber(ARGV[1])
local req_time = tonumber(ARGV[2])
if type(need_token) ~= "number" or type(req_time) ~= "number" then
return 0
end

-- 2. 校验redis:
local key = KEYS[1]
local info = redis.pcall("HMGET", key, "last_time", "cur_token_num", "max_token", "rate")
local last_time = tonumber(info[1])
local cur_token_num = tonumber(info[2])
local max_token = tonumber(info[3])
local rate = tonumber(info[4])

if type(last_time) ~= "number" then -- init
last_time = req_time
max_token = 2 -- 最大token弹性
cur_token_num = max_token -- 假设已经预热完毕
rate = 1
-- 初始化redis:
redis.pcall("HMSET", key, "last_time", req_time, "cur_token_num", cur_token_num)
redis.pcall("HMSET", key, "max_token", max_token, "rate", rate)
end

-- 3. 处理请求:
local result = 0
if tonumber(need_token) > tonumber(max_token) then
result = 0
end

local new_gen_token = math.floor((req_time - last_time) / 1000) * rate
local cur_token_num = math.min(cur_token_num + new_gen_token, max_token)

if tonumber(need_token) > tonumber(cur_token_num) then
result = 0
else -- do consume token
cur_token_num = cur_token_num - need_token
result = 1
-- 保存进度:
redis.pcall("HMSET", key, "last_time", req_time, "cur_token_num", cur_token_num)
end

return result

RTP/RTCP协议

实时传输协议RTP/RTCP

参考:
https://www.jianshu.com/p/631273bc9847

底层理论上也不一定用tcp/udp,实际上为了性能大多用udp。

RTP: Real-time Transport Protocol
RTCP: RTP Control Protocol

1
2
RTP: 用udp来传数据;      (偶数端口)
RTCP: 用udp来传控制信息; (奇数端口, 一般是RTP的端口+1) 服务质量的监视与反馈、媒体间的同步,以及多播组中成员的标识

计算机网络

交换机级别: 组播协议,一对多
多对多可以转换成一对多。

利用序列样本超越A/B Test: interleaving和多臂老虎机

interleaving实验: 快速找到最优解;
多臂老虎机: 降低实验成本;

背景

经典统计分析的样本: 农业统计中样本是同时产生的。
在线服务中的样本: 序列样本,在线服务中样本是依次到达的;

利用这个特性,可以有比传统统计分析方法成本更小、更灵活的实验方法。
序列样本环境下的假设检验,在相同显著性和检验力下,经典t检验结束更早,需要样本更少。

Interleaving实验

交错实验
优先确定哪个方案更好,而不是统计出具体效果好多少.
参考: https://zhuanlan.zhihu.com/p/31770319
优点: 更少的样本达到与Abtest相同的偏向结果;
缺点: 无法知道究竟有多好的效果;(比如A组究竟能提高多少阅读时长)

解决方案

Interleaving+ABtest两阶段完成整个评估。

要点:抛硬币交错,保证公平。
灵敏度: 高于AB;
准确度: 与AB有很强的相关性;
场景: 排名算法

案例: Netflix的方法

第一阶段: 快速筛选,从众多算法中挑选出最有前途的排名算法;
第二阶段,进行传统的A/B测试,以测量长期效应;

使用条件

  1. 指标对排序算法的质量非常敏感;
  2. 对第二阶段进行准确预测:第一阶段衡量的指标与A/B 评估的指标正相关;

指标示例

传统AB指标: 留存、观看时长;
交错测试指标: 观看时长比例;

MAB: 多臂老虎机

参考: http://www.fengjunchen.com/%E9%80%82%E7%94%A8%E4%BA%8E%E5%9C%A8%E7%BA%BF%E6%9C%8D%E5%8A%A1%E7%9A%84ab%E6%B5%8B%E8%AF%95%E6%96%B9%E6%B3%95%E8%AE%BA/

What: 什么是多臂老虎机

一种在线实验的方法,特点是兼顾实验结果和实验成本,不会让差劲的方案暴露给用户太久。

在线上进行大规模实验的时候,可能会影响用户体验,从而影响用户留存等关键业务指标。因此我们希望在实验过程中,尽可能降低差方案曝光的频率。为了达到这个目的,大致有两种思路:
思路1: 动态结束;一旦A方案胜出,立即停止实验,采用A方案;
思路2: 非均衡分组;在优势方案下多分配用户.(多臂老虎机)

使用场景: 序列样本/在线服务

背景

Bandit: 老虎机

多臂老虎机问题:
老虎机有N个摇杆,每个得到奖励的概率为p1~pN;
假设可以拉很多次摇杆,使用什么策略可以使总收益最大化?

一种可能的思路

先抽样统计收益较好的摇杆,然后利用统计解决快速调整策略。
也就是分两个阶段:

  1. 探索规律;
  2. 利用规律;

汤普森抽样

在每个周期计算”摇杆i是最优摇杆”的后验概率,作为下一个周期随机分配摇杆的依据。

优点: 灵活性、扩展性、保证实验过程坑用户比例最小;
缺点: 没有人公开发表过简单靠谱的停止条件。

PVR: 停止条件的解决方案

google analytics: PVR(潜在剩余价值)。
定义分界线为a,分界概率为b:

P(潜在最优收益>(1+a%)*目前最优收益) < b

google analytics的参数设置是a=0.01,b=0.05,即潜在最优收益比目前最优收益大1%的概率不到5%。

PVR方案的缺点

即使A/B方案没有任何区别,PVR也会导致相当一部分实验在有限时间内终止。
第一类错误的概率随时间延长增加。

scott: 线上实验不需要考虑第一类错误,因为代价极小;而第二类错误是致命的。

案例

谷歌analytics/yahoo/bing: 汤普森抽样的贝叶斯解法
论文: http://quinonero.net/Publications/AdPredictorICML2010-final.pdf
数学要求: 超精深
相关图灵书: 《Bandit Algorithms for Website Optimization》

相关论文: http://www.economics.uci.edu/~ivan/asmb.874.pdf

[论文梗概]在线服务的实验评估面临的挑战

论文原文: https://exp-platform.com/Documents/2019-FirstPracticalOnlineControlledExperimentsSummit_SIGKDDExplorations.pdf

概念

OEC: Overall Evaluation Criterion/总体评价标准
OCEs: Online controlled experiments/在线受控实验=A/B Tests

挑战汇总

  1. 如何评估长期效应;
  2. 如何解决社交网络对实验的影响;
  3. 如何解决多次实验间的影响;
  4. 如何寻找OEC指标;
  5. 如何进行细分人群的实验(HTE模型);
  6. 如何确保数据质量、进行数据治理;
  7. 如何建立实验文化;
  8. 如何处理配置文件膨胀问题;

如何评估长期效应

问题

  1. 大部分abtest持续两周内;
  2. 我们真正关心的是比两周更长的长期效应;

如何确保上述两者吻合?

长期效应:常见解决方案

长期实验;

Twitter: 实验通常持续4周,从最后两周的数据开始统计。

保留反向桶;(holdout)

代理指标

Netflix: 使用logistic回归,寻找反映留存指标的代理指标。
LinkedIn: lifetime value模型;
Uber : macro-economic模型;
facebook: 分位数回归、GBDT

缺点: 相关性!=因果关系
可能的歧路:代理指标涨、长期指标不动。

bing/google: 心理学模型,从因果关系出发

对用户学习效应进行建模

google: 用长期实验,观察用户学习的效应(参见原文论文[38])
实验流程:

  1. 第一组:立刻使用B配置;
  2. 第二组:滞后一段时间后使用B配置;
  3. 第N组:依次类推。
  4. 随机组: 随机滞后。

比较不同学习阶段的用户。

结果: 得出指数曲线,预测长期的情况。

如何解决社交网络对实验的影响

问题

传统A/B test的假设: 实验间不相互影响;

假设不成立:

  1. 社交网络影响;
  2. 多次反复实验残留效应影响。
    对照组的用户可能与实验组认识/有交互,因此也受实验影响。

问题:

  1. 如何避免这个问题?(无法避免)
  2. 如何检测这个问题?(评估影响)

常见解决方案

生产者消费者模型

LinkedIn: 生产者消费者模型
实验:
生产者:允许在帖子上打标签;
消费者:允许看到帖子的标签;

当95%的生产者可以打标签:可以测试50%的消费者是否允许看到标签的影响;
当95%的消费者可以看到标签: 可以测试50%的生产者是否打标签的影响。

类似的模型可以用于@功能。

已知影响力模型

Link/facebook/谷歌: 用户影响力已知,将用户聚类成子网络。
实验在各个子网络进行。

相关论文: [20, 26, 9].
[20]:
http://www.unofficialgoogledatascience.com/2018/01/designing-ab-tests-in-collaboration.html

一对一通信

对于1对1通信相关的feature:
Linkin: 数据统计时分为4类:
1.实验组=>实验组;
2.对照组=>对照组;
3.实验组=>对照组;
4.对照组=>实验组;

Skype: 以会话为维度实验。每个用户都可能随机位于实验、对照组。

市场影响

实验结果受供求关系影响。(如打车)
忽视供求关系随机分组,可能会导致一些Bias。
Lyft: 构造小规模近似供求关系的市场,进行实验。

广告领域:预算窃取
实验组:窃取对照组预算(对照组的广告预算也花在了实验组)
当放量时,整体收入并没有达到预期幅度。
解决方案:按预算比例投放到实验组。

挑战:多个实验之间的影响

问题

如果两个实验之间不是完全独立。
(例1个实验改变背景,1个实验改变字体颜色)

解决方案

微软、谷歌:相互影响(互斥)的实验放在同一层内,这一个层由一个团队负责(相关特性);
微软:每天跑任务检测层间影响;

挑战:如何寻找OEC指标

问题

HiPPO: 工资最高的人说了算。
OEC: 数据驱动,核心指标说了算。

指标=OEC指标+其他护栏、诊断指标;
OEC指标: 决策;(KPI)
诊断指标: OEC指标为啥变化。

解决方案

OEC指标满足的条件

  1. 反映KPI;
  2. 难以作弊;
  3. 必须敏感;
  4. 计算的成本不能太高(可行性)、每天都要给至少上百个实验计算;(不能是只能打电话确认的指标)
  5. 考虑到影响KPI的各种场景;
  6. 能顾及到新的场景。

自己发现OEC

对于搜索引擎: 查询量不是好的OEC,查询量大可能是因为不准。
好的OEC: 每个用户的会话数。(不准的话,单次会话查询量很大、会话数少。)

一般来说:
OEC指标属性:
HEART:

Happiness, Engagement, Adoption, Retention, and Task success

护栏指标:pv、vv、时长、延迟、七日用户数。

对于浏览类场景,找到HEART指标仍是挑战。
挑战:识别用户的目的。
目的1:快速找到特定东西;
目的2:不想找特定东西,只是想探索新东西;

对于目的1:点击次数越多越不好;
对于目的2:点击次数越多越好。
两者矛盾,因此存在挑战。

权衡产品目标

我们潜意识假定产品目标是确定不变的、统一的。
然而这不是小问题。因此假定不一定总是成立。

  1. 团队可能调整战略(例如每个阶段的盈利策略);
  2. 每个局部的实际方向可能和总体不符。
  3. 多目标(指标)冲突。(收入与体验)

机器学习建立OEC

  1. 基于用户行为,打分预测满意度;
  2. 组合多个指标,创造敏感又准确的OEC指标;

缺点: 不可解释、难以发现误判。

如何进行细分人群的实验

细分人群可能可以有更多洞察。

常见的分类方法

  1. 国家、使用语言;
  2. 活跃度;
  3. 设备和平台;
  4. 周中周末、新奇效应;
  5. 产品特定分类:
    (1)linked: 招聘者、应聘者;
    (2)Netflix: 不同网速;
    (3)Airbnb: 是否曾经预定、渠道来源;

相关挑战和解决方案

  1. 计算规模变大; =>按需分析: 某些需求可以用更高成本的分析方法;
  2. 样本变少; => 使用稀疏模型
  3. 分层的正交性依赖大样本;
  4. 实验者不是统计学专家,实验结果必须明确易懂; =>用回归和聚类来简化结果。(Fused Lasso、Total Variation Regularization)
  5. 相关性!=因果关系: 细分人群的结果不同,但相应分类属性可能不是导致结果不同的原因,可能只是共线。
    解决方案:可以从历史数据中寻求更多信息。

数据质量与数据治理

问题

实验可信度依赖数据质量。如何确保数据质量?

解决方案

工业界测试

工业界测试: Ratio Mismatch test (见原文引用[13, 22, 23, 45])
抽样校验告警。

数据规范

Netflix\MSN\Bing:json
优点:拓展性好;
缺点: 格式变化很快;

Airbnb/facebook:每个实验自己设定格式、打点。(bring-your-own-data)
Microsoft Office: 事件日志;
其他: 固定部分列,预留json列。

及时可靠的指标

不断新增指标对实验平台的挑战。

LinkedIn/Facebook/Airbnb: 实验平台与指标框架分离,各个业务对自己的指标负责。实验平台仅负责从统计结果进行评估。
Microsoft/Google/Booking.com/Lyft:指标由实验团队从日志中统计。

LinkedIn:
指标委员会:批准新增、修改指标,确保指标质量;
某些公司:
指标必须经过验证敏感性、精确性,确定和实验结果有有意义的区别。
Booking.com:
自动检测比较两组独立数据。

指标的ownership

指标通常有特定的关注者和归属者。
微软:特定指标下降=>通知指标归属者(含相关实验信息)。

其他公司:提供工具搜索:影响特定指标的实验。

新分析方法

需要考虑:

  1. 新方法适用场景;
  2. 新方法的计算、复杂性成本 < 收益;
  3. 不同方法冲突时,相信哪一种;
  4. 保证正确解释结果;

如何建立实验文化

日志、实验、实验记录、实验评估的文化。
原则:
谦逊: 直觉判断是贫瘠的(HIPPO);
微软:只有1/3的创意在数据上有显著结果。

能接受实验效果不好。

解决方案: 建立规范的实验平台。

Booking.com: 将实验平台变得像游戏。

Yandex: 中央实验小组:由各个组的专家组成
Amazon: 类似,酒吧专家;(酒吧是Amazon的实验平台)
Twitter:实验牧羊人:50个牧羊人,一个星期值班时间,
Booking.com: 实验大使制度、同行评议制度(按钮:随机选择一个实验来评议)
微软:1~2个数据科学家跟着一个业务团队。随着业务团队接受培训、能力提高,数据科学家回到中心、转跟另一个业务团队。定期分享。
谷歌: checklist。每次实验填写:假设、指标、变化幅度。

配置文件膨胀的挑战

问题

随着实验越来越多,客户端要拉的配置文件越来越大。

解决方案

定期合代码,固化一些feature;

java8笔记

lambda语法

i->{return "xxx";}
本质上是借用匿名类来实现,并没有超脱原来的java的解题思路。

这种类似于传一个函数指针的模式,底层是@FunctionalInterface注解的接口,因此我们可以自己声明一个接口作为方法的行参:

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
@FunctionalInterface
public interface GetIdataFunction {
int getI(int i);

default String getInfo() {
return "找下标为i的数据";
}

}

public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0 || target < matrix[0][0]) {
return false;
}

int lowerBound = getLowerBound(target, 0, matrix.length - 1, i -> matrix[i][0]);
if (lowerBound >= 0) {
return true;
}
final int finalLowerBound = -lowerBound - 2;
int x = getLowerBound(target, 0, matrix[0].length - 1, i -> matrix[finalLowerBound][i]);
return x >= 0;
}

private int getLowerBound(int target, int left, int right, GetIdataFunction helper) {
int mid;
while (left <= right) {
mid = (left + right) >>> 1;
int midVal = helper.getI(mid);
if (target < midVal) {
right = mid - 1;
} else if (target > midVal) {
left = mid + 1;
} else {
return mid;
}
}
return -left - 1;
}

现成的Function声明汇总

java8给了一些统一的描述术语(DSL)来分类这些函数接口:

接口 lambda 含义
Predicate< T> T->boolean 类似于filter,接收T类型返回boolean
Consumer< T> T->void 类似于foreach,接收T类型返回void
Function< T,R> T->R 类似于Map,接收T类型返回R类型
Supplier< T> ()->T
UnaryOperator< T> T->T
BinaryOperator< T> (T,T)->T
BiPredicate< L,R> (L,R)->boolean
BiConsumer< T,U> (T,U)->void
BiFunction< T,U,R> (T,U)->R

常见的几种函数指针都可以用这些现成的声明,如果要抛异常,或者比较特殊的入参、出参,就自己用@FunctionalInterface注解声明个接口和相应方法即可。

此外,上述接口的T,R泛型都需要类、引用,因此如果要节省装箱开销,可以用相应的原始类型声明,比如一个int->int的变换,相应的接口就是IntUnaryOperator
因此上一节中的代码可以简化成:

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
public boolean searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0 || target < matrix[0][0]) {
return false;
}

int lowerBound = getLowerBound(target, 0, matrix.length - 1, i -> matrix[i][0]);// 闭包捕获matrix
if (lowerBound >= 0) {
return true;
}
final int finalLowerBound = -lowerBound - 2;
int x = getLowerBound(target, 0, matrix[0].length - 1, i -> matrix[finalLowerBound][i]);// 闭包捕获matrix和finalLowerBound
return x >= 0;
}

private int getLowerBound(int target, int left, int right, IntUnaryOperator helper) {
int mid;
while (left <= right) {
mid = (left + right) >>> 1;
int midVal = helper.applyAsInt(mid);
if (target < midVal) {
right = mid - 1;
} else if (target > midVal) {
left = mid + 1;
} else {
return mid;
}
}
return -left - 1;
}

其他的原始类型(免装箱)的现成有的声明如下:(要用的时候可以查表)

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
// 1. Predicate<T> T->boolean :
IntPredicate,LongPredicate, DoublePredicate
// 2. Consumer<T> T->void :
IntConsumer,LongConsumer, DoubleConsumer
// 3. Function<T,R> T->R :
IntFunction<R>,
IntToDoubleFunction,
IntToLongFunction,
LongFunction<R>,
LongToDoubleFunction,
LongToIntFunction,
DoubleFunction<R>,
ToIntFunction<T>,
ToDoubleFunction<T>,
ToLongFunction<T>
// 4. Supplier<T> ()->T :
BooleanSupplier,IntSupplier,LongSupplier,DoubleSupplier
// 5. UnaryOperator<T> T->T :
IntUnaryOperator,
LongUnaryOperator,
DoubleUnaryOperator
// 6. BinaryOperator<T> (T,T)->T :
IntBinaryOperator,
LongBinaryOperator,
DoubleBinaryOperator
// 7. BiConsumer<T,U> (T,U)->void :
ObjIntConsumer<T>,
ObjLongConsumer<T>,
ObjDoubleConsumer<T>
// 8. BiFunction<T,U,R> (T,U)->R :
ToIntBiFunction<T,U>,
ToLongBiFunction<T,U>,
ToDoubleBiFunction<T,U>

无状态

默认无状态的操作: map,filter,collect; // 假如不使用外部变量(闭包)
有状态的操作: reduce,sum,max,sorted,distinct,skip,limit; // 有内部状态、初始值

// flatmap?

原始类型流

原始类型流如: IntStream

1
2
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream< Integer> stream = intStream.boxed();

原始类型的Optional:OptionalInt

1
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();

java中的蜂巢结构

d3中有个蜂巢结构,java8中也可以有:

1
2
3
4
5
6
7
8
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = menu.stream().collect(
groupingBy(Dish::getType,
groupingBy(dish -> {
if (dish.getCalories() <= 400) return CaloricLevel.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
});
));

还可以分组计数:

1
2
Map<Dish.Type, Long> typesCount = menu.stream().collect(
groupingBy(Dish::getType, counting()));

collectors中的characteristics

返回枚举值的list:

1
2
3
UNORDERED: 结果不受顺序影响;
CONCURRENT: 可以并行运行;
IDENTITY_FINISH: 累加器和最后的结果类型是一样的,不需要再转换。

一般是是返回后两者,因为一般都会希望维持顺序(如toList):

1
2
3
4
5
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(
IDENTITY_FINISH, CONCURRENT));
}

如果是生成质数这种,实现上依赖之前已经生成的质数,有状态,则不能并行。只返回一个IDENTITY_FINISH.

Spliterator

Spliterator的特性:

1
2
3
4
5
6
7
8
ORDERED 元素有既定的顺序(例如List),因此Spliterator在遍历和划分时也会遵循这一顺序
DISTINCT 对于任意一对遍历过的元素x和y,x.equals(y)返回false
SORTED 遍历的元素按照一个预定义的顺序排序
SIZED 该Spliterator由一个已知大小的源建立(例如Set),因此estimatedSize()返回的是准确值
NONNULL 保证遍历的元素不会为null
IMMUTABLE Spliterator的数据源不能修改。这意味着在遍历时不能添加、删除或修改任何元素
CONCURRENT 该Spliterator的数据源可以被其他线程同时修改而无需同步
SUBSIZED 该Spliterator和所有从它拆分出来的Spliterator都是SIZED

DISTINCT和SORTED比较难达到;
一般是:

1
2
3
4
@Override
public int characteristics() {
return ORDERED + SIZED + SUBSIZED + NONNULL + IMMUTABLE;
}

设计模式

策略模式: 策略发生变化,只修改用到的函数。(例如输入校验策略)
模版方法: 修改一大段代码中其中一个函数的调用实现。(传入函数指针)
观察者模式: 例如GUI场景下,注册一大堆监听事件。(传入函数指针)

责任链模式:

每个处理者可以增加后继处理节点,形成一个链表,每次处理的时候,调用下一个处理者。用lamda中的addThen的话:

1
2
3
4
5
6
7
UnaryOperator<String> headerProcessing =
(String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator<String> spellCheckerProcessing =
(String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline =
headerProcessing.andThen(spellCheckerProcessing);
String result = pipeline.apply("Aren't labdas really sexy?!!")

工厂模式

可以把构造函数放map里:

1
2
3
4
5
6
final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
map.put("loan", Loan::new);
map.put("stock", Stock::new);
map.put("bond", Bond::new);
}

lamda方法的单元测试

  1. 存成成员变量(函数指针);
  2. 匿名方法粒度太小了,应该测试上层方法;

lambda的副作用

  1. 出错信息模糊: 错误栈缺失准确的信息;

调试技巧

使用peek函数。

1
2
3
4
5
6
7
8
9
10
List<Integer> result =
numbers.stream()
.peek(x -> System.out.println("from stream: " + x))
.map(x -> x + 17)
.peek(x -> System.out.println("after map: " + x))
.filter(x -> x % 2 == 0)
.peek(x -> System.out.println("after filter: " + x))
.limit(3) // 只取3个.
.peek(x -> System.out.println("after limit: " + x))
.collect(toList());

3种兼容性

二进制级兼容: 现有二进制执行文件能无缝链接;
源码级兼容: 现有程序能够成功编译通过;
函数行为级兼容:程序接受同样的输入能得到同样的结果。

接口中加入新的方法:
二进制级兼容:兼容;(不会被调用)
源码级兼容:不兼容;(不符合语法)
函数行为级兼容: 兼容;(不影响原有函数)

抽象类和有默认方法的接口区别:抽象类可以有成员、不能多继承。

默认方法的接口多继承优先级问题:

  1. 类中的实现> 接口中的;
  2. 子接口中实现>父接口中的;
  3. 上述两种无法判断时,必须显式调用,声明调用的是哪个实现:
    1
    2
    // class C implement B,A
    B.super.hello(); // 显式调用B接口中的方法;

Option

每次新建一个类的时候,所有引用类成员都尽量搞成Option.
如果可能为null,就搞成Option;
如果不可能为null,就维持朴素类型。// 比如构造时就自动填充确定值的final

相当于领域中将是否可选这个属性也封装了进去,而不是靠注释来弱约束。

1
2
3
4
// 初始化可以这样:
Optional<Car> optCar = Optional.empty();
// 根据空或非空值创建:
Optional<Car> optCar = Optional.ofNullable(car); // 推荐初始化方式

用了Optional以后,后续用Stream api处理(疯狂调用flatMap),整个过程就变成函数式,再也不需要null关键字。

对于可选成员(Option<T>): 调用flatMap;
对于必选成员(final String): 调用map;

Option的缺点: 无法序列化。
如果需要可以序列化的类,只能隐藏Option变量:

1
2
3
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}

Optional不能滥用(不能序列化):(java Lambda(JSR-335)专家组考虑并拒绝了它)

尽量避免将Optional用于类属性、方法参数及集合元素中,因为以上三种情况,完全可以使用null值来代替Optional,没有必要必须使用Optional,另外Optional本身为引用类型,大量使用Optional会出现类似(这样描述不完全准确)封箱、拆箱的操作,在一定程度上会影响JVM的堆内存及内存回收。
相关tip:
https://segmentfault.com/a/1190000018936877?utm_source=tag-newest

  • 不要放进Map里,因为Optional不是作为值类型设计的,它的equals、hashcode、==方法都不能保证正确性(和唯一性有关的方法)。

最后: POJO还是保持纯粹,这样也有向前先后兼容性。

异步编程

无法从线程失败中恢复的版本:

1
2
3
4
5
6
7
8
public Future<Double> getPriceAsync(String product) {     
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread( () -> {
double price = calculatePrice(product);
futurePrice.complete(price);// here
}).start();
return futurePrice;
}

加上try catch:

1
2
3
4
5
6
try {
double price = calculatePrice(product);
futurePrice.complete(price);
} catch (Exception ex) {
futurePrice.completeExceptionally(ex);
}

精简后:

1
2
3
public Future<Double> getPriceAsync(String product) {
return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}

并行查询(无状态)

无状态:

1
2
shops.parallelStream().map(shop->String.format("%s price is %.2f"
,shop.getName(), shop.getPrice(product)))).collect(ToList());

定制Executor:

1
2
3
4
5
6
7
8
9
private final Executor executor =
Executors.newFixedThreadPool(Math.min(shops.size(), 100),
new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);// 守护线程,主线程结束后关闭
return t;
}
});

计算密集,无IO: 使用Stream接口; // 不加线程数
IO密集,计算简单: 使用CompletableFuture,自定义线程池; // 可以疯狂加线程

时间api

LocalDate和LocalTime

1
2
3
4
5
6
LocalDate date = LocalDate.of(2014, 3, 18);
LocalDate today = LocalDate.now();
LocalTime time = LocalTime.of(13, 45, 20);
// 字符串:
LocalDate date = LocalDate.parse("2014-03-18");
LocalTime time = LocalTime.parse("13:45:20");

复合后: LocalDateTime

1
LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20); LocalDateTime dt2 = LocalDateTime.of(date, time);// 复合localDate和localTime

unix时间: java.time.Instant

1
Instant.ofEpochSecond(3);// 3

时间间隔: Duration

1
2
3
4
Duration d1 = Duration.between(time1, time2);
Duration d1 = Duration.between(dateTime1, dateTime2);
Duration d2 = Duration.between(instant1, instant2);
Duration threeMinutes = Duration.ofMinutes(3);

天间隔: Period

1
2
3
Period tenDays = Period.between(LocalDate.of(2014, 3, 8),
LocalDate.of(2014, 3, 18));
Period tenDays = Period.ofDays(10);

上述类都是不可变的。// 线程安全

修改直接返回新变量:

1
2
LocalDate date2 = date1.withYear(2011);
LocalDate date2 = date1.plusWeeks(1);

个性化操作: TemporalAdjuster

下个周日、下个工作日、本月最后一天:

1
2
3
4
import static java.time.temporal.TemporalAdjusters.*;
LocalDate date1 = LocalDate.of(2014, 3, 18);
LocalDate date2 = date1.with(nextOrSame(DayOfWeek.SUNDAY));
LocalDate date3 = date2.with(lastDayOfMonth());

格式化、格式转换

localData<=>String

1
2
3
4
5
6
7
8
LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE); // 20140318
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2014-03-18

LocalDate date1 = LocalDate.parse("20140318",
DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18",
DateTimeFormatter.ISO_LOCAL_DATE);

定制格式:

1
2
3
4
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate, formatter);

时区

java.time.ZoneIdZonedDateTime:
从LocalData转换到ZonedDateTime:(其他几个也一样)

1
2
3
4
ZoneId romeZone = ZoneId.of("Europe/Rome");
// 或者: ZoneId zoneId = TimeZone.getDefault().toZoneId();
LocalDate date = LocalDate.of(2014, Month.MARCH, 18); 6
ZonedDateTime zdt1 = date.atStartOfDay(romeZone);

时区差: ZoneOffset

其他日历系统: ThaiBuddhistDate,MinguoDate,JapaneseDate,HijrahDate

其他tips

ConcurrentHashMap类:
mappingCount方法代替size方法;
mappingCount: 返回long;
size: 返回int; (实际大小超过int时无效)

重复遍历流: 可以使用StreamForker,缺点本质上是存了内存。

lambda底层实现

匿名类的实现

匿名内部类底层是生成一个class$1的类。(外层类名+数字)
缺点: 每个类都需要加载、验证的操作,如果类太多,开销太大;

查看字节码和常量池

使用命令:

1
javap -c -v ClassName

lambda的实现: InvokeDynamic

本质上是在所在类里新增了一个方法。将Lambda表达式的代码体填入到运行时动态创建的静态方法,就完成了Lambda表达式的字节码转换。
动态链接方法,然后用InvokeDynamic调用:

1
2
3
4
5
6
public class Lambda {
Function<Object, String> f = [dynamic invocation of lambda$1]
static String lambda$1(Object obj) {
return obj.toString();
}
}

优点:

  1. lamda相关字节码的生成推迟到使用的时候; (懒加载)
  2. 如果不是闭包:有类似于static final变量的缓存效果;(闭包的话,因为可能捕获不同的局部变量,不可以缓存)
  3. 没有额外的开销;
    如果lambda是个闭包,也就是捕获了其他作用域内的变量(当然编译时会要求是final或者实质上是final),也很简单,就是给这个函数加一个参数即可。

rust入门笔记_2

解引用强制多态(deref coercions

coercions的意思就是强制多态。
解引用强制多态的意思通俗来说就是,编译器根据目标类型,自动调用deref方法解引用,搜索路径来达到目标类型,最终达到节省程序员编写成本,但最后运行时开销维持最小的目标。(语法糖成本为0)

背景知识: 智能指针的解引用

自定义的智能指针,实现Deref trait(解引用特性)后,就可以使用*操作符来解引用了。
例如:

1
2
3
4
5
6
7
8
9
10
use std::ops::Deref;

# struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;

fn deref(&self) -> &T { // 注意这里的返回值需要是引用,以便维持所有权
&self.0
}
}

编译器每次遇到*操作符,都会尝试调用复杂类型的deref方法来获得基本类型的引用,以便进行解引用,也就是代码等效于:*(y.deref())

实例

1
2
3
4
let m = MyBox::new(String::from("Rust"));
hello(&m); // 这里hello的形参是&str,实参是&MyBox
// 强制解引用多态后,等效于调用了: hello(&(*m)[..]);
// &MyBox =通过deref=> String =通过slice=> &str

更多解引用多态规则

1
2
3
1. 当 T: Deref<Target=U> 时从 &T 到 &U。 // 不可变=>不可变
2. 当 T: DerefMut<Target=U> 时从 &mut T 到 &mut U。 // 可变=>可变
3. 当 T: Deref<Target=U> 时从 &mut T 到 &U。 // 可变=>不可变

只会发生安全的转换,而不可变=>可变这种属于不安全,因此不支持自动转换。

(上面的实例属于第一种。)

(析构函数) std::mem::drop

自定义Drop trait:

1
2
3
4
5
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}

定义以后drop函数不能显式调用,也不能禁用。
只能在离开作用域的时候自动调用。
如果要显式调用,只能使用标准库的函数std::mem::drop.
(默认预引入,可以直接调用drop)

智能指针汇总

Box: 没有特殊功能,类似于java中的普通引用,让编译器能确定分配空间大小;
Rc: 多所有权、单线程、引用计数;
RefCell: 单所有权、单线程、引用计数、内部可变性;
Arc: 多所有权、多线程、引用计数;
Mutex: 单所有权、多线程、引用计数、内部可变性;
Week: 弱引用。

多所有权、单线程、不可变:引用计数=>Rc

多所有权示例: b,c都拥有a的所有权。

1
2
3
4
5
6
7
8
9
10
11
12
13
enum List {
Cons(i32, Rc<List>),
Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;

fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}

每次调用Rc::clone,都将引用计数加1.// 这里是浅拷贝
b: 3->5->10->Nil
c: 4->5->10->Nil

打印引用数:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

单一所有权、单线程、内部可变:引用计数=>RefCell

借用规则

  1. 每时每刻,要么有一个可变引用;要么有n个不可变引用;(这两者互斥)
  2. 引用必须总是有效的。

对于引用和Box<T>: 编译时进行借用规则检查;(编译错误)
对于RefCell<T>: 运行时进行借用规则检查;(panic)

引用和Box<T>: 第一级指针不可变,则级联得每一级都不可变。
RefCell: 第一级指针不可变,第二级可以借用可变引用,然后修改;

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
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;

struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}

impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger { sent_messages: RefCell::new(vec![]) }
}
}

impl Messenger for MockMessenger {
fn send(&self, message: &str) {
// 注意这里的borrow_mut,借出可变:
self.sent_messages.borrow_mut().push(String::from(message));
}
}

#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
# let mock_messenger = MockMessenger::new();
# let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
# limit_tracker.set_value(75);
// 注意这里的borrow,借出不可变:
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}

borrow 方法返回 Ref 类型的智能指针;
borrow_mut 方法返回 RefMut 类型的智能指针。

这两个类型都实现了 Deref 所以可以当作常规引用对待。
借用规则在运行时检查,例如同时借出两个可变引用将会panic:

1
2
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();

底层原理依然是引用计数。

结合Rc和RefCell

结合Rc和RefCell的话,就可以结合多所有权和单所有权,不可变和可变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),// 注意这里
Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
let value = Rc::new(RefCell::new(5));

let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

*value.borrow_mut() += 10;

println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}

混合使用RcRefCell可以构造循环引用,造成内存泄露。
可以用弱引用来消除这一隐患: 将 Rc<T> 变为 Weak<T>

弱引用

https://rustlang-cn.org/office/rust/book/smart-pointers/ch15-06-reference-cycles.html

并发

线程模型

rust标准库提供1:1线程,有其他第三方库提供M:N的。
创建线程: thread::spawn+闭包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::thread;
use std::time::Duration;

fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});

for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();// 阻塞等待子线程
}

子线程中可以捕获主线程的变量,获得所有权. 使用move,以免主线程把它drop了:

1
2
3
4
5
6
7
8
9
10
11
use std::thread;

fn main() {
let v = vec![1, 2, 3];

let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});

handle.join().unwrap();
}

消息传递: channel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use std::thread;
use std::sync::mpsc;

fn main() {
let (tx, rx) = mpsc::channel();
// tx: 发送者
// rx: 接收者
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();// 单所有权,发送后不能再使用val
});

let received = rx.recv().unwrap();
println!("Got: {}", received);
}

mpsc: 多生产者、单消费者
recv: 阻塞接收
try_recv: 非阻塞接收,立即返回

这里之所以是多生产者,是因为可以把生产者无限克隆出去,然后发送消息:

1
let tx1 = mpsc::Sender::clone(&tx);

共享状态并发

互斥器(mutex)

rust中的锁是一种特殊的智能指针,通过重载drop trait来确保离开作用域的时候释放锁。

1
2
3
4
5
6
7
8
9
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
}
println!("m = {:?}", m);
}

由于多个线程都需要访问同一个锁,因此需要多所有权的智能指针,并且能够并发使用:

1
2
Rc<T>: 单线程,多所有权;
Arc<T>: 多线程,多所有权。

并发访问的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
let counter = Arc::new(Mutex::new(0));// 智能指针包装
let mut handles = vec![];

for _ in 0..10 {
let counter = Arc::clone(&counter);// 克隆来增加引用计数
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();

*num += 1; // move+离开作用域,引用-1
});
handles.push(handle);
}

for handle in handles {
handle.join().unwrap();
}

println!("Result: {}", *counter.lock().unwrap());
}

这里可以看出Mutex具有内部可变性。同时可以用Mutex构造死锁(循环依赖)。

Send与Sync的trait

rust中用两个trait来标记所有权在线程中的转移以及引用的多线程访问:

Send trait: 支持多线程所有权转移,所有权在线程之间转移; 除Rc以外的大部分类型是Send trait;
Sync trait: 支持多线程访问, 线程之间可以共享值的引用
除Rc以外的大部分类型是Sync trait;

unsafe rust中的裸指针也没有实现这两个trait.

绝大部分情况不需要手动实现send与sync的trait。

rust入门笔记_1

周末无事看看tidb开发者说很好用的rust,觉得很有趣,原来现代编程语言就是这种感觉,有一些细节上的简化。

文档参见: https://rustlang-cn.org/office/rust/book/getting-started/ch01-03-hello-cargo.html

源码:

1
2
3
fn main() {
println!("Hello, world!");
}

编译和运行:

1
2
rustc main.rs
./main

cargo(类似于maven)

1
2
3
4
5
6
7
8
cargo new hello_cargo # 创建项目
cargo build # 生成debug程序
cargo run # 运行debug程序, 会自动build
cargo check # 仅检查语法

cargo build --release # 生成优化后的程序(release)
# 类似的可以猜到run release程序的命令:
cargo run --rebase # 运行relase,会自动检测改动重新编译

可以看到专门提供了一个cargo check命令来避免编译、只是检查语法,看来网上大家说rust编译慢很可能是真的。

变量

1
2
let foo = 5; // 不可变
let mut bar = 5; // 可变

类方法/静态函数: 在rust中叫关联函数(associated function);

crate: 库

类似于mvn的中央仓库: https://crates.io/
crate不是创建的意思,差了一个字母,是rust的库的意思。

数据类型

i32: 32位数字;
u32: 32位无符号数字;
i64: 64位数字等等。

Rust默认使用i32.
但是如果你用u32类型和一个变量a比较,Rust会推断出a也是u32类型.

let关键字

let类似于js里的let,用来定义一个变量,而且支持shadowing
比如一开始定义了一个string类型的a;
后来转换成数字以后,可以直接

1
2
let a:u32 = a.guess.trim().parse()
.expect("Please type a number!");

新的定义会覆盖以前的,这样就不用定义两个变量了(一个string_a,一个u32_a)。
原理上其实底层是生成了两个变量,因此可以把let mut覆盖成let,或者把不可变的覆盖成可变的。实测了一下确实也是可以的。

match关键字

match和scala里的一样

1
2
3
4
5
match xxx {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}

通用概念(与其他编程语言核心对应的)

关键字转义

比如match在rust中是一个关键字,所以如果恰好有一个函数叫这个名字,需要转义以后才能调用:(用r#前缀)

1
r#match(); // 调用名为 'match' 的函数

不可变

1
2
3
const MAX_POINTS: u32 = 100_000; // 不可变,直接赋值;
let a = get_val(); // 不可变,可以运行时赋值;
let mut a = xxx; // 可变。

数据类型

rust会尝试推断数据类型,推断不出来则会报错;

标量: 整型、浮点型、布尔类型和字符类型;

长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
arch isize usize

这里的arch: 64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。
isizeusize主要作为索引类型。

赋值的时候: (还能在中间随意加横杠_):

数字字面值 例子
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte (u8 only) b’A’

57u8表示57,数据类型是u8

数字溢出: debug版检查溢出并报错;
release版会进行溢出。
可以用Wrapping类型来使用溢出特性,以免被debug版本报错。

f32: 32位浮点数;
f64: 64位浮点数. (默认类型,现代cpu下性能与f32几乎一样)
bool: 布尔值。
char: Unicode字符。

元组和数组

元组下标从0开始(和scala不同,scala从1开始)

1
2
let tup: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;

数组:

1
2
3
let a = [1, 2, 3, 4, 5];
// 或:
let a: [i32; 5] = [1, 2, 3, 4, 5];

函数

用fn声明.

1
2
3
4
5
6
7
8
9
10
11
fn main() {
another_function(5, 6);
}

fn another_function(x: i32, y: i32) {
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}
fn five() -> i32 { // 返回i32类型
5
}

表达式

表达式的结尾没有分号

1
2
3
4
5
let y = {
let x = 3;
x + 1 // 没有分号
};
// y=4

循环

loop,while,for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
assert_eq!(result, 20);
while index < 5 {
println!("the value is: {}", a[index]);
index = index + 1;
}
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
for number in (1..4).rev() {
println!("{}!", number);
}

ownership 所有权

rust无需gc。
要学习的点包括: 借用、slice、内存布局。

所有权:管理堆数据

所有权的三大法则:

  1. Rust中的每一个值都有一个被称为其 所有者(owner)的变量。
  2. 值有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

创建一个堆上的变量:

1
2
3
4
{
let s = String::from("hello");
s.push_str(", world!"); // 追加
}// rust自动调用s的drop,回收内存(类似于free\RAII模式)

s的大小运行时可变,因此它显然分配在堆上。(栈每个slot大小相同)

浅拷贝、深拷贝、移动

rust对复杂类型默认是移动;
基本类型直接深拷贝。
rust没有浅拷贝、只有移动。

1
2
3
4
5
6
7
8
9
10
11
12
// 移动:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // fail,s1已经无效,被移动为s2了。
// 深拷贝:
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
// 基本类型:
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);

总结就是:
浅拷贝: 无;
深拷贝: 显式调用clone、或者是基本类型;
移动: 复杂类型;

如果一个类型拥有Copy trait,
一个旧的变量在将其赋值给其他变量后仍然可用。
rust的逻辑是,如果发现一个类型没有实现Copy,它就进行move

除了用等号,调用函数时也会发生移动或者深拷贝。
例如:

1
2
3
let s = String::from("hello");  // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 ...
// ... 所以到这里s不再有效

函数return的时候也类似于等号,也会发生移动或者深拷贝,因此可以用return再取回所有权。

1
2
let s = String::from("hello");
let s = takes_back_ownership(s);

可以用&号来简化这个过程:

1
2
let s1 = String::from("hello");
let len = calculate_length(&s1); // 多一个&来取回所有权

这里calculate_length函数没有所有权,因此是借用了s1变量。

借用

函数借用变量s,不拥有所有权。

1
2
3
4
5
6
7
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}

借用并且要修改的话,要显式写上&mut.

借用的竞态

rust默认禁止竞态,编译不予通过:

1
2
3
4
5
6
7
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;// 如果后续都用的话报错,两个变量都借用了s,而且都是可写,有竞态,在同一作用域内。
// 只读引用的话可以有多个:
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM 有只读引用的时候,也不能再有可写引用

借用结束的话可以消除竞态:

1
2
3
4
5
let mut a = String::from("hello world");
let b = &a[0..4];
println!("{}", b);
append_a(&mut a);// 这里没问题,因为b后面没有用到。
// println!("{}", b); // 这里会报错,因为b的作用域和a的有交叉。

悬挂指针

Rust 中编译器确保永远不会有悬挂指针。
构造悬挂指针:

1
2
3
4
5
6
7
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s // 编译失败
}

slice

slice的类型多一个&,属于不可变引用。
比如string的slice类型: &str
字符串的字面量的类型:str
slice语法很简单:

1
2
3
4
5
6
7
8
let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
let slice = &s[0..len]; // 整个字符串
let slice = &s[..]; // 省略头尾
// 包含右端点:
let hello = &s[0..=4];

用slice的好处是可以预防错误,因为持有了不可变引用,其他试图修改s的操作就会被阻止,因为修改s的时候会申请可变引用,根据上一节中的竞态阻止,申请可变引用会失败。

数组的slice

类型是 &[i32]

1
2
3
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
println!("{:?}",slice);

结构体struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# struct User {
# username: String,
# email: String,
# sign_in_count: u64,
# active: bool,
# }
#
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};

user1.email = String::from("anotheremail@example.com");

以前写代码经常会有this.email=email这种机械重复的代码,rust提供了简写省略的方法。
构造函数的简写: (new)

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
User {
email, // 这里省略了同名输入变量
username,
active: true,
sign_in_count: 1,
}
}

类似的,结构体的update也有相应的简写:

1
2
3
4
5
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1 // 这句话的意思是其他变量都按user1的值来赋值就好
};

结构体的实例方法和类方法区别在于有没有第一个&self参数,方法可以位于不同impl块中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# #[derive(Debug)]
# struct Rectangle {
# width: u32,
# height: u32,
# }
#
impl Rectangle {
fn area(&self) -> u32 { // 实例方法
self.width * self.height
}
}

impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
impl Rectangle {
fn square(size: u32) -> Rectangle {// 关联方法、类方法
Rectangle { width: size, height: size }
}
}

自动解引用功能

统一obj.xxx()操作和obj->xxx().
rust自动解引用:

当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &、&mut 或 * 以便使 object 与方法签名匹配。

枚举

枚举可以直接绑定数据类型:(类似于一种typedef)

1
2
3
4
5
6
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

也可以作为朴素的数据(数字):

1
2
3
4
5
6
7
# enum IpAddrKind {
# V4,
# V6,
# }
#
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

Option也是一种枚举类型。

match

match的时候可以自动unapply枚举型:

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
match x {
None => None,
Some(i) => Some(i + 1),
}
// 更复杂:
# #[derive(Debug)] // 支持直接打印
# enum UsState {
# Alabama,
# Alaska,
# }
#
# enum Coin {
# Penny,
# Nickel,
# Dime,
# Quarter(UsState),
# }
#
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
},
_ => (),
}

这里类似于最后default兜底的值是_,返回值是()也就是unit类型。
if let是match的语法糖:

1
2
3
4
5
6
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}

package: 包。cargo的功能.cargo new生成,带有Cargo.toml文件;里面可以有多个库。
Crates: 库。很多模块构成的库;(或者程序)
Modules: 模块。

包默认生成的库:

  1. src/main.rs; (程序、数量任意)
  2. src/lib.rs; (库、最多1个)

模块

1
2
3
4
5
6
7
8
9
10
11
12
13
mod sound {// 同包下可以访问
pub mod instrument {// 公有
pub fn clarinet() {// 公有
// 函数体
}
}
}
fn main() {
// 绝对路径
crate::sound::instrument::clarinet();
// 相对路径
sound::instrument::clarinet();
}

super相对路径

1
2
3
4
5
6
7
8
9
10
mod sound {
mod instrument {
fn clarinet() {
super::breathe_in();
}
}
fn breathe_in() {
// 函数体
}
}

use关键字引用

类似于import

1
2
3
4
5
6
7
use crate::sound::instrument;
// 相对路径引入:
use self::sound::instrument; // 一般还是用绝对路径引入

// 同名冲突处理: 使用as重命名
use std::fmt::Result;
use std::io::Result as IoResult;

默认use引入的项变成了私有,可以再加上pub让引入的项维持公有:

1
pub use crate::sound::instrument;

访问修饰符pub

枚举enum: 一旦pub,则所有字段pub;
结构体struct: 必须显式设定每个字段为pub,默认是private;

模块化

每个mod放在自己的同名文件中,其他文件中要用的时候,声明一下即可:

1
mod sound;

末尾是分号。

JNI如何优雅引用so文件

JNI背景知识参见: http://xiaoyue26.github.io/2019/09/07/2019-09/JNI%E6%80%BB%E7%BB%93/

总之假设我们到了临门一脚想要引用so文件到时候,方法有很多种,大致分为两大类:

  1. 预先把so文件部署到运行的机器特定目录,代码里使用绝对路径加载;
  2. 把so文件打包到resource目录,运行时用相对路径加载。

主要推荐绝对路径的姿势2和相对路径的姿势5.

优劣势:

绝对路径:

  • 多个java程序可以引用同一个so文件,不用都打包到jar包里,降低jar包大小;
  • 可以灵活切换so文件实现,不用重新打包jar包, 符合c++中动态链接库的思想。

相对路径:

  • 一般一个so文件就一个java程序使用,相对路径用起来省心,不用配置多个运行环境.
  • 比较符合jvm平台无关的思想,当然so文件肯定是平台有关的。一般so文件是某个公开库,不是我们自己写的,也不需要修改其实现。

目前我个人使用的是第5种姿势。

绝对路径

姿势1: 直接写死:

1
System.load("/opt/ld_path/libtest.so");

姿势2: 结合环境变量,这里第一行代码可以在运行时由命令java -Djava.library.path=/opt/ld_path代替:

1
2
3
4
5
6
7
8
9
10
11
12
13
System.setProperty("java.library.path", "/opt/ld_path");
try {
Field sysPath = ClassLoader.class.getDeclaredField("sys_paths");
sysPath.setAccessible(true);
sysPath.set(null, null);
// System.out.println(System.mapLibraryName("dynamic"));
System.loadLibrary("dynamic");
// 注意mac需要.dylib结尾的依赖文件
// linux需要.so结尾的依赖文件
} catch (NoSuchFieldException | IllegalAccessException e) {
System.out.println("error");
e.printStackTrace();
}

相对路径: 仅在ide里可用的方法

这两种方法都需要首先把libdynamic.so文件放到resource目录。

姿势3:

1
2
URL url = JNIDyn.class.getClassLoader().getResource("libdynamic.so");
System.load(url.getPath());

姿势4:

1
2
3
4
5
6
7
8
ClassPathResource resource = new ClassPathResource("libdynamic.so");
// System.out.println(resource.getPath());
try {
File file = resource.getFile();
System.load(file.getAbsolutePath());
} catch (IOException e) {
e.printStackTrace();
}

相对路径: ide和jar包都能用的方法

姿势5:

需要首先把libdynamic.so文件放到resource目录。

然后需要创建NativeUtils工具类;
加载代码:

1
2
3
4
5
6
7
static {
try {
NativeUtils.loadLibraryFromJar("/libnative.so");
} catch (IOException e) {
e.printStackTrace();
}
}

这里使用到的NativeUtils源码如下:

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
import java.io.*;
import java.nio.file.*;
public class NativeUtils {
private static final int MIN_PREFIX_LENGTH = 3;
public static final String NATIVE_FOLDER_PATH_PREFIX = "nativeutils";
private static File temporaryDir;
private NativeUtils() {}
public static void loadLibraryFromJar(String path) throws IOException {
if (null == path || !path.startsWith("/")) {
throw new IllegalArgumentException("The path has to be absolute (start with '/').");
}
String[] parts = path.split("/");
String filename = (parts.length > 1) ? parts[parts.length - 1] : null;
if (filename == null || filename.length() < MIN_PREFIX_LENGTH) {
throw new IllegalArgumentException("The filename has to be at least 3 characters long.");
}
if (temporaryDir == null) {
temporaryDir = createTempDirectory(NATIVE_FOLDER_PATH_PREFIX);
temporaryDir.deleteOnExit();
}
File temp = new File(temporaryDir, filename);
try (InputStream is = NativeUtils.class.getResourceAsStream(path)) {
Files.copy(is, temp.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
temp.delete();
throw e;
} catch (NullPointerException e) {
temp.delete();
throw new FileNotFoundException("File " + path + " was not found inside JAR.");
}
try {
System.load(temp.getAbsolutePath());
} finally {
if (isPosixCompliant()) {
temp.delete();
} else {
temp.deleteOnExit();
}
}
}

private static boolean isPosixCompliant() {
try {
return FileSystems.getDefault()
.supportedFileAttributeViews()
.contains("posix");
} catch (FileSystemNotFoundException
| ProviderNotFoundException
| SecurityException e) {
return false;
}
}

private static File createTempDirectory(String prefix) throws IOException {
String tempDir = System.getProperty("java.io.tmpdir");
File generatedDir = new File(tempDir, prefix + System.nanoTime());

if (!generatedDir.mkdir())
throw new IOException("Failed to create temp directory " + generatedDir.getName());

return generatedDir;
}

}