码农戏码

新生代农民工的自我修养

0%

经过多年职场成长,发现架构与管理有很多的相似之处,最大相似之处就是要与个人特性融合实践。也就是纸上得来终觉浅,绝知此事要躬行。

读再多的架构或者管理书籍,那不过是个最基础的起点,你外围触之可及的手段,但离最终的知行合一还存在着很远的距离。

《架构师的能力模型》中提出架构师的能力模型:

架构师能力模型

程序员只需要专注于“专”,而架构师需要“博而不专”,牺牲技术深度来提高技术广度。

对于一个全能型架构师对架构的决策,需要广度+深度+经验三者兼顾

广度决定能找到的方法+深度决定选择方法的正确性+经验决定找到正确方法的速度

架构师的能力维度从技术+业务+管理升华为技能+影响力+领导力

1.技能是实践架构的基础。它需要知识以及应用知识的能力

2.影响力用来衡量架构师在项目中应用技能后给项目或公司带来多大的效益

3.领导力确保了架构实践的状态能稳步向前推进,同时培养更多的架构师

Read more »

并发问题的源头

并发?为啥需要并发呢?自然是为了性能,增强算力以及协调能力

在现今计算机器体系中,涉及性能的主要有CPU、内存、IO三方面,而这三者的速度也是天壤之别,形象之讲,CPU天上一天,内存是地上一年,IO则要地上十年

怎么应对:

1、CPU增加了多级缓存,均衡与内存的速度差异,并且还从单核发展为多核增加算力

2、操作系统增加线程,分时复用CPU,均衡CPU与IO的速度差异

3、通过即时编译器重排序,处理器乱序执行,以及内存系统重排序优化指令执行次序,更好地利用缓存

但这些措施并不是百利无害的,并发问题就是其中一害。

1、缓存导致的可见性问题

多核时代,每个核都有各自的L1,L2缓存,在各自缓存中修改的数据相互不可见。

《缓存是个面子工程》提到的硬件缓存,也带来了并发问题。

Java内存模型

2、线程切换带来的原子性问题

这主要有些看似一行的代码,其实需要多条CPU指令才能完成

如count+=1,需要三条指令

指令1:把变量count从内存加载到CPU的寄存器

指令2:在寄存器中执行+1操作

指令3:最后将结果写入内存

当多线程时,线程切换时三条指令就会被错误执行,打破了原子性,导致逻辑的错误。

3、编译优化带来的有序性问题

编译器为了优化性能,有时改变了程序中语句的先后顺序。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}

new Singleton()这句话感觉是

  1. 分配一块内存M
  2. 在内存M上初始化Singleton对象
  3. 然后M的地址赋值给instance变量

实际执行路径却是:

  1. 分配一块内存M
  2. 将M的地址赋值给instance变量
  3. 最后在内存M上初始化Singleton对象

JMM

如何解决上述的三大问题,JSR-133定义了内存模型JMM

A memory model describes, given a program and an execution trace of that program, whether the execution trace is a legal execution of the program. For the Java programming language, the memory model works by examining each read in an execution trace and checking that the write observed by that read is valid according to certain rules.

也就是说一个内存模型描述了一个给定的程序和和它的执行路径是否一个合法的执行路径。对于java序言来说,内存模型通过考察在程序执行路径中每一个读操作,根据特定的规则,检查写操作对应的读操作是否能是有效的。
java内存模型只是定义了一个规范,具体的实现可以是根据实际情况自由实现的。但是实现要满足java内存模型定义的规范。

内存模型的种类大致有两种:

Sequential Consistency Memory Model: 连续一致性模型。这个模型定义了程序执行的顺序和代码执行的顺序是一致的。也就是说 如果两个线程,一个线程T1对共享变量A进行写操作,另外一个线程T2对A进行读操作。如果线程T1在时间上先于T2执行,那么T2就可以看见T1修改之后的值。
这个内存模型比较简单,也比较直观,比较符合现实世界的逻辑。但是这个模型定义比较严格,在多处理器并发执行程序的时候,会严重的影响程序的性能。因为每次对共享变量的修改都要立刻同步会主内存,不能把变量保存到处理器寄存器里面或者处理器缓存里面。导致频繁的读写内存影响性能。

