java错误处理百科

这篇具有很好参考价值的文章主要介绍了java错误处理百科。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、业务开发缺陷

        ① 工期紧、逻辑复杂,开发人员会更多地考虑主流程逻辑的正确实现,忽略非主流程逻辑,或保障、补偿、一致性逻辑的实现;

        ②  往往缺乏详细的设计、监控和容量规划的闭环,结果就是随着业务发展出现各种各样的事故。

二、学习方法

        ① 对于每一个坑点,实际运行调试一下源码,使用文中提到的工具和方法重现问题,眼见为实。

        ② 对于每一个坑点,再思考下除了文内的解决方案和思路外,是否还有其他修正方式。

        ③ 对于坑点根因中涉及的 JDK 或框架源码分析,你可以找到相关类再系统阅读一下源码。

        ④ 实践课后思考题。这些思考题,有的是对文章内容的补充,有的是额外容易踩的坑。

三、如何尽量避免踩坑

        ① 遇到自己不熟悉的新类,在不了解之前不要随意使用

                例如:CopyOnWriteArrayList 是 ArrayList 的线程安全版本,在不知晓原理之前把它用于大量写操作的场景,那么很可能会遇到性能问题。

        ② 尽量使用更高层次的框架

                而高层次的框架,则会更多地考虑怎么方便开发者开箱即用

        ③ 关注各种框架和组件的安全补丁和版本更新

                我们使用的 Tomcat 服务器、序列化框架等,就是黑客关注的安全突破口

        ④ 尽量少自己造轮子,使用流行的框架

                因此使用 Netty 开发 NIO 网络程序,不但简单而且可以少踩很多坑

        ⑤ 开发的时候遇到错误,除了搜索解决方案外,更重要的是理解原理

        ⑥ 网络上的资料有很多,但不一定可靠,最可靠的还是官方文档

        ⑦ 做好单元测试和性能测试

        ⑧ 做好设计评审和代码审查工作

        ⑨ 借助工具帮我们避坑

        ⑩ 做好完善的监控报警

        如果一开始我们就可以对应用程序的内存使用、文件句柄使用、IO 使用量、网络带宽、TCP 连接、线程数等各种指标进行监控,并且基于合理阈值设置报警,那么可能就能在事故的婴儿阶段及时发现问题、解决问题。                

        在遇到报警的时候,我们不能凭经验想当然地认为这些问题都是已知的,对报警置之不理。我们要牢记,所有报警都需要处理和记录

四、并发工具类库的线程安全问题

1、没有意识到线程重用导致用户信息错乱的 Bug

错误示例:单线程存储查看用户信息会取到错误数据

private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);


@GetMapping("wrong")
public Map wrong(@RequestParam("userId") Integer userId) {
    //设置用户信息之前先查询一次ThreadLocal中的用户信息
    String before  = Thread.currentThread().getName() + ":" + currentUser.get();
    //设置用户信息到ThreadLocal
    currentUser.set(userId);
    //设置用户信息之后再查询一次ThreadLocal中的用户信息
    String after  = Thread.currentThread().getName() + ":" + currentUser.get();
    //汇总输出两次查询结果
    Map result = new HashMap();
    result.put("before", before);
    result.put("after", after);
    return result;
}

         原因:线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。

正确用法:使用类似 ThreadLocal 工具来存放一些数据时,在代码运行完后清空设置的数据

private static final ThreadLocal currentUser = ThreadLocal.withInitial(() -> null);

@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {
    String before  = Thread.currentThread().getName() + ":" + currentUser.get();
    currentUser.set(userId);
    try {
        String after = Thread.currentThread().getName() + ":" + currentUser.get();
        Map result = new HashMap();
        result.put("before", before);
        result.put("after", after);
        return result;
    } finally {
        //在finally代码块中删除ThreadLocal中的数据,确保数据不串
        currentUser.remove();
    }
}

2、使用了线程安全的并发工具,并不代表解决了所有线程安全问题

        场景:有一个含 900 个元素的 Map,现在再补充 100 个元素进去,这个补充操作由 10 个线程并发进行

错误示例:在每一个线程的代码逻辑中先通过 size 方法拿到当前元素数量,计算ConcurrentHashMap 目前还需要补充多少元素,并在日志中输出了这个值,然后通过 putAll 方法把缺少的元素添加进去。

//线程个数
private static int THREAD_COUNT = 10;
//总元素数量
private static int ITEM_COUNT = 1000;

//帮助方法,用来获得一个指定元素数量模拟数据的ConcurrentHashMap
private ConcurrentHashMap<String, Long> getData(int count) {
    return LongStream.rangeClosed(1, count)
            .boxed()
            .collect(Collectors.toConcurrentMap(i -> UUID.randomUUID().toString(), Function.identity(),
                    (o1, o2) -> o1, ConcurrentHashMap::new));
}

@GetMapping("wrong")
public String wrong() throws InterruptedException {
    ConcurrentHashMap<String, Long> concurrentHashMap = getData(ITEM_COUNT - 100);
    //初始900个元素
    log.info("init size:{}", concurrentHashMap.size());

    ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
    //使用线程池并发处理逻辑
    forkJoinPool.execute(() -> IntStream.rangeClosed(1, 10).parallel().forEach(i -> {
        //查询还需要补充多少个元素
        int gap = ITEM_COUNT - concurrentHashMap.size();
        log.info("gap size:{}", gap);
        //补充元素
        concurrentHashMap.putAll(getData(gap));
    }));
    //等待所有任务完成
    forkJoinPool.shutdown();
    forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
    //最后元素个数会是1000吗?
    log.info("finish size:{}", concurrentHashMap.size());
    return "OK";
}

从日志中可以看到:

        初始大小 900 符合预期,还需要填充 100 个元素。

        worker1 线程查询到当前需要填充的元素为 36,竟然还不是 100 的倍数。

        worker13 线程查询到需要填充的元素数是负的,显然已经过度填充了。

        最后 HashMap 的总项目数是 1536,显然不符合填充满 1000 的预期。

原因:

        ① ConcurrentHashMap 只能保证提供的原子性读写操作是线程安全的

        ② 使用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是一致的,是没有其他线程在操作它的,如果需要确保需要手动加锁。

        ③ 诸如 size、isEmpty 和 containsValue 等聚合方法,在并发情况下可能会反映 ConcurrentHashMap 的中间状态。因此在并发情况下,这些方法的返回值只能用作参考,而不能用于流程控制。显然,利用 size 方法计算差异值,是一个流程控制。

        ④ 诸如 putAll 这样的聚合方法也不能确保原子性,在 putAll 的过程中去获取数据可能会获取到部分数据。

解决方案:整段逻辑加锁即可

@GetMapping("right")
public String right() throws InterruptedException {
    ConcurrentHashMap<String, Long> concurrentHashMap = getData(ITEM_COUNT - 100);
    log.info("init size:{}", concurrentHashMap.size());


    ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
    forkJoinPool.execute(() -> IntStream.rangeClosed(1, 10).parallel().forEach(i -> {
        //下面的这段复合逻辑需要锁一下这个ConcurrentHashMap
        synchronized (concurrentHashMap) {
            int gap = ITEM_COUNT - concurrentHashMap.size();
            log.info("gap size:{}", gap);
            concurrentHashMap.putAll(getData(gap));
        }
    }));
    forkJoinPool.shutdown();
    forkJoinPool.awaitTermination(1, TimeUnit.HOURS);


    log.info("finish size:{}", concurrentHashMap.size());
    return "OK";
}

        重新调用接口,程序的日志输出结果符合预期:

        ConcurrentHashMap 提供了一些原子性的简单复合逻辑方法,用好这些方法就可以发挥其威力。

3、没有充分了解并发工具的特性,从而无法发挥其威力

        场景:使用 Map 来统计 Key 出现次数的场景

        使用 ConcurrentHashMap 来统计,Key 的范围是 10。

        使用最多 10 个并发,循环操作 1000 万次,每次操作累加随机的 Key。

        如果 Key 不存在的话,首次设置值为 1。

//循环次数
private static int LOOP_COUNT = 10000000;
//线程数量
private static int THREAD_COUNT = 10;
//元素数量
private static int ITEM_COUNT = 10;
private Map<String, Long> normaluse() throws InterruptedException {
    ConcurrentHashMap<String, Long> freqs = new ConcurrentHashMap<>(ITEM_COUNT);
    ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
    forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT).parallel().forEach(i -> {
        //获得一个随机的Key
        String key = "item" + ThreadLocalRandom.current().nextInt(ITEM_COUNT);
                synchronized (freqs) {      
                    if (freqs.containsKey(key)) {
                        //Key存在则+1
                        freqs.put(key, freqs.get(key) + 1);
                    } else {
                        //Key不存在则初始化为1
                        freqs.put(key, 1L);
                    }
                }
            }
    ));
    forkJoinPool.shutdown();
    forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
    return freqs;
}

        直接通过锁的方式锁住 Map,然后做判断、读取现在的累计值、加 1、保存累加后值的逻辑。这段代码在功能上没有问题,但无法充分发挥 ConcurrentHashMap 的威力

  优化方案:

