关于敏捷的一些思考(上)
最近公司为了提高协作的效率,把几十号人的科技部门拆成好几个敏捷小组,小组独立运转。然后同事们又搞了个关于敏捷的知识分享,这个分享活动上有一个很有意思的游戏,游戏的目的是为了告诉大家,怎样做才会更敏捷?为什么?
1.游戏
10个人翻30张牌,每个人要把这30张牌的每1张牌都翻1遍,计算第1个人翻完30张牌所耗费的时间和所有人翻完30张牌所耗费的时间。
image假设每一个红色方块耗时x,每一个橙色方块耗时y,每一个黄色方块耗时z(x, y, z > 0)。
1-1.方案1
第n个人必须等第n-1个人翻完所有的30张牌才能开始翻牌,第1个人不用等。
解答:参考上图的结构,先计算第1~5个人的翻完30张牌的时间,然后再乘以2就是所有的耗时,且不存在影响时间的意外情况,那最后所耗费的时间是:
1-2.方案2
第n个人必须等第n-1个人翻完第5张牌才能开始翻牌,第1个人不用等;且第n个人必须等第n-1个人翻完第m张牌,才能翻第m张牌,第1个人不用等。
解答:参考上图结构,先计算1 ~ 10个人翻完前5张牌的时间,然后再计算第10个人翻完6 ~ 30张牌的时间,且不存在影响时间的意外情况,那最后所耗费的时间是:
1-3.方案3
第n个人必须等第n-1个人翻完第m张牌,才能翻第m张牌,第1个人不用等。
解答:参考上图结构,先计算1 ~ 10个人翻完第1张牌的时间,然后再计算第10个人翻完第2 ~ 5张牌的时间,最后再加上第10个人翻完第6 ~ 30张牌的时间,且不存在影响时间的意外情况,那最后所耗费的时间是:
2.用程序来验证正确性
2-1.预期结果
假设,那最终结果应该是:
2-2.实际结果
现在,咱们来基于上面三种方案来编写程序:
public class AgileTest {
private static final Logger LOGGER = LoggerFactory.getLogger(AgileTest.class);
public static void main(String[] args) {
long time1 = way1();
long time2 = way2();
long time3 = way3();
LOGGER.info("time1: {} ms", time1);
LOGGER.info("time2: {} ms", time2);
LOGGER.info("time3: {} ms", time3);
}
/**
* <p>方案1</p>
* <p>第n个人必须等第n-1个人翻完所有的30张牌才能开始翻牌,第1个人不用等。</p>
* @return 耗时 ms
*/
public static long way1() {
final String way = "way1";
long startTime = System.currentTimeMillis();
// 1.10个人
for (int i = 1; i < 11; i++) {
AtomicInteger atomicI = new AtomicInteger(i);
// 2.30张牌,每个人翻完30张牌,下个人才能开始
for (int j = 1; j < 31; j++) {
node(way, atomicI, j);
}
}
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
/**
* <p>方案2</p>
* <p>第n个人必须等第n-1个人翻完第5张牌才能开始翻牌,第1个人不用等;且第n个人必须等第n-1个人翻完第m张牌,才能翻第m张牌,第1个人不用等。</p>
* @return 耗时 ms
*/
public static long way2() {
final String way = "way2";
long startTime = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(10);
Map<Integer, Integer> map = new ConcurrentHashMap<>();
// 1.10个人
for (int outUser = 1; outUser < 11; outUser++) {
int outLastUser = outUser - 1;
Integer lastOutUserNode = 0;
// 2.每个人必须等前面那个人翻完5张牌才能开始翻牌,第1个人例外
while (outUser != 1 && ((lastOutUserNode = map.get(outLastUser)) == null || lastOutUserNode < 5)) {
}
AtomicInteger atomicI = new AtomicInteger(outUser);
Thread thread = new Thread(() -> {
try {
int innerUser = atomicI.get();
int lastInnerUser = innerUser - 1;
// 3.30张牌
for (int j = 1; j < 31; j++) {
// 4.翻牌的速度不能超过前1个人,第1个人例外,第30张除外
Integer lastInnerUserNode = 0;
while (innerUser != 1 && ((lastInnerUserNode = map.get(lastInnerUser)) == null || lastInnerUserNode < j)) {
}
node(way, atomicI, j);
map.put(innerUser, j);
}
} catch (Exception e) {
LOGGER.error("Current Thread: " + Thread.currentThread().getName() + "+, exception: ", e);
} finally {
latch.countDown();
}
}, "thread-user-" + outUser);
thread.start();
}
try {
latch.await();
} catch (InterruptedException e) {
LOGGER.error("wait thread exception: ", e);
}
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
/**
* <p>方案3</p>
* <p>第n个人必须等第n-1个人翻完第m张牌,才能翻第m张牌,第1个人不用等。</p>
* @return 耗时 ms
*/
public static long way3() {
final String way = "way3";
long startTime = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(10);
Map<Integer, Integer> map = new ConcurrentHashMap<>();
// 1.10个人
for (int outUser = 1; outUser < 11; outUser++) {
AtomicInteger atomicI = new AtomicInteger(outUser);
Thread thread = new Thread(() -> {
try {
int innerUser = atomicI.get();
int lastInnerUser = innerUser - 1;
// 2.30张牌
for (int j = 1; j < 31; j++) {
// 3.翻牌的速度不能超过前1个人,第1个人例外
Integer lastInnerUserNode = 0;
while (innerUser != 1 && ((lastInnerUserNode = map.get(lastInnerUser)) == null || lastInnerUserNode < j)) {
}
node(way, atomicI, j);
map.put(innerUser, j);
}
} catch (Exception e) {
LOGGER.error("Current Thread: " + Thread.currentThread().getName() + "+, exception: ", e);
} finally {
latch.countDown();
}
}, "thread-user-" + outUser);
thread.start();
}
try {
latch.await();
} catch (InterruptedException e) {
LOGGER.error("wait thread exception: ", e);
}
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
/**
* 执行节点
* @param atomicI 第i个人
* @param j 第j张牌
*/
private static void node(String way, AtomicInteger atomicI, int j) {
int i = atomicI.get();
long currentTime = System.currentTimeMillis() / 1000;
// System.out.printf("[%s] %s i-j: %d-%d %d\n", Thread.currentThread().getName(), way, i, j, currentTime);
try {
if ((i >= 1 && i <= 5 && j >= 1 && j <= 5 && i + j <= 6)
|| (i >= 6 && i <= 10 && j >= 1 && j <= 5 && i + j <= 11)) {
Thread.sleep(1000);
} else if ((i >= 1 && i <= 5 && j >= 1 && j <= 5 && i + j > 6)
|| (i >= 6 && i <= 10 && j >= 1 && j <= 5 && i + j > 11)) {
Thread.sleep(1000);
} else if (j >= 6) {
Thread.sleep(1000);
}
} catch (Exception e) {
LOGGER.error("node sleep exception: ", e);
}
}
}
日志输出:
17:52:10.645 [main] INFO com.peng.java_study.practice.zhikan.AgileTest - time1: 302746 ms
17:52:10.648 [main] INFO com.peng.java_study.practice.zhikan.AgileTest - time2: 75632 ms
17:52:10.648 [main] INFO com.peng.java_study.practice.zhikan.AgileTest - time3: 39348 ms
从日志中可以看出,实际结果与预期基本一致,多出来的几百毫秒是程序在运行过程中不可避免的消耗。
2-3.小结
不考虑其他因素影响的前提下,3种方案的耗时排行:
从结果中确实可以看出方案3是最敏捷的,值得提倡!
3.深入思考
方案3一定会更快吗?
答案是否定的,在某些特殊情况下,方案3反而更慢!
3-1.方案1'
假设第n - 1个人翻完1 ~ 30张牌后,一起交接给第n个人的准备时间和沟通时间为a。那么最终耗时为:
3-2.方案2'
假设第n - 1个人翻完第1 ~ 5张牌后,一起交接给第n个人时,需要准备时间和沟通时间为b;第n - 1个人翻完第6 ~ 30张牌中的任意一张牌后,交接给第n个人时,需要的准备时间与沟通时间c。那么最终耗时为:
3-3.方案3'
假设第n - 1个人翻完1 ~ 30张牌中的任意一张牌后,交接给第n个人时,需要的准备时间与沟通时间为d。那么最终耗时为:
3-4.比较方案1'与方案3'
当【准备时间与沟通时间】与【单个任务的执行时间时】的大小满足一定的条件时,方案3是否有可能比方案1慢。现在来计算下:
假设,那么:
再次假设,那么上式可转化为:
或
比如,,那就可以让方案1的速度比方案3更快。
3-5.小结
从3-4小结的结论中可以看出,在一定条件下,方案3可能并不是最优的,而且这种情况很有可能发生。
4.结论
可能在大多数情况下,方案3会比方案1更敏捷,但在3-4小节中的特殊情况下,方案3反而会是最慢的。因此在真实的生活场景中,还是要根据不同的情况分别对待,因地制宜,不能一概而论!