这种模型相当于禁用了缓存。如果再禁止编译器优化,就算是彻底解决上述问题了,但性能将受到严重影响。

Happens-Before Memory Model: 先行发生模型。这个模型理解起来就比较困难。先介绍一个先行发生关系 (Happens-Before Relationship)
  如果有两个操作A和B存在A Happens-Before B,那么操作A对变量的修改对操作B来说是可见的这个先行并不是代码执行时间上的先后关系,而是保证执行结果是顺序的

happens-before

happens-before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 happens-before 规则。

happens-before规则:

程序次序规则(program order rule): 在一个线程内,先在前面的代码操作先行。准确的说控制流顺序而不是代码顺序。需要考虑分支,循环等结构。

管程锁定规则(monitor lock rule):同一个资源锁,先unlock,之后才能lock。

Volatile变量规则(volatile variable rule):一个变量被volatile修饰,多线程操作,先执行操作,再执行读操作。(同时写操作只能有一个)

线程启动规则(Thread start rule):Thread对象的start方法,先行发生于此线程的每一个方法。

线程终止规则(Thread Termination rule):该线程的所有方法,先行发生于该线程的终止检测方法。例如:可以通过Thread.join方法结束,Thread.isAlive()的返回值等手段检测到线程已经终止执行。

线程中断规则(Thread Interruption Rule): 中断方法先行发生于,中断检测方法。中断方法interrupt(),中断检测interrupted()方法。

对象终结规则(finalizer rule): 一个对象的初始化完成(构造函数执行结束)先行发生于它的finalizer方法的开始。

传递性(Transitivity): 如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

as-if-serial

在谈happens-befre常会提到as-if-serial

即时编译器保证程序能够遵守as-if-serial属性。通俗地说,就是在单线程情况下,要给程序一个顺序执行的假象。也就是经过重排序的执行结果要与顺序执行的结果保持一致。

而且,如果两个操作之间存在数据依赖时,编译器不能调整它们的顺序,否则将造成程序语义的改变。

1
2
3
4
5
6
7
public class AsIfSerialDemo {
public static void main(String[] args) {
int a = 10;//1
int b = 10;//2
int c = a+ b;//3
}
}

上面示例中:1和3之间存在数据依赖关系,同时2和3之间也存在数据依赖关系。因此在最终执行的指令序列中,3不能被重排序到1和2的前面(3排到1和2的前面,程序的结果将会被改变)。但1和2之间没有数据依赖关系,编译器和处理器可以重排序1和2之间的执行顺序。

VS 时间先行

对于happens-before先行发生,怎么理解,最常与“时间先后发生”搞混淆。

happens-before 关系是用来描述两个操作的内存可见性的。

如果操作 X happens-before 操作 Y,那么 X 的结果对于 Y 可见。那么与“时间先后发生”顺序有什么区别?

《JSR-133: JavaTM Memory Model and Thread Specification》,happens-before是这样定义的:

Two actions can be ordered by a happens-before relationship.
If one action happens-before another, then the first is visible to and ordered before the second.
It should be stressed that a happens-before relationship between two actions does not imply that
those actions must occur in that order in a Java platform implementation. The happens-before
relation mostly stresses orderings between two actions that conflict with each other, and defines
when data races take place.

从定义中可以看出两点:

1、the first is visible to and ordered before the second

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前

2、does not imply that those actions must occur in that order in a Java platform implementation

两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行


由这两条可以得出,JMM是要求当有happens-before关系时,不仅要求了可见性,而且在时间上也得保证有序。然而在不改变语义的前提下,Java平台的实现可以自主决定。这也就表明了happens-before与时间先后没有更大的关联性。

A happens-before B does not imply A happening before B.