private Map<String, Long> gooduse() throws InterruptedException {
    ConcurrentHashMap<String, LongAdder> freqs = new ConcurrentHashMap<>(ITEM_COUNT);
    ForkJoinPool forkJoinPool = new ForkJoinPool(THREAD_COUNT);
    forkJoinPool.execute(() -> IntStream.rangeClosed(1, LOOP_COUNT).parallel().forEach(i -> {
        String key = "item" + ThreadLocalRandom.current().nextInt(ITEM_COUNT);
                //利用computeIfAbsent()方法来实例化LongAdder,然后利用LongAdder来进行线程安全计数
                freqs.computeIfAbsent(key, k -> new LongAdder()).increment();
            }
    ));
    forkJoinPool.shutdown();
    forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
    //因为我们的Value是LongAdder而不是Long,所以需要做一次转换才能返回
    return freqs.entrySet().stream()
            .collect(Collectors.toMap(
                    e -> e.getKey(),
                    e -> e.getValue().longValue())
            );
}

        使用 ConcurrentHashMap 的原子性方法 computeIfAbsent 来做复合逻辑操作,判断 Key 是否存在 Value,如果不存在则把 Lambda 表达式运行后的结果放入 Map 作为 Value,也就是新创建一个 LongAdder 对象,最后返回 Value。

        由于 computeIfAbsent 方法返回的 Value 是 LongAdder,是一个线程安全的累加器,因此可以直接调用其 increment 方法进行累加。

4、没有认清并发工具的使用场景,因而导致性能问题

        在 Java 中,CopyOnWriteArrayList 虽然是一个线程安全的 ArrayList,但因为其实现方式是,每次修改数据时都会复制一份数据出来,所以有明显的适用场景,即读多写少或者说希望无锁读的场景。

 处理方案:

        一定要认真阅读官方文档(比如 Oracle JDK 文档)。充分阅读官方文档,理解工具的适用场景及其 API 的用法,并做一些小实验。了解之后再去使用,就可以避免大部分坑。

        如果你的代码运行在多线程环境下,那么就会有并发问题,并发问题不那么容易重现,可能需要使用压力测试模拟并发场景,来发现其中的 Bug 或性能问题。

五、代码加锁问题

场景:在一个类里有两个 int 类型的字段 a 和 b,有一个 add 方法循环 1 万次对 a 和 b 进行 ++ 操作,有另一个 compare 方法,同样循环 1 万次判断 a 是否小于 b,条件成立就打印 a 和 b 的值,并判断 a>b 是否成立。

@Slf4j
public class Interesting {

    volatile int a = 1;
    volatile int b = 1;

    public void add() {
        log.info("add start");
        for (int i = 0; i < 10000; i++) {
            a++;
            b++;
        }
        log.info("add done");
    }

    public void compare() {
        log.info("compare start");
        for (int i = 0; i < 10000; i++) {
            //a始终等于b吗?
            if (a < b) {
                log.info("a:{},b:{},{}", a, b, a > b);
                //最后的a>b应该始终是false吗?
            }
        }
        log.info("compare done");
    }
}

起了两个线程来分别执行 add 和 compare 方法:

Interesting interesting = new Interesting();
new Thread(() -> interesting.add()).start();
new Thread(() -> interesting.compare()).start();

        按道理,a 和 b 同样进行累加操作,应该始终相等,compare 中的第一次判断应该始终不会成立,不会输出任何日志。但,执行代码后发现不但输出了日志,而且更诡异的是,compare 方法在判断 ab 也成立:

原因:之所以出现这种错乱,是因为两个线程是交错执行 add 和 compare 方法中的业务逻辑,而且这些业务逻辑不是原子性的:a++ 和 b++ 操作中可以穿插在 compare 方法的比较代码中;更需要注意的是,a<b 这种比较操作在字节码层面是加载 a、加载 b 和比较三步,代码虽然是一行但也不是原子性的。

解决方案:为 add 和 compare 都加上方法锁,确保 add 方法执行时,compare 无法读取 a 和 b:

public synchronized void add()
public synchronized void compare()

        使用锁解决问题之前一定要理清楚,我们要保护的是什么逻辑,多线程执行的情况又是怎样的。

1、加锁前要清楚锁和被保护的对象是不是一个层面的

        静态字段属于类,类级别的锁才能保护;而非静态字段属于类实例,实例级别的锁就可以保护。

错误示例:

class Data {
    @Getter
    private static int counter = 0;
    
    public static int reset() {
        counter = 0;
        return counter;
    }

    public synchronized void wrong() {
        counter++;
    }
}

写一段代码测试下:

@GetMapping("wrong")
public int wrong(@RequestParam(value = "count", defaultValue = "1000000") int count) {
    Data.reset();
    //多线程循环一定次数调用Data类不同实例的wrong方法
    IntStream.rangeClosed(1, count).parallel().forEach(i -> new Data().wrong());
    return Data.getCounter();
}

        因为默认运行 100 万次,所以执行后应该输出 100 万,但页面输出的是 639242

原因:在非静态的 wrong 方法上加锁,只能确保多个线程无法执行同一个实例的 wrong 方法,却不能保证不会执行不同实例的 wrong 方法。而静态的 counter 在多个实例中共享,所以必然会出现线程安全问题。

解决方案:同样在类中定义一个 Object 类型的静态字段,在操作 counter 之前对这个字段加锁。

class Data {
    @Getter
    private static int counter = 0;
    private static Object locker = new Object();

    public void right() {
        synchronized (locker) {
            counter++;
        }
    }
}

2、加锁要考虑锁的粒度和场景问题

滥用 synchronized 的问题:

     ①  一是,没必要。通常情况下 60% 的业务代码是三层架构,数据经过无状态的 Controller、Service、Repository 流转到数据库,没必要使用 synchronized 来保护什么数据。

      ② 二是,可能会极大地降低性能。使用 Spring 框架时,默认情况下 Controller、Service、Repository 是单例的,加上 synchronized 会导致整个程序几乎就只能支持单线程,造成极大的性能问题。                 

        即使我们确实有一些共享资源需要保护,也要尽可能降低锁的粒度,仅对必要的代码块甚至是需要保护的资源本身加锁。

场景: 在业务代码中,有一个 ArrayList 因为会被多个线程操作而需要保护,又有一段比较耗时的操作(代码中的 slow 方法)不涉及线程安全问题

错误的做法是,给整段业务逻辑加锁,把 slow 方法和操作 ArrayList 的代码同时纳入 synchronized 代码块

正确的做法,把加锁的粒度降到最低,只在操作 ArrayList 的时候给这个 ArrayList 加锁。

private List<Integer> data = new ArrayList<>();

//不涉及共享资源的慢方法
private void slow() {
    try {
        TimeUnit.MILLISECONDS.sleep(10);
    } catch (InterruptedException e) {
    }
}

//错误的加锁方法
@GetMapping("wrong")
public int wrong() {
    long begin = System.currentTimeMillis();
    IntStream.rangeClosed(1, 1000).parallel().forEach(i -> {
        //加锁粒度太粗了
        synchronized (this) {
            slow();
            data.add(i);
        }
    });
    log.info("took:{}", System.currentTimeMillis() - begin);
    return data.size();
}

//正确的加锁方法
@GetMapping("right")
public int right() {
    long begin = System.currentTimeMillis();
    IntStream.rangeClosed(1, 1000).parallel().forEach(i -> {
        slow();
        //只对List加锁
        synchronized (data) {
            data.add(i);
        }
    });
    log.info("took:{}", System.currentTimeMillis() - begin);
    return data.size();
}

        如果精细化考虑了锁应用范围后,性能还无法满足需求的话,我们就要考虑另一个维度的粒度问题了,即:区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁。

        ① 对于读写比例差异明显的场景,考虑使用 ReentrantReadWriteLock 细化区分读写锁,来提高性能。

        ② 如果你的 JDK 版本高于 1.8、共享资源的冲突概率也没那么大的话,考虑使用 StampedLock 的乐观读的特性,进一步提高性能。

        ③ JDK 里 ReentrantLock 和 ReentrantReadWriteLock 都提供了公平锁的版本,在没有明确需求的情况下不要轻易开启公平锁特性,在任务很轻的情况下开启公平锁可能会让性能下降上百倍。

3、多把锁要小心死锁问题

场景:

        之前我遇到过这样一个案例:下单操作需要锁定订单中多个商品的库存,拿到所有商品的锁之后进行下单扣减库存操作,全部操作完成之后释放所有的锁。代码上线后发现,下单失败概率很高,失败后需要用户重新下单,极大影响了用户体验,还影响到了销量。

        经排查发现是死锁引起的问题,背后原因是扣减库存的顺序不同,导致并发的情况下多个线程可能相互持有部分商品的锁,又等待其他线程释放另一部分商品的锁,于是出现了死锁问题。

        首先,定义一个商品类型,包含商品名、库存剩余和商品的库存锁三个属性,每一种商品默认库存 1000 个;然后,初始化 10 个这样的商品对象来模拟商品清单:

@Data
@RequiredArgsConstructor
static class Item {
    final String name; //商品名
    int remaining = 1000; //库存剩余
    @ToString.Exclude //ToString不包含这个字段 
    ReentrantLock lock = new ReentrantLock();
}

        随后,写一个方法模拟在购物车进行商品选购,每次从商品清单(items 字段)中随机选购三个商品(为了逻辑简单,我们不考虑每次选购多个同类商品的逻辑,购物车中不体现商品数量):

