网站制作多少页,网站备案需先做网站吗,制作网页时我们应当规避侵权风险,免费找图片素材的网站很多时候#xff0c;我们为了提升接口的性能#xff0c;会把之前单线程同步执行的代码#xff0c;改成多线程异步执行。比如#xff1a;查询用户信息接口#xff0c;需要返回用户基本信息、积分信息、成长值信息#xff0c;而用户、积分和成长值#xff0c;需要调用不同…很多时候我们为了提升接口的性能会把之前单线程同步执行的代码改成多线程异步执行。比如查询用户信息接口需要返回用户基本信息、积分信息、成长值信息而用户、积分和成长值需要调用不同的接口获取数据。如果查询用户信息接口同步调用三个接口获取数据会非常耗时。这就非常有必要把三个接口调用改成异步调用最后汇总结果。再比如注册用户接口该接口主要包含写用户表分配权限配置用户导航页发通知消息等功能。该用户注册接口包含的业务逻辑比较多如果在接口中同步执行这些代码该接口响应时间会非常慢。这时就需要把业务逻辑梳理一下划分核心逻辑和非核心逻辑。这个例子中的核心逻辑是写用户表和分配权限非核心逻辑是配置用户导航页和发通知消息。显然核心逻辑必须在接口中同步执行而非核心逻辑可以多线程异步执行。等等。需要使用多线程的业务场景太多了使用多线程异步执行的好处不言而喻。但我要说的是如果多线程没有使用好它也会给我们带来很多意想不到的问题不信往后继续看。今天跟大家一起聊聊代码改成多线程调用之后带来的9大问题。1.获取不到返回值如果你通过直接继承Thread类或者实现Runnable接口的方式去创建线程。那么恭喜你你将没法获取该线程方法的返回值。使用线程的场景有两种不需要关注线程方法的返回值。需要关注线程方法的返回值。大部分业务场景是不需要关注线程方法返回值的但如果我们有些业务需要关注线程方法的返回值该怎么处理呢查询用户信息接口需要返回用户基本信息、积分信息、成长值信息而用户、积分和成长值需要调用不同的接口获取数据。如下图所示在Java8之前可以通过实现Callable接口获取线程返回结果。Java8以后通过CompleteFuture类实现该功能。我们这里以CompleteFuture为例public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException {final UserInfo userInfo new UserInfo();CompletableFuture userFuture CompletableFuture.supplyAsync(() - {getRemoteUserAndFill(id, userInfo);return Boolean.TRUE;}, executor);CompletableFuture bonusFuture CompletableFuture.supplyAsync(() - {getRemoteBonusAndFill(id, userInfo);return Boolean.TRUE;}, executor);CompletableFuture growthFuture CompletableFuture.supplyAsync(() - {getRemoteGrowthAndFill(id, userInfo);return Boolean.TRUE;}, executor);CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join();userFuture.get();bonusFuture.get();growthFuture.get();return userInfo;
}温馨提醒一下这两种方式别忘了使用线程池。示例中我用到了executor表示自定义的线程池为了防止高并发场景下出现线程过多的问题。此外Fork/join框架也提供了执行任务并返回结果的能力。2.数据丢失我们还是以注册用户接口为例该接口主要包含写用户表分配权限配置用户导航页发通知消息等功能。其中写用户表和分配权限功能需要在一个事务中同步执行。而剩余的配置用户导航页和发通知消息功能使用多线程异步执行。表面上看起来没问题。但如果前面的写用户表和分配权限功能成功了用户注册接口就直接返回成功了。但如果后面异步执行的配置用户导航页或发通知消息功能失败了怎么办如下图所示该接口前面明明已经提示用户成功了但结果后面又有一部分功能在多线程异步执行中失败了。这时该如何处理呢没错你可以做失败重试。但如果重试了一定的次数还是没有成功这条请求数据该如何处理呢如果不做任何处理该数据是不是就丢掉了为了防止数据丢失可以用如下方案使用mq异步处理。在分配权限之后发送一条mq消息到mq服务器然后在mq的消费者中使用多线程去配置用户导航页和发通知消息。如果mq消费者中处理失败了可以自己重试。使用job异步处理。在分配权限之后往任务表中写一条数据。然后有个job定时扫描该表然后配置用户导航页和发通知消息。如果job处理某条数据失败了可以在表中记录一个重试次数然后不断重试。但该方案有个缺点就是实时性可能不太高。3.顺序问题如果你使用了多线程就必须接受一个非常现实的问题即顺序问题。假如之前代码的执行顺序是a,b,c改成多线程执行之后代码的执行顺序可能变成了a,c,b。这个跟cpu调度算法有关例如public static void main(String[] args) {Thread thread1 new Thread(() - System.out.println(a));Thread thread2 new Thread(() - System.out.println(b));Thread thread3 new Thread(() - System.out.println(c));thread1.start();thread2.start();thread3.start();
}执行结果a
c
b那么来自灵魂的一问如何保证线程的顺序呢即线程启动的顺序是a,b,c执行的顺序也是a,b,c。如下图所示3.1 joinThread类的join方法它会让主线程等待子线程运行结束后才能继续运行。列如public static void main(String[] args) throws InterruptedException {Thread thread1 new Thread(() - System.out.println(a));Thread thread2 new Thread(() - System.out.println(b));Thread thread3 new Thread(() - System.out.println(c));thread1.start();thread1.join();thread2.start();thread2.join();thread3.start();
}执行结果永远都是a
b
c3.2 newSingleThreadExecutor我们可以使用JDK自带的Excutors类的newSingleThreadExecutor方法创建一个单线程的线程池。例如public static void main(String[] args) {ExecutorService executorService Executors.newSingleThreadExecutor();Thread thread1 new Thread(() - System.out.println(a));Thread thread2 new Thread(() - System.out.println(b));Thread thread3 new Thread(() - System.out.println(c));executorService.submit(thread1);executorService.submit(thread2);executorService.submit(thread3);executorService.shutdown();
}执行结果永远都是a
b
c使用Excutors类的newSingleThreadExecutor方法创建的单线程的线程池使用了LinkedBlockingQueue作为队列而此队列按 FIFO先进先出排序元素。添加到队列的顺序是a,b,c则执行的顺序也是a,b,c。3.3 CountDownLatchCountDownLatch是一个同步工具类它允许一个或多个线程一直等待直到其他线程执行完后再执行。例如public class ThreadTest {public static void main(String[] args) throws InterruptedException {CountDownLatch latch1 new CountDownLatch(0);CountDownLatch latch2 new CountDownLatch(1);CountDownLatch latch3 new CountDownLatch(1);Thread thread1 new Thread(new TestRunnable(latch1, latch2, a));Thread thread2 new Thread(new TestRunnable(latch2, latch3, b));Thread thread3 new Thread(new TestRunnable(latch3, latch3, c));thread1.start();thread2.start();thread3.start();}
}class TestRunnable implements Runnable {private CountDownLatch latch1;private CountDownLatch latch2;private String message;TestRunnable(CountDownLatch latch1, CountDownLatch latch2, String message) {this.latch1 latch1;this.latch2 latch2;this.message message;}Overridepublic void run() {try {latch1.await();System.out.println(message);} catch (InterruptedException e) {e.printStackTrace();}latch2.countDown();}
}执行结果永远都是a
b
c此外使用CompletableFuture的thenRun方法也能多线程的执行顺序在这里就不一一介绍了。4.线程安全问题既然使用了线程伴随而来的还会有线程安全问题。假如现在有这样一个需求用多线程执行查询方法然后把执行结果添加到一个list集合中。代码如下ListUser list Lists.newArrayList();dataList.stream().map(data - CompletableFuture.supplyAsync(() - query(list, data), asyncExecutor)));
CompletableFuture.allOf(futureArray).join();使用CompletableFuture异步多线程执行query方法public void query(ListUser list, UserEntity condition) {User user queryByCondition(condition);if(Objects.isNull(user)) {return;}list.add(user);UserExtend userExtend queryByOther(condition);if(Objects.nonNull(userExtend)) {user.setExtend(userExtend.getInfo());}
}在query方法中将获取的查询结果添加到list集合中。结果list会出现线程安全问题有时候会少数据当然也不一定是必现的。这是因为ArrayList是非线程安全的没有使用synchronized等关键字修饰。如何解决这个问题呢答使用CopyOnWriteArrayList集合代替普通的ArrayList集合CopyOnWriteArrayList是一个线程安全的机会。只需一行小小的改动即可ListUser list Lists.newCopyOnWriteArrayList();温馨的提醒一下这里创建集合的方式用了google的collect包。5.ThreadLocal获取数据异常我们都知道JDK为了解决线程安全问题提供了一种用空间换时间的新思路ThreadLocal。它的核心思想是共享变量在每个线程都有一个副本每个线程操作的都是自己的副本对另外的线程没有影响。例如Service
public class ThreadLocalService {private static final ThreadLocalInteger threadLocal new ThreadLocal();public void add() {threadLocal.set(1);doSamething();Integer integer threadLocal.get();}
}ThreadLocal在普通中线程中的确能够获取正确的数据。但在真实的业务场景中一般很少用单独的线程绝大多数都是用的线程池。那么在线程池中如何获取ThreadLocal对象生成的数据呢如果直接使用普通ThreadLocal显然是获取不到正确数据的。我们先试试InheritableThreadLocal具体代码如下private static void fun1() {InheritableThreadLocalInteger threadLocal new InheritableThreadLocal();threadLocal.set(6);System.out.println(父线程获取数据 threadLocal.get());ExecutorService executorService Executors.newSingleThreadExecutor();threadLocal.set(6);executorService.submit(() - {System.out.println(第一次从线程池中获取数据 threadLocal.get());});threadLocal.set(7);executorService.submit(() - {System.out.println(第二次从线程池中获取数据 threadLocal.get());});
}执行结果父线程获取数据6
第一次从线程池中获取数据6
第二次从线程池中获取数据6由于这个例子中使用了单例线程池固定线程数是1。第一次submit任务的时候该线程池会自动创建一个线程。因为使用了InheritableThreadLocal所以创建线程时会调用它的init方法将父线程中的inheritableThreadLocals数据复制到子线程中。所以我们看到在主线程中将数据设置成6第一次从线程池中获取了正确的数据6。之后在主线程中又将数据改成7但在第二次从线程池中获取数据却依然是6。因为第二次submit任务的时候线程池中已经有一个线程了就直接拿过来复用不会再重新创建线程了。所以不会再调用线程的init方法所以第二次其实没有获取到最新的数据7还是获取的老数据6。那么这该怎么办呢答使用TransmittableThreadLocal它并非JDK自带的类而是阿里巴巴开源jar包中的类。可以通过如下pom文件引入该jar包dependencygroupIdcom.alibaba/groupIdartifactIdtransmittable-thread-local/artifactIdversion2.11.0/versionscopecompile/scope
/dependency代码调整如下private static void fun2() throws Exception {TransmittableThreadLocalInteger threadLocal new TransmittableThreadLocal();threadLocal.set(6);System.out.println(父线程获取数据 threadLocal.get());ExecutorService ttlExecutorService TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));threadLocal.set(6);ttlExecutorService.submit(() - {System.out.println(第一次从线程池中获取数据 threadLocal.get());});threadLocal.set(7);ttlExecutorService.submit(() - {System.out.println(第二次从线程池中获取数据 threadLocal.get());});}执行结果父线程获取数据6
第一次从线程池中获取数据6
第二次从线程池中获取数据7我们看到使用了TransmittableThreadLocal之后第二次从线程中也能正确获取最新的数据7了。nice。如果你仔细观察这个例子你可能会发现代码中除了使用TransmittableThreadLocal类之外还使用了TtlExecutors.getTtlExecutorService方法去创建ExecutorService对象。这是非常重要的地方如果没有这一步TransmittableThreadLocal在线程池中共享数据将不会起作用。创建ExecutorService对象底层的submit方法会TtlRunnable或TtlCallable对象。以TtlRunnable类为例它实现了Runnable接口同时还实现了它的run方法public void run() {MapTransmittableThreadLocal?, Object copied (Map)this.copiedRef.get();if (copied ! null (!this.releaseTtlValueReferenceAfterRun || this.copiedRef.compareAndSet(copied, (Object)null))) {Map backup TransmittableThreadLocal.backupAndSetToCopied(copied);try {this.runnable.run();} finally {TransmittableThreadLocal.restoreBackup(backup);}} else {throw new IllegalStateException(TTL value reference is released after run!);}
}这段代码的主要逻辑如下把当时的ThreadLocal做个备份然后将父类的ThreadLocal拷贝过来。执行真正的run方法可以获取到父类最新的ThreadLocal数据。从备份的数据中恢复当时的ThreadLocal数据。如果你想进一步了解ThreadLocal的工作原理可以看看我的另一篇文章《ThreadLocal夺命11连问》6.OOM问题众所周知使用多线程可以提升代码执行效率但也不是绝对的。对于一些耗时的操作使用多线程确实可以提升代码执行效率。但线程不是创建越多越好如果线程创建多了也可能会导致OOM异常。例如Caused by:
java.lang.OutOfMemoryError: unable to create new native thread在JVM中创建一个线程默认需要占用1M的内存空间。如果创建了过多的线程必然会导致内存空间不足从而出现OOM异常。除此之外如果使用线程池的话特别是使用固定大小线程池即使用Executors.newFixedThreadPool方法创建的线程池。该线程池的核心线程数和最大线程数是一样的是一个固定值而存放消息的队列是LinkedBlockingQueue。该队列的最大容量是Integer.MAX_VALUE也就是说如果使用固定大小线程池存放了太多的任务有可能也会导致OOM异常。java.lang.OutOfMemeryError:Java heap space7.CPU使用率飙高不知道你有没有做过excel数据导入功能需要将一批excel的数据导入到系统中。每条数据都有些业务逻辑如果单线程导入所有的数据导入效率会非常低。于是改成了多线程导入。如果excel中有大量的数据很可能会出现CPU使用率飙高的问题。我们都知道如果代码出现死循环cpu使用率会飚的很多高。因为代码一直在某个线程中循环没法切换到其他线程cpu一直被占用着所以会导致cpu使用率一直高居不下。而多线程导入大量的数据虽说没有死循环代码但由于多个线程一直在不停的处理数据导致占用了cpu很长的时间。也会出现cpu使用率很高的问题。那么如何解决这个问题呢答使用Thread.sleep休眠一下。在线程中处理完一条数据休眠10毫秒。当然CPU使用率飙高的原因很多多线程处理数据和死循环只是其中两种还有比如频繁GC、正则匹配、频繁序列化和反序列化等。后面我会写一篇介绍CPU使用率飙高的原因的专题文章感兴趣的小伙伴可以关注一下我后续的文章。8.事务问题在实际项目开发中多线程的使用场景还是挺多的。如果spring事务用在多线程场景中会有问题吗例如Slf4j
Service
public class UserService {Autowiredprivate UserMapper userMapper;Autowiredprivate RoleService roleService;Transactionalpublic void add(UserModel userModel) throws Exception {userMapper.insertUser(userModel);new Thread(() - {roleService.doOtherThing();}).start();}
}Service
public class RoleService {Transactionalpublic void doOtherThing() {System.out.println(保存role表数据);}
}从上面的例子中我们可以看到事务方法add中调用了事务方法doOtherThing但是事务方法doOtherThing是在另外一个线程中调用的。这样会导致两个方法不在同一个线程中获取到的数据库连接不一样从而是两个不同的事务。如果想doOtherThing方法中抛了异常add方法也回滚是不可能的。如果看过spring事务源码的朋友可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个mapkey是数据源value是数据库连接。private static final ThreadLocalMapObject, Object resources new NamedThreadLocal(Transactional resources);我们说的同一个事务其实是指同一个数据库连接只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程拿到的数据库连接肯定是不一样的所以是不同的事务。所以不要在事务中开启另外的线程去处理业务逻辑这样会导致事务失效。9.导致服务挂掉使用多线程会导致服务挂掉这不是危言耸听而是确有其事。假设现在有这样一种业务场景在mq的消费者中需要调用订单查询接口查到数据之后写入业务表中。本来是没啥问题的。突然有一天mq生产者跑了一个批量数据处理的job导致mq服务器上堆积了大量的消息。此时mq消费者的处理速度远远跟不上mq消息的生产速度导致的结果是出现了大量的消息堆积对用户有很大的影响。为了解决这个问题mq消费者改成多线程处理直接使用了线程池并且最大线程数配置成了20。这样调整之后消息堆积问题确实得到了解决。但带来了另外一个更严重的问题订单查询接口并发量太大了有点扛不住压力导致部分节点的服务直接挂掉。为了解决问题不得不临时加服务节点。在mq的消费者中使用多线程调用接口时一定要评估好接口能够承受的最大访问量防止因为压力过大而导致服务挂掉的问题。