A happening before B does not imply A happens-before B.

一个操作 “先行发生” 并不意味着这个操作必定是“时间上的先发生”

1
2
3
4
// 以下操作在同一个线程中执行
int i = 1;
int j = 2;

根据happens-before规则第一条,“int i = 1” 的操作先行发生(Happens-before)于 “int j = 2”,但在保证语义不改变的前提下,重排序了两条语句,那在时间上,“int j=2”先执行了。

《The Happens-Before Relation》这篇文章中,作者还举了个示例:

1
2
3
4
5
6
7
8
int A = 0;
int B = 0;

void foo()
{
A = B + 1; // (1)
B = 1; // (2)
}

虽然(1) happens-before (2),而且从上面的as-if-serial判断,(1) 得happen before (2) ,但作者观察并不是。

从图上可看出,A被赋值为0,B被赋值为1,但 (1) 没被执行呢。

关于这个问题,在stackoverflow happens-before 被讨论了。有人指出作者说得不对,而也有人给出解答:

A and B are locations in memory. However the operation B+1 does not happen in memory, it happens in the CPU. Specifically, the author is describing these two operations.

A = B + 1 (1)

  • A1 - The value in memory location B (0) is loaded into a CPU register
  • A2 - The CPU register is incremented by 1
  • A3 - The value in the CPU register (1) is written to memory location A

B = 1 (2)

  • B1 - The value 1 is written to memory location B

Happens-Before requires that the read of B (step A1) happens before the write of B (step B1). However, the rest of the operations have no interdependence and can be reordered without affecting the result. Any of these sequences will produce the same outcome

  • A1, B1, A2, A3
  • A1, A2, B1, A3
  • A1, A2, A3, B1

一个操作 “时间上的先发生” 也不能代表这个操作会是“先行发生”

1
2
3
4
5
6
7
8
9
10
11
private int value = 0;

// 线程 A 调用
pubilc void setValue(int value){
this.value = value;
}

// 线程 B 调用
public int getValue(){
return value;
}

假设存在线程 A 和 B,线程 A 先(时间上的先后)调用了 setValue(1),然后线程 B 调用了同一个对象的 getValue() ,那么线程 B 收到的返回值是什么? 0和1都有可能。因为两个操作之间没有happens-before关系。

volatile

volatile字段的happens-before关系指的是在两个不同线程中,【volatile的写操作】 happens-before之后【对同一字段的读操作】。这里有个关键字“之后”,指的是时间上的先后。
也就是我这边写,你之后再读就一定能读得到我刚刚写的值。普通字段则没有这个保证。也就是上面的setValue()与getValue()示例问题

1
2
3
4
5
6
7
8
9
10
11
12
13

int a=0;
volatile int b=0;

public void method1() {
int r2 = a;
b = 1;
}

public void method2() {
int r1 = b;
a = 2;
}

首先,b加了volatile之后,并不能保证b=1一定先于r1=b,而是保证r1=b始终能够看到b的最新值。比如说b=1;b=2,之后在另一个CPU上执行r1=b,那么r1会被赋值为2。
如果先执行r1=b,然后在另外一个CPU上执行b=1和b=2,那么r1将看到b=1之前的值。

在没有标记volatile的时候,同一线程中,r2=a和b=1存在happens before关系,但因为没有数据依赖可以重排列。一旦标记了volatile,即时编译器和CPU需要考虑到多线程happens-before关系,因此不能自由地重排序。

volatile与synchronized的区别,可以查看《volatile synchronized cas》

总结

本篇总结了Java并发问题的本质:可见性、原子性、有序性;以及应对这些问题,JMM中happens-before模型的规则。以及happens-before与happen before的区别。

先前已经对异常如何设计,如何实践异常都写了几篇阐述了。再一次从Clean Code角度来谈谈异常的使用。

Read more »

之前学习了极客时间上的一个专栏《软件设计之美》,作者对软件设计、编程范式、设计原则与模式、设计方法进行了讲解,内容全面。