private List<Item> createCart() {
    return IntStream.rangeClosed(1, 3)
            .mapToObj(i -> "item" + ThreadLocalRandom.current().nextInt(items.size()))
            .map(name -> items.get(name)).collect(Collectors.toList());
}

        下单代码如下:先声明一个 List 来保存所有获得的锁,然后遍历购物车中的商品依次尝试获得商品的锁,最长等待 10 秒,获得全部锁之后再扣减库存;如果有无法获得锁的情况则解锁之前获得的所有锁,返回 false 下单失败。

private boolean createOrder(List<Item> order) {
    //存放所有获得的锁
    List<ReentrantLock> locks = new ArrayList<>();

    for (Item item : order) {
        try {
            //获得锁10秒超时
            if (item.lock.tryLock(10, TimeUnit.SECONDS)) {
                locks.add(item.lock);
            } else {
                locks.forEach(ReentrantLock::unlock);
                return false;
            }
        } catch (InterruptedException e) {
        }
    }
    //锁全部拿到之后执行扣减库存业务逻辑
    try {
        order.forEach(item -> item.remaining--);
    } finally {
        locks.forEach(ReentrantLock::unlock);
    }
    return true;
}

        模拟在多线程情况下进行 100 次创建购物车和下单操作,最后通过日志输出成功的下单次数、总剩余的商品个数、100 次下单耗时,以及下单完成后的商品库存明细:

@GetMapping("wrong")
public long wrong() {
    long begin = System.currentTimeMillis();
    //并发进行100次下单操作,统计成功次数
    long success = IntStream.rangeClosed(1, 100).parallel()
            .mapToObj(i -> {
                List<Item> cart = createCart();
                return createOrder(cart);
            })
            .filter(result -> result)
            .count();
    log.info("success:{} totalRemaining:{} took:{}ms items:{}",
            success,
            items.entrySet().stream().map(item -> item.getValue().remaining).reduce(0, Integer::sum),
            System.currentTimeMillis() - begin, items);
    return success;
}

        100 次下单操作成功了 65 次,10 种商品总计 10000 件,库存总计为 9805,消耗了 195 件符合预期(65 次下单成功,每次下单包含三件商品),总耗时 50 秒。

原因:

        使用 JDK 自带的 VisualVM 工具来跟踪一下,重新执行方法后不久就可以看到,线程 Tab 中提示了死锁问题,根据提示点击右侧线程 Dump 按钮进行线程抓取操作:

java错误处理百科,java,开发语言

 查看抓取出的线程栈,在页面中部可以看到如下日志:

java错误处理百科,java,开发语言

         是出现了死锁,线程 4 在等待的一个锁被线程 3 持有,线程 3 在等待的另一把锁被线程 4 持有。

死锁问题原因:

        个线程先获取到了 item1 的锁,同时另一个线程获取到了 item2 的锁,然后两个线程接下来要分别获取 item2 和 item1 的锁,这个时候锁已经被对方获取了,只能相互等待一直到 10 秒超时

解决方案:        

        为购物车中的商品排一下序,让所有的线程一定是先获取 item1 的锁然后获取 item2 的锁,就不会有问题了

@GetMapping("right")
public long right() {
    ...
.    
    long success = IntStream.rangeClosed(1, 100).parallel()
            .mapToObj(i -> {
                List<Item> cart = createCart().stream()
                        .sorted(Comparator.comparing(Item::getName))
                        .collect(Collectors.toList());
                return createOrder(cart);
            })
            .filter(result -> result)
            .count();
    ...
    return success;
}

4、线程池

① 线程池的声明需要手动进行

        newFixedThreadPool 方法的源码不难发现,线程池的工作队列直接 new 了一个 LinkedBlockingQueue,而默认构造方法的 LinkedBlockingQueue 是一个 Integer.MAX_VALUE 长度的队列,可以认为是无界的:

        newCachedThreadPool 的源码可以看到,这种线程池的最大线程数是 Integer.MAX_VALUE,可以认为是没有上限的,而其工作队列 SynchronousQueue 是一个没有存储空间的阻塞队列。

② 不建议使用 Executors 提供的两种快捷的线程池,原因如下:

        我们需要根据自己的场景、并发情况来评估线程池的几个核心参数,包括核心线程数、最大线程数、线程回收策略、工作队列的类型,以及拒绝策略,确保线程池的工作行为符合需求,一般都需要设置有界的工作队列和可控的线程数

        任何时候,都应该为自定义线程池指定有意义的名称,以方便排查问题。当出现线程数量暴增、线程死锁、线程占用大量 CPU、线程执行出现异常等问题时,我们往往会抓取线程栈。此时,有意义的线程名称,就可以方便我们定位问题。

③ 线程池线程管理策略详解

        用一个 printStats 方法实现了最简陋的监控,每秒输出一次线程池的基本内部信息,包括线程数、活跃线程数、完成了多少任务,以及队列中还有多少积压任务等信息:

private void printStats(ThreadPoolExecutor threadPool) {
   Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
        log.info("=========================");
        log.info("Pool Size: {}", threadPool.getPoolSize());
        log.info("Active Threads: {}", threadPool.getActiveCount());
        log.info("Number of Tasks Completed: {}", threadPool.getCompletedTaskCount());
        log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());

        log.info("=========================");
    }, 0, 1, TimeUnit.SECONDS);
}

        自定义一个线程池。这个线程池具有 2 个核心线程、5 个最大线程、使用容量为 10 的 ArrayBlockingQueue 阻塞队列作为工作队列,使用默认的 AbortPolicy 拒绝策略,也就是任务添加到线程池失败会抛出 RejectedExecutionException。此外,我们借助了 Jodd 类库的 ThreadFactoryBuilder 方法来构造一个线程工厂,实现线程池线程的自定义命名。

 场景:每次间隔 1 秒向线程池提交任务,循环 20 次,每个任务需要 10 秒才能执行完成

  错误场景:

@GetMapping("right")
public int right() throws InterruptedException {
    //使用一个计数器跟踪完成的任务数
    AtomicInteger atomicInteger = new AtomicInteger();
    //创建一个具有2个核心线程、5个最大线程,使用容量为10的ArrayBlockingQueue阻塞队列作为工作队列的线程池,使用默认的AbortPolicy拒绝策略
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            2, 5,
            5, TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(10),
            new ThreadFactoryBuilder().setNameFormat("demo-threadpool-%d").get(),
            new ThreadPoolExecutor.AbortPolicy());

    printStats(threadPool);
    //每隔1秒提交一次,一共提交20次任务
    IntStream.rangeClosed(1, 20).forEach(i -> {
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int id = atomicInteger.incrementAndGet();
        try {
            threadPool.submit(() -> {
                log.info("{} started", id);
                //每个任务耗时10秒
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                }
                log.info("{} finished", id);
            });
        } catch (Exception ex) {
            //提交出现异常的话,打印出错信息并为计数器减一
            log.error("error submitting task {}", id, ex);
            atomicInteger.decrementAndGet();
        }
    });

    TimeUnit.SECONDS.sleep(60);
    return atomicInteger.intValue();
}

        60 秒后页面输出了 17,有 3 次提交失败了:

原因:

        线程池默认的工作行为:

        a、不会初始化 corePoolSize 个线程,有任务来了才创建工作线程;

        b、当核心线程满了之后不会立即扩容线程池,而是把任务堆积到工作队列中;

        c、当工作队列满了后扩容线程池,一直到线程个数达到 maximumPoolSize 为止;

        d、如果队列已满且达到了最大线程后还有任务进来,按照拒绝策略处理;

        e、当线程数大于核心线程数时,线程等待 keepAliveTime 后还是没有任务需要处理的话,收缩线程到核心线程数。        

 处理方案:

        a、声明线程池后立即调用 prestartAllCoreThreads 方法,来启动所有核心线程;

        b、传入 true 给 allowCoreThreadTimeOut 方法,来让线程池在空闲的时候同样回收核心线程。

④ 务必确认清楚线程池本身是不是复用的

错误问题:

        在线程数比较高的时候进行线程栈抓取,抓取后发现内存中有 1000 多个自定义线程池。一般而言,线程池肯定是复用的,有 5 个以内的线程池都可以认为正常,而 1000 多个线程池肯定不正常。

@GetMapping("wrong")
public String wrong() throws InterruptedException {
    ThreadPoolExecutor threadPool = ThreadPoolHelper.getThreadPool();
    IntStream.rangeClosed(1, 10).forEach(i -> {
        threadPool.execute(() -> {
            ...
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        });
    });
    return "OK";
}

  原因: 

          ThreadPoolHelper 的实现让人大跌眼镜,getThreadPool 方法居然是每次都使用 Executors.newCachedThreadPool 来创建一个线程池。

class ThreadPoolHelper {
    public static ThreadPoolExecutor getThreadPool() {
        //线程池没有复用
        return (ThreadPoolExecutor) Executors.newCachedThreadPool();
    }
}

        回到 newCachedThreadPool 的定义就会发现,它的核心线程数是 0,而 keepAliveTime 是 60 秒,也就是在 60 秒之后所有的线程都是可以回收的。

解决方案:

        使用一个静态字段来存放线程池的引用,返回线程池的代码直接返回这个静态字段即可。

