菜鸟笔记
提升您的技术认知

Java线程安全

前段时间有测试一个后端对账单和话单采集服务,在测试过程中有涉及到数据库读写逻辑和并发的场景,所以结合经验针对系统技术架构设计了部分并发场景结合数据库读写时可能出现的一些问题的用例,也确实出现了一些测试环境容易忽视,线上环境确确实实可能出现的问题,当然最后还是得到了妥善的解决.下面说说后端测试应该考虑的一些线程安全和数据读写方面的问题.

前提:测试环境的架构尽量向线上环境的架构靠拢,比如线上如果采用分布式集群,测试环境如果是单机,那么部分问题自然无法暴露,如果测试环境无足够资源模拟线上的几十台集群环境,那么我们采用3~4台资源来模拟集群,也不是不可以.

1.线程安全

什么是线程安全?

        线程安全就是保证多个线程同时对某一对象或资源进行操作时不会出错.比如我们购物,下订单前某商品库存数为2,两个用户同时对该商品进行下单购买,系统会记录商品库存总数,如果不加以并发控制,那么就会出现某个用户对当前库存总数的脏读的情况.

        线程安全问题出现的3个必要条件:

        1.多线程环境

        2.多线程共享同一资源

        3.对资源进行非原子性操作

示例:

 上图是利用TestNG实现并发,效果为10个线程执行100个操作,控制台输出中可以看到前9个的号码都是15100000000,这里就有线程安全的问题,10个线程最开始会同时读取第一个号码资源,读取到的都是15100000000,所以执行的都是这个号码,然后才会递增.那么我们通过干预各线程之间对资源的操作来解决这种问题.

保证线程安全的常用方法:

        1.Synchronized:保证方法内部或代码块内部资源或数据的互斥访问(哪里需要Synchronized加哪里).达到某资源在同一时间最多只能有一个线程访问的效果.

        2.java.util.concurrent.atomic:提供一列类,包括:AtomicBoolean,AtomicInteger,AtomicLong类,使用这些类来声明变量或资源时可以保证对其操作时具有原子性,以此达到线程安全的效果.

 适用场景:数据流转逻辑多,逻辑复杂,中间件较多的逻辑都需要考虑并发场景的测试.

 例如,测试话单采集服务的时候,有一个逻辑为生成的话单要从A平台经数据清洗后传递到B平台,A平台的话单原单和清洗处理后的话单数据都要利用mq(消息队列,消息中间件,实现各模块数据存取速度不一致的问题,达到各模块解耦的效果),利用脚本实现B平台在消费mq中消息时,如果话单数据较多会出现消费速度忽快忽慢的情况,后面经定位为并发情况下,生产了大量消息在消息队列中,逻辑中采用了将所有消息数据都加载进内存,造成了内存吃紧,处理慢的情况.如果仅仅是功能测试,是发现不了此问题的,但是线上会真正暴露的问题.

2.数据库锁:

        系统性能为题,如果定位到是数据库性能瓶颈,可能很大一部分原因都是因为索引的问题.例如:

        以mysql为例,就存在行锁定表锁定以及事务中的死锁.

事务中的死锁:事务都遵从ACID特性,即原子性,一致性,持久性,隔离性.如果两个事务都持有对方需要的数据,并且都在等待对方释放锁,就会出现死锁的情况(B态度:A放我才放,A态度:B放我才放). 

行锁定:我们在update更新/delete删除某行数据时,mysql会自动添加排它锁,即行锁定,update和delete某行数据时,如果where条件中的字段存在未添加索引的情况,mysql就会自动将行锁定升级为表锁定(行锁定时,其他逻辑操作无法操作该行数据,表锁定时,其他逻辑无法操作该整张表).

拿遇到过的一个bug举例:混合场景下,有的用户会读取某行数据是否存在,不存在的话会插入数据;有的用户会写入更新某行数据,利用多线程实现并发场景时,某用户写入更新数据时因为检索条件中的字段未加合适的索引,mysql会自动将行级锁定升级为表级锁定,导致整张表被锁定,线程中那些做插入数据的操作就会失败,导致大量报错.所以,业务逻辑中如果存在多个逻辑同时处理某个表的数据,就需要考虑这些逻辑并行处理的场景.