This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,523 @@
<audio id="audio" title="31 | 加餐1带你吃透课程中Java 8的那些重要知识点" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d1/02/d18d0d701acdb2459e3eeb9644acae02.mp3"></audio>
你好,我是朱晔。
Java 8是目前最常用的JDK版本在增强代码可读性、简化代码方面相比Java 7增加了很多功能比如Lambda、Stream流操作、并行流ParallelStream、Optional可空类型、新日期时间类型等。
这个课程中的所有案例都充分使用了Java 8的各种特性来简化代码。这也就意味着如果你不了解这些特性的话理解课程内的Demo可能会有些困难。因此我将这些特性单独拎了出来组成了两篇加餐。由于后面有单独一节课去讲Java 8的日期时间类型所以这里就不赘述了。
## 如何在项目中用上Lambda表达式和Stream操作
Java 8的特性有很多除了这两篇加餐外我再给你推荐一本全面介绍Java 8的书叫《Java实战第二版》。此外有同学在留言区问怎么把Lambda表达式和Stream操作运用到项目中。其实业务代码中可以使用这些特性的地方有很多。
这里,为了帮助你学习,并把这些特性用到业务开发中,我有三个小建议。
第一从List的操作开始先尝试把遍历List来筛选数据和转换数据的操作使用Stream的filter和map实现这是Stream最常用、最基本的两个API。你可以重点看看接下来两节的内容来入门。
第二使用高级的IDE来写代码以此找到可以利用Java 8语言特性简化代码的地方。比如对于IDEA我们可以把匿名类型使用Lambda替换的检测规则设置为Error级别严重程度
<img src="https://static001.geekbang.org/resource/image/67/77/6707ccf4415c2d8715ed2529cfdec877.png" alt="">
这样运行IDEA的Inspect Code的功能可以在Error级别的错误中看到这个问题引起更多关注帮助我们建立使用Lambda表达式的习惯
<img src="https://static001.geekbang.org/resource/image/50/e4/5062b3ef6ec57ccde0f3f4b182811be4.png" alt="">
第三如果你不知道如何把匿名类转换为Lambda表达式可以借助IDE来重构
<img src="https://static001.geekbang.org/resource/image/5a/e7/5a55c4284e4b10f659b7bcf0129cbde7.png" alt="">
反过来如果你在学习课程内案例时如果感觉阅读Lambda表达式和Stream API比较吃力同样可以借助IDE把Java 8的写法转换为使用循环的写法
<img src="https://static001.geekbang.org/resource/image/98/8a/98828a36d6bb7b7972a647b37a64f08a.png" alt="">
或者是把Lambda表达式替换为匿名类
<img src="https://static001.geekbang.org/resource/image/ee/7c/ee9401683b19e57462cb2574c285d67c.png" alt="">
## Lambda表达式
Lambda表达式的初衷是进一步简化匿名类的语法不过实现上Lambda表达式并不是匿名类的语法糖使Java走向函数式编程。对于匿名类虽然没有类名但还是要给出方法定义。这里有个例子分别使用匿名类和Lambda表达式创建一个线程打印字符串
```
//匿名类
new Thread(new Runnable(){
@Override
public void run(){
System.out.println(&quot;hello1&quot;);
}
}).start();
//Lambda表达式
new Thread(() -&gt; System.out.println(&quot;hello2&quot;)).start();
```
那么Lambda表达式如何匹配Java的类型系统呢
答案就是,函数式接口。
函数式接口是一种只有单一抽象方法的接口,使用@FunctionalInterface来描述,可以隐式地转换成 Lambda 表达式。使用Lambda表达式来实现函数式接口不需要提供类名和方法定义通过一行代码提供函数式接口的实例就可以让函数成为程序中的头等公民可以像普通数据一样作为参数传递而不是作为一个固定的类中的固定方法。
函数式接口到底是什么样的呢java.util.function包中定义了各种函数式接口。比如用于提供数据的Supplier接口就只有一个get抽象方法没有任何入参、有一个返回值
```
@FunctionalInterface
public interface Supplier&lt;T&gt; {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
```
我们可以使用Lambda表达式或方法引用来得到Supplier接口的实例
```
//使用Lambda表达式提供Supplier接口实现返回OK字符串
Supplier&lt;String&gt; stringSupplier = ()-&gt;&quot;OK&quot;;
//使用方法引用提供Supplier接口实现返回空字符串
Supplier&lt;String&gt; supplier = String::new;
```
这样是不是很方便为了帮你掌握函数式接口及其用法我再举几个使用Lambda表达式或方法引用来构建函数的例子
```
//Predicate接口是输入一个参数返回布尔值。我们通过and方法组合两个Predicate条件判断是否值大于0并且是偶数
Predicate&lt;Integer&gt; positiveNumber = i -&gt; i &gt; 0;
Predicate&lt;Integer&gt; evenNumber = i -&gt; i % 2 == 0;
assertTrue(positiveNumber.and(evenNumber).test(2));
//Consumer接口是消费一个数据。我们通过andThen方法组合调用两个Consumer输出两行abcdefg
Consumer&lt;String&gt; println = System.out::println;
println.andThen(println).accept(&quot;abcdefg&quot;);
//Function接口是输入一个数据计算后输出一个数据。我们先把字符串转换为大写然后通过andThen组合另一个Function实现字符串拼接
Function&lt;String, String&gt; upperCase = String::toUpperCase;
Function&lt;String, String&gt; duplicate = s -&gt; s.concat(s);
assertThat(upperCase.andThen(duplicate).apply(&quot;test&quot;), is(&quot;TESTTEST&quot;));
//Supplier是提供一个数据的接口。这里我们实现获取一个随机数
Supplier&lt;Integer&gt; random = ()-&gt;ThreadLocalRandom.current().nextInt();
System.out.println(random.get());
//BinaryOperator是输入两个同类型参数输出一个同类型参数的接口。这里我们通过方法引用获得一个整数加法操作通过Lambda表达式定义一个减法操作然后依次调用
BinaryOperator&lt;Integer&gt; add = Integer::sum;
BinaryOperator&lt;Integer&gt; subtraction = (a, b) -&gt; a - b;
assertThat(subtraction.apply(add.apply(1, 2), 3), is(0));
```
Predicate、Function等函数式接口还使用default关键字实现了几个默认方法。这样一来它们既可以满足函数式接口只有一个抽象方法又能为接口提供额外的功能
```
@FunctionalInterface
public interface Function&lt;T, R&gt; {
R apply(T t);
default &lt;V&gt; Function&lt;V, R&gt; compose(Function&lt;? super V, ? extends T&gt; before) {
Objects.requireNonNull(before);
return (V v) -&gt; apply(before.apply(v));
}
default &lt;V&gt; Function&lt;T, V&gt; andThen(Function&lt;? super R, ? extends V&gt; after) {
Objects.requireNonNull(after);
return (T t) -&gt; after.apply(apply(t));
}
}
```
很明显Lambda表达式给了我们复用代码的更多可能性我们可以把一大段逻辑中变化的部分抽象出函数式接口由外部方法提供函数实现重用方法内的整体逻辑处理。
不过需要注意的是,在自定义函数式接口之前,可以先确认下[java.util.function包](https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html)中的43个标准函数式接口是否能满足需求我们要尽可能重用这些接口因为使用大家熟悉的标准接口可以提高代码的可读性。
## 使用Java 8简化代码
这一部分我会通过几个具体的例子带你感受一下使用Java 8简化代码的三个重要方面
- 使用Stream简化集合操作
- 使用Optional简化判空逻辑
- JDK8结合Lambda和Stream对各种类的增强。
### 使用Stream简化集合操作
Lambda表达式可以帮我们用简短的代码实现方法的定义给了我们复用代码的更多可能性。利用这个特性我们可以把集合的投影、转换、过滤等操作抽象成通用的接口然后通过Lambda表达式传入其具体实现这也就是Stream操作。
我们看一个具体的例子。这里有一段20行左右的代码实现了如下的逻辑
- 把整数列表转换为Point2D列表
- 遍历Point2D列表过滤出Y轴&gt;1的对象
- 计算Point2D点到原点的距离
- 累加所有计算出的距离,并计算距离的平均值。
```
private static double calc(List&lt;Integer&gt; ints) {
//临时中间集合
List&lt;Point2D&gt; point2DList = new ArrayList&lt;&gt;();
for (Integer i : ints) {
point2DList.add(new Point2D.Double((double) i % 3, (double) i / 3));
}
//临时变量,纯粹是为了获得最后结果需要的中间变量
double total = 0;
int count = 0;
for (Point2D point2D : point2DList) {
//过滤
if (point2D.getY() &gt; 1) {
//算距离
double distance = point2D.distance(0, 0);
total += distance;
count++;
}
}
//注意count可能为0的可能
return count &gt;0 ? total / count : 0;
}
```
现在我们可以使用Stream配合Lambda表达式来简化这段代码。简化后一行代码就可以实现这样的逻辑更重要的是代码可读性更强了通过方法名就可以知晓大概是在做什么事情。比如
- map方法传入的是一个Function可以实现对象转换
- filter方法传入一个Predicate实现对象的布尔判断只保留返回true的数据
- mapToDouble用于把对象转换为double
- 通过average方法返回一个OptionalDouble代表可能包含值也可能不包含值的可空double。
下面的第三行代码,就实现了上面方法的所有工作:
```
List&lt;Integer&gt; ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
double average = calc(ints);
double streamResult = ints.stream()
.map(i -&gt; new Point2D.Double((double) i % 3, (double) i / 3))
.filter(point -&gt; point.getY() &gt; 1)
.mapToDouble(point -&gt; point.distance(0, 0))
.average()
.orElse(0);
//如何用一行代码来实现,比较一下可读性
assertThat(average, is(streamResult));
```
到这里你可能会问了OptionalDouble又是怎么回事儿
### 有关Optional可空类型
其实类似OptionalDouble、OptionalInt、OptionalLong等是服务于基本类型的可空对象。此外Java8还定义了用于引用类型的Optional类。使用Optional不仅可以避免使用Stream进行级联调用的空指针问题更重要的是它提供了一些实用的方法帮我们避免判空逻辑。
如下是一些例子演示了如何使用Optional来避免空指针以及如何使用它的fluent API简化冗长的if-else判空逻辑
```
@Test(expected = IllegalArgumentException.class)
public void optional() {
//通过get方法获取Optional中的实际值
assertThat(Optional.of(1).get(), is(1));
//通过ofNullable来初始化一个null通过orElse方法实现Optional中无数据的时候返回一个默认值
assertThat(Optional.ofNullable(null).orElse(&quot;A&quot;), is(&quot;A&quot;));
//OptionalDouble是基本类型double的Optional对象isPresent判断有无数据
assertFalse(OptionalDouble.empty().isPresent());
//通过map方法可以对Optional对象进行级联转换不会出现空指针转换后还是一个Optional
assertThat(Optional.of(1).map(Math::incrementExact).get(), is(2));
//通过filter实现Optional中数据的过滤得到一个Optional然后级联使用orElse提供默认值
assertThat(Optional.of(1).filter(integer -&gt; integer % 2 == 0).orElse(null), is(nullValue()));
//通过orElseThrow实现无数据时抛出异常
Optional.empty().orElseThrow(IllegalArgumentException::new);
}
```
我把Optional类的常用方法整理成了一张图你可以对照案例再复习一下
<img src="https://static001.geekbang.org/resource/image/c8/52/c8a901bb16b9fca07ae0fc8bb222b252.jpg" alt="">
### Java 8类对于函数式API的增强
除了Stream之外Java 8中有很多类也都实现了函数式的功能。
比如要通过HashMap实现一个缓存的操作在Java 8之前我们可能会写出这样的getProductAndCache方法先判断缓存中是否有值如果没有值就从数据库搜索取值最后把数据加入缓存。
```
private Map&lt;Long, Product&gt; cache = new ConcurrentHashMap&lt;&gt;();
private Product getProductAndCache(Long id) {
Product product = null;
//Key存在返回Value
if (cache.containsKey(id)) {
product = cache.get(id);
} else {
//不存在则获取Value
//需要遍历数据源查询获得Product
for (Product p : Product.getData()) {
if (p.getId().equals(id)) {
product = p;
break;
}
}
//加入ConcurrentHashMap
if (product != null)
cache.put(id, product);
}
return product;
}
@Test
public void notcoolCache() {
getProductAndCache(1L);
getProductAndCache(100L);
System.out.println(cache);
assertThat(cache.size(), is(1));
assertTrue(cache.containsKey(1L));
}
```
而在Java 8中我们利用ConcurrentHashMap的computeIfAbsent方法用一行代码就可以实现这样的繁琐操作
```
private Product getProductAndCacheCool(Long id) {
return cache.computeIfAbsent(id, i -&gt; //当Key不存在的时候提供一个Function来代表根据Key获取Value的过程
Product.getData().stream()
.filter(p -&gt; p.getId().equals(i)) //过滤
.findFirst() //找第一个得到Optional&lt;Product&gt;
.orElse(null)); //如果找不到Product则使用null
}
@Test
public void coolCache()
{
getProductAndCacheCool(1L);
getProductAndCacheCool(100L);
System.out.println(cache);
assertThat(cache.size(), is(1));
assertTrue(cache.containsKey(1L));
}
```
computeIfAbsent方法在逻辑上相当于
```
if (map.get(key) == null) {
V newValue = mappingFunction.apply(key);
if (newValue != null)
map.put(key, newValue);
}
```
又比如利用Files.walk返回一个Path的流通过两行代码就能实现递归搜索+grep的操作。整个逻辑是递归搜索文件夹查找所有的.java文件然后读取文件每一行内容用正则表达式匹配public class关键字最后输出文件名和这行内容。
```
@Test
public void filesExample() throws IOException {
//无限深度,递归遍历文件夹
try (Stream&lt;Path&gt; pathStream = Files.walk(Paths.get(&quot;.&quot;))) {
pathStream.filter(Files::isRegularFile) //只查普通文件
.filter(FileSystems.getDefault().getPathMatcher(&quot;glob:**/*.java&quot;)::matches) //搜索java源码文件
.flatMap(ThrowingFunction.unchecked(path -&gt;
Files.readAllLines(path).stream() //读取文件内容转换为Stream&lt;List&gt;
.filter(line -&gt; Pattern.compile(&quot;public class&quot;).matcher(line).find()) //使用正则过滤带有public class的行
.map(line -&gt; path.getFileName() + &quot; &gt;&gt; &quot; + line))) //把这行文件内容转换为文件名+行
.forEach(System.out::println); //打印所有的行
}
}
```
输出结果如下:
<img src="https://static001.geekbang.org/resource/image/84/12/84349a90ef4aaf30032d0a8f64ab4512.png" alt="">
我再和你分享一个小技巧吧。因为Files.readAllLines方法会抛出一个受检异常IOException所以我使用了一个自定义的函数式接口用ThrowingFunction包装这个方法把受检异常转换为运行时异常让代码更清晰
```
@FunctionalInterface
public interface ThrowingFunction&lt;T, R, E extends Throwable&gt; {
static &lt;T, R, E extends Throwable&gt; Function&lt;T, R&gt; unchecked(ThrowingFunction&lt;T, R, E&gt; f) {
return t -&gt; {
try {
return f.apply(t);
} catch (Throwable e) {
throw new RuntimeException(e);
}
};
}
R apply(T t) throws E;
}
```
如果用Java 7实现类似逻辑的话大概需要几十行代码你可以尝试下。
## 并行流
前面我们看到的Stream操作都是串行Stream操作只是在一个线程中执行此外Java 8还提供了并行流的功能通过parallel方法一键把Stream转换为并行操作提交到线程池处理。
比如如下代码通过线程池来并行消费处理1到100
```
IntStream.rangeClosed(1,100).parallel().forEach(i-&gt;{
System.out.println(LocalDateTime.now() + &quot; : &quot; + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) { }
});
```
并行流不确保执行顺序并且因为每次处理耗时1秒所以可以看到在8核机器上数组是按照8个一组1秒输出一次
<img src="https://static001.geekbang.org/resource/image/f1/d6/f114d98aa2530c3f7e91b06aaa4ee1d6.png" alt="">
在这个课程中有很多类似使用threadCount个线程对某个方法总计执行taskCount次操作的案例用于演示并发情况下的多线程问题或多线程处理性能。除了会用到并行流我们有时也会使用线程池或直接使用线程进行类似操作。为了方便你对比各种实现这里我一次性给出实现此类操作的五种方式。
为了测试这五种实现方式我们设计一个场景使用20个线程threadCount以并行方式总计执行10000次taskCount操作。因为单个任务单线程执行需要10毫秒任务代码如下也就是每秒吞吐量是100个操作那20个线程QPS是2000执行完10000次操作最少耗时5秒。
```
private void increment(AtomicInteger atomicInteger) {
atomicInteger.incrementAndGet();
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
```
现在我们测试一下这五种方式,是否都可以利用更多的线程并行执行操作。
第一种方式是使用线程。直接把任务按照线程数均匀分割分配到不同的线程执行使用CountDownLatch来阻塞主线程直到所有线程都完成操作。这种方式需要我们自己分割任务
```
private int thread(int taskCount, int threadCount) throws InterruptedException {
//总操作次数计数器
AtomicInteger atomicInteger = new AtomicInteger();
//使用CountDownLatch来等待所有线程执行完成
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
//使用IntStream把数字直接转为Thread
IntStream.rangeClosed(1, threadCount).mapToObj(i -&gt; new Thread(() -&gt; {
//手动把taskCount分成taskCount份每一份有一个线程执行
IntStream.rangeClosed(1, taskCount / threadCount).forEach(j -&gt; increment(atomicInteger));
//每一个线程处理完成自己那部分数据之后countDown一次
countDownLatch.countDown();
})).forEach(Thread::start);
//等到所有线程执行完成
countDownLatch.await();
//查询计数器当前值
return atomicInteger.get();
}
```
第二种方式是使用Executors.newFixedThreadPool来获得固定线程数的线程池使用execute提交所有任务到线程池执行最后关闭线程池等待所有任务执行完成
```
private int threadpool(int taskCount, int threadCount) throws InterruptedException {
//总操作次数计数器
AtomicInteger atomicInteger = new AtomicInteger();
//初始化一个线程数量=threadCount的线程池
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
//所有任务直接提交到线程池处理
IntStream.rangeClosed(1, taskCount).forEach(i -&gt; executorService.execute(() -&gt; increment(atomicInteger)));
//提交关闭线程池申请,等待之前所有任务执行完成
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.HOURS);
//查询计数器当前值
return atomicInteger.get();
}
```
第三种方式是使用ForkJoinPool而不是普通线程池执行任务。
ForkJoinPool和传统的ThreadPoolExecutor区别在于前者对于n并行度有n个独立队列后者是共享队列。如果有大量执行耗时比较短的任务ThreadPoolExecutor的单队列就可能会成为瓶颈。这时使用ForkJoinPool性能会更好。
因此ForkJoinPool更适合大任务分割成许多小任务并行执行的场景而ThreadPoolExecutor适合许多独立任务并发执行的场景。
在这里我们先自定义一个具有指定并行数的ForkJoinPool再通过这个ForkJoinPool并行执行操作
```
private int forkjoin(int taskCount, int threadCount) throws InterruptedException {
//总操作次数计数器
AtomicInteger atomicInteger = new AtomicInteger();
//自定义一个并行度=threadCount的ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(threadCount);
//所有任务直接提交到线程池处理
forkJoinPool.execute(() -&gt; IntStream.rangeClosed(1, taskCount).parallel().forEach(i -&gt; increment(atomicInteger)));
//提交关闭线程池申请,等待之前所有任务执行完成
forkJoinPool.shutdown();
forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
//查询计数器当前值
return atomicInteger.get();
}
```
第四种方式是直接使用并行流并行流使用公共的ForkJoinPool也就是ForkJoinPool.commonPool()。
公共的ForkJoinPool默认的并行度是CPU核心数-1原因是对于CPU绑定的任务分配超过CPU个数的线程没有意义。由于并行流还会使用主线程执行任务也会占用一个CPU核心所以公共ForkJoinPool的并行度即使-1也能用满所有CPU核心。
这里我们通过配置强制指定增大了并行数但因为使用的是公共ForkJoinPool所以可能会存在干扰你可以回顾下[第3讲](https://time.geekbang.org/column/article/210337)有关线程池混用产生的问题:
```
private int stream(int taskCount, int threadCount) {
//设置公共ForkJoinPool的并行度
System.setProperty(&quot;java.util.concurrent.ForkJoinPool.common.parallelism&quot;, String.valueOf(threadCount));
//总操作次数计数器
AtomicInteger atomicInteger = new AtomicInteger();
//由于我们设置了公共ForkJoinPool的并行度直接使用parallel提交任务即可
IntStream.rangeClosed(1, taskCount).parallel().forEach(i -&gt; increment(atomicInteger));
//查询计数器当前值
return atomicInteger.get();
}
```
第五种方式是使用CompletableFuture来实现。CompletableFuture.runAsync方法可以指定一个线程池一般会在使用CompletableFuture的时候用到
```
private int completableFuture(int taskCount, int threadCount) throws InterruptedException, ExecutionException {
//总操作次数计数器
AtomicInteger atomicInteger = new AtomicInteger();
//自定义一个并行度=threadCount的ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(threadCount);
//使用CompletableFuture.runAsync通过指定线程池异步执行任务
CompletableFuture.runAsync(() -&gt; IntStream.rangeClosed(1, taskCount).parallel().forEach(i -&gt; increment(atomicInteger)), forkJoinPool).get();
//查询计数器当前值
return atomicInteger.get();
}
```
上面这五种方法都可以实现类似的效果:
<img src="https://static001.geekbang.org/resource/image/77/cc/77c42149013fd82c18d39b5e0d0292cc.png" alt="">
可以看到这5种方式执行完10000个任务的耗时都在5.4秒到6秒之间。这里的结果只是证明并行度的设置是有效的并不是性能比较。
如果你的程序对性能要求特别敏感建议通过性能测试根据场景决定适合的模式。一般而言使用线程池第二种和直接使用并行流第四种的方式在业务代码中比较常用。但需要注意的是我们通常会重用线程池而不会像Demo中那样在业务逻辑中直接声明新的线程池等操作完成后再关闭。
**另外需要注意的是在上面的例子中我们一定是先运行stream方法再运行forkjoin方法对公共ForkJoinPool默认并行度的修改才能生效。**
这是因为ForkJoinPool类初始化公共线程池是在静态代码块里加载类时就会进行的如果forkjoin方法中先使用了ForkJoinPool即便stream方法中设置了系统属性也不会起作用。因此我的建议是设置ForkJoinPool公共线程池默认并行度的操作应该放在应用启动时设置。
## 重点回顾
今天我和你简单介绍了Java 8中最重要的几个功能包括Lambda表达式、Stream流式操作、Optional可空对象、并行流操作。这些特性可以帮助我们写出简单易懂、可读性更强的代码。特别是使用Stream的链式方法可以用一行代码完成之前几十行代码的工作。
因为Stream的API非常多使用方法也是千变万化因此我会在下一讲和你详细介绍Stream API的一些使用细节。
今天用到的代码我都放在了GitHub上你可以点击[这个链接](https://github.com/JosephZhu1983/java-common-mistakes)查看。
## 思考与讨论
1. 检查下代码中是否有使用匿名类以及通过遍历List进行数据过滤、转换和聚合的代码看看能否使用Lambda表达式和Stream来重新实现呢
1. 对于并行流部分的并行消费处理1到100的例子如果把forEach替换为forEachOrdered你觉得会发生什么呢
关于Java 8你还有什么使用心得吗我是朱晔欢迎在评论区与我留言分享你的想法也欢迎你把这篇文章分享给你的朋友或同事一起交流。

View File

@@ -0,0 +1,419 @@
<audio id="audio" title="32 | 加餐2带你吃透课程中Java 8的那些重要知识点" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/60/4c/60f75bbd9b617b4e4991117b1803e64c.mp3"></audio>
你好,我是朱晔。
上一讲的几个例子中其实都涉及了Stream API的最基本使用方法。今天我会与你详细介绍复杂、功能强大的Stream API。
Stream流式操作用于对集合进行投影、转换、过滤、排序等更进一步地这些操作能链式串联在一起使用类似于SQL语句可以大大简化代码。可以说Stream操作是Java 8中最重要的内容也是这个课程大部分代码都会用到的操作。
我先说明下有些案例可能不太好理解建议你对着代码逐一到源码中查看Stream操作的方法定义以及JDK中的代码注释。
## Stream操作详解
为了方便你理解Stream的各种操作以及后面的案例我先把这节课涉及的Stream操作汇总到了一张图中。你可以先熟悉一下。
<img src="https://static001.geekbang.org/resource/image/44/04/44a6f4cb8b413ef62c40a272cb474104.jpg" alt="">
在接下来的讲述中我会围绕订单场景给出如何使用Stream的各种API完成订单的统计、搜索、查询等功能和你一起学习Stream流式操作的各种方法。你可以结合代码中的注释理解案例也可以自己运行源码观察输出。
我们先定义一个订单类、一个订单商品类和一个顾客类用作后续Demo代码的数据结构
```
//订单类
@Data
public class Order {
private Long id;
private Long customerId;//顾客ID
private String customerName;//顾客姓名
private List&lt;OrderItem&gt; orderItemList;//订单商品明细
private Double totalPrice;//总价格
private LocalDateTime placedAt;//下单时间
}
//订单商品类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderItem {
private Long productId;//商品ID
private String productName;//商品名称
private Double productPrice;//商品价格
private Integer productQuantity;//商品数量
}
//顾客类
@Data
@AllArgsConstructor
public class Customer {
private Long id;
private String name;//顾客姓名
}
```
在这里我们有一个orders字段保存了一些模拟数据类型是List<order>。这里,我就不贴出生成模拟数据的代码了。这不会影响你理解后面的代码,你也可以自己下载源码阅读。</order>
### 创建流
要使用流,就要先创建流。创建流一般有五种方式:
- 通过stream方法把List或数组转换为流
- 通过Stream.of方法直接传入多个元素构成一个流
- 通过Stream.iterate方法使用迭代的方式构造一个无限流然后使用limit限制流元素个数
- 通过Stream.generate方法从外部传入一个提供元素的Supplier来构造无限流然后使用limit限制流元素个数
- 通过IntStream或DoubleStream构造基本类型的流。
```
//通过stream方法把List或数组转换为流
@Test
public void stream()
{
Arrays.asList(&quot;a1&quot;, &quot;a2&quot;, &quot;a3&quot;).stream().forEach(System.out::println);
Arrays.stream(new int[]{1, 2, 3}).forEach(System.out::println);
}
//通过Stream.of方法直接传入多个元素构成一个流
@Test
public void of()
{
String[] arr = {&quot;a&quot;, &quot;b&quot;, &quot;c&quot;};
Stream.of(arr).forEach(System.out::println);
Stream.of(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;).forEach(System.out::println);
Stream.of(1, 2, &quot;a&quot;).map(item -&gt; item.getClass().getName()).forEach(System.out::println);
}
//通过Stream.iterate方法使用迭代的方式构造一个无限流然后使用limit限制流元素个数
@Test
public void iterate()
{
Stream.iterate(2, item -&gt; item * 2).limit(10).forEach(System.out::println);
Stream.iterate(BigInteger.ZERO, n -&gt; n.add(BigInteger.TEN)).limit(10).forEach(System.out::println);
}
//通过Stream.generate方法从外部传入一个提供元素的Supplier来构造无限流然后使用limit限制流元素个数
@Test
public void generate()
{
Stream.generate(() -&gt; &quot;test&quot;).limit(3).forEach(System.out::println);
Stream.generate(Math::random).limit(10).forEach(System.out::println);
}
//通过IntStream或DoubleStream构造基本类型的流
@Test
public void primitive()
{
//演示IntStream和DoubleStream
IntStream.range(1, 3).forEach(System.out::println);
IntStream.range(0, 3).mapToObj(i -&gt; &quot;x&quot;).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);
DoubleStream.of(1.1, 2.2, 3.3).forEach(System.out::println);
//各种转换,后面注释代表了输出结果
System.out.println(IntStream.of(1, 2).toArray().getClass()); //class [I
System.out.println(Stream.of(1, 2).mapToInt(Integer::intValue).toArray().getClass()); //class [I
System.out.println(IntStream.of(1, 2).boxed().toArray().getClass()); //class [Ljava.lang.Object;
System.out.println(IntStream.of(1, 2).asDoubleStream().toArray().getClass()); //class [D
System.out.println(IntStream.of(1, 2).asLongStream().toArray().getClass()); //class [J
//注意基本类型流和装箱后的流的区别
Arrays.asList(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;).stream() // Stream&lt;String&gt;
.mapToInt(String::length) // IntStream
.asLongStream() // LongStream
.mapToDouble(x -&gt; x / 10.0) // DoubleStream
.boxed() // Stream&lt;Double&gt;
.mapToLong(x -&gt; 1L) // LongStream
.mapToObj(x -&gt; &quot;&quot;) // Stream&lt;String&gt;
.collect(Collectors.toList());
}
```
### filter
filter方法可以实现过滤操作类似SQL中的where。我们可以使用一行代码通过filter方法实现查询所有订单中最近半年金额大于40的订单通过连续叠加filter方法进行多次条件过滤
```
//最近半年的金额大于40的订单
orders.stream()
.filter(Objects::nonNull) //过滤null值
.filter(order -&gt; order.getPlacedAt().isAfter(LocalDateTime.now().minusMonths(6))) //最近半年的订单
.filter(order -&gt; order.getTotalPrice() &gt; 40) //金额大于40的订单
.forEach(System.out::println);
```
如果不使用Stream的话必然需要一个中间集合来收集过滤后的结果而且所有的过滤条件会堆积在一起代码冗长且不易读。
### map
map操作可以做转换或者说投影类似SQL中的select。为了对比我用两种方式统计订单中所有商品的数量前一种是通过两次遍历实现后一种是通过两次mapToLong+sum方法实现
```
//计算所有订单商品数量
//通过两次遍历实现
LongAdder longAdder = new LongAdder();
orders.stream().forEach(order -&gt;
order.getOrderItemList().forEach(orderItem -&gt; longAdder.add(orderItem.getProductQuantity())));
//使用两次mapToLong+sum方法实现
assertThat(longAdder.longValue(), is(orders.stream().mapToLong(order -&gt;
order.getOrderItemList().stream()
.mapToLong(OrderItem::getProductQuantity).sum()).sum()));
```
显然后一种方式无需中间变量longAdder更直观。
这里再补充一下使用for循环生成数据是我们平时常用的操作也是这个课程会大量用到的。现在我们可以用一行代码使用IntStream配合mapToObj替代for循环来生成数据比如生成10个Product元素构成List
```
//把IntStream通过转换Stream&lt;Project&gt;
System.out.println(IntStream.rangeClosed(1,10)
.mapToObj(i-&gt;new Product((long)i, &quot;product&quot;+i, i*100.0))
.collect(toList()));
```
### flatMap
接下来我们看看flatMap展开或者叫扁平化操作相当于map+flat通过map把每一个元素替换为一个流然后展开这个流。
比如,我们要统计所有订单的总价格,可以有两种方式:
- 直接通过原始商品列表的商品个数*商品单价统计的话可以先把订单通过flatMap展开成商品清单也就是把Order替换为Stream<orderitem>然后对每一个OrderItem用mapToDouble转换获得商品总价最后进行一次sum求和</orderitem>
- 利用flatMapToDouble方法把列表中每一项展开替换为一个DoubleStream也就是直接把每一个订单转换为每一个商品的总价然后求和。
```
//直接展开订单商品进行价格统计
System.out.println(orders.stream()
.flatMap(order -&gt; order.getOrderItemList().stream())
.mapToDouble(item -&gt; item.getProductQuantity() * item.getProductPrice()).sum());
//另一种方式flatMap+mapToDouble=flatMapToDouble
System.out.println(orders.stream()
.flatMapToDouble(order -&gt;
order.getOrderItemList()
.stream().mapToDouble(item -&gt; item.getProductQuantity() * item.getProductPrice()))
.sum());
```
这两种方式可以得到相同的结果,并无本质区别。
### sorted
sorted操作可以用于行内排序的场景类似SQL中的order by。比如要实现大于50元订单的按价格倒序取前5可以通过Order::getTotalPrice方法引用直接指定需要排序的依据字段通过reversed()实现倒序:
```
//大于50的订单,按照订单价格倒序前5
orders.stream().filter(order -&gt; order.getTotalPrice() &gt; 50)
.sorted(comparing(Order::getTotalPrice).reversed())
.limit(5)
.forEach(System.out::println);
```
### distinct
distinct操作的作用是去重类似SQL中的distinct。比如下面的代码实现
- 查询去重后的下单用户。使用map从订单提取出购买用户然后使用distinct去重。
- 查询购买过的商品名。使用flatMap+map提取出订单中所有的商品名然后使用distinct去重。
```
//去重的下单用户
System.out.println(orders.stream().map(order -&gt; order.getCustomerName()).distinct().collect(joining(&quot;,&quot;)));
//所有购买过的商品
System.out.println(orders.stream()
.flatMap(order -&gt; order.getOrderItemList().stream())
.map(OrderItem::getProductName)
.distinct().collect(joining(&quot;,&quot;)));
```
### skip &amp; limit
skip和limit操作用于分页类似MySQL中的limit。其中skip实现跳过一定的项limit用于限制项总数。比如下面的两段代码
- 按照下单时间排序查询前2个订单的顾客姓名和下单时间
- 按照下单时间排序查询第3和第4个订单的顾客姓名和下单时间。
```
//按照下单时间排序查询前2个订单的顾客姓名和下单时间
orders.stream()
.sorted(comparing(Order::getPlacedAt))
.map(order -&gt; order.getCustomerName() + &quot;@&quot; + order.getPlacedAt())
.limit(2).forEach(System.out::println);
//按照下单时间排序查询第3和第4个订单的顾客姓名和下单时间
orders.stream()
.sorted(comparing(Order::getPlacedAt))
.map(order -&gt; order.getCustomerName() + &quot;@&quot; + order.getPlacedAt())
.skip(2).limit(2).forEach(System.out::println);
```
### collect
collect是收集操作对流进行终结终止操作把流导出为我们需要的数据结构。“终结”是指导出后无法再串联使用其他中间操作比如filter、map、flatmap、sorted、distinct、limit、skip。
在Stream操作中collect是最复杂的终结操作比较简单的终结操作还有forEach、toArray、min、max、count、anyMatch等我就不再展开了你可以查询[JDK文档](https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html)搜索terminal operation或intermediate operation。
接下来我通过6个案例来演示下几种比较常用的collect操作
- 第一个案例,实现了字符串拼接操作,生成一定位数的随机字符串。
- 第二个案例通过Collectors.toSet静态方法收集为Set去重得到去重后的下单用户再通过Collectors.joining静态方法实现字符串拼接。
- 第三个案例通过Collectors.toCollection静态方法获得指定类型的集合比如把List<order>转换为LinkedList<order></order></order>
- 第四个案例通过Collectors.toMap静态方法将对象快速转换为MapKey是订单ID、Value是下单用户名。
- 第五个案例通过Collectors.toMap静态方法将对象转换为Map。Key是下单用户名Value是下单时间一个用户可能多次下单所以直接在这里进行了合并只获取最近一次的下单时间。
- 第六个案例使用Collectors.summingInt方法对商品数量求和再使用Collectors.averagingInt方法对结果求平均值以统计所有订单平均购买的商品数量。
```
//生成一定位数的随机字符串
System.out.println(random.ints(48, 122)
.filter(i -&gt; (i &lt; 57 || i &gt; 65) &amp;&amp; (i &lt; 90 || i &gt; 97))
.mapToObj(i -&gt; (char) i)
.limit(20)
.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
.toString());
//所有下单的用户使用toSet去重后实现字符串拼接
System.out.println(orders.stream()
.map(order -&gt; order.getCustomerName()).collect(toSet())
.stream().collect(joining(&quot;,&quot;, &quot;[&quot;, &quot;]&quot;)));
//用toCollection收集器指定集合类型
System.out.println(orders.stream().limit(2).collect(toCollection(LinkedList::new)).getClass());
//使用toMap获取订单ID+下单用户名的Map
orders.stream()
.collect(toMap(Order::getId, Order::getCustomerName))
.entrySet().forEach(System.out::println);
//使用toMap获取下单用户名+最近一次下单时间的Map
orders.stream()
.collect(toMap(Order::getCustomerName, Order::getPlacedAt, (x, y) -&gt; x.isAfter(y) ? x : y))
.entrySet().forEach(System.out::println);
//订单平均购买的商品数量
System.out.println(orders.stream().collect(averagingInt(order -&gt;
order.getOrderItemList().stream()
.collect(summingInt(OrderItem::getProductQuantity)))));
```
可以看到这6个操作使用Stream方式一行代码就可以实现但使用非Stream方式实现的话都需要几行甚至十几行代码。
有关Collectors类的一些常用静态方法我总结到了一张图中你可以再整理一下思路
<img src="https://static001.geekbang.org/resource/image/5a/de/5af5ba60d7af2c8780b69bc6c71cf3de.png" alt="">
其中groupBy和partitionBy比较复杂我和你举例介绍。
### groupBy
groupBy是分组统计操作类似SQL中的group by子句。它和后面介绍的partitioningBy都是特殊的收集器同样也是终结操作。分组操作比较复杂为帮你理解得更透彻我准备了8个案例
- 第一个案例按照用户名分组使用Collectors.counting方法统计每个人的下单数量再按照下单数量倒序输出。
- 第二个案例按照用户名分组使用Collectors.summingDouble方法统计订单总金额再按总金额倒序输出。
- 第三个案例按照用户名分组使用两次Collectors.summingInt方法统计商品采购数量再按总数量倒序输出。
- 第四个案例统计被采购最多的商品。先通过flatMap把订单转换为商品然后把商品名作为Key、Collectors.summingInt作为Value分组统计采购数量再按Value倒序获取第一个Entry最后查询Key就得到了售出最多的商品。
- 第五个案例同样统计采购最多的商品。相比第四个案例排序Map的方式这次直接使用Collectors.maxBy收集器获得最大的Entry。
- 第六个案例按照用户名分组统计用户下的金额最高的订单。Key是用户名Value是Order直接通过Collectors.maxBy方法拿到金额最高的订单然后通过collectingAndThen实现Optional.get的内容提取最后遍历Key/Value即可。
- 第七个案例根据下单年月分组统计订单ID列表。Key是格式化成年月后的下单时间Value直接通过Collectors.mapping方法进行了转换把订单列表转换为订单ID构成的List。
- 第八个案例,根据下单年月+用户名两次分组统计订单ID列表相比上一个案例多了一次分组操作第二次分组是按照用户名进行分组。
```
//按照用户名分组,统计下单数量
System.out.println(orders.stream().collect(groupingBy(Order::getCustomerName, counting()))
.entrySet().stream().sorted(Map.Entry.&lt;String, Long&gt;comparingByValue().reversed()).collect(toList()));
//按照用户名分组,统计订单总金额
System.out.println(orders.stream().collect(groupingBy(Order::getCustomerName, summingDouble(Order::getTotalPrice)))
.entrySet().stream().sorted(Map.Entry.&lt;String, Double&gt;comparingByValue().reversed()).collect(toList()));
//按照用户名分组,统计商品采购数量
System.out.println(orders.stream().collect(groupingBy(Order::getCustomerName,
summingInt(order -&gt; order.getOrderItemList().stream()
.collect(summingInt(OrderItem::getProductQuantity)))))
.entrySet().stream().sorted(Map.Entry.&lt;String, Integer&gt;comparingByValue().reversed()).collect(toList()));
//统计最受欢迎的商品,倒序后取第一个
orders.stream()
.flatMap(order -&gt; order.getOrderItemList().stream())
.collect(groupingBy(OrderItem::getProductName, summingInt(OrderItem::getProductQuantity)))
.entrySet().stream()
.sorted(Map.Entry.&lt;String, Integer&gt;comparingByValue().reversed())
.map(Map.Entry::getKey)
.findFirst()
.ifPresent(System.out::println);
//统计最受欢迎的商品的另一种方式直接利用maxBy
orders.stream()
.flatMap(order -&gt; order.getOrderItemList().stream())
.collect(groupingBy(OrderItem::getProductName, summingInt(OrderItem::getProductQuantity)))
.entrySet().stream()
.collect(maxBy(Map.Entry.comparingByValue()))
.map(Map.Entry::getKey)
.ifPresent(System.out::println);
//按照用户名分组,选用户下的总金额最大的订单
orders.stream().collect(groupingBy(Order::getCustomerName, collectingAndThen(maxBy(comparingDouble(Order::getTotalPrice)), Optional::get)))
.forEach((k, v) -&gt; System.out.println(k + &quot;#&quot; + v.getTotalPrice() + &quot;@&quot; + v.getPlacedAt()));
//根据下单年月分组统计订单ID列表
System.out.println(orders.stream().collect
(groupingBy(order -&gt; order.getPlacedAt().format(DateTimeFormatter.ofPattern(&quot;yyyyMM&quot;)),
mapping(order -&gt; order.getId(), toList()))));
//根据下单年月+用户名两次分组统计订单ID列表
System.out.println(orders.stream().collect
(groupingBy(order -&gt; order.getPlacedAt().format(DateTimeFormatter.ofPattern(&quot;yyyyMM&quot;)),
groupingBy(order -&gt; order.getCustomerName(),
mapping(order -&gt; order.getId(), toList())))));
```
如果不借助Stream转换为普通的Java代码实现这些复杂的操作可能需要几十行代码。
### partitionBy
partitioningBy用于分区分区是特殊的分组只有true和false两组。比如我们把用户按照是否下单进行分区给partitioningBy方法传入一个Predicate作为数据分区的区分输出是Map&lt;Boolean, List&lt;T&gt;&gt;
```
public static &lt;T&gt;
Collector&lt;T, ?, Map&lt;Boolean, List&lt;T&gt;&gt;&gt; partitioningBy(Predicate&lt;? super T&gt; predicate) {
return partitioningBy(predicate, toList());
}
```
测试一下partitioningBy配合anyMatch可以把用户分为下过订单和没下过订单两组
```
//根据是否有下单记录进行分区
System.out.println(Customer.getData().stream().collect(
partitioningBy(customer -&gt; orders.stream().mapToLong(Order::getCustomerId)
.anyMatch(id -&gt; id == customer.getId()))));
```
## 重点回顾
今天我用了大量的篇幅和案例和你展开介绍了Stream中很多具体的流式操作方法。有些案例可能不太好理解我建议你对着代码逐一到源码中查看这些操作的方法定义以及JDK中的代码注释。
最后我建议你思考下在日常工作中还会使用SQL统计哪些信息这些SQL是否也可以用Stream来改写呢Stream的API博大精深但其中又有规律可循。这其中的规律主要就是理清楚这些API传参的函数式接口定义就能搞明白到底是需要我们提供数据、消费数据、还是转换数据等。那掌握Stream的方法便是多测试多练习以强化记忆、加深理解。
今天用到的代码我都放在了GitHub上你可以点击[这个链接](https://github.com/JosephZhu1983/java-common-mistakes)查看。
## 思考与讨论
1. 使用Stream可以非常方便地对List做各种操作那有没有什么办法可以实现在整个过程中观察数据变化呢比如我们进行filter+map操作如何观察filter后map的原始数据呢
1. Collectors类提供了很多现成的收集器那我们有没有办法实现自定义的收集器呢比如实现一个MostPopularCollector来得到List中出现次数最多的元素满足下面两个测试用例
```
assertThat(Stream.of(1, 1, 2, 2, 2, 3, 4, 5, 5).collect(new MostPopularCollector&lt;&gt;()).get(), is(2));
assertThat(Stream.of('a', 'b', 'c', 'c', 'c', 'd').collect(new MostPopularCollector&lt;&gt;()).get(), is('c'));
```
关于Java 8你还有什么使用心得吗我是朱晔欢迎在评论区与我留言分享你的想法也欢迎你把这篇文章分享给你的朋友或同事一起交流。

View File

@@ -0,0 +1,158 @@
<audio id="audio" title="33 | 加餐3定位应用问题排错套路很重要" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a3/89/a386559811a46f95445534120cc39889.mp3"></audio>
你好,我是朱晔。
咱们这个课程已经更新13讲了感谢各位同学一直在坚持学习并在评论区留下了很多高质量的留言。这些留言有的是分享自己曾经踩的坑有的是对课后思考题的详细解答还有的是提出了非常好的问题进一步丰富了这个课程的内容。
有同学说这个课程的案例非常实用都是工作中会遇到的。正如我在开篇词中所说这个课程涉及的100个案例、约130个小坑有40%来自于我经历过或者是见过的200多个线上生产事故剩下的60%来自于我开发业务项目,以及日常审核别人的代码发现的问题。确实,我在整理这些案例上花费了很多精力,也特别感谢各位同学的认可,更希望你们能继续坚持学习,继续在评论区和我交流。
也有同学反馈,排查问题的思路很重要,希望自己遇到问题时,也能够从容、高效地定位到根因。因此,今天这一讲,我就与你说说我在应急排错方面积累的心得。这都是我多年担任技术负责人和架构师自己总结出来的,希望对你有所帮助。当然了,也期待你能留言与我说说,自己平时的排错套路。
## 在不同环境排查问题,有不同的方式
要说排查问题的思路,我们首先得明白是在什么环境排错。
- 如果是在自己的开发环境排查问题那你几乎可以使用任何自己熟悉的工具来排查甚至可以进行单步调试。只要问题能重现排查就不会太困难最多就是把程序调试到JDK或三方类库内部进行分析。
- 如果是在测试环境排查问题相比开发环境少的是调试不过你可以使用JDK自带的jvisualvm或阿里的[Arthas](https://github.com/alibaba/arthas)附加到远程的JVM进程排查问题。另外测试环境允许造数据、造压力模拟我们需要的场景因此遇到偶发问题时我们可以尝试去造一些场景让问题更容易出现方便测试。
- 如果是在生产环境排查问题,往往比较难:一方面,生产环境权限管控严格,一般不允许调试工具从远程附加进程;另一方面,生产环境出现问题要求以恢复为先,难以留出充足的时间去慢慢排查问题。但,因为生产环境的流量真实、访问量大、网络权限管控严格、环境复杂,因此更容易出问题,也是出问题最多的环境。
接下来,我就与你详细说说,如何在生产环境排查问题吧。
## 生产问题的排查很大程度依赖监控
其实,排查问题就像在破案,生产环境出现问题时,因为要尽快恢复应用,就不可能保留完整现场用于排查和测试。因此,是否有充足的信息可以了解过去、还原现场就成了破案的关键。这里说的信息,主要就是日志、监控和快照。
日志就不用多说了,主要注意两点:
- 确保错误、异常信息可以被完整地记录到文件日志中;
- 确保生产上程序的日志级别是INFO以上。记录日志要使用合理的日志优先级DEBUG用于开发调试、INFO用于重要流程信息、WARN用于需要关注的问题、ERROR用于阻断流程的错误。
对于监控,在生产环境排查问题时,首先就需要开发和运维团队做好充足的监控,而且是多个层次的监控。
- 主机层面对CPU、内存、磁盘、网络等资源做监控。如果应用部署在虚拟机或Kubernetes集群中那么除了对物理机做基础资源监控外还要对虚拟机或Pod做同样的监控。监控层数取决于应用的部署方案有一层OS就要做一层监控。
- 网络层面,需要监控专线带宽、交换机基本情况、网络延迟。
- 所有的中间件和存储都要做好监控不仅仅是监控进程对CPU、内存、磁盘IO、网络使用的基本指标更重要的是监控组件内部的一些重要指标。比如著名的监控工具Prometheus就提供了大量的[exporter](https://prometheus.io/docs/instrumenting/exporters/)来对接各种中间件和存储系统。
- 应用层面需要监控JVM进程的类加载、内存、GC、线程等常见指标比如使用[Micrometer](https://micrometer.io/)来做应用监控此外还要确保能够收集、保存应用日志、GC日志。
我们再来看看快照。这里的“快照”是指应用进程在某一时刻的快照。通常情况下我们会为生产环境的Java应用设置-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath=…这2个JVM参数用于在出现OOM时保留堆快照。这个课程中我们也多次使用MAT工具来分析堆快照。
了解过去、还原现场后,接下来我们就看看定位问题的套路。
## 分析定位问题的套路
定位问题首先要定位问题出在哪个层次上。比如是Java应用程序自身的问题还是外部因素导致的问题。我们可以先查看程序是否有异常异常信息一般比较具体可以马上定位到大概的问题方向如果是一些资源消耗型的问题可能不会有异常我们可以通过指标监控配合显性问题点来定位。
一般情况下,程序的问题来自以下三个方面。
第一程序发布后的Bug回滚后可以立即解决。这类问题的排查可以回滚后再慢慢分析版本差异。
第二,外部因素,比如主机、中间件或数据库的问题。这类问题的排查方式,按照主机层面的问题、中间件或存储(统称组件)的问题分为两类。
主机层面的问题,可以使用工具排查:
- CPU相关问题可以使用top、vmstat、pidstat、ps等工具排查
- 内存相关问题可以使用free、top、ps、vmstat、cachestat、sar等工具排查
- IO相关问题可以使用lsof、iostat、pidstat、sar、iotop、df、du等工具排查
- 网络相关问题可以使用ifconfig、ip、nslookup、dig、ping、tcpdump、iptables等工具排查。
组件的问题,可以从以下几个方面排查:
- 排查组件所在主机是否有问题;
- 排查组件进程基本情况,观察各种监控指标;
- 查看组件的日志输出,特别是错误日志;
- 进入组件控制台,使用一些命令查看其运作情况。
第三因为系统资源不够造成系统假死的问题通常需要先通过重启和扩容解决问题之后再进行分析不过最好能留一个节点作为现场。系统资源不够一般体现在CPU使用高、内存泄漏或OOM的问题、IO问题、网络相关问题这四个方面。
对于CPU使用高的问题如果现场还在具体的分析流程是
- 首先在Linux服务器上运行top -Hp pid命令来查看进程中哪个线程CPU使用高
- 然后输入大写的P将线程按照 CPU 使用率排序并把明显占用CPU的线程ID转换为16进制
- 最后在jstack命令输出的线程栈中搜索这个线程ID定位出问题的线程当时的调用栈。
如果没有条件直接在服务器上运行top命令的话我们可以用采样的方式定位问题间隔固定秒数比如10秒运行一次jstack命令采样几次后对比采样得出哪些线程始终处于运行状态分析出问题的线程。
如果现场没有了我们可以通过排除法来分析。CPU使用高一般是由下面的因素引起的
- 突发压力。这类问题我们可以通过应用之前的负载均衡的流量或日志量来确认诸如Nginx等反向代理都会记录URL可以依靠代理的Access Log进行细化定位也可以通过监控观察JVM线程数的情况。压力问题导致CPU使用高的情况下如果程序的各资源使用没有明显不正常之后可以通过压测+Profilerjvisualvm就有这个功能进一步定位热点方法如果资源使用不正常比如产生了几千个线程就需要考虑调参。
- GC。这种情况我们可以通过JVM监控GC相关指标、GC Log进行确认。如果确认是GC的压力那么内存使用也很可能会不正常需要按照内存问题分析流程做进一步分析。
- 程序中死循环逻辑或不正常的处理流程。这类问题,我们可以结合应用日志分析。一般情况下,应用执行过程中都会产生一些日志,可以重点关注日志量异常部分。
对于内存泄露或OOM的问题最简单的分析方式就是堆转储后使用MAT分析。堆转储包含了堆现场全貌和线程栈信息一般观察支配树图、直方图就可以马上看到占用大量内存的对象可以快速定位到内存相关问题。这一点我们会在[第5篇加餐](https://time.geekbang.org/column/article/230534)中详细介绍。
需要注意的是Java进程对内存的使用不仅仅是堆区还包括线程使用的内存线程个数*每一个线程的线程栈和元数据区。每一个内存区都可能产生OOM可以结合监控观察线程数、已加载类数量等指标分析。另外我们需要注意看一下JVM参数的设置是否有明显不合理的地方限制了资源使用。
IO相关的问题除非是代码问题引起的资源不释放等问题否则通常都不是由Java进程内部因素引发的。
网络相关的问题一般也是由外部因素引起的。对于连通性问题结合异常信息通常比较容易定位对于性能或瞬断问题可以先尝试使用ping等工具简单判断如果不行再使用tcpdump或Wireshark来分析。
## 分析和定位问题需要注意的九个点
有些时候,我们分析和定位问题时,会陷入误区或是找不到方向。遇到这种情况,你可以借鉴下我的九个心得。
**第一,考虑“鸡”和“蛋”的问题。**比如,发现业务逻辑执行很慢且线程数增多的情况时,我们需要考虑两种可能性:
- 一是程序逻辑有问题或外部依赖慢使得业务逻辑执行慢在访问量不变的情况下需要更多的线程数来应对。比如10TPS的并发原先一次请求1s可以执行完成10个线程可以支撑现在执行完成需要10s那就需要100个线程。
- 二是有可能是请求量增大了使得线程数增多应用本身的CPU资源不足再加上上下文切换问题导致处理变慢了。
出现问题的时候,我们需要结合内部表现和入口流量一起看,确认这里的“慢”到底是根因还是结果。
**第二,考虑通过分类寻找规律。**在定位问题没有头绪的时候,我们可以尝试总结规律。
比如我们有10台应用服务器做负载均衡出问题时可以通过日志分析是否是均匀分布的还是问题都出现在1台机器。又比如应用日志一般会记录线程名称出问题时我们可以分析日志是否集中在某一类线程上。再比如如果发现应用开启了大量TCP连接通过netstat我们可以分析出主要集中连接到哪个服务。
如果能总结出规律,很可能就找到了突破点。
**第三,分析问题需要根据调用拓扑来,不能想当然。**比如我们看到Nginx返回502错误一般可以认为是下游服务的问题导致网关无法完成请求转发。对于下游服务不能想当然就认为是我们的Java程序比如在拓扑上可能Nginx代理的是Kubernetes的Traefik Ingress链路是Nginx-&gt;Traefik-&gt;应用如果一味排查Java程序的健康情况那么始终不会找到根因。
又比如我们虽然使用了Spring Cloud Feign来进行服务调用出现连接超时也不一定就是服务端的问题有可能是客户端通过URL来调用服务端并不是通过Eureka的服务发现实现的客户端负载均衡。换句话说客户端连接的是Nginx代理而不是直接连接应用客户端连接服务出现的超时其实是Nginx代理宕机所致。
**第四,考虑资源限制类问题。**观察各种曲线指标,如果发现曲线慢慢上升然后稳定在一个水平线上,那么一般就是资源达到了限制或瓶颈。
比如在观察网络带宽曲线的时候如果发现带宽上升到120MB左右不动了那么很可能就是打满了1GB的网卡或传输带宽。又比如观察到数据库活跃连接数上升到10个就不动了那么很可能是连接池打满了。观察监控一旦看到任何这样的曲线都要引起重视。
**第五,考虑资源相互影响。**CPU、内存、IO和网络这四类资源就像人的五脏六腑是相辅相成的一个资源出现了明显的瓶颈很可能会引起其他资源的连锁反应。
比如内存泄露后对象无法回收会造成大量Full GC此时CPU会大量消耗在GC上从而引起CPU使用增加。又比如我们经常会把数据缓存在内存队列中进行异步IO处理网络或磁盘出现问题时就很可能会引起内存的暴涨。因此出问题的时候我们要考虑到这一点以避免误判。
**第六,排查网络问题要考虑三个方面,到底是客户端问题,还是服务端问题,还是传输问题。**比如出现数据库访问慢的现象可能是客户端的原因连接池不够导致连接获取慢、GC停顿、CPU占满等也可能是传输环节的问题包括光纤、防火墙、路由表设置等问题也可能是真正的服务端问题需要逐一排查来进行区分。
服务端慢一般可以看到MySQL出慢日志传输慢一般可以通过ping来简单定位排除了这两个可能并且仅仅是部分客户端出现访问慢的情况就需要怀疑是客户端本身的问题。对于第三方系统、服务或存储访问出现慢的情况不能完全假设是服务端的问题。
**第七,快照类工具和趋势类工具需要结合使用。**比如jstat、top、各种监控曲线是趋势类工具可以让我们观察各个指标的变化情况定位大概的问题点而jstack和分析堆快照的MAT是快照类工具用于详细分析某一时刻应用程序某一个点的细节。
一般情况下,我们会先使用趋势类工具来总结规律,再使用快照类工具来分析问题。如果反过来可能就会误判,因为快照类工具反映的只是一个瞬间程序的情况,不能仅仅通过分析单一快照得出结论,如果缺少趋势类工具的帮助,那至少也要提取多个快照来对比。
**第八,不要轻易怀疑监控。**我曾看过一个空难事故的分析,飞行员在空中发现仪表显示飞机所有油箱都处于缺油的状态,他第一时间的怀疑是油表出现故障了,始终不愿意相信是真的缺油,结果飞行不久后引擎就断油熄火了。同样地,在应用出现问题时,我们会查看各种监控系统,但有些时候我们宁愿相信自己的经验,也不相信监控图表的显示。这可能会导致我们完全朝着错误的方向来排查问题。
如果你真的怀疑是监控系统有问题,可以看一下这套监控系统对于不出问题的应用显示是否正常,如果正常那就应该相信监控而不是自己的经验。
**第九,如果因为监控缺失等原因无法定位到根因的话,相同问题就有再出现的风险**,需要做好三项工作:
- 做好日志、监控和快照补漏工作,下次遇到问题时可以定位根因;
- 针对问题的症状做好实时报警,确保出现问题后可以第一时间发现;
- 考虑做一套热备的方案,出现问题后可以第一时间切换到热备系统快速解决问题,同时又可以保留老系统的现场。
## 重点回顾
今天,我和你总结分享了分析生产环境问题的套路。
第一,分析问题一定是需要依据的,靠猜是猜不出来的,需要提前做好基础监控的建设。监控的话,需要在基础运维层、应用层、业务层等多个层次进行。定位问题的时候,我们同样需要参照多个监控层的指标表现综合分析。
第二定位问题要先对原因进行大致分类比如是内部问题还是外部问题、CPU相关问题还是内存相关问题、仅仅是A接口的问题还是整个应用的问题然后再去进一步细化探索一定是从大到小来思考问题在追查问题遇到瓶颈的时候我们可以先退出细节再从大的方面捋一下涉及的点再重新来看问题。
第三,分析问题很多时候靠的是经验,很难找到完整的方法论。遇到重大问题的时候,往往也需要根据直觉来第一时间找到最有可能的点,这里甚至有运气成分。我还和你分享了我的九条经验,建议你在平时解决问题的时候多思考、多总结,提炼出更多自己分析问题的套路和拿手工具。
最后,值得一提的是,定位到问题原因后,我们要做好记录和复盘。每一次故障和问题都是宝贵的资源,复盘不仅仅是记录问题,更重要的是改进。复盘时,我们需要做到以下四点:
- 记录完整的时间线、处理措施、上报流程等信息;
- 分析问题的根本原因;
- 给出短、中、长期改进方案包括但不限于代码改动、SOP、流程并记录跟踪每一个方案进行闭环
- 定期组织团队回顾过去的故障。
## 思考与讨论
1. 如果你现在打开一个App后发现首页展示了一片空白那这到底是客户端兼容性的问题还是服务端的问题呢如果是服务端的问题又如何进一步细化定位呢你有什么分析思路吗
1. 对于分析定位问题,你会做哪些监控或是使用哪些工具呢?
你有没有遇到过什么花了很长时间才定位到的,或是让你印象深刻的问题或事故呢?我是朱晔,欢迎在评论区与我留言分享你的想法,也欢迎你把这篇文章分享给你的朋友或同事,一起交流。

View File

@@ -0,0 +1,524 @@
<audio id="audio" title="34 | 加餐4分析定位Java问题一定要用好这些工具" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ba/35/ba5d4704e07cf01be6a394dc45bbda35.mp3"></audio>
你好我是朱晔。今天我要和你分享的内容是分析定位Java问题常用的一些工具。
到这里我们的课程更新17讲了已经更新过半了。在学习过程中你会发现我在介绍各种坑的时候并不是直接给出问题的结论而是通过工具来亲眼看到问题。
为什么这么做呢?因为我始终认为,遇到问题尽量不要去猜,一定要眼见为实。只有通过日志、监控或工具真正看到问题,然后再回到代码中进行比对确认,我们才能认为是找到了根本原因。
你可能一开始会比较畏惧使用复杂的工具去排查问题又或者是打开了工具感觉无从下手但是随着实践越来越多对Java程序和各种框架的运作越来越熟悉你会发现使用这些工具越来越顺手。
其实呢,工具只是我们定位问题的手段,要用好工具主要还是得对程序本身的运作有大概的认识,这需要长期的积累。
因此我会通过两篇加餐和你分享4个案例分别展示使用JDK自带的工具来排查JVM参数配置问题、使用Wireshark来分析网络问题、通过MAT来分析内存问题以及使用Arthas来分析CPU使用高的问题。这些案例也只是冰山一角你可以自己再通过些例子进一步学习和探索。
在今天这篇加餐中我们就先学习下如何使用JDK自带工具、Wireshark来分析和定位Java程序的问题吧。
## 使用JDK自带工具查看JVM情况
JDK自带了很多命令行甚至是图形界面工具帮助我们查看JVM的一些信息。比如在我的机器上运行ls命令可以看到JDK 8提供了非常多的工具或程序
<img src="https://static001.geekbang.org/resource/image/22/bd/22456d9186a4f36f83209168b782dbbd.png" alt="">
接下来,我会与你介绍些常用的监控工具。你也可以先通过下面这张图了解下各种工具的基本作用:
<img src="https://static001.geekbang.org/resource/image/b4/0d/b4e8ab0a76a8665879e0fc13964ebc0d.jpg" alt="">
为了测试这些工具我们先来写一段代码启动10个死循环的线程每个线程分配一个10MB左右的字符串然后休眠10秒。可以想象到这个程序会对GC造成压力。
```
//启动10个线程
IntStream.rangeClosed(1, 10).mapToObj(i -&gt; new Thread(() -&gt; {
while (true) {
//每一个线程都是一个死循环休眠10秒打印10M数据
String payload = IntStream.rangeClosed(1, 10000000)
.mapToObj(__ -&gt; &quot;a&quot;)
.collect(Collectors.joining(&quot;&quot;)) + UUID.randomUUID().toString();
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(payload.length());
}
})).forEach(Thread::start);
TimeUnit.HOURS.sleep(1);
```
修改pom.xml配置spring-boot-maven-plugin插件打包的Java程序的main方法类
```
&lt;plugin&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
&lt;configuration&gt;
&lt;mainClass&gt;org.geekbang.time.commonmistakes.troubleshootingtools.jdktool.CommonMistakesApplication
&lt;/mainClass&gt;
&lt;/configuration&gt;
&lt;/plugin&gt;
```
然后使用java -jar启动进程设置JVM参数让堆最小最大都是1GB
```
java -jar common-mistakes-0.0.1-SNAPSHOT.jar -Xms1g -Xmx1g
```
完成这些准备工作后我们就可以使用JDK提供的工具来观察分析这个测试程序了。
### jps
首先使用jps得到Java进程列表这会比使用ps来的方便
```
➜ ~ jps
12707
22261 Launcher
23864 common-mistakes-0.0.1-SNAPSHOT.jar
15608 RemoteMavenServer36
23243 Main
23868 Jps
22893 KotlinCompileDaemon
```
### jinfo
然后可以使用jinfo打印JVM的各种参数
```
➜ ~ jinfo 23864
Java System Properties:
#Wed Jan 29 12:49:47 CST 2020
...
user.name=zhuye
path.separator=\:
os.version=10.15.2
java.runtime.name=Java(TM) SE Runtime Environment
file.encoding=UTF-8
java.vm.name=Java HotSpot(TM) 64-Bit Server VM
...
VM Flags:
-XX:CICompilerCount=4 -XX:ConcGCThreads=2 -XX:G1ConcRefinementThreads=8 -XX:G1HeapRegionSize=1048576 -XX:GCDrainStackTargetSize=64 -XX:InitialHeapSize=268435456 -XX:MarkStackSize=4194304 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=2576351232 -XX:MinHeapDeltaBytes=1048576 -XX:NonNMethodCodeHeapSize=5835340 -XX:NonProfiledCodeHeapSize=122911450 -XX:ProfiledCodeHeapSize=122911450 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
VM Arguments:
java_command: common-mistakes-0.0.1-SNAPSHOT.jar -Xms1g -Xmx1g
java_class_path (initial): common-mistakes-0.0.1-SNAPSHOT.jar
Launcher Type: SUN_STANDARD
```
查看第15行和19行可以发现**我们设置JVM参数的方式不对-Xms1g和-Xmx1g这两个参数被当成了Java程序的启动参数**整个JVM目前最大内存是4GB左右而不是1GB。
因此当我们怀疑JVM的配置很不正常的时候要第一时间使用工具来确认参数。除了使用工具确认JVM参数外你也可以打印VM参数和程序参数
```
System.out.println(&quot;VM options&quot;);
System.out.println(ManagementFactory.getRuntimeMXBean().getInputArguments().stream().collect(Collectors.joining(System.lineSeparator())));
System.out.println(&quot;Program arguments&quot;);
System.out.println(Arrays.stream(args).collect(Collectors.joining(System.lineSeparator())));
```
把JVM参数放到-jar之前重新启动程序可以看到如下输出从输出也可以确认这次JVM参数的配置正确了
```
➜ target git:(master) ✗ java -Xms1g -Xmx1g -jar common-mistakes-0.0.1-SNAPSHOT.jar test
VM options
-Xms1g
-Xmx1g
Program arguments
test
```
### jvisualvm
然后启动另一个重量级工具jvisualvm观察一下程序可以在概述面板再次确认JVM参数设置成功了
<img src="https://static001.geekbang.org/resource/image/4d/e4/4d8a600072b0b1aea3943dee584c72e4.png" alt="">
继续观察监视面板可以看到JVM的GC活动基本是10秒发生一次堆内存在250MB到900MB之间波动活动线程数是22。我们可以在监视面板看到JVM的基本情况也可以直接在这里进行手动GC和堆Dump操作
<img src="https://static001.geekbang.org/resource/image/5b/02/5be531e51f6e49d5511d419c90b29302.png" alt="">
### jconsole
如果希望看到各个内存区的GC曲线图可以使用jconsole观察。jconsole也是一个综合性图形界面监控工具比jvisualvm更方便的一点是可以用曲线的形式监控各种数据包括MBean中的属性值
<img src="https://static001.geekbang.org/resource/image/6b/12/6b4c08d384eea532842d386638dddb12.png" alt="">
### jstat
同样如果没有条件使用图形界面毕竟在Linux服务器上我们主要使用命令行工具又希望看到GC趋势的话我们可以使用jstat工具。
jstat工具允许以固定的监控频次输出JVM的各种监控指标比如使用-gcutil输出GC和内存占用汇总信息每隔5秒输出一次输出100次可以看到Young GC比较频繁而Full GC基本10秒一次
```
➜ ~ jstat -gcutil 23940 5000 100
S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
0.00 100.00 0.36 87.63 94.30 81.06 539 14.021 33 3.972 837 0.976 18.968
0.00 100.00 0.60 69.51 94.30 81.06 540 14.029 33 3.972 839 0.978 18.979
0.00 0.00 0.50 99.81 94.27 81.03 548 14.143 34 4.002 840 0.981 19.126
0.00 100.00 0.59 70.47 94.27 81.03 549 14.177 34 4.002 844 0.985 19.164
0.00 100.00 0.57 99.85 94.32 81.09 550 14.204 34 4.002 845 0.990 19.196
0.00 100.00 0.65 77.69 94.32 81.09 559 14.469 36 4.198 847 0.993 19.659
0.00 100.00 0.65 77.69 94.32 81.09 559 14.469 36 4.198 847 0.993 19.659
0.00 100.00 0.70 35.54 94.32 81.09 567 14.763 37 4.378 853 1.001 20.142
0.00 100.00 0.70 41.22 94.32 81.09 567 14.763 37 4.378 853 1.001 20.142
0.00 100.00 1.89 96.76 94.32 81.09 574 14.943 38 4.487 859 1.007 20.438
0.00 100.00 1.39 39.20 94.32 81.09 575 14.946 38 4.487 861 1.010 20.442
```
>
其中S0表示Survivor0区占用百分比S1表示Survivor1区占用百分比E表示Eden区占用百分比O表示老年代占用百分比M表示元数据区占用百分比YGC表示年轻代回收次数YGCT表示年轻代回收耗时FGC表示老年代回收次数FGCT表示老年代回收耗时。
jstat命令的参数众多包含-class、-compiler、-gc等。Java 8、Linux/Unix平台jstat工具的完整介绍你可以查看[这里](https://docs.oracle.com/javase/8/docs/technotes/tools/#monitor)。jstat定时输出的特性可以方便我们持续观察程序的各项指标。
继续来到线程面板可以看到大量以Thread开头的线程基本都是有节奏的10秒运行一下其他时间都在休眠和我们的代码逻辑匹配
<img src="https://static001.geekbang.org/resource/image/7a/85/7a1616295b4ec51c56437d2a92652185.png" alt="">
点击面板的线程Dump按钮可以查看线程瞬时的线程栈
<img src="https://static001.geekbang.org/resource/image/0d/00/0ddcd3348d1c8b0bba16736f9221a900.png" alt="">
### jstack
通过命令行工具jstack也可以实现抓取线程栈的操作
```
➜ ~ jstack 23940
2020-01-29 13:08:15
Full thread dump Java HotSpot(TM) 64-Bit Server VM (11.0.3+12-LTS mixed mode):
...
&quot;main&quot; #1 prio=5 os_prio=31 cpu=440.66ms elapsed=574.86s tid=0x00007ffdd9800000 nid=0x2803 waiting on condition [0x0000700003849000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(java.base@11.0.3/Native Method)
at java.lang.Thread.sleep(java.base@11.0.3/Thread.java:339)
at java.util.concurrent.TimeUnit.sleep(java.base@11.0.3/TimeUnit.java:446)
at org.geekbang.time.commonmistakes.troubleshootingtools.jdktool.CommonMistakesApplication.main(CommonMistakesApplication.java:41)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(java.base@11.0.3/Native Method)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(java.base@11.0.3/NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(java.base@11.0.3/DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(java.base@11.0.3/Method.java:566)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:87)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:51)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:52)
&quot;Thread-1&quot; #13 prio=5 os_prio=31 cpu=17851.77ms elapsed=574.41s tid=0x00007ffdda029000 nid=0x9803 waiting on condition [0x000070000539d000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(java.base@11.0.3/Native Method)
at java.lang.Thread.sleep(java.base@11.0.3/Thread.java:339)
at java.util.concurrent.TimeUnit.sleep(java.base@11.0.3/TimeUnit.java:446)
at org.geekbang.time.commonmistakes.troubleshootingtools.jdktool.CommonMistakesApplication.lambda$null$1(CommonMistakesApplication.java:33)
at org.geekbang.time.commonmistakes.troubleshootingtools.jdktool.CommonMistakesApplication$$Lambda$41/0x00000008000a8c40.run(Unknown Source)
at java.lang.Thread.run(java.base@11.0.3/Thread.java:834)
...
```
抓取后可以使用类似[fastthread](https://fastthread.io/)这样的在线分析工具来分析线程栈。
### jcmd
最后我们来看一下Java HotSpot虚拟机的NMT功能。
通过NMT我们可以观察细粒度内存使用情况设置-XX:NativeMemoryTracking=summary/detail可以开启NMT功能开启后可以使用jcmd工具查看NMT数据。
我们重新启动一次程序这次加上JVM参数以detail方式开启NMT
```
-Xms1g -Xmx1g -XX:ThreadStackSize=256k -XX:NativeMemoryTracking=detail
```
在这里,我们还增加了-XX:ThreadStackSize参数并将其值设置为256k也就是期望把线程栈设置为256KB。我们通过NMT观察一下设置是否成功。
启动程序后执行如下jcmd命令以概要形式输出NMT结果。可以看到**当前有32个线程线程栈总共保留了差不多4GB左右的内存**。我们明明配置线程栈最大256KB啊为什么会出现4GB这么夸张的数字呢到底哪里出了问题呢
```
➜ ~ jcmd 24404 VM.native_memory summary
24404:
Native Memory Tracking:
Total: reserved=6635310KB, committed=5337110KB
- Java Heap (reserved=1048576KB, committed=1048576KB)
(mmap: reserved=1048576KB, committed=1048576KB)
- Class (reserved=1066233KB, committed=15097KB)
(classes #902)
(malloc=9465KB #908)
(mmap: reserved=1056768KB, committed=5632KB)
- Thread (reserved=4209797KB, committed=4209797KB)
(thread #32)
(stack: reserved=4209664KB, committed=4209664KB)
(malloc=96KB #165)
(arena=37KB #59)
- Code (reserved=249823KB, committed=2759KB)
(malloc=223KB #730)
(mmap: reserved=249600KB, committed=2536KB)
- GC (reserved=48700KB, committed=48700KB)
(malloc=10384KB #135)
(mmap: reserved=38316KB, committed=38316KB)
- Compiler (reserved=186KB, committed=186KB)
(malloc=56KB #105)
(arena=131KB #7)
- Internal (reserved=9693KB, committed=9693KB)
(malloc=9661KB #2585)
(mmap: reserved=32KB, committed=32KB)
- Symbol (reserved=2021KB, committed=2021KB)
(malloc=1182KB #334)
(arena=839KB #1)
- Native Memory Tracking (reserved=85KB, committed=85KB)
(malloc=5KB #53)
(tracking overhead=80KB)
- Arena Chunk (reserved=196KB, committed=196KB)
(malloc=196KB)
```
重新以VM.native_memory detail参数运行jcmd
```
jcmd 24404 VM.native_memory detail
```
可以看到,**有16个可疑线程每一个线程保留了262144KB内存也就是256MB**通过下图红框可以看到使用关键字262144KB for Thread Stack from搜索到了16个结果
<img src="https://static001.geekbang.org/resource/image/f2/6b/f24869cbd1190c508e085c9f3400d06b.png" alt="">
其实ThreadStackSize参数的单位是KB**所以我们如果要设置线程栈256KB那么应该设置256而不是256k**。重新设置正确的参数后使用jcmd再次验证下
<img src="https://static001.geekbang.org/resource/image/d7/c9/d7228ec216003d31064698e7e16c81c9.png" alt="">
除了用于查看NMT外jcmd还有许多功能。我们可以通过help看到它的所有功能
```
jcmd 24781 help
```
对于其中每一种功能我们都可以进一步使用help来查看介绍。比如使用GC.heap_info命令可以打印Java堆的一些信息
```
jcmd 24781 help GC.heap_info
```
除了jps、jinfo、jcmd、jstack、jstat、jconsole、jvisualvm外JDK中还有一些工具你可以通过[官方文档](https://docs.oracle.com/javase/8/docs/technotes/tools/)查看完整介绍。
## 使用Wireshark分析SQL批量插入慢的问题
我之前遇到过这样一个案例有一个数据导入程序需要导入大量的数据开发同学就想到了使用Spring JdbcTemplate的批量操作功能进行数据批量导入但是发现性能非常差和普通的单条SQL执行性能差不多。
我们重现下这个案例。启动程序后首先创建一个testuser表其中只有一列name然后使用JdbcTemplate的batchUpdate方法批量插入10000条记录到testuser表
```
@SpringBootApplication
@Slf4j
public class BatchInsertAppliation implements CommandLineRunner {
@Autowired
private JdbcTemplate jdbcTemplate;
public static void main(String[] args) {
SpringApplication.run(BatchInsertApplication.class, args);
}
@PostConstruct
public void init() {
//初始化表
jdbcTemplate.execute(&quot;drop table IF EXISTS `testuser`;&quot;);
jdbcTemplate.execute(&quot;create TABLE `testuser` (\n&quot; +
&quot; `id` bigint(20) NOT NULL AUTO_INCREMENT,\n&quot; +
&quot; `name` varchar(255) NOT NULL,\n&quot; +
&quot; PRIMARY KEY (`id`)\n&quot; +
&quot;) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;&quot;);
}
@Override
public void run(String... args) {
long begin = System.currentTimeMillis();
String sql = &quot;INSERT INTO `testuser` (`name`) VALUES (?)&quot;;
//使用JDBC批量更新
jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement preparedStatement, int i) throws SQLException {
//第一个参数(索引从1开始)也就是name列赋值
preparedStatement.setString(1, &quot;usera&quot; + i);
}
@Override
public int getBatchSize() {
//批次大小为10000
return 10000;
}
});
log.info(&quot;took : {} ms&quot;, System.currentTimeMillis() - begin);
}
}
```
执行程序后可以看到1万条数据插入耗时26秒
```
[14:44:19.094] [main] [INFO ] [o.g.t.c.t.network.BatchInsertApplication:52 ] - took : 26144 ms
```
其实对于批量操作我们希望程序可以把多条insert SQL语句合并成一条或至少是一次性提交多条语句到数据库以减少和MySQL交互次数、提高性能。那么我们的程序是这样运作的吗
我在[加餐3](https://time.geekbang.org/column/article/221982)中提到一条原则“分析问题一定是需要依据的靠猜是猜不出来的”。现在我们就使用网络分析工具Wireshark来分析一下这个案例眼见为实。
首先,我们可以在[这里](https://www.wireshark.org/download.html)下载Wireshark启动后选择某个需要捕获的网卡。由于我们连接的是本地的MySQL因此选择loopback回环网卡
<img src="https://static001.geekbang.org/resource/image/d7/9b/d7c3cc2d997990d0c4b94f72f1679c9b.png" alt="">
然后Wireshark捕捉这个网卡的所有网络流量。我们可以在上方的显示过滤栏输入tcp.port == 6657来过滤出所有6657端口的TCP请求因为我们是通过6657端口连接MySQL的
可以看到程序运行期间和MySQL有大量交互。因为Wireshark直接把TCP数据包解析为了MySQL协议所以下方窗口可以直接显示MySQL请求的SQL查询语句。**我们看到testuser表的每次insert操作插入的都是一行记录**
<img src="https://static001.geekbang.org/resource/image/bc/a2/bcb987cab3cccf4d8729cfe44f01a2a2.png" alt="">
如果列表中的Protocol没有显示MySQL的话你可以手动点击Analyze菜单的Decode As菜单然后加一条规则把6657端口设置为MySQL协议
<img src="https://static001.geekbang.org/resource/image/6a/f2/6ae982e2013cf1c60300332068b58cf2.png" alt="">
这就说明我们的程序并不是在做批量插入操作和普通的单条循环插入没有区别。调试程序进入ClientPreparedStatement类可以看到执行批量操作的是executeBatchInternal方法。executeBatchInternal方法的源码如下
```
@Override
protected long[] executeBatchInternal() throws SQLException {
synchronized (checkClosed().getConnectionMutex()) {
if (this.connection.isReadOnly()) {
throw new SQLException(Messages.getString(&quot;PreparedStatement.25&quot;) + Messages.getString(&quot;PreparedStatement.26&quot;),
MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT);
}
if (this.query.getBatchedArgs() == null || this.query.getBatchedArgs().size() == 0) {
return new long[0];
}
// we timeout the entire batch, not individual statements
int batchTimeout = getTimeoutInMillis();
setTimeoutInMillis(0);
resetCancelledState();
try {
statementBegins();
clearWarnings();
if (!this.batchHasPlainStatements &amp;&amp; this.rewriteBatchedStatements.getValue()) {
if (((PreparedQuery&lt;?&gt;) this.query).getParseInfo().canRewriteAsMultiValueInsertAtSqlLevel()) {
return executeBatchedInserts(batchTimeout);
}
if (!this.batchHasPlainStatements &amp;&amp; this.query.getBatchedArgs() != null
&amp;&amp; this.query.getBatchedArgs().size() &gt; 3 /* cost of option setting rt-wise */) {
return executePreparedBatchAsMultiStatement(batchTimeout);
}
}
return executeBatchSerially(batchTimeout);
} finally {
this.query.getStatementExecuting().set(false);
clearBatch();
}
}
}
```
注意第18行判断了rewriteBatchedStatements参数是否为true是才会开启批量的优化。优化方式有2种
- 如果有条件的话优先把insert语句优化为一条语句也就是executeBatchedInserts方法
- 如果不行的话再尝试把insert语句优化为多条语句一起提交也就是executePreparedBatchAsMultiStatement方法。
到这里就明朗了实现批量提交优化的关键在于rewriteBatchedStatements参数。我们修改连接字符串并将其值设置为true
```
spring.datasource.url=jdbc:mysql://localhost:6657/common_mistakes?characterEncoding=UTF-8&amp;useSSL=false&amp;rewriteBatchedStatements=true
```
重新按照之前的步骤打开Wireshark验证可以看到
- 这次insert SQL语句被拼接成了一条语句如第二个红框所示
- 这个TCP包因为太大被分割成了11个片段传输#699请求是最后一个片段其实际内容是insert语句的最后一部分内容如第一和第三个红框显示
<img src="https://static001.geekbang.org/resource/image/3b/bc/3b7406c96a90e454a00e3c8ba82ecfbc.png" alt="">
为了查看整个TCP连接的所有数据包你可以在请求上点击右键选择Follow-&gt;TCP Stream
<img src="https://static001.geekbang.org/resource/image/5b/c2/5b18a8c6c227df50ad493f5aa546f9c2.png" alt="">
打开后可以看到从MySQL认证开始到insert语句的所有数据包的内容
<img src="https://static001.geekbang.org/resource/image/e1/5a/e154da637a2b44a65f9257beb842575a.png" alt="">
查看最开始的握手数据包可以发现TCP的最大分段大小MSS是16344字节而我们的MySQL超长insert的数据一共138933字节因此被分成了11段传输其中最大的一段是16332字节低于MSS要求的16344字节。
<img src="https://static001.geekbang.org/resource/image/3e/9e/3e66a004fd4b7dba14047751a57e089e.png" alt="">
最后可以看到插入1万条数据仅耗时253毫秒性能提升了100倍
```
[20:19:30.185] [main] [INFO ] [o.g.t.c.t.network.BatchInsertApplication:52 ] - took : 253 ms
```
虽然我们一直在使用MySQL但我们很少会考虑MySQL Connector Java是怎么和MySQL交互的实际发送给MySQL的SQL语句又是怎样的。有没有感觉到MySQL协议其实并不遥远我们完全可以使用Wireshark来观察、分析应用程序与MySQL交互的整个流程。
## 重点回顾
今天我就使用JDK自带工具查看JVM情况、使用Wireshark分析SQL批量插入慢的问题和你展示了一些工具及其用法。
首先JDK自带的一些监控和故障诊断工具中有命令行工具也有图形工具。其中命令行工具更适合在服务器上使用图形界面工具用于本地观察数据更直观。为了帮助你用好这些工具我们带你使用这些工具分析了程序错误设置JVM参数的两个问题并且观察了GC工作的情况。
然后我们使用Wireshark分析了MySQL批量insert操作慢的问题。我们看到通过Wireshark分析网络包可以让一切变得如此透明。因此学好Wireshark对我们排查C/S网络程序的Bug或性能问题会有非常大的帮助。
比如遇到诸如Connection reset、Broken pipe等网络问题的时候你可以利用Wireshark来定位问题观察客户端和服务端之间到底出了什么问题。
此外如果你需要开发网络程序的话Wireshark更是分析协议、确认程序是否正确实现的必备工具。
今天用到的代码我都放在了GitHub上你可以点击[这个链接](https://github.com/JosephZhu1983/java-common-mistakes)查看。
## 思考与讨论
1. JDK中还有一个jmap工具我们会使用jmap -dump命令来进行堆转储。那么这条命令和jmap -dump:live有什么区别呢你能否设计一个实验来证明下它们的区别呢
1. 你有没有想过客户端是如何和MySQL进行认证的呢你能否对照[MySQL的文档](https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake)使用Wireshark观察分析这一过程呢
在平时工作中你还会使用什么工具来分析排查Java应用程序的问题呢我是朱晔欢迎在评论区与我留言分享你的想法也欢迎你把今天的内容分享给你的朋友或同事一起交流。

View File

@@ -0,0 +1,283 @@
<audio id="audio" title="35 | 加餐5分析定位Java问题一定要用好这些工具" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/07/c0/07f593c7a0cf0e1ce8ddab21b56528c0.mp3"></audio>
你好,我是朱晔。
在[上一篇加餐](https://time.geekbang.org/column/article/224816)中我们介绍了使用JDK内置的一些工具、网络抓包工具Wireshark去分析、定位Java程序的问题。很多同学看完这一讲留言反馈说是“打开了一片新天地之前没有关注过JVM”“利用JVM工具发现了生产OOM的原因”。
其实,工具正是帮助我们深入到框架和组件内部,了解其运作方式和原理的重要抓手。所以,我们一定要用好它们。
今天我继续和你介绍如何使用JVM堆转储的工具MAT来分析OOM问题以及如何使用全能的故障诊断工具Arthas来分析、定位高CPU问题。
## 使用MAT分析OOM问题
对于排查OOM问题、分析程序堆内存使用情况最好的方式就是分析堆转储。
堆转储包含了堆现场全貌和线程栈信息Java 6 Update 14开始包含。我们在上一篇加餐中看到使用jstat等工具虽然可以观察堆内存使用情况的变化但是对程序内到底有多少对象、哪些是大对象还一无所知也就是说只能看到问题但无法定位问题。而堆转储就好似得到了病人在某个瞬间的全景核磁影像可以拿着慢慢分析。
Java的OutOfMemoryError是比较严重的问题需要分析出根因所以对生产应用一般都会这样设置JVM参数方便发生OOM时进行堆转储
```
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=.
```
上一篇加餐中我们提到的jvisualvm工具同样可以进行一键堆转储后直接打开这个dump查看。但是jvisualvm的堆转储分析功能并不是很强大只能查看类使用内存的直方图无法有效跟踪内存使用的引用关系所以我更推荐使用Eclipse的Memory Analyzer也叫做MAT做堆转储的分析。你可以点击[这个链接](https://www.eclipse.org/mat/)下载MAT。
使用MAT分析OOM问题一般可以按照以下思路进行
1. 通过支配树功能或直方图功能查看消耗内存最大的类型,来分析内存泄露的大概原因;
1. 查看那些消耗内存最大的类型、详细的对象明细列表,以及它们的引用链,来定位内存泄露的具体点;
1. 配合查看对象属性的功能,可以脱离源码看到对象的各种属性的值和依赖关系,帮助我们理清程序逻辑和参数;
1. 辅助使用查看线程栈来看OOM问题是否和过多线程有关甚至可以在线程栈看到OOM最后一刻出现异常的线程。
比如我手头有一个OOM后得到的转储文件java_pid29569.hprof现在要使用MAT的直方图、支配树、线程栈、OQL等功能来分析此次OOM的原因。
首先用MAT打开后先进入的是概览信息界面可以看到整个堆是437.6MB
<img src="https://static001.geekbang.org/resource/image/63/61/63ecdaf5ff7ac431f0d05661855b2e61.png" alt="">
那么这437.6MB都是什么对象呢?
如图所示工具栏的第二个按钮可以打开直方图直方图按照类型进行分组列出了每个类有多少个实例以及占用的内存。可以看到char[]字节数组占用内存最多对象数量也很多结合第二位的String类型对象数量也很多大概可以猜出String使用char[]作为实际数据存储程序可能是被字符串占满了内存导致OOM。
<img src="https://static001.geekbang.org/resource/image/0b/b9/0b3ca076b31a2d571a47c64d622b0db9.png" alt="">
我们继续分析下,到底是不是这样呢。
在char[]上点击右键选择List objects-&gt;with incoming references就可以列出所有的char[]实例以及每个char[]的整个引用关系链:
<img src="https://static001.geekbang.org/resource/image/f1/a3/f162fb9c6505dc9a8f1ea9900437ada3.png" alt="">
随机展开一个char[],如下图所示:
<img src="https://static001.geekbang.org/resource/image/dd/ac/dd4cb44ad54edee3a51f56a646c5f2ac.png" alt="">
接下来我们按照红色框中的引用链来查看尝试找到这些大char[]的来源:
- 在①处看到这些char[]几乎都是10000个字符、占用20000字节左右char是UTF-16每一个字符占用2字节
- 在②处看到char[]被String的value字段引用说明char[]来自字符串;
- 在③处看到String被ArrayList的elementData字段引用说明这些字符串加入了一个ArrayList中
- 在④处看到ArrayList又被FooService的data字段引用这个ArrayList整个RetainedHeap列的值是431MB。
Retained Heap深堆代表对象本身和对象关联的对象占用的内存Shallow Heap浅堆代表对象本身占用的内存。比如我们的FooService中的data这个ArrayList对象本身只有16字节但是其所有关联的对象占用了431MB内存。这些就可以说明肯定有哪里在不断向这个List中添加String数据导致了OOM。
左侧的蓝色框可以查看每一个实例的内部属性图中显示FooService有一个data属性类型是ArrayList。
如果我们希望看到字符串完整内容的话可以右键选择Copy-&gt;Value把值复制到剪贴板或保存到文件中
<img src="https://static001.geekbang.org/resource/image/cc/8f/cc1d53eb9570582da415c1aec5cc228f.png" alt="">
这里我们复制出的是10000个字符a下图红色部分可以看到。对于真实案例查看大字符串、大数据的实际内容对于识别数据来源有很大意义
<img src="https://static001.geekbang.org/resource/image/7b/a0/7b3198574113fecdd2a7de8cde8994a0.png" alt="">
看到这些,我们已经基本可以还原出真实的代码是怎样的了。
其实我们之前使用直方图定位FooService已经走了些弯路。你可以点击工具栏中第三个按钮下图左上角的红框所示进入支配树界面有关支配树的具体概念参考[这里](https://help.eclipse.org/2020-03/index.jsp?topic=%2Forg.eclipse.mat.ui.help%2Fconcepts%2Fdominatortree.html&amp;resultof%3D%2522%2564%256f%256d%2569%256e%2561%2574%256f%2572%2522%2520%2522%2564%256f%256d%2569%256e%2522%2520%2522%2574%2572%2565%2565%2522%2520)。这个界面会按照对象保留的Retained Heap倒序直接列出占用内存最大的对象。
可以看到第一位就是FooService整个路径是FooSerice-&gt;ArrayList-&gt;Object[]-&gt;String-&gt;char[]蓝色框部分一共有21523个字符串绿色方框部分
<img src="https://static001.geekbang.org/resource/image/7a/57/7adafa4178a4c72f8621b7eb49ee2757.png" alt="">
这样我们就从内存角度定位到FooService是根源了。那么OOM的时候FooService是在执行什么逻辑呢
为解决这个问题我们可以点击工具栏的第五个按钮下图红色框所示。打开线程视图首先看到的就是一个名为main的线程Name列展开后果然发现了FooService
<img src="https://static001.geekbang.org/resource/image/3a/ce/3a2c3d159e1599d906cc428d812cccce.png" alt="">
先执行的方法先入栈所以线程栈最上面是线程当前执行的方法逐一往下看能看到整个调用路径。因为我们希望了解FooService.oom()方法看看是谁在调用它它的内部又调用了谁所以选择以FooService.oom()方法(蓝色框)为起点来分析这个调用栈。
往下看整个绿色框部分oom()方法被OOMApplication的run方法调用而这个run方法又被SpringAppliction.callRunner方法调用。看到参数中的CommandLineRunner你应该能想到OOMApplication其实是实现了CommandLineRunner接口所以是SpringBoot应用程序启动后执行的。
以FooService为起点往上看从紫色框中的Collectors和IntPipeline你大概也可以猜出这些字符串是由Stream操作产生的。再往上看可以发现在StringBuilder的append操作的时候出现了OutOfMemoryError异常黑色框部分说明这这个线程抛出了OOM异常。
我们看到整个程序是Spring Boot应用程序那么FooService是不是Spring的Bean呢又是不是单例呢如果能分析出这点的话就更能确认是因为反复调用同一个FooService的oom方法然后导致其内部的ArrayList不断增加数据的。
点击工具栏的第四个按钮如下图红框所示来到OQL界面。在这个界面我们可以使用类似SQL的语法在dump中搜索数据你可以直接在MAT帮助菜单搜索OQL Syntax来查看OQL的详细语法
比如输入如下语句搜索FooService的实例
```
SELECT * FROM org.geekbang.time.commonmistakes.troubleshootingtools.oom.FooService
```
可以看到只有一个实例然后我们通过List objects功能搜索引用FooService的对象
<img src="https://static001.geekbang.org/resource/image/19/43/1973846815bd9d78f85bef05b499e843.png" alt="">
得到以下结果:
<img src="https://static001.geekbang.org/resource/image/07/a8/07e1216a6cc93bd146535b5809649ea8.png" alt="">
可以看到,一共两处引用:
- 第一处是OOMApplication使用了FooService这个我们已经知道了。
- 第二处是一个ConcurrentHashMap。可以看到这个HashMap是DefaultListableBeanFactory的singletonObjects字段可以证实FooService是Spring容器管理的单例的Bean。
你甚至可以在这个HashMap上点击右键选择Java Collections-&gt;Hash Entries功能来查看其内容
<img src="https://static001.geekbang.org/resource/image/ce/5f/ce4020b8f63db060a94fd039314b2d5f.png" alt="">
这样就列出了所有的Bean可以在Value上的Regex进一步过滤。输入FooService后可以看到类型为FooService的Bean只有一个其名字是fooService
<img src="https://static001.geekbang.org/resource/image/02/1a/023141fb717704cde9a57c5be6118d1a.png" alt="">
到现在为止我们虽然没看程序代码但是已经大概知道程序出现OOM的原因和大概的调用栈了。我们再贴出程序来对比一下果然和我们看到得一模一样
```
@SpringBootApplication
public class OOMApplication implements CommandLineRunner {
@Autowired
FooService fooService;
public static void main(String[] args) {
SpringApplication.run(OOMApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
//程序启动后不断调用Fooservice.oom()方法
while (true) {
fooService.oom();
}
}
}
@Component
public class FooService {
List&lt;String&gt; data = new ArrayList&lt;&gt;();
public void oom() {
//往同一个ArrayList中不断加入大小为10KB的字符串
data.add(IntStream.rangeClosed(1, 10_000)
.mapToObj(__ -&gt; &quot;a&quot;)
.collect(Collectors.joining(&quot;&quot;)));
}
}
```
到这里我们使用MAT工具从对象清单、大对象、线程栈等视角分析了一个OOM程序的堆转储。可以发现有了堆转储几乎相当于拿到了应用程序的源码+当时那一刻的快照OOM的问题无从遁形。
## 使用Arthas分析高CPU问题
[Arthas](https://alibaba.github.io/arthas/)是阿里开源的Java诊断工具相比JDK内置的诊断工具要更人性化并且功能强大可以实现许多问题的一键定位而且可以一键反编译类查看源码甚至是直接进行生产代码热修复实现在一个工具内快速定位和修复问题的一站式服务。今天我就带你使用Arthas定位一个CPU使用高的问题系统学习下这个工具的使用。
首先下载并启动Arthas
```
curl -O https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar
```
启动后直接找到我们要排查的JVM进程然后可以看到Arthas附加进程成功
```
[INFO] arthas-boot version: 3.1.7
[INFO] Found existing java process, please choose one and hit RETURN.
* [1]: 12707
[2]: 30724 org.jetbrains.jps.cmdline.Launcher
[3]: 30725 org.geekbang.time.commonmistakes.troubleshootingtools.highcpu.HighCPUApplication
[4]: 24312 sun.tools.jconsole.JConsole
[5]: 26328 org.jetbrains.jps.cmdline.Launcher
[6]: 24106 org.netbeans.lib.profiler.server.ProfilerServer
3
[INFO] arthas home: /Users/zhuye/.arthas/lib/3.1.7/arthas
[INFO] Try to attach process 30725
[INFO] Attach process 30725 success.
[INFO] arthas-client connect 127.0.0.1 3658
,---. ,------. ,--------.,--. ,--. ,---. ,---.
/ O \ | .--. ''--. .--'| '--' | / O \ ' .-'
| .-. || '--'.' | | | .--. || .-. |`. `-.
| | | || |\ \ | | | | | || | | |.-' |
`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----'
wiki https://alibaba.github.io/arthas
tutorials https://alibaba.github.io/arthas/arthas-tutorials
version 3.1.7
pid 30725
time 2020-01-30 15:48:33
```
输出help命令可以看到所有支持的命令列表。今天我们会用到dashboard、thread、jad、watch、ognl命令来定位这个HighCPUApplication进程。你可以通过[官方文档](https://alibaba.github.io/arthas/commands.html),查看这些命令的完整介绍:
<img src="https://static001.geekbang.org/resource/image/47/73/47b2abc1c3a8c0670a60c6ed74761873.png" alt="">
dashboard命令用于整体展示进程所有线程、内存、GC等情况其输出如下
<img src="https://static001.geekbang.org/resource/image/ce/4c/ce59c22389ba95104531e46edd9afa4c.png" alt="">
可以看到CPU高并不是GC引起的占用CPU较多的线程有8个其中7个是ForkJoinPool.commonPool。学习过[加餐1](https://time.geekbang.org/column/article/212374)的话你应该就知道了ForkJoinPool.commonPool是并行流默认使用的线程池。所以此次CPU高的问题应该出现在某段并行流的代码上。
接下来要查看最繁忙的线程在执行的线程栈可以使用thread -n命令。这里我们查看下最忙的8个线程
```
thread -n 8
```
输出如下:
<img src="https://static001.geekbang.org/resource/image/96/00/96cca0708e211ea7f7de413d40c72c00.png" alt="">
可以看到由于这些线程都在处理MD5的操作所以占用了大量CPU资源。我们希望分析出代码中哪些逻辑可能会执行这个操作所以需要从方法栈上找出我们自己写的类并重点关注。
由于主线程也参与了ForkJoinPool的任务处理因此我们可以通过主线程的栈看到需要重点关注org.geekbang.time.commonmistakes.troubleshootingtools.highcpu.HighCPUApplication类的doTask方法。
接下来使用jad命令直接对HighCPUApplication类反编译
```
jad org.geekbang.time.commonmistakes.troubleshootingtools.highcpu.HighCPUApplication
```
可以看到调用路径是main-&gt;task()-&gt;doTask()当doTask方法接收到的int参数等于某个常量的时候会进行1万次的MD5操作这就是耗费CPU的来源。那么这个魔法值到底是多少呢
<img src="https://static001.geekbang.org/resource/image/45/e5/4594c58363316d8ff69178d7a341d5e5.png" alt="">
你可能想到了通过jad命令继续查看User类即可。这里因为是Demo所以我没有给出很复杂的逻辑。在业务逻辑很复杂的代码中判断逻辑不可能这么直白我们可能还需要分析出doTask的“慢”会慢在什么入参上。
这时我们可以使用watch命令来观察方法入参。如下命令表示需要监控耗时超过100毫秒的doTask方法的入参并且输出入参展开2层入参参数
```
watch org.geekbang.time.commonmistakes.troubleshootingtools.highcpu.HighCPUApplication doTask '{params}' '#cost&gt;100' -x 2
```
可以看到所有耗时较久的doTask方法的入参都是0意味着User.ADMN_ID常量应该是0。
<img src="https://static001.geekbang.org/resource/image/04/3a/04e7a4e54c09052ab937f184ab31e03a.png" alt="">
最后我们使用ognl命令来运行一个表达式直接查询User类的ADMIN_ID静态字段来验证是不是这样得到的结果果然是0
```
[arthas@31126]$ ognl '@org.geekbang.time.commonmistakes.troubleshootingtools.highcpu.User@ADMIN_ID'
@Integer[0]
```
需要额外说明的是由于monitor、trace、watch等命令是通过字节码增强技术来实现的会在指定类的方法中插入一些切面来实现数据统计和观测因此诊断结束要执行shutdown来还原类或方法字节码然后退出Arthas。
在这个案例中我们通过Arthas工具排查了高CPU的问题
- 首先通过dashboard + thread命令基本可以在几秒钟内一键定位问题找出消耗CPU最多的线程和方法栈
- 然后直接jad反编译相关代码来确认根因
- 此外如果调用入参不明确的话可以使用watch观察方法入参并根据方法执行时间来过滤慢请求的入参。
可见使用Arthas来定位生产问题根本用不着原始代码也用不着通过增加日志来帮助我们分析入参一个工具即可完成定位问题、分析问题的全套流程。
对于应用故障分析除了阿里Arthas之外还可以关注去哪儿的[Bistoury工具](https://github.com/qunarcorp/bistoury)其提供了可视化界面并且可以针对多台机器进行管理甚至提供了在线断点调试等功能模拟IDE的调试体验。
## 重点回顾
最后,我再和你分享一个案例吧。
有一次开发同学遇到一个OOM问题通过查监控、查日志、查调用链路排查了数小时也无法定位问题但我拿到堆转储文件后直接打开支配树图一眼就看到了可疑点。Mybatis每次查询都查询出了几百万条数据通过查看线程栈马上可以定位到出现Bug的方法名然后来到代码果然发现因为参数条件为null导致了全表查询整个定位过程不足5分钟。
从这个案例我们看到使用正确的工具、正确的方法来分析问题几乎可以在几分钟内定位到问题根因。今天我和你介绍的MAT正是分析Java堆内存问题的利器而Arthas是快速定位分析Java程序生产Bug的利器。利用好这两个工具就可以帮助我们在分钟级定位生产故障。
## 思考与讨论
1. 在介绍[线程池](https://time.geekbang.org/column/article/210337)的时候我们模拟了两种可能的OOM情况一种是使用Executors.newFixedThreadPool一种是使用Executors.newCachedThreadPool你能回忆起OOM的原因吗假设并不知道OOM的原因拿到了这两种OOM后的堆转储你能否尝试使用MAT分析堆转储来定位问题呢
1. Arthas还有一个强大的热修复功能。比如遇到高CPU问题时我们定位出是管理员用户会执行很多次MD5消耗大量CPU资源。这时我们可以直接在服务器上进行热修复步骤是jad命令反编译代码-&gt;使用文本编辑器比如Vim直接修改代码-&gt;使用sc命令查找代码所在类的ClassLoader-&gt;使用redefine命令热更新代码。你可以尝试使用这个流程直接修复程序注释doTask方法中的相关代码
在平时工作中你还会使用什么工具来分析排查Java应用程序的问题呢我是朱晔欢迎在评论区与我留言分享你的想法也欢迎你把今天的内容分享给你的朋友或同事一起交流。

View File

@@ -0,0 +1,146 @@
<audio id="audio" title="36 | 加餐6这15年来我是如何在工作中学习技术和英语的" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/70/0c/7045230e84b0712f43b28fbbe2c3b30c.mp3"></audio>
你好,我是朱晔。今天,我来和你聊聊如何在工作中,让自己成长得更快。
工作这些年来,经常会有同学来找我沟通学习和成长,他们的问题可以归结为两个。
一是长期参与CRUD业务开发项目技术提升出现瓶颈学不到新知识完全没有办法实践各种新技术以后会不会被淘汰、找不到工作
二是,英语学得比较晚,大学的时候也只是为了应试,英语水平很低,看不了英文的技术资料,更别说去外企找工作了。
不知道你是不是也面临这两个问题呢?今天,我就通过自己的经历和你分享一下,如何利用有限的环境、有限的时间来学习技术和英语?
## 学好技术
在我看来,知识网络的搭建就是在造楼房:基础也就是地基的承载力,决定了你能把楼造多高;广度就像是把房子造大、造宽;深度就是楼房的高度。因此,如果你想要提升自己的水平,那这三个方面的发展缺一不可。
### 第一,学习必须靠自觉。
虽说工作经历和项目经验是实践技术、提升技术的一个重要手段,但不可能所有的工作经历和项目都能持续地提升我们的技术。所以,我们要想提升自己的技术水平,就必须打消仅仅通过工作经历来提升的念头,要靠业余时间主动地持续学习和积累来提升。
比如你可以针对项目中用到的技术全面阅读官方文档做各种Demo来论证其技术特性。在这个过程中你一定还会产生许多技术疑问那就继续展开学习。
### 第二,不要吝啬分享。
刚毕业那会我花了很多时间和精力在CSDN回答问题积极写博客、写书和翻译书。这些经历对我的技术成长帮助非常大。
很多知识点我们自认为完全掌握了但其实并不是那么回事儿。当我们要说出来教别人的时候就必须100%了解每一个细节。因此,分享不仅是帮助自己进一步理清每一个知识点、锻炼自己的表达能力,更是一种强迫自己学习的手段,因为你要保证按时交付。
当然了分享的过程也需要些正向激励让自己保持分享的激情。就比如说我获得的几次微软MVP、CSDN TOP3专家等荣誉就对我激励很大可以让我保持热情去不断地学习并帮助别人。
### 第三,不要停留在舒适区。
分享一段我的真实经历吧。我加入一家公司组建新团队后,在做技术选型的时候,考虑到成本等因素,放弃了从事了七年的.NET技术转型Java。有了.NET的积累我自己转型Java只用了两周。其实一开始做这个决定非常痛苦但是突破自己的舒适区并没有想象得那么困难。随后我又自学了iOS、深度学习、Python等技术或语言。
随着掌握的技术越来越多这些技术不但让我触类旁通更让我理解了技术只是工具解决问题需要使用合适的技术。因此我也建议你利用业余时间多学习几门不同类型的编程语言比如Java、Python和Go。
有些时候,我们因为恐惧跳出舒适区而不愿意学习和引入合适的新技术来解决问题,虽然省去了前期的学习转型成本,但是后期却会投入更多的时间来弥补技术上的短板。
### 第四,打好基础很重要。
这里的“基础”是指和编程语言无关的那部分知识包括硬件基础、操作系统原理、TCP/IP、HTTP、数据结构和算法、安全基础、设计模式、数据库原理等。学习基础知识是比较枯燥的过程需要大块的时间来系统阅读相关书籍并要尝试把学到的知识付诸实践。只有实践过的技术才能映入脑子里否则只是书本上的知识。
比如学习TCP/IP的时候我们可以使用Wireshark来观察网络数据。又比如学习设计模式的时候我们可以结合身边的业务案例来思考下是否有对应的适用场景如果没有能否模拟一个场景然后使用所有设计模式和自己熟悉的语言开发一个实际的Demo。
这些看似和我们日常业务开发关系不大的基础知识,是我们是否能够深入理解技术的最重要的基石。
### 第五,想办法积累技术深度。
对开发者而言技术深度体现在从一个框架、组件或SDK的使用者转变为开发者。
虽然不建议大家重复去造轮子、造框架但我们完全可以阅读各种框架的源码去了解其实现并亲手实现一些框架的原型。比如你可以尝试把MVC、RPC、ORM、IoC、AOP等框架都实现一个最基本功能点原型。在实现的过程中你一定会遇到很多问题和难点然后再继续研究一下Spring、Hibernate、Dubbo等框架是如何实现的。
当把自己的小框架实现出来的那一刻,你收获的不仅是满满的成就感,更是在技术深度积累上的更进一步。在这个过程中,你肯定会遇到不少问题、解决不少问题。有了这些积累,之后再去学习甚至二次开发那些流行的开源框架,就会更容易了。
除了实现一些框架外我还建议你选择一个中间件比如Redis、RocketMQ来练手学习网络知识。
我们可以先实现它的客户端用Netty实现TCP通信层的功能之后参照官方文档实现协议封装、客户端连接池等功能。在实现的过程中你可以对自己实现的客户端进行压测分析和官方实现的性能差距。这样一来你不仅可以对TCP/IP网络有更深入的了解还可以获得很多网络方面的优化经验。
然后,再尝试实现服务端,进一步加深对网络的认识。最后,尝试把服务端扩展为支持高可用的集群,来加深对分布式通信技术的理解。
在实现这样一个分布式C/S中间件的过程中你对技术的理解肯定会深入了许多。在这个过程中你会发现技术深度的“下探”和基础知识的积累息息相关。基础知识不扎实往深了走往往会步履维艰。这时你可以再回过头来重新系统学习一些基础理论。
### 第六,扩大技术广度也重要。
除了之前提到的多学几门编程语言之外,在技术广度的拓展上,我们还可以在两个方面下功夫。
第一阅读大量的技术书籍。新出来的各种技术图书不只是编程相关的一般我都会买。十几年来我买了500多本技术图书大概有三分之一是完整看过的还有三分之一只翻了一个大概还有三分之一只看了目录。
广泛的阅读,让我能够了解目前各种技术的主流框架和平台。这样的好处是,在整体看技术方案的时候,我可以知道大家都在做什么,不至于只能理解方案中的一部分。对于看不完的、又比较有价值的书,我会做好标签,等空闲的时候再看。
第二在开发程序的时候我们会直接使用运维搭建的数据库比如Elasticsearch、MySQL、中间件比如RabbitMQ、ZooKeeper、容器云比如Kubernetes。但如果我们只会使用这些组件而不会搭建的话对它们的理解很可能只是停留在API或客户端层面。
因此,我建议你去尝试下从头搭建和配置这些组件,在遇到性能问题的时候自己着手分析一下。把实现技术的前后打通,遇到问题时我们就不至于手足无措了。我通常会购买公有云按小时收费的服务器,来构建一些服务器集群,尝试搭建和测试这些系统,加深对运维的理解。
## 学好英语
为啥要单独说英语的学习方法呢,这是因为学好英语对做技术的同学非常重要:
- 国外的社区环境比较好许多技术问题只有通过英文关键字才能在Google或Stackoverflow上搜到答案
- 可以第一时间学习各种新技术、阅读第一手资料,中文翻译资料往往至少有半年左右的延迟;
- 参与或研究各种开源项目,和老外沟通需要使用英语来提问,以及阅读别人的答复。
所以说,学好英语可以整体拓宽个人视野。不过,对于上班族来说,我们可能没有太多的大块时间投入英语学习,那如何利用碎片时间、相对休闲地学习英语呢?还有一个问题是,学好英语需要大量的练习和训练,但不在外企工作就连个英语环境都没有,那如何解决这样的矛盾呢?
接下来,我将从读、听、写和说四个方面,和你分享一下我学习英语的方法。
### 读方面
读对于我们这些搞技术的人来说是最重要的,并且也是最容易掌握的。我建议你这么学:
- 先从阅读身边的技术文档开始,有英语文档的一定要选择阅读英语文档。一来,贴近实际工作,是我们真正用得到的知识,比较容易有兴趣去读;二来,这些文档中大部分词汇,我们日常基本都接触过,难度不会太大。
- 技术书籍的常用词汇量不大,有了一些基础后,你可以正式或非正式地参与翻译一些英语书籍或文档。从我的经验来看,翻译过一本书之后,你在日常阅读任何技术资料时基本都不需要查字典了。
- 订阅一些英语报纸比如ChinaDaily。第一贴近日常生活都是我们身边发生的事儿不会很枯燥第二可以进一步积累词汇量。在这个过程中你肯定需要大量查字典打断阅读让你感觉很痛苦。但一般来说一个单词最多查三次也就记住了所以随着时间推移你慢慢可以摆脱字典词汇量也可以上一个台阶了。
技术方面阅读能力的培养,通常只需要三个月左右的时间,但生活方面资料的阅读可能需要一年甚至更长的时间。
### 听方面
读需要积累词汇量,听力的训练需要通过时间来磨耳朵。每个人都可以选择适合自己的材料来磨耳朵,比如我是通过看美剧来训练听力的。
我就以看美剧为例,说说练听力的几个关键点。
- 量变到质变的过程需要1000小时的量。如果一部美剧是100小时那么看前9部的时候可能都很痛苦直到某一天你突然觉得一下子都可以听懂了。
- 需要确保看美剧时没有中文字幕,否则很难忍住不看,看了字幕就无法起到训练听力的效果。
- 在美剧的选择上可以先选择对话比较少也可以选择自己感兴趣的题材这样不容易放弃。如果第一次听下来听懂率低于30%,连理解剧情都困难,那么可以先带着中文字幕看一遍,然后再脱离字幕看。
- 看美剧不在乎看的多少而是要找适合的素材反复训练。有人说反复看100遍《老友记》英语的听说能力可以接近母语是英语的人的水平。
如果看美剧不适合你的话你可以选择其他方式比如开车或坐地铁的时候听一些感兴趣的PodCast等。
总而言之选择自己喜欢的材料和内容从简单开始不断听。如果你有一定词汇量的话查字典其实不是必须的很多时候不借助字典同一个单词出现10遍后我们也就知道它的意思了。
一定要记住在积累1000小时之前别轻易放弃。
### 写方面
如果有外企经历,那么平时写英语邮件和文档基本就可以让你的工作英语过关;如果没有外企经历也没关系,你可以尝试通过下面的方式锻炼写作:
- 每天写英语日记。日记是自己看的,没人会嘲笑你,可以从简单的开始。
- 在保持写作的同时,需要确保自己能有持续的一定量的阅读。因为,写作要实现从正确到准确到优雅,离不开阅读的积累。
- 写程序的时候使用英语注释,或者尝试写英语博客,总之利用好一切写的机会,来提升自己的英语表达。
再和你分享一个小技巧。当你要通过查词典知道中文的英语翻译时,尽量不要直接用找到的英文单词,最好先在英语例句中确认这个翻译的准确性再使用,以免闹笑话。
### 说方面
训练说英语的机会是最少的,毕竟身边说英语的人少,很难自己主动练习。
这里我和你分享两个方法吧。
第一是买外教的1-1对话课程来训练。这些课程一般按小时计费由母语是英语的人在线和你聊一些话题帮助你训练对话能力。
买不买课程不重要,只要能有母语是英语的人来帮你提升就可以。同时,大量的听力训练也可以帮助你提升说的能力,很多英语短句经过反复强化会成为脱口而出的下意识反应。所以,你会发现在听力达到质变的时候,说的能力也会上一个台阶。
第二,大胆说,不要担心有语法错误、单词发音问题、表达不流畅问题而被嘲笑。其实,你可以反过来想想,老外说中文时出现这些问题,你会嘲笑他吗。
这里有一个技巧是,尽量选用简单的表达和词汇去说,先尝试把内容说出来,甚至是只说几个关键字,而不是憋着在脑子里尝试整理一个完整的句子。灵活运用有限的单词,尽可能地流畅、准确表达,才是聪明的做法。
## 总结
最后我想说如果你感觉学得很累、进步很慢也不要放弃坚持下来就会越来越好。我刚毕业那会儿有一阵子也对OOP很迷茫感觉根本无法理解OOP的理念写出的代码完全是过程化的代码。但我没有放弃参与写了几年的复杂业务程序再加上系统自学设计模式到某一个时刻我突然就能写出OOP的业务代码了。
学习一定是一个日积月累、量变到质变的过程,希望我分享的学习方法能对你有启发。不过,每个人的情况都不同,一定要找到适合自己的学习方式,才更容易坚持下去。
持续学习很重要,不一定要短时间突击学习,而最好是慢慢学、持续积累,积累越多学习就会越轻松。如果学习遇到瓶颈感觉怎么都学不会,也不要沮丧,这其实还是因为积累不够。你一定也有过这样的经验:一本去年觉得很难啃的书,到今年再看会觉得恰到好处,明年就会觉得比较简单,就是这个道理。
我是朱晔,欢迎在评论区与我留言分享你学习技术和英语的心得,也欢迎你把今天的内容分享给你的朋友或同事,一起交流。

View File

@@ -0,0 +1,236 @@
你好,我是朱晔。
今天直播我准备和你聊聊程序员成长的话题。我从毕业后入行到现在作为一个高级管理者已经在互联网领域拼搏了15年了。我把这些年自己的成长历程、心得体会整理成了“程序员成长28计”。
今天我就和你分别聊聊这28计。
## 入门 0.5年
**第1计**:不要过于纠结方向选择问题。
开始入门的时候我们可能都会纠结于选择前端还是后端选择了后端还犹豫到底选Java、Go还是Python。
其实,我觉得不用过于纠结。如果说你对偏前端的内容感兴趣,那就从前端入手;对数据库方面的内容感兴趣,那就从后端入手。等你真正入门以后,你再去转方向、转技术栈都会非常容易,因为技术都是相通的。
**第2计**学习一定要敢于踏出真正的第一步。
这里我说的第一步不是说开始看某个领域的书了而是真正把IDE下载好、把编程环境搭建好并实现一个最简单的程序。我一直觉得把编程环境搭建好就已经算是入门一半了。
如果你只是停留在看书这个层次上的话,是永远入不了门的。因为这些知识只是停留在书上,还没有真正变成你自己的。只有自己写过、实践过,才能真正掌握。
**第3计**找人给你指一下方向。
刚入门的时候面对各种各样的语言、技术你很可能会迷茫。就比如说刚入门后端的时候Spring全家桶有十几样还有各种数据库方面的Java程序本身的语法和框架那到底先学什么呢这个时候只要找人给你指点一下学习的顺序以及按照怎样的主线来学习就会事半功倍。否则你会在大量的资料里花费大量的时间、消耗大量的精力。
**第4计**:找准合适的入门资料。
在我看来,选择入门资料,需要注意两点:
1. 一定要选择手把手的资料,也就是从搭环境开始怎么一步步地去操作,并带一些实战项目。这样看,视频课程可能更合适。
1. 要难度合适。
那怎么理解“难度合适”呢举个例子你看的这本书的知识深度在70分而你自己的知识深度在60分那这本书非常合适。因为从60到65的感觉是非常爽的。在60分的时候你有能力去汲取70分深度的书里面的知识点然后你会变成65分。而如果你现在的知识深度在20分去看70分的书或者你的知识深度在75分却去看70分的书就不会有任何感觉、任何收益。所以很多同学看我的专栏课程会有共鸣也是这个原因。
## 程序员2年
**第5计**:想办法系统性地学习。
步入两年这个阶段后,我们要开始想办法系统性地学习了,比如系统性地学习设计模式、算法、数据库。只有系统性地学习,才能给我们建立起完整的知识框架。因为一定是先有知识网络,才能在网络上继续铺更多的东西。
那怎么才能有系统性学习的动力呢?
第一,分享可以让自己有动力。比如,你说要写一个什么系列的文章,那话说出去了,就会逼着自己去实现。
第二,花钱买课程,做系统性的学习。当你花了几百甚至几千块钱去买课程的时候,就会逼着自己的学习,不然钱就浪费掉了。
**第6计**选择一份好工作。
选择一份好工作,也就是选择一个好的项目,从而积累一些人脉资源,是非常重要的,可能要比技术成长更重要些。
比如说,你能够进入到一个相对较大的公司,它能带给你的最最主要的就是人脉资源,也就是你能够认识更多、更优秀的人。认识这些人,就是你日后的机会。
**第7计**学习必须靠自觉。
我们不能期望项目经验一定或者说一直会给自己带来技术提升。即使是你能接触一些高并发的、比较复杂的项目,它们带来的提升也是有限的,或者说持续的时间通常会比较短。
因为大多数公司在乎的都是你的输出,输出你的能力和经验。所以说,学习和成长这件事儿,必须靠自觉,包括自觉地去想如何系统性地学习、如何有计划地学习,以及平时要多问为什么。
**第8计**看适合自己的书。
这里也是说,我们在看书的过程中,要注意去鉴别书的层次,选择难度合适的书。
其实,在做程序员前两年的时间里,我不太建议去广泛地看书,要先想办法能够专注些,打好自己主要的编程语言的基础;然后,围绕着自己主要的编程语言或者主要使用的技术去看书。
**第9计**想办法积累技术广度。
将来踏上技术管理路线之后,你有可能管的团队不是你这个领域,比如你是后端出身可能要带领移动团队。如果你不知道移动端最基本的东西的话,是没有办法跟团队成员沟通的。所以说,你可以有自己的一个专长,但是你要知道其他领域最基本的东西。
积累技术广度的方式,主要有下面三种。
第一,体验全栈。如果你是做后端的,就应该去大概了解下客户端、移动端,或者说大前端;可以了解下测试和运维怎么做,了解运维的话帮助可能会更大。你还可以动手做一个自己的项目,就从云服务器的采购开始。在搭建项目部署的过程中,你可以自己去搭建运维相关的部分,甚至是自己搭建一些中间件。
因为在大厂,一般都有自动化发布系统、有工程化平台、有自己的运维体系、有自己的监控系统等等。但是,如果只是使用这些工具的话,我们是没法建立一个全局观的,因为我们不知道它们是怎么运作的。
第二,多学一些编程语言。但是学了几门编程语言后,你会发现每门语言都有自己的特色和软肋。这就会引发你很多的思考,比如为什么这个语言没有这个特性,又怎么样去解决。另外,每门语言他都有自己的技术栈,你会来回地比较。这些思考和比较,对自己的成长都很有用。
如果你对一个语言的掌握比较透彻的话,再去学其他语言不会花很久。我刚毕业是做.net后来转了Java再后来又去学Python。因为高级语言的特性基本上都差不多你只要学一些语法用到的时候再去查更多的内容然后做个项目所以学一门语言可能也就需要一个月甚至会更快一些。
第三,广泛看书。
**第10计**想办法积累技术深度。
主要的方式是造轮子、看源码和学底层。
第一,造轮子。所谓的造轮子,不一定是要造完要用,你可以拿造轮子来练手,比如徒手写一个框架。在这个过程中,你会遇到很多困难,然后可能会想办法去学习一些现有技术的源码,这对技术深度的理解是非常有帮助的。
第二,看一些源码。如果你能够理清楚一些源码的主线,然后你能积累很多设计模式的知识。
第三,学一些偏向于底层的东西,可以帮助你理解技术的本质。上层的技术都依赖于底层的技术,所以你学完了底层的技术后,就会发现上层的技术再变也没有什么本质上的区别,然后学起来就会非常快。
**第11计**学会使用搜索引擎。
对于程序员来说最好可以使用Google来搜索也就是说要使用英文的关键字来搜索。一方面通过Google你可以搜到更多的内容另一方面国外的技术圈或者网站关于纯技术的讨论会多一些。
**第12计**学会和适应画图、写文档。
我觉得写文档是在锻炼自己的总结能力和表达能力画图更多的是在锻炼自己的抽象能力。写文档、画架构图不仅仅是架构师需要具备的能力还是你准确表达自己观点的必备方式。所以我们不要觉得宁肯写100行代码也不愿意写一句话。
## 架构师3年
**第13计**注意软素质的提升。
这时候你已经有了好几年的经验了,那除了技术方面,还要注意软素质,比如沟通、自我驱动、总结等能力的提升。比如说沟通能力,就是你能不能很流畅地表达自己的观点,能不能比较主动地去沟通。
这些素质在日常工作中还是挺重要的,因为你做了架构师之后,免不了要去跟业务方和技术团队,甚至是其他的团队的架构师去沟通。 如果你的这些软素质不过硬,那可能你的方案就得不到认可,没办法达成自己的目标。
**第14计**积累领域经验也很重要。
当你在一个领域工作几年之后,你就会对这个领域的产品非常熟悉,甚至比产品经理更懂产品。也就是说,即使这个产品没有别人的帮助,你也可以确保它朝着正确的方向发展。如果你想一直在这个领域工作的话,这种领域经验的积累就对自己的发展非常有帮助。
所以说,有些人做的是业务架构师,他可能在技术上并不是特别擅长,但对这个领域的系统设计或者说产品设计特别在行。如果说,你不想纯做技术的话,可以考虑积累更多的领域经验。
**第15计**架构工作要接地气。
我以前做架构师的时候发现,有些架构师给出的方案非常漂亮,但就是不接地气、很难去落地。所以,在我看来,架构工作必须要接地气,包括三个方面:产出符合实际情况的方案、方案要落地实际项目、不要太技术化。
这里其实会有一个矛盾点:如果你想要提升自己的经验、技术,很多时候就需要去引入一些新技术,但是这些新技术的引入需要成本。而这里的成本不仅仅是你自己学习的成本,还需要整个团队有一定的经验。
比如Kubernetes不是你引入了团队用就完事儿整个团队的技术都需要得到提升才能够驾驭这个系统。如果我们是为了自己的利益去引入一些不太符合公司实际情况的技术的话其实对公司来说是不负责任的而且这个方案很大程度上有可能会失败。
所以说,我觉得做架构工作是要产出一些更接地气的方案。比如同样是解决一个问题,有些架构方式或设计比较“老土”,但往往是很稳定的;而一些复杂的技术,虽然有先进的理念和设计,但你要驾驭它就需要很多成本,而且因为它的“新”往往还会存在各种各样的问题。
这也就是说,我们在设计架构的时候,必须要权衡方案是否接地气。
**第16计**打造个人品牌。
我觉得,个人品牌包括口碑和影响力两个方面。
口碑就是你日常工作的态度,包括你的能力和沟通,会让人知道你靠不靠谱、能力是不是够强。好的口碑再加上宝贵的人脉,就是你非常重要的资源。口碑好的人基本上是不需要主动去找工作的,因为一直会有一些老领导或者朋友、同事会千方百计地想要给你机会。
很多人的技术非常不错,但就是没人知道他,问题就出在影响力上。而提升影响力的方法,无外乎就是参加技术大会、做分享、写博客、写书等等。
有了影响力和口碑,让更多的人能接触到你、认识你,你就会有更多的机会。
## 技术管理
**第17计**掌握管事的方法。
“管事”就是你怎样去安排,这里包括了制定项目管理流程、制定技术标准、工具化和自动化三个方面。
刚转做技术管理时容易犯的一个错的是,把事情都抓在自己手里。这时,你一定要想通,不是你自己在干活,你的产出是靠团队的。与其说什么事情都自己干,还不如说你去制定规范、流程和方向,然后让团队去做,否则你很容易就成了整个团队的瓶颈。
**第18计**掌握带团队的方法。
第一,招人&amp;放权。带团队的话,最重要是招到优秀的人,然后就是放权。不要因为担心招到的人会比自己优秀,就想要找“弱”一些的。只有团队的事情做得更好了,你的整个团队的产出才是最高。
第二,工程师文化。通过建立工程师文化,让大家去互相交流、学习,从而建立一个良好的学习工作氛围。
第三,适当的沟通汇报制度。这也属于制定流程里面的,也是要建立一个沟通汇报的制度。
**第19计**关注前沿技术,思考技术创新。
做了技术管理之后,你的视角要更高。你团队的成员,可能只是看到、接触到这一个部分、这一个模块,没有更多的信息,也没办法想得更远。这时,你就必须去创新、去关注更多的前沿技术,去思考自己的项目能不能用上这些技术。
**第20计**关注产品。
在我看来,一个产品的形态很多时候决定了公司的命运,在产品上多想一些点子,往往要比技术上的重构带来的收益更大。这里不仅仅包括这个产品是怎么运作的,还包括产品中包含的创新、你能否挖掘一些衍生品。
## 高级技术管理
在这个层次上面我们更高级的技术管理可能是总监级别甚至以上我以前在两家百人以上的小公司做过CTO。我当时的感觉是所做的事情不能仅限于产品技术本身了。
**第21计**搭建团队最重要。
这和招人还不太一样,招人肯定招的是下属,而搭建团队是必须让团队有一个梯队。一旦你把一些核心的人固化下来以后,整个团队就发展起来了。所以,你要在招人方面花费更多的精力,当然不仅仅是指面试。
搭建团队最重要的是你自己要有一个想法,知道自己需要一个什么样的职位来填补空缺,这个岗位上又需要什么样的人。
**第22计**打造技术文化。
虽然在做技术管理的时候,我强调说要建立制度,但文化会更高于制度,而且文化没有那么强势。因为制度其实是列出来,要求大家去遵守,有“强迫”的感觉;而文化更强调潜移默化,通过耳濡目染获得大同感。这样一来,大家慢慢地就不会觉得这是文化了,而是说我现在就是这么干事儿的。
**第23计**提炼价值观。
价值观是说公司按照这个理念去运作,希望有一些志同道合的人在一起干活。所以价值观又会高于文化,是整个公司层面的,对大家的影响也会更多。
虽然说价值观不会那么显性,但可以长久地确保公司里面的整个团队的心都是齐的,大家都知道公司是怎么运作的,有相同的目标。
**第24计**关注运营和财务。
到了高级技术管理的位置,你就不仅仅是一个打工的了,你的命运是和公司紧紧绑定在一起的。所以,你需要更多地关注公司的运营和财务。
当你觉得自己的团队很小却要做那么多项目的时候,可以站在更高的角度去换位思考下。这时你可能就发现,你的团队做的事情并没有那么重要,对整个公司的发展来说你的团队规模已经足够了。如果说我们再大量招人的话,那么财务上就会入不敷出,整个公司的情况肯定也不会好。
## 职场心得
**第25计**掌握工作汇报的方式方法。
首先,我们不要把汇报当作负担、当作浪费时间。汇报其实是双向的,你跟上级多沟通的话,他可以反馈给你更多的信息,这个信息可能是你工作的方向,也可能是给你的一些资源,还可能是告诉你上级想要什么。因为你和你的上级其实在一个信息层面上是不对等的,他能收到更上级的信息,比如公司策略方面的信息。
**第26计**坚持+信念。
第一,如果说你的目标就是成功的话,那没有什么可以阻挡你。职场上的扯皮和甩锅,都是避免不了的。举个例子吧。
我以前在一家公司工作的时候,别人不愿意配合我的工作。那怎么办呢,我知道自己的目标是把这件事儿做成。当时,这个项目的很多内容,比如说运维,都不在我这边,需要其他同事来负责。但人家就是不配合,群里艾特也不看,打电话也不接,那我怎么办呢?多打两次呗,实在不行我就发邮件抄送大家的上级。总之,就是想尽办法去沟通,因为你的目标就是成功。
第二,很多时候,创新就是相信一定可以实现才有的。
很多时候,你觉得这个事情是做不成的,然后直接拒绝掉了,创新就没有了。但如果相信这个事情一定是可以做成的,你就会想方设法去实现它,这个时候你想出来的东西就是有开创性的,就是创新。
**第27计**持续的思考和总结。
在职场上提炼方法论是非常重要的。你要去思考自己在工作中对各种各样的事情的处理,是不是妥当,是不是能够总结出一些方法论。把这些方法论提炼保留下来,将来是能够帮到你的。很多东西,比如复盘自己的工作经历、复盘自己的选择,都要动脑子、都要去写,不能说过去了就过去了。这些经历提炼出的方法论,都是你的经验,是非常有价值的。
**第28计**有关和平级同事的相处。
和平级同事之间,要以帮助别人的心态来合作。我们和上下级的同事来沟通,一般是不会有什么问题的,但跟平级的,尤其是跨部门的平级同事去沟通的时候,往往会因为利益问题,不会很愉快。
我觉得,这里最重要的就是以帮助别人的心态来合作。 比如这样说“你有什么困难的话,可以来问我”“你人手是不是不够,我可以帮你一起把这个项目做好”。这样大家的合作会比较顺畅,别人也不会有那么多戒心。
人和人的沟通,还在于有一层纱,突破了这层纱以后,大家就都会相信你,觉得你是一个靠谱的人。这样,平级同事也会愿意和你分享一些东西,因为他放心。
## 管理格言
接下来我要推荐的8条管理格言是曹操管理和用人的理念不是我自己总结出来的。
第一,真心诚意,以情感人。人和人之间去沟通的时候,不管是和上级或者下级的沟通,都要以非常诚恳的态度去沟通。
第二,推心置腹,以诚待人。有事情不要藏在心里,做“城府很深”的管理者。我觉得更好的方式是,让大家尽可能地知道更多的事儿,统一战线,站在一个角度来考虑问题。
第三,开诚布公,以理服人。把管理策略公布出来,不管是奖励也好惩罚也罢,让团队成员感觉公平公正,
第四,言行一致,以信取人。说到做到,对于管理下属、和别人沟通都非常重要。
第五,令行禁止,依法治人。管理上,你要制定好相关的制度,而且要公开出来。如果触犯了制度就需要惩罚,做得好了就要有奖赏。
第六,设身处地,以宽容人。很多时候,我们和别人的矛盾是没有足够的换位思考,没有设身处地地去想。如果说你的下属犯了错,还是要想一想是不是多给些机会,是不是能宽容一些。
第七,扬人责己,以功归人。这是非常重要的一点。事情是团队一起做的话,那就是团队的功劳,甚至下属的功劳。如果别人做得好的话,就要多表扬一些。对自己要严格一些,很多时候团队的问题就是管理者的问题,跟下属没太多关系。
第八,论功行赏,以奖励人。做得好了,要多给别人一些奖励。这也是公平公正的,大家都能看得到。
最后我将关于程序员成长的28计整理在了一张思维导图上以方便你收藏、转发。<br>
<img src="https://static001.geekbang.org/resource/image/b7/bf/b72104acfeeeecef49ab6c0a5908cebf.jpg" alt="">
我是朱晔,欢迎在评论区与我留言分享你的想法,也欢迎你把今天的内容分享给你的朋友或同事,一起交流。

View File

@@ -0,0 +1,272 @@
<audio id="audio" title="答疑篇:加餐篇思考题答案合集" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8d/db/8d3847da3b36e5c590f376694eda7bdb.mp3"></audio>
你好,我是朱晔。
今天我们继续一起分析这门课的“不定期加餐”篇中5讲的课后思考题。这些题目涉及了Java 8基础知识、定位和分析应用问题相关的几大知识点。
接下来,我们就一一具体分析吧。
### [加餐1 | 带你吃透课程中Java 8的那些重要知识点](https://time.geekbang.org/column/article/212374)
**问题:**对于并行流部分的并行消费处理1到100的例子如果把forEach替换为forEachOrdered你觉得会发生什么呢
forEachOrdered 会让parallelStream丧失部分的并行能力主要原因是forEach遍历的逻辑无法并行起来需要按照循序遍历无法并行
我们来比较下面的三种写法:
```
//模拟消息数据需要1秒时间
private static void consume(int i) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(i);
}
//模拟过滤数据需要1秒时间
private static boolean filter(int i) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
return i % 2 == 0;
}
@Test
public void test() {
System.setProperty(&quot;java.util.concurrent.ForkJoinPool.common.parallelism&quot;, String.valueOf(10));
StopWatch stopWatch = new StopWatch();
stopWatch.start(&quot;stream&quot;);
stream();
stopWatch.stop();
stopWatch.start(&quot;parallelStream&quot;);
parallelStream();
stopWatch.stop();
stopWatch.start(&quot;parallelStreamForEachOrdered&quot;);
parallelStreamForEachOrdered();
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
}
//filtre和forEach串行
private void stream() {
IntStream.rangeClosed(1, 10)
.filter(ForEachOrderedTest::filter)
.forEach(ForEachOrderedTest::consume);
}
//filter和forEach并行
private void parallelStream() {
IntStream.rangeClosed(1, 10).parallel()
.filter(ForEachOrderedTest::filter)
.forEach(ForEachOrderedTest::consume);
}
//filter并行而forEach串行
private void parallelStreamForEachOrdered() {
IntStream.rangeClosed(1, 10).parallel()
.filter(ForEachOrderedTest::filter)
.forEachOrdered(ForEachOrderedTest::consume);
}
```
得到输出:
```
---------------------------------------------
ns % Task name
---------------------------------------------
15119607359 065% stream
2011398298 009% parallelStream
6033800802 026% parallelStreamForEachOrdered
```
从输出中,我们可以看到:
- stream方法的过滤和遍历全部串行执行总时间是10秒+5秒=15秒
- parallelStream方法的过滤和遍历全部并行执行总时间是1秒+1秒=2秒
- parallelStreamForEachOrdered方法的过滤并行执行遍历串行执行总时间是1秒+5秒=6秒。
### [加餐2 | 带你吃透课程中Java 8的那些重要知识点](https://time.geekbang.org/column/article/212398)
**问题1**使用Stream可以非常方便地对List做各种操作那有没有什么办法可以实现在整个过程中观察数据变化呢比如我们进行filter+map操作如何观察filter后map的原始数据呢
要想观察使用Stream对List的各种操作的过程中的数据变化主要有下面两个办法。
第一,**使用peek方法**。比如如下代码我们对数字1~10进行了两次过滤分别是找出大于5的数字和找出偶数我们通过peek方法把两次过滤操作之前的原始数据保存了下来
```
List&lt;Integer&gt; firstPeek = new ArrayList&lt;&gt;();
List&lt;Integer&gt; secondPeek = new ArrayList&lt;&gt;();
List&lt;Integer&gt; result = IntStream.rangeClosed(1, 10)
.boxed()
.peek(i -&gt; firstPeek.add(i))
.filter(i -&gt; i &gt; 5)
.peek(i -&gt; secondPeek.add(i))
.filter(i -&gt; i % 2 == 0)
.collect(Collectors.toList());
System.out.println(&quot;firstPeek&quot; + firstPeek);
System.out.println(&quot;secondPeek&quot; + secondPeek);
System.out.println(&quot;result&quot; + result);
```
最后得到输出可以看到第一次过滤之前是数字1~10一次过滤后变为6~10最终输出6、8、10三个数字
```
firstPeek[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
secondPeek[6, 7, 8, 9, 10]
result[6, 8, 10]
```
第二,**借助IDEA的Stream的调试功能**。详见[这里](https://www.jetbrains.com/help/idea/analyze-java-stream-operations.html),效果类似下图:
<img src="https://static001.geekbang.org/resource/image/3e/5e/3ee49c0589286bba37dd66032530d65e.png" alt="">
**问题2**Collectors类提供了很多现成的收集器那我们有没有办法实现自定义的收集器呢比如实现一个MostPopularCollector来得到List中出现次数最多的元素满足下面两个测试用例
```
assertThat(Stream.of(1, 1, 2, 2, 2, 3, 4, 5, 5).collect(new MostPopularCollector&lt;&gt;()).get(), is(2));
assertThat(Stream.of('a', 'b', 'c', 'c', 'c', 'd').collect(new MostPopularCollector&lt;&gt;()).get(), is('c'));
```
我来说下我的实现思路和方式通过一个HashMap来保存元素的出现次数最后在收集的时候找出Map中出现次数最多的元素
```
public class MostPopularCollector&lt;T&gt; implements Collector&lt;T, Map&lt;T, Integer&gt;, Optional&lt;T&gt;&gt; {
//使用HashMap保存中间数据
@Override
public Supplier&lt;Map&lt;T, Integer&gt;&gt; supplier() {
return HashMap::new;
}
//每次累积数据则累加Value
@Override
public BiConsumer&lt;Map&lt;T, Integer&gt;, T&gt; accumulator() {
return (acc, elem) -&gt; acc.merge(elem, 1, (old, value) -&gt; old + value);
}
//合并多个Map就是合并其Value
@Override
public BinaryOperator&lt;Map&lt;T, Integer&gt;&gt; combiner() {
return (a, b) -&gt; Stream.concat(a.entrySet().stream(), b.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey, summingInt(Map.Entry::getValue)));
}
//找出Map中Value最大的Key
@Override
public Function&lt;Map&lt;T, Integer&gt;, Optional&lt;T&gt;&gt; finisher() {
return (acc) -&gt; acc.entrySet().stream()
.reduce(BinaryOperator.maxBy(Map.Entry.comparingByValue()))
.map(Map.Entry::getKey);
}
@Override
public Set&lt;Characteristics&gt; characteristics() {
return Collections.emptySet();
}
}
```
### [加餐3 | 定位应用问题,排错套路很重要](https://time.geekbang.org/column/article/221982)
**问题:**如果你现在打开一个App后发现首页展示了一片空白那这到底是客户端兼容性的问题还是服务端的问题呢如果是服务端的问题又如何进一步细化定位呢你有什么分析思路吗
答:首先,我们需要区分客户端还是服务端错误。我们可以先从客户端下手,排查看看是否是服务端问题,也就是通过抓包来看服务端的返回(一般而言客户端发布之前会经过测试,而且无法随时变更,所以服务端出错的可能性会更大一点)。因为一个客户端程序可能对应几百个服务端接口,先从客户端(发出请求的根源)开始排查问题,更容易找到方向。
服务端没有返回正确的输出,那么就需要继续排查服务端接口或是上层的负载均衡了,排查方式为:
- 查看负载均衡比如Nginx的日志
- 查看服务端日志;
- 查看服务端监控。
如果服务端返回了正确的输出那么要么是由于客户端的Bug要么就是外部配置等问题了排查方式为
- 查看客户端报错一般而言客户端都会对接SAAS的异常服务
- 直接本地启动客户端调试。
### [加餐4 | 分析定位Java问题一定要用好这些工具](https://time.geekbang.org/column/article/224816)
**问题1**JDK中还有一个jmap工具我们会使用jmap -dump命令来进行堆转储。那么这条命令和jmap -dump:live有什么区别呢你能否设计一个实验来证明下它们的区别呢
jmap -dump命令是转储堆中的所有对象而jmap -dump:live是转储堆中所有活着的对象。因为jmap -dump:live会触发一次FullGC。
写一个程序测试一下:
```
@SpringBootApplication
@Slf4j
public class JMapApplication implements CommandLineRunner {
//-Xmx512m -Xms512m
public static void main(String[] args) {
SpringApplication.run(JMapApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
while (true) {
//模拟产生字符串每次循环后这个字符串就会失去引用可以GC
String payload = IntStream.rangeClosed(1, 1000000)
.mapToObj(__ -&gt; &quot;a&quot;)
.collect(Collectors.joining(&quot;&quot;)) + UUID.randomUUID().toString();
log.debug(payload);
TimeUnit.MILLISECONDS.sleep(1);
}
}
}
```
然后使用jmap不带和带live分别生成两个转储
```
jmap -dump:format=b,file=nolive.hprof 57323
jmap -dump:live,format=b,file=live.hprof 5732
```
可以看到nolive这个转储的不可到达对象包含了164MB char[](可以认为基本是字符串):
<img src="https://static001.geekbang.org/resource/image/8e/9b/8e4f3eea80edfe6d867cab754967589b.png" alt="">
而live这个转储只有1.3MB的char[]说明程序循环中的这些字符串都被GC了
<img src="https://static001.geekbang.org/resource/image/18/87/18403a0b683c3b726700d5624f968287.png" alt="">
**问题2**你有没有想过客户端是如何和MySQL进行认证的呢你能否对照[MySQL的文档](https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake)使用Wireshark观察分析这一过程呢
答:一般而言,认证(握手)过程分为三步。
首先,服务端给客户端主动发送握手消息:
<img src="https://static001.geekbang.org/resource/image/29/b2/29f5e4a9056b7b6aeb9d9ac2yy5e97b2.png" alt="">
Wireshark已经把消息的字段做了解析你可以对比[官方文档](https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake)的协议格式一起查看。HandshakeV10消息体的第一个字节是消息版本0a见图中红色框标注的部分。前面四个字节是MySQL的消息头其中前三个字节是消息体长度16进制4a=74字节最后一个字节是消息序列号。
然后客户端给服务端回复的HandshakeResponse41消息体包含了登录的用户名和密码
<img src="https://static001.geekbang.org/resource/image/a0/96/a0b37df4f3e92f7e8602409a7ca0f696.png" alt="">
可以看到用户名是string[NUL]类型的说明字符串以00结尾代表字符串结束。关于MySQL协议中的字段类型你可以参考[这里](https://dev.mysql.com/doc/internals/en/string.html)。
最后服务端回复的OK消息代表握手成功
<img src="https://static001.geekbang.org/resource/image/d7/e7/d746f34df74cfedb4d294db1e2b771e7.png" alt="">
这样分析下来我们可以发现使用Wireshark观察客户端和MySQL的认证过程非常方便。而如果不借助Wireshark工具我们只能一个字节一个字节地对照协议文档分析内容。
其实各种CS系统定义的通讯协议本身并不深奥甚至可以说对着协议文档写通讯客户端是体力活。你可以继续按照这里我说的方式结合抓包和文档分析一下MySQL的查询协议。
### [加餐5 | 分析定位Java问题一定要用好这些工具](https://time.geekbang.org/column/article/230534)
**问题:**Arthas还有一个强大的热修复功能。比如遇到高CPU问题时我们定位出是管理员用户会执行很多次MD5消耗大量CPU资源。这时我们可以直接在服务器上进行热修复步骤是jad命令反编译代码-&gt;使用文本编辑器比如Vim直接修改代码-&gt;使用sc命令查找代码所在类的ClassLoader-&gt;使用redefine命令热更新代码。你可以尝试使用这个流程直接修复程序注释doTask方法中的相关代码
Arthas的官方文档有[详细的操作步骤](https://alibaba.github.io/arthas/redefine.html)实现jad-&gt;sc-&gt;redefine的整个流程需要注意的是
- redefine命令和jad/watch/trace/monitor/tt等命令会冲突。执行完redefine之后如果再执行上面提到的命令则会把redefine的字节码重置。 原因是JDK本身redefine和Retransform是不同的机制同时使用两种机制来更新字节码只有最后的修改会生效。
- 使用redefine不允许新增或者删除field/method并且运行中的方法不会立即生效需要等下次运行才能生效。
以上就是咱们这门课里面5篇加餐文章的思考题答案了。至此咱们这个课程的“答疑篇”模块也就结束了。
关于这些题目,以及背后涉及的知识点,如果你还有哪里感觉不清楚的,欢迎在评论区与我留言,也欢迎你把今天的内容分享给你的朋友或同事,一起交流。