class ThreadPoolHelper {
  private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
    10, 50,
    2, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1000),
    new ThreadFactoryBuilder().setNameFormat("demo-threadpool-%d").get());
  public static ThreadPoolExecutor getRightThreadPool() {
    return threadPoolExecutor;
  }
}

⑤ 需要仔细斟酌线程池的混用策略

        要根据任务的“轻重缓急”来指定线程池的核心参数,包括线程数、回收策略和任务队列:

                a、对于执行比较慢、数量不大的 IO 任务,或许要考虑更多的线程数,而不需要太大的队列。

                b、而对于吞吐量较大的计算型任务,线程数量不宜过多,可以是 CPU 核数或核数 *2(理由是,线程一定调度到某个 CPU 进行执行,如果任务本身是 CPU 绑定的任务,那么过多的线程只会增加线程切换的开销,并不能提升吞吐量),但可能需要较长的队列来做缓冲。

场景:

        业务代码使用了线程池异步处理一些内存中的数据,但通过监控发现处理得非常慢,整个处理过程都是内存中的计算不涉及 IO 操作,也需要数秒的处理时间,应用程序 CPU 占用也不是特别高,有点不可思议。

错误:

        经排查发现,业务代码使用的线程池,还被一个后台的文件批处理任务用到了。

        这个线程池只有 2 个核心线程,最大线程也是 2,使用了容量为 100 的 ArrayBlockingQueue 作为工作队列,使用了 CallerRunsPolicy 拒绝策略:

private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
        2, 2,
        1, TimeUnit.HOURS,
        new ArrayBlockingQueue<>(100),
        new ThreadFactoryBuilder().setNameFormat("batchfileprocess-threadpool-%d").get(),
        new ThreadPoolExecutor.CallerRunsPolicy());

原因:

        线程池的 2 个线程始终处于活跃状态,队列也基本处于打满状态。因为开启了 CallerRunsPolicy 拒绝处理策略,所以当线程满载队列也满的情况下,任务会在提交任务的线程,或者说调用 execute 方法的线程执行,也就是说不能认为提交到线程池的任务就一定是异步处理的。如果使用了 CallerRunsPolicy 策略,那么有可能异步任务变为同步执行。从日志的第四行也可以看到这点。这也是这个拒绝策略比较特别的原因。

        压测结果:TPS 为 75,性能的确非常差。

        因为原来执行 IO 任务的线程池使用的是 CallerRunsPolicy 策略,所以直接使用这个线程池进行异步计算的话,当线程池饱和的时候,计算任务会在执行 Web 请求的 Tomcat 线程执行,这时就会进一步影响到其他同步处理的线程,甚至造成整个应用程序崩溃。

解决方案:

        使用独立的线程池来做这样的“计算任务”即可。计算任务打了双引号,是因为我们的模拟代码执行的是休眠操作,并不属于 CPU 绑定的操作,更类似 IO 绑定的操作,如果线程池线程数设置太小会限制吞吐能力:

private static ThreadPoolExecutor asyncCalcThreadPool = new ThreadPoolExecutor(
  200, 200,
  1, TimeUnit.HOURS,
  new ArrayBlockingQueue<>(1000),
  new ThreadFactoryBuilder().setNameFormat("asynccalc-threadpool-%d").get());


@GetMapping("right")
public int right() throws ExecutionException, InterruptedException {
  return asyncCalcThreadPool.submit(calcTask()).get();
}

        使用单独的线程池改造代码后再来测试一下性能,TPS 提高到了 1727

补充:

        Java 8 的 parallel stream 功能,可以让我们很方便地并行处理集合中的元素,其背后是共享同一个 ForkJoinPool,默认并行度是 CPU 核数 -1。

        对于 CPU 绑定的任务来说,使用这样的配置比较合适,但如果集合操作涉及同步 IO 操作的话(比如数据库操作、外部服务调用等),建议自定义一个 ForkJoinPool(或普通线程池)。

        程池作为应用程序内部的核心组件往往缺乏监控(如果你使用类似 RabbitMQ 这样的 MQ 中间件,运维同学一般会帮我们做好中间件监控),往往到程序崩溃后才发现线程池的问题,很被动。

5、连接池

① 连接池的结构

java错误处理百科,java,开发语言

② 注意鉴别客户端 SDK 是否基于连接池

        TCP 是面向连接的基于字节流的协议:

                面向连接,意味着连接需要先创建再使用,创建连接的三次握手有一定开销;

                TCP 协议本身无法区分哪几个字节是完整的消息体,也无法感知是否有多个客户端在使用同一个 TCP 连接,TCP 只是一个读写数据的管道。

③ 如果客户端 SDK 没有使用连接池,而直接是 TCP 连接,那么就需要考虑每次建立 TCP 连接的开销,并且因为 TCP 基于字节流,在多线程的情况下对同一连接进行复用,可能会产生线程安全问题。

④ 涉及 TCP 连接的客户端 SDK,对外提供 API 的三种方式。

        a、连接池和连接分离的 API:有一个 XXXPool 类负责连接池实现,先从其获得连接 XXXConnection,然后用获得的连接进行服务端请求,完成后使用者需要归还连接。通常,XXXPool 是线程安全的,可以并发获取和归还连接,而 XXXConnection 是非线程安全的。对应到连接池的结构示意图中,XXXPool 就是右边连接池那个框,左边的客户端是我们自己的代码。

        b、内部带有连接池的 API:对外提供一个 XXXClient 类,通过这个类可以直接进行服务端请求;这个类内部维护了连接池,SDK 使用者无需考虑连接的获取和归还问题。一般而言,XXXClient 是线程安全的。对应到连接池的结构示意图中,整个 API 就是蓝色框包裹的部分。

        c、非连接池的 API:一般命名为 XXXConnection,以区分其是基于连接池还是单连接的,而不建议命名为 XXXClient 或直接是 XXX。直接连接方式的 API 基于单一连接,每次使用都需要创建和断开连接,性能一般,且通常不是线程安全的。对应到连接池的结构示意图中,这种形式相当于没有右边连接池那个框,客户端直接连接服务端创建连接。

⑤ 使用 SDK 的最佳实践

      a、如果是分离方式,那么连接池本身一般是线程安全的,可以复用。每次使用需要从连接池获取连接,使用后归还,归还的工作由使用者负责。

        b、  如果是内置连接池,SDK 会负责连接的获取和归还,使用的时候直接复用客户端。

        c、如果 SDK 没有实现连接池(大多数中间件、数据库的客户端 SDK 都会支持连接池),那通常不是线程安全的,而且短连接的方式性能不会很高,使用的时候需要考虑是否自己封装一个连接池。

场景:源码角度分析下 Jedis 类到底属于哪种类型的 API,直接在多线程环境下复用一个连接会产生什么问题

        向 Redis 初始化 2 组数据,Key=a、Value=1,Key=b、Value=2:

@PostConstruct
public void init() {
    try (Jedis jedis = new Jedis("127.0.0.1", 6379)) {
        Assert.isTrue("OK".equals(jedis.set("a", "1")), "set a = 1 return OK");
        Assert.isTrue("OK".equals(jedis.set("b", "2")), "set b = 2 return OK");
    }
}

        然后,启动两个线程,共享操作同一个 Jedis 实例,每一个线程循环 1000 次,分别读取 Key 为 a 和 b 的 Value,判断是否分别为 1 和 2:

Jedis jedis = new Jedis("127.0.0.1", 6379);
new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        String result = jedis.get("a");
        if (!result.equals("1")) {
            log.warn("Expect a to be 1 but found {}", result);
            return;
        }
    }
}).start();
new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        String result = jedis.get("b");
        if (!result.equals("2")) {
            log.warn("Expect b to be 2 but found {}", result);
            return;
        }
    }
}).start();
TimeUnit.SECONDS.sleep(5);

        执行程序多次,可以看到日志中出现了各种奇怪的异常信息,有的是读取 Key 为 b 的 Value 读取到了 1,有的是流非正常结束,还有的是连接关闭异常

原因:

        Jedis 类

java错误处理百科,java,开发语言

        BinaryClient 封装了各种 Redis 命令,其最终会调用基类 Connection 的方法,使用 Protocol 类发送命令。看一下 Protocol 类的 sendCommand 方法的源码,可以发现其发送命令时是直接操作 RedisOutputStream 写入字节。

        我们在多线程环境下复用 Jedis 对象,其实就是在复用 RedisOutputStream。如果多个线程在执行操作,那么既无法确保整条命令以一个原子操作写入 Socket,也无法确保写入后、读取前没有其他数据写到远端

         比如,写操作互相干扰,多条命令相互穿插的话,必然不是合法的 Redis 命令,那么 Redis 会关闭客户端连接,导致连接断开;又比如,线程 1 和 2 先后写入了 get a 和 get b 操作的请求,Redis 也返回了值 1 和 2,但是线程 2 先读取了数据 1 就会出现数据错乱的问题。

解决方案:

        使用 Jedis 提供的另一个线程安全的类 JedisPool 来获得 Jedis 的实例。JedisPool 可以声明为 static 在多个线程之间共享,扮演连接池的角色。使用时,按需使用 try-with-resources 模式从 JedisPool 获得和归还 Jedis 实例。

private static JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);