专栏里面的一些内容,我也有些接触,但认知还不够深,比如面向对象。而且专栏把这些内容都串联起来,跟着专栏内容总结梳理一下。

Read more »

在《领域驱动设计》这本书里面,列举了三种可将业务逻辑建模为软件模型的模式,也就是大家常听说的事务脚本、贫血模型、DDD。

之前我还把这三种模式搞混淆了,too young too simple了。

举个简单的示例:

用户转帐,从一个帐户转到另一个帐户

Read more »

CQRS全称Command Query Responsibility Segregation

在CQRS中,来自客户端的命令通过单独的路径抵达命令模型,而查询操作则采用不同的数据源,这样的好处在于可以优化对查询数据的获取,比如用于展现、用于接口或报告的数据。

CQRS这些年火起来了,常被人挂在嘴边提起。为什么?因为DDD提倡富模型,但从资源库查找所有需要显示的数据是困难的,特别是在需要显示来自不同聚合类型与实例的数据时。领域越复杂,这种困难程度越大。

有没有一种完全不同的方法可以将领域数据映射到界面显示中呢?答案正是CQRS。

From CRUD to CQRS文章中,作者比对了CRUD模式与CQRS模式

Read more »

迪米特法则 Law of Demeter

LOD

最早出现于 1987年,由美国东北大学的伊恩·霍兰德(Ian Holland)提出。也叫做“最少知识原则”。

Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.

Read more »

Twitter上有人发了一个推,说他之前问过一个问题:“你最好的一条职业建议是什么?”,他得到了1300多个答案,最后他整理了12条最好的建议。
🔗 twitter、com/chrishlad/status/1502650707274608644

1、 尽可能为别人减少不确定性

  • Uber解决了打车的不确定性
  • 亚马逊解决了送包裹的不确定性
  • 你也可以通过及时更新项目进展来帮老板解决不确定性

2、 公司比职位更重要

3、 一旦接受了一个任务,无论多小或者多么不起眼,要把它做的特别好,超出别人的预期。这样你就能建立起一个良好的声誉,让别人知道你总能高质量的完成工作。当你建立了这种声誉,你就能得到更多的机会,更大的知名度,以及更大的成功。

4、 如果我不能信任你,你再聪明都没用。

5、 在你的职业生涯中,陪你走到最后的只有你自己。不是你的公司,不是你的经理,不是你的团队,只有你自己。
在做你所有职业生涯的决定时,优先考虑你自己。

6、 影响你职业生涯的三件事:

  • 你做什么?(工作)
  • 你为谁工作?(客户)
  • 和你一起工作的人是谁?(团队)

如果你热爱你的工作、客户和团队,你会非常非常幸运。

7、 和一个聪明的能激励你走向伟大的人结婚。

8、 要么能学东西,要么能赚钱。
否则果断离职,去找一个这两者至少占一样的工作。

9、 如果一个问题你不问,那么答案一定是“不”。

10、 选择你的老板。
你有权选择谁当你的老板,而在找工作的过程中很多人没有考虑到这一点。
一个优秀的老板可以为你的职业发展提供极大的助力。

11、 学会阐明你所做的事情的商业价值,而不仅仅是你的工作头衔或者项目。
不好的例子:“我是一个数据科学家。我创建了3个自服务数据应用”
更好的例子:“我帮助管理层发现了一个可以节约2300万美元成本的机会”

12、 “职业”,本质是一个营销名词,是由那些经营特定类别的梦想的人卖给你的,而他们在贩卖这个梦想时赚了很多钱。
赚钱,承担风险,有冒险精神。
但不要让“职业”来限制自己

在做程序测试时,常会用到测试替身来协助我们快速完成测试。

有时候被测试系统(system under test(SUT))很难测试,因为在测试环境下依赖的组件不能正常使用。如外部系统。

当在一个不能使用真实依赖组件(depended-on component(DOC))的地方写test时,我们可以使用Test Double

Read more »