new Thread(() -> {
    try (Jedis jedis = jedisPool.getResource()) {
        for (int i = 0; i < 1000; i++) {
            String result = jedis.get("a");
            if (!result.equals("1")) {
                log.warn("Expect a to be 1 but found {}", result);
                return;
            }
        }
    }
}).start();
new Thread(() -> {
    try (Jedis jedis = jedisPool.getResource()) {
        for (int i = 0; i < 1000; i++) {
            String result = jedis.get("b");
            if (!result.equals("2")) {
                log.warn("Expect b to be 2 but found {}", result);
                return;
            }
        }
    }
}).start();

        通过 shutdownhook,在程序退出之前关闭 JedisPool:

@PostConstruct
public void init() {
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        jedisPool.close();
    }));
}

        Jedis 的 API 实现是我们说的三种类型中的第一种,也就是连接池和连接分离的 API,JedisPool 是线程安全的连接池,Jedis 是非线程安全的单一连接。

⑥ 使用连接池务必确保复用

        池一定是用来复用的,否则其使用代价会比每次创建单一对象更大。对连接池来说更是如此,原因如下:

                a、创建连接池的时候很可能一次性创建了多个连接,大多数连接池考虑到性能,会在初始化的时候维护一定数量的最小连接(毕竟初始化连接池的过程一般是一次性的),可以直接使用。如果每次使用连接池都按需创建连接池,那么很可能你只用到一个连接,但是创建了 N 个连接。

                b、连接池一般会有一些管理模块,也就是连接池的结构示意图中的绿色部分。举个例子,大多数的连接池都有闲置超时的概念。连接池会检测连接的闲置时间,定期回收闲置的连接,把活跃连接数降到最低(闲置)连接的配置值,减轻服务端的压力。一般情况下,闲置连接由独立线程管理,启动了空闲检测的连接池相当于还会启动一个线程。此外,有些连接池还需要独立线程负责连接保活等功能。因此,启动一个连接池相当于启动了 N 个线程。

错误示例:

        Apache HttpClient 连接池不复用的问题

        创建一个 CloseableHttpClient,设置使用 PoolingHttpClientConnectionManager 连接池并启用空闲连接驱逐策略,最大空闲时间为 60 秒,然后使用这个连接来请求一个会返回 OK 字符串的服务端接口:

@GetMapping("wrong1")
public String wrong1() {
    CloseableHttpClient client = HttpClients.custom()
            .setConnectionManager(new PoolingHttpClientConnectionManager())
            .evictIdleConnections(60, TimeUnit.SECONDS).build();
    try (CloseableHttpResponse response = client.execute(new HttpGet("http://127.0.0.1:45678/httpclientnotreuse/test"))) {
        return EntityUtils.toString(response.getEntity());
    } catch (Exception ex) {
        ex.printStackTrace();
    }
    return null;
}

 原因:       

        CloseableHttpClient 属于第二种模式,即内部带有连接池的 API,其背后是连接池,最佳实践一定是复用。

解决方案:

        CloseableHttpClient 声明为 static,只创建一次,并且在 JVM 关闭之前通过 addShutdownHook 钩子关闭连接池,在使用的时候直接使用 CloseableHttpClient 即可,无需每次都创建。

private static CloseableHttpClient httpClient = null;
static {
    //当然,也可以把CloseableHttpClient定义为Bean,然后在@PreDestroy标记的方法内close这个HttpClient
    httpClient = HttpClients.custom().setMaxConnPerRoute(1).setMaxConnTotal(1).evictIdleConnections(60, TimeUnit.SECONDS).build();
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        try {
            httpClient.close();
        } catch (IOException ignored) {
        }
    }));
}

@GetMapping("right")
public String right() {
    try (CloseableHttpResponse response = httpClient.execute(new HttpGet("http://127.0.0.1:45678/httpclientnotreuse/test"))) {
        return EntityUtils.toString(response.getEntity());
    } catch (Exception ex) {
        ex.printStackTrace();
    }
    return null;
}

        修复之前按需创建 CloseableHttpClient 的代码,每次用完之后确保连接池可以关闭

@GetMapping("wrong2")
public String wrong2() {
    try (CloseableHttpClient client = HttpClients.custom()
            .setConnectionManager(new PoolingHttpClientConnectionManager())
            .evictIdleConnections(60, TimeUnit.SECONDS).build();
         CloseableHttpResponse response = client.execute(new HttpGet("http://127.0.0.1:45678/httpclientnotreuse/test"))) {
            return EntityUtils.toString(response.getEntity());
        } catch (Exception ex) {
        ex.printStackTrace();
    }
    return null;
}

⑦ 连接池的配置不是一成不变的

        最大连接数不是设置得越大越好

        连接池最大连接数设置得太小,很可能会因为获取连接的等待时间太长,导致吞吐量低下,甚至超时无法获取连接。

        对类似数据库连接池的重要资源进行持续检测,并设置一半的使用量作为报警阈值,出现预警后及时扩容。

        强调的是,修改配置参数务必验证是否生效,并且在监控系统中确认参数是否生效、是否合理。之所以要“强调”,是因为这里有坑。

6、HTTP调用:你考虑到超时、重试、并发

① 网络请求必然有超时的可能性

        a、 首先,框架设置的默认超时是否合理;

        b、 其次,考虑到网络的不稳定,超时后的请求重试是一个不错的选择,但需要考虑服务端接口的幂等性设计是否允许我们重试;

        c、 最后,需要考虑框架是否会像浏览器那样限制并发连接数,以免在服务并发很大的情况下,HTTP 调用的并发数限制成为瓶颈。

② 配置连接超时和读取超时参数的学问

        a、连接超时参数 ConnectTimeout,让用户配置建连阶段的最长等待时间;

        b、读取超时参数 ReadTimeout,用来控制从 Socket 上读取数据的最长等待时间。

③ 连接超时参数和连接超时的误区有这么两个:

        a、连接超时配置得特别长,比如 60 秒。

                如果是纯内网调用的话,这个参数可以设置得更短,在下游服务离线无法连接的时候,可以快速失败。

        b、排查连接超时问题,却没理清连的是哪里。

                而如果服务端通过类似 Nginx 的反向代理来负载均衡,客户端连接的其实是 Nginx,而不是服务端,此时出现连接超时应该排查 Nginx。

④ 读取超时参数和读取超时则会有更多的误区,我将其归纳为如下三个

        a、第一个误区:认为出现了读取超时,服务端的执行就会中断。

                只要服务端收到了请求,网络层面的超时和断开便不会影响服务端的执行。

        b、第二个误区:认为读取超时只是 Socket 网络层面的概念,是数据传输的最长耗时,故将其配置得非常短,比如 100 毫秒。

                通常可以认为出现连接超时是网络问题或服务不在线,而出现读取超时是服务处理超时。

                读取超时指的是,向 Socket 写入数据后,我们等到 Socket 返回数据的超时时间,其中包含的时间或者说绝大部分的时间,是服务端处理业务逻辑的时间。

        c、第三个误区:认为超时时间越长任务接口成功率就越高,将读取超时参数配置得太长。

                进行 HTTP 请求一般是需要获得结果的,属于同步调用。如果超时时间很长,在等待服务端返回数据的同时,客户端线程(通常是 Tomcat 线程)也在等待,当下游服务出现大量超时的时候,程序可能也会受到拖累创建大量线程,最终崩溃。

                但面向用户响应的请求或是微服务短平快的同步接口调用,并发量一般较大,我们应该设置一个较短的读取超时时间,以防止被下游服务拖慢,通常不会设置超过 30 秒的读取超时。

 ⑤ Feign 和 Ribbon 配合使用,你知道怎么配置超时吗?

        a、结论一,默认情况下 Feign 的读取超时是 1 秒,如此短的读取超时算是坑点一。

        b、结论二,也是坑点二,如果要配置 Feign 的读取超时,就必须同时配置连接超时,才能生效。

        c、结论三,单独的超时可以覆盖全局超时,这符合预期,不算坑:

        d、结论四,除了可以配置 Feign,也可以配置 Ribbon 组件的参数来修改两个超时时间。这里的坑点三是,参数首字母要大写,和 Feign 的配置不同。

        e、结论五,同时配置 Feign 和 Ribbon 的超时,以 Feign 为准

⑥ Ribbon 会自动重试请求

        解决办法有两个:

                a、一是,把发短信接口从 Get 改为 Post。这里的一个误区是,Get 请求的参数包含在 Url QueryString 中,会受浏览器长度限制,所以一些同学会选择使用 JSON 以 Post 提交大参数,使用 Get 提交小参数。

                b、二是,将 MaxAutoRetriesNextServer 参数配置为 0,禁用服务调用失败后在下一个服务端节点的自动重试。在配置文件中添加一行即可:

⑦ 并发限制了爬虫的抓取能力

原因:

        PoolingHttpClientConnectionManager 源码,可以注意到有两个重要参数:        

        a、defaultMaxPerRoute=2,也就是同一个主机 / 域名的最大并发请求数为 2。我们的爬虫需要 10 个并发,显然是默认值太小限制了爬虫的效率。

        b、maxTotal=20,也就是所有主机整体最大并发为 20,这也是 HttpClient 整体的并发度。目前,我们请求数是 10 最大并发是 10,20 不会成为瓶颈。举一个例子,使用同一个 HttpClient 访问 10 个域名,defaultMaxPerRoute 设置为 10,为确保每一个域名都能达到 10 并发,需要把 maxTotal 设置为 100。

解决方案

        声明一个新的 HttpClient 放开相关限制,设置 maxPerRoute 为 50、maxTotal 为 100,然后修改一下刚才的 wrong 方法,

httpClient2 = HttpClients.custom().setMaxConnPerRoute(10).setMaxConnTotal(20).build();

7、小心 Spring 的事务可能没有生效

@Transactional 生效原则

        ① 除非特殊配置(比如使用 AspectJ 静态织入实现 AOP),否则只有定义在 public 方法上的 @Transactional 才能生效。

        ② 必须通过代理过的类从外部调用目标方法才能生效。

                Spring 通过 AOP 技术对方法进行增强,要调用增强过的方法必然是调用代理后的对象。我们尝试修改下 UserService 的代码,注入一个 self,然后再通过 self 实例调用标记有 @Transactional 注解的 createUserPublic 方法。设置断点可以看到,self 是由 Spring 通过 CGLIB 方式增强过的类:

                a、CGLIB 通过继承方式实现代理类,private 方法在子类不可见,自然也就无法进行事务增强;

                b、this 指针代表对象自己,Spring 不可能注入 this,所以通过 this 访问方法必然不是代理。

java错误处理百科,java,开发语言

        this 自调用、通过 self 调用,以及在 Controller 中调用 UserService 三种实现的区别:

java错误处理百科,java,开发语言

        强烈建议你在开发时打开相关的 Debug 日志,以方便了解 Spring 事务实现的细节,并及时判断事务的执行情况。

8、事务即便生效也不一定能回滚

        当方法出现了异常并且满足一定条件的时候,在 catch 里面我们可以设置事务回滚,没有异常则直接提交事务。

        ① 只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。

        ② 默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务。

错误示例:

        ① 在 createUserWrong1 方法中会抛出一个 RuntimeException,但由于方法内 catch 了所有异常,异常无法从方法传播出去,事务自然无法回滚。

        ② 在 createUserWrong2 方法中,注册用户的同时会有一次 otherTask 文件读取操作,如果文件读取失败,我们希望用户注册的数据库操作回滚。虽然这里没有捕获异常,但因为 otherTask 方法抛出的是受检异常,createUserWrong2 传播出去的也是受检异常,事务同样不会回滚。

处理方案:

        ① 第一,如果你希望自己捕获异常进行处理的话,也没关系,可以手动设置让当前事务处于回滚状态

@Transactional
public void createUserRight1(String name) {
    try {
        userRepository.save(new UserEntity(name));
        throw new RuntimeException("error");
    } catch (Exception ex) {
        log.error("create user failed", ex);
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

        ② 第二,在注解中声明,期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制)

@Transactional(rollbackFor = Exception.class)
public void createUserRight2(String name) throws IOException {
    userRepository.save(new UserEntity(name));
    otherTask();
}

9、请确认事务传播配置是否符合自己的业务逻辑

        ① 出了异常事务不一定回滚,这里说的却是不出异常,事务也不一定可以提交。原因是,主方法注册主用户的逻辑和子方法注册子用户的逻辑是同一个事务,子逻辑标记了事务需要回滚,主逻辑自然也不能提交了。

        ② 想办法让子逻辑在独立事务中运行,也就是改一下 SubUserService 注册子用户的方法,为注解加上 propagation = Propagation.REQUIRES_NEW 来设置 REQUIRES_NEW 方式的事务传播策略,也就是执行到这个方法时需要开启新的事务,并挂起当前事务:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createSubUserWithExceptionRight(UserEntity entity) {
    log.info("createSubUserWithExceptionRight start");
    userRepository.save(entity);
    throw new RuntimeException("invalid status");
}

10、InnoDB 是如何存储数据的

        ① 虽然数据保存在磁盘中,但其处理是在内存中进行的。为了减少磁盘随机读取次数,InnoDB 采用页而不是行的粒度来保存数据,即数据被分成若干页,以页为单位保存在磁盘中。InnoDB 的页大小,一般是 16KB。

        各个数据页组成一个双向链表,每个数据页中的记录按照主键顺序组成单向链表;每一个数据页中有一个页目录,方便按照主键查询记录。数据页的结构如下:

java错误处理百科,java,开发语言

11、聚簇索引和二级索引

java错误处理百科,java,开发语言

① B+ 树的特点包括:

        最底层的节点叫作叶子节点,用来存放数据;

        其他上层节点叫作非叶子节点,仅用来存放目录项,作为索引;

        非叶子节点分为不同层次,通过分层来降低每一层的搜索量;

        所有节点按照索引键大小排序,构成一个双向链表,加速范围查找。

② 由于数据在物理上只会保存一份,所以包含实际数据的聚簇索引只能有一个。

        为了实现非主键字段的快速搜索,就引出了二级索引,也叫作非聚簇索引、辅助索引。二级索引,也是利用的 B+ 树的数据结构,如下图所示:

java错误处理百科,java,开发语言

        这次二级索引的叶子节点中保存的不是实际数据,而是主键,获得主键值后去聚簇索引中获得数据行。这个过程就叫作回表。

12、考虑额外创建二级索引的代价

        ① 首先是维护代价。

                创建 N 个二级索引,就需要再创建 N 棵 B+ 树,新增数据时不仅要修改聚簇索引,还需要修改这 N 个二级索引。

        ② 其次是空间代价。虽然二级索引不保存原始数据,但要保存索引列的数据,所以会占用更多的空间。

        ③ 最后是回表的代价。二级索引不保存原始数据,通过索引找到主键后需要再查询聚簇索引,才能得到我们要的数据。

 13、索引开销的最佳实践

       ① 第一,无需一开始就建立索引,可以等到业务场景明确后,或者是数据量超过 1 万、查询变慢后,再针对需要查询、排序或分组的字段创建索引。创建索引后可以使用 EXPLAIN 命令,确认查询是否可以使用索引。我会在下一小节展开说明。

       ②  第二,尽量索引轻量级的字段,比如能索引 int 字段就不要索引 varchar 字段。索引字段也可以是部分前缀,在创建的时候指定字段索引长度。针对长文本的搜索,可以考虑使用 Elasticsearch 等专门用于文本搜索的索引数据库。

        ③ 第三,尽量不要在 SQL 语句中 SELECT *,而是 SELECT 必要的字段,甚至可以考虑使用联合索引来包含我们要搜索的字段,既能实现索引加速,又可以避免回表的开销。

 14、不是所有针对索引列的查询都能用上索引

        ① 第一,索引只能匹配列前缀

        ② 第二,条件涉及函数操作无法走索引

        ③ 第三,联合索引只能匹配左边的列

  15、数据库基于成本决定是否走索引

        ① IO 成本,是从磁盘把数据加载到内存的成本。默认情况下,读取数据页的 IO 成本常数是 1(也就是读取 1 个页成本是 1)

                聚簇索引占用的页面数,用来计算读取数据的 IO 成本;

        ② CPU 成本,是检测数据是否满足条件和排序等 CPU 操作的成本。默认情况下,检测记录的成本是 0.2。

                表中的记录数,用来计算搜索的 CPU 成本。

16、mysql选错索引

        在 MySQL 5.6 及之后的版本中,我们可以使用 optimizer trace 功能查看优化器生成执行计划的整个过程。有了这个功能,我们不仅可以了解优化器的选择过程,更可以了解每一个执行环节的成本,然后依靠这些信息进一步优化查询。

        打开 optimizer_trace 后,再执行 SQL 就可以查询 information_schema.OPTIMIZER_TRACE 表查看执行计划了,最后可以关闭 optimizer_trace 功能:

SET optimizer_trace="enabled=on";
SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-01-24 05:00:00';
SELECT * FROM information_schema.OPTIMIZER_TRACE;
SET optimizer_trace="enabled=off";

17、注意 equals 和 == 的区别

        ① 对基本类型,比如 int、long,进行判等,只能使用 ==,比较的是直接值。因为基本类型的值就是其数值。

        ② 对引用类型,比如 Integer、Long 和 String,进行判等,需要使用 equals 进行内容判等。因为引用类型的直接值是指针,使用 == 的话,比较的是指针,也就是两个对象在内存中的地址,即比较它们是不是同一个对象,而不是比较对象的内容。

        需要记得比较 Integer 的值请使用 equals,而不是 ==

18、实现一个更好的 equals 应该注意的点

        ① 考虑到性能,可以先进行指针判等,如果对象是同一个那么直接返回 true;

        ② 需要对另一方进行判空,空对象和自身进行比较,结果一定是 fasle;

        ③ 需要判断两个对象的类型,如果类型都不同,那么直接返回 false;

        ④ 确保类型相同的情况下再进行类型强制转换,然后逐一判断所有字段。

19、注意 compareTo 和 equals 的逻辑一致性

        ① 我们使用了 Lombok 的 @Data 标记了 Student,@Data 注解(详见这里)其实包含了 @EqualsAndHashCode 注解(详见这里)的作用,也就是默认情况下使用类型所有的字段(不包括 static 和 transient 字段)参与到 equals 和 hashCode 方法的实现中。因为这两个方法的实现不是我们自己实现的,所以容易忽略其逻辑。

       ② compareTo 方法需要返回数值,作为排序的依据,容易让人使用数值类型的字段随意实现。

20、小心 Lombok 生成代码的“坑”

        ① 问题场景:

                有继承关系时,Lombok 自动生成的方法可能就不是我们期望的了。

        ② 解决方案

@Data
@EqualsAndHashCode(callSuper = true)
class Employee extends Person {

21、数值计算:注意精度、舍入和溢出问题

        ① 切记,要精确表示浮点数应该使用 BigDecimal。并且,使用 BigDecimal 的 Double 入参的构造方法同样存在精度丢失问题,应该使用 String 入参的构造方法或者 BigDecimal.valueOf 方法来初始化。

        ② 对浮点数做精确计算,参与计算的各种数值应该始终使用 BigDecimal,所有的计算都要通过 BigDecimal 的方法进行,切勿只是让 BigDecimal 来走过场。任何一个环节出现精度损失,最后的计算结果可能都会出现误差。

        ③ 对于浮点数的格式化,如果使用 String.format 的话,需要认识到它使用的是四舍五入,可以考虑使用 DecimalFormat 来明确指定舍入方式。但考虑到精度问题,我更建议使用 BigDecimal 来表示浮点数,并使用其 setScale 方法指定舍入的位数和方式。

        ④ 进行数值运算时要小心溢出问题,虽然溢出后不会出现异常,但得到的计算结果是完全错误的。我们考虑使用 Math.xxxExact 方法来进行运算,在溢出时能抛出异常,更建议对于可能会出现溢出的大数运算使用 BigInteger 类。

22、List 列表相关的错误案例

① 想当然认为,Arrays.asList 和 List.subList 得到的 List 是普通的、独立的 ArrayList,在使用时出现各种奇怪的问题。

        a、Arrays.asList 得到的是 Arrays 的内部类 ArrayList,List.subList 得到的是 ArrayList 的内部类 SubList,不能把这两个内部类转换为 ArrayList 使用。

        b、Arrays.asList 直接使用了原始数组,可以认为是共享“存储”,而且不支持增删元素;List.subList 直接引用了原始的 List,也可以认为是共享“存储”,而且对原始 List 直接进行结构性修改会导致 SubList 出现异常。

        c、对 Arrays.asList 和 List.subList 容易忽略的是,新的 List 持有了原始数据的引用,可能会导致原始数据也无法 GC 的问题,最终导致 OOM。

② 想当然认为,Arrays.asList 一定可以把所有数组转换为正确的 List。当传入基本类型数组的时候,List 的元素是数组本身,而不是数组中的元素。

③ 想当然认为,内存中任何集合的搜索都是很快的,结果在搜索超大 ArrayList 的时候遇到性能问题。我们考虑利用 HashMap 哈希表随机查找的时间复杂度为 O(1) 这个特性来优化性能,不过也要考虑 HashMap 存储空间上的代价,要平衡时间和空间。

④ 想当然认为,链表适合元素增删的场景,选用 LinkedList 作为数据结构。在真实场景中读写增删一般是平衡的,而且增删不可能只是对头尾对象进行操作,可能在 90% 的情况下都得不到性能增益,建议使用之前通过性能测试评估一下。

       解决方案:

                重新 new 一个 ArrayList 初始化 Arrays.asList 返回的 List 即可:

        

String[] arr = {"1", "2", "3"};
List list = new ArrayList(Arrays.asList(arr));

 23、空值处理

        ① 对于 Integer 的判空,可以使用 Optional.ofNullable 来构造一个 Optional,然后使用 orElse(0) 把 null 替换为默认值再进行 +1 操作。

        ② 对于 String 和字面量的比较,可以把字面量放在前面,比如"OK".equals(s),这样即使 s 是 null 也不会出现空指针异常;而对于两个可能为 null 的字符串变量的 equals 比较,可以使用 Objects.equals,它会做判空处理。

        ③  对于 ConcurrentHashMap,既然其 Key 和 Value 都不支持 null,修复方式就是不要把 null 存进去。HashMap 的 Key 和 Value 可以存入 null,而 ConcurrentHashMap 看似是 HashMap 的线程安全版本,却不支持 null 值的 Key 和 Value,这是容易产生误区的一个地方。

        ④ 对于类似 fooService.getBarService().bar().equals(“OK”) 的级联调用,需要判空的地方有很多,包括 fooService、getBarService() 方法的返回值,以及 bar 方法返回的字符串。如果使用 if-else 来判空的话可能需要好几行代码,但使用 Optional 的话一行代码就够了。

        ⑤ 对于 rightMethod 返回的 List,由于不能确认其是否为 null,所以在调用 size 方法获得列表大小之前,同样可以使用 Optional.ofNullable 包装一下返回值,然后通过.orElse(Collections.emptyList()) 实现在 List 为 null 的时候获得一个空的 List,最后再调用 size 方法。

24、姓名年龄昵称校验

        ① 对于姓名,我们认为客户端传 null 是希望把姓名重置为空,允许这样的操作,使用 Optional 的 orElse 方法一键把空转换为空字符串即可。

        ② 对于年龄,我们认为如果客户端希望更新年龄就必须传一个有效的年龄,年龄不存在重置操作,可以使用 Optional 的 orElseThrow 方法在值为空的时候抛出 IllegalArgumentException。

        ③ 对于昵称,因为数据库中姓名不可能为 null,所以可以放心地把昵称设置为 guest 加上数据库取出来的姓名。

25、mysql空值问题

        ① MySQL 中 sum 函数没统计到任何记录时,会返回 null 而不是 0,可以使用 IFNULL 函数把 null 转换为 0;

        ② MySQL 中 count 字段不统计 null 值,COUNT(*) 才是统计所有记录数量的正确方式。

        ③ MySQL 中使用诸如 =、<、> 这样的算数比较操作符比较 NULL 的结果总是 NULL,这种比较就显得没有任何意义,需要使用 IS NULL、IS NOT NULL 或 ISNULL() 函数来比较。

26、异常处理:

      第一,注意捕获和处理异常的最佳实践。首先,不应该用 AOP 对所有方法进行统一异常处理,异常要么不捕获不处理,要么根据不同的业务逻辑、不同的异常类型进行精细化、针对性处理;其次,处理异常应该杜绝生吞,并确保异常栈信息得到保留;最后,如果需要重新抛出异常的话,请使用具有意义的异常类型和异常消息。

        第二,务必小心 finally 代码块中资源回收逻辑,确保 finally 代码块不出现异常,内部把异常处理完毕,避免 finally 中的异常覆盖 try 中的异常;或者考虑使用 addSuppressed 方法把 finally 中的异常附加到 try 中的异常上,确保主异常信息不丢失。此外,使用实现了 AutoCloseable 接口的资源,务必使用 try-with-resources 模式来使用资源,确保资源可以正确释放,也同时确保异常可以正确处理。

        第三,虽然在统一的地方定义收口所有的业务异常是一个不错的实践,但务必确保异常是每次 new 出来的,而不能使用一个预先定义的 static 字段存放异常,否则可能会引起栈信息的错乱。

        第四,确保正确处理了线程池中任务的异常,如果任务通过 execute 提交,那么出现异常会导致线程退出,大量的异常会导致线程重复创建引起性能问题,我们应该尽可能确保任务不出异常,同时设置默认的未捕获异常处理程序来兜底;如果任务通过 submit 提交意味着我们关心任务的执行结果,应该通过拿到的 Future 调用其 get 方法来获得任务运行结果和可能出现的异常,否则异常可能就被生吞了。  

26、字符集乱码处理问题修复

private static void right1() throws IOException {
    char[] chars = new char[10];
    String content = "";
    try (FileInputStream fileInputStream = new FileInputStream("hello.txt");
        InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, Charset.forName("GBK"))) {
        int count;
        while ((count = inputStreamReader.read(chars)) != -1) {
            content += new String(chars, 0, count);
        }
    }
    log.info("result: {}", content);
}

27、流读取关闭

        注意使用 try-with-resources 方式来配合,确保流的 close 方法可以调用释放资源。

LongAdder longAdder = new LongAdder();
IntStream.rangeClosed(1, 1000000).forEach(i -> {
    try (Stream<String> lines = Files.lines(Paths.get("demo.txt"))) {
        lines.forEach(line -> longAdder.increment());
    } catch (IOException e) {
        e.printStackTrace();
    }
});
log.info("total : {}", longAdder.longValue());

28、缓冲区读取数据

private static void bufferOperationWith100Buffer() throws IOException {
    try (FileInputStream fileInputStream = new FileInputStream("src.txt");
         FileOutputStream fileOutputStream = new FileOutputStream("dest.txt")) {
        byte[] buffer = new byte[100];
        int len = 0;
        while ((len = fileInputStream.read(buffer)) != -1) {
            fileOutputStream.write(buffer, 0, len);
        }
    }
}

29、java 8时间处理java错误处理百科,java,开发语言

30、时间计算下一个月

Date today = new Date();
Date nextMonth = new Date(today.getTime() + 30 * 1000 * 60 * 60 * 24);
System.out.println(today);
System.out.println(nextMonth);

其实是因为 int 发生了溢出。修复方式就是把 30 改为 30L,让其成为一个 long:

Date today = new Date();
Date nextMonth = new Date(today.getTime() + 30L * 1000 * 60 * 60 * 24);
System.out.println(today);
System.out.println(nextMonth);

31、Java 8 的日期时间类型,可以直接进行各种计算,更加简洁和方便:

① 可以使用各种 minus 和 plus 方法直接对日期进行加减操作,比如如下代码实现了减一天和加一天,以及减一个月和加一个月:

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime.plusDays(30));
System.out.println("//测试操作日期");
System.out.println(LocalDate.now()
        .minus(Period.ofDays(1))
        .plus(1, ChronoUnit.DAYS)
        .minusMonths(1)
        .plus(Period.ofMonths(1)));

② 还可以通过 with 方法进行快捷时间调节,比如:

        使用 TemporalAdjusters.firstDayOfMonth 得到当前月的第一天;使用         TemporalAdjusters.firstDayOfYear() 得到当前年的第一天;使用         TemporalAdjusters.previous(DayOfWeek.SATURDAY) 得到上一个周六;使用         TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY) 得到本月最后一个周五。

System.out.println("//本月的第一天");
System.out.println(LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()));

System.out.println("//今年的程序员日");
System.out.println(LocalDate.now().with(TemporalAdjusters.firstDayOfYear()).plusDays(255));

System.out.println("//今天之前的一个周六");
System.out.println(LocalDate.now().with(TemporalAdjusters.previous(DayOfWeek.SATURDAY)));

System.out.println("//本月最后一个工作日");
System.out.println(LocalDate.now().with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)));

③ 可以直接使用 lambda 表达式进行自定义的时间调整。比如,为当前时间增加 100 天以内的随机天数:

System.out.println(LocalDate.now().with(temporal -> temporal.plus(ThreadLocalRandom.current().nextInt(100), ChronoUnit.DAYS)));

④ 除了计算外,还可以判断日期是否符合某个条件。比如,自定义函数,判断指定日期是否是家庭成员的生日:

public static Boolean isFamilyBirthday(TemporalAccessor date) {
    int month = date.get(MONTH_OF_YEAR);
    int day = date.get(DAY_OF_MONTH);
    if (month == Month.FEBRUARY.getValue() && day == 17)
        return Boolean.TRUE;
    if (month == Month.SEPTEMBER.getValue() && day == 21)
        return Boolean.TRUE;
    if (month == Month.MAY.getValue() && day == 22)
        return Boolean.TRUE;
    return Boolean.FALSE;
}

        然后,使用 query 方法查询是否匹配条件:

System.out.println("//查询是否是今天要举办生日");
System.out.println(LocalDate.now().query(CommonMistakesApplication::isFamilyBirthday));

⑤ Java 8 中有一个专门的类 Period 定义了日期间隔,通过 Period.between 得到了两个 LocalDate 的差,返回的是两个日期差几年零几月零几天。如果希望得知两个日期之间差几天,直接调用 Period 的 getDays() 方法得到的只是最后的“零几天”,而不是算总的间隔天数。

        比如,计算 2019 年 12 月 12 日和 2019 年 10 月 1 日的日期间隔,很明显日期差是 2 个月零 11 天,但获取 getDays 方法得到的结果只是 11 天,而不是 72 天:

System.out.println("//计算日期差");
LocalDate today = LocalDate.of(2019, 12, 12);
LocalDate specifyDate = LocalDate.of(2019, 10, 1);
System.out.println(Period.between(specifyDate, today).getDays());
System.out.println(Period.between(specifyDate, today));
System.out.println(ChronoUnit.DAYS.between(specifyDate, today));

32、缓存重复信息导致OOM

        解决方案 :HashSet 来缓存用户信息,防止多份重复的用户信息

 33、spring注意事项

        第一,让 Spring 容器管理对象,要考虑对象默认的 Scope 单例是否适合,对于有状态的类型,单例可能产生内存泄露问题。

        第二,如果要为单例的 Bean 注入 Prototype 的 Bean,绝不是仅仅修改 Scope 属性这么简单。由于单例的 Bean 在容器启动时就会完成一次性初始化。最简单的解决方案是,把 Prototype 的 Bean 设置为通过代理注入,也就是设置 proxyMode 属性为 TARGET_CLASS。

        第三,如果一组相同类型的 Bean 是有顺序的,需要明确使用 @Order 注解来设置顺序。你可以再回顾下,两个不同优先级切面中 @Before、@After 和 @Around 三种增强的执行顺序,是什么样的。文章来源地址https://www.toymoban.com/news/detail-680625.html

到了这里,关于java错误处理百科的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 对比编程语言的四种错误处理方法,哪种才是最优方案?

    作者:Andrea Bergia 译者:豌豆花下猫@Python猫 英文:Error handling patterns 转载请保留作者及译者信息! 错误处理是编程的一个基本要素。除非你写的是“hello world”,否则就必须处理代码中的错误。在本文中,我将讨论各种编程语言在处理错误时使用的最常见的四种方法,并分析

    2024年02月03日
    浏览(77)
  • Java - 处理“拒绝访问“错误的解决方案

    Java - 处理\\\"拒绝访问\\\"错误的解决方案 在Java编程中,当使用FileOutputStream类时,有时会遇到\\\"拒绝访问\\\"(Access Denied)的错误。这通常是由于操作系统权限限制或文件被其他进程锁定所引起的。在本篇文章中,我们将探讨如何处理这个问题,并提供相应的源代码示例。 解决方案一

    2024年02月07日
    浏览(46)
  • 100天精通Golang(基础入门篇)——第23天:错误处理的艺术: Go语言实战指南

    🌷🍁 博主猫头虎🐅🐾 带您进入 Golang 语言的新世界✨✨🍁 🦄 博客首页 ——🐅🐾猫头虎的博客🎐 🐳 《面试题大全专栏》 🦕 文章图文并茂🦖生动形象🐅简单易学!欢迎大家来踩踩~🌺 🌊 《IDEA开发秘籍专栏》 🐾 学会IDEA常用操作,工作效率翻倍~💐 🌊 《100天精通

    2024年02月07日
    浏览(67)
  • Rust Web 全栈开发之 Web Service 中的错误处理

    数据库 数据库错误 串行化 serde 错误 I/O 操作 I/O 错误 Actix-Web 库 Actix 错误 用户非法输入 用户非法输入错误 编程语言常用的两种错误处理方式: 异常 返回值( Rust 使用这种) Rust 希望开发者显式的处理错误,因此,可能出错的函数返回Result 枚举类型,其定义如下: 例子 在

    2024年02月07日
    浏览(40)
  • 处理Java中的“Failed to determine a suitable driver class“错误

    在Java开发过程中,经常会使用数据库进行数据存储和检索。当连接到数据库时,我们通常会使用JDBC(Java Database Connectivity)来与数据库进行通信。然而,有时候在连接数据库时可能会出现\\\"Failed to determine a suitable driver class\\\"的错误。本文将介绍这个错误的原因,并提供一些解决

    2024年02月04日
    浏览(50)
  • exec: “java“: executable file not found in $PATH: unknown错误处理

    kubesphere部署springboot项目时,出现下边错误信息 exec: \\\"java\\\": executable file not found in $PATH: unknown 原因: 本来是从docker仓库取镜像,所以源头应该是docker镜像打包时出的问题 处理方式: 修改dockerfile的ENTRYPOINT值: 然后重新发布就可以了

    2024年02月16日
    浏览(38)
  • 自然语言处理NLP在Java语言的应用

    自然语言处理(Natural Language Processing, NLP)是计算机科学中的一个重要分支,旨在让机器能够理解、处理人类语言。 自然语言处理(Natural Language Processing, NLP)技术主要可以分为 文本处理 和 语音处理 两种。 文本处理 主要包括以下方面: 1.情感分析(Sentiment Analysis)。 2.实

    2024年02月16日
    浏览(56)
  • JAVA开发(记一次504 gateway timeout错误排查过程)

    一、问题与背景: 最近在发布一个web项目,在测试环境都是可以的,发布到生产环境通过IP访问也是可以的,但是通过域名访问就出现504 gateway timeout。通过postman去测试接口也是一样。ip和端口都可以通,域名却不行,百思不得其解。通过一顿百度搜索,解析说通过nginx配置文

    2024年02月11日
    浏览(43)
  • Java语言----异常处理(看了必会)

    目录 一.异常的概述 二.异常类的层次结构和种类 二.异常的基本用法 2.1异常的捕捉 2.2异常处理代码实现 三.抛出异常 3.1 throw 3.2 throws 四.finally的进一步详解 五.自定义异常类 总结 😽个人主页:tq02的博客_CSDN博客-C语言,Java领域博主  🌈理想目标:努力学习,向Java进发,拼搏

    2023年04月11日
    浏览(59)
  • 用Python编程实现百度自然语言处理接口的对接,助力你开发智能化处理程序

    用Python编程实现百度自然语言处理接口的对接,助力你开发智能化处理程序 随着人工智能的不断进步,自然语言处理(Natural Language Processing,NLP)成为了解决文本处理问题的重要工具。百度自然语言处理接口提供了一系列强大的功能,如提取、文本分类、情感分析等,

    2024年02月13日
    浏览(105)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包