记一次Nacos的issue修复之并发导致的NPE异常
ISSUE
Spring boot 應用啟動被終止 #21
錯誤分析
DeferredApplicationEventPublisher的繼承關系
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.event.ContextRefreshedEvent;public class DeferredApplicationEventPublisher implements ApplicationEventPublisher, ApplicationListener<ContextRefreshedEvent> {... } 復制代碼DeferredApplicationEventPublisher的依賴圖
現在來分析具體出現NPE錯誤的原因
先看EventPublishingConfigService中的addListener
public void addListener(String dataId, String group, Listener listener) throws NacosException {Listener listenerAdapter = new DelegatingEventPublishingListener(configService, dataId, group, applicationEventPublisher, executor, listener);configService.addListener(dataId, group, listenerAdapter);publishEvent(new NacosConfigListenerRegisteredEvent(configService, dataId, group, listener, true)); } 復制代碼然后看DelegatingEventPublishingListener代碼的繼承關系
import com.alibaba.nacos.api.config.ConfigService; import com.alibaba.nacos.api.config.listener.Listener; import org.springframework.context.ApplicationEventPublisher;import java.util.concurrent.Executor;final class DelegatingEventPublishingListener implements Listener {DelegatingEventPublishingListener(ConfigService configService, String dataId, String groupId, ApplicationEventPublisher applicationEventPublisher, Executor executor, Listener delegate) {this.configService = configService;this.dataId = dataId;this.groupId = groupId;this.applicationEventPublisher = applicationEventPublisher;this.executor = executor;this.delegate = delegate;} } 復制代碼可以看到,在創建DelegatingEventPublishingListener對象的時候,會傳入一個線程池Executor,以及一個ApplicationEventPublisher(其實就是DeferredApplicationEventPublisher)
然后再看看CacheData.safeNotifyListener()方法做了什么操作
private void safeNotifyListener(final String dataId, final String group, final String content, final String md5, final ManagerListenerWrap listenerWrap) {final Listener listener = listenerWrap.listener;Runnable job = new Runnable() {public void run() {ClassLoader myClassLoader = Thread.currentThread().getContextClassLoader();ClassLoader appClassLoader = listener.getClass().getClassLoader();try {if (listener instanceof AbstractSharedListener) {AbstractSharedListener adapter = (AbstractSharedListener)listener;adapter.fillContext(dataId, group);LOGGER.info("[{}] [notify-context] dataId={}, group={}, md5={}", name, dataId, group, md5);}// 執行回調之前先將線程classloader設置為具體webapp的classloader,以免回調方法中調用spi接口是出現異?;蝈e用(多應用部署才會有該問題)。Thread.currentThread().setContextClassLoader(appClassLoader);ConfigResponse cr = new ConfigResponse();cr.setDataId(dataId);cr.setGroup(group);cr.setContent(content);configFilterChainManager.doFilter(null, cr);String contentTmp = cr.getContent();listener.receiveConfigInfo(contentTmp);listenerWrap.lastCallMd5 = md5;LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,listener);} catch (NacosException de) {LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} errCode={} errMsg={}", name,dataId, group, md5, listener, de.getErrCode(), de.getErrMsg());} catch (Throwable t) {LOGGER.error("[{}] [notify-error] dataId={}, group={}, md5={}, listener={} tx={}", name, dataId, group,md5, listener, t.getCause());} finally {Thread.currentThread().setContextClassLoader(myClassLoader);}}};final long startNotify = System.currentTimeMillis();try {if (null != listener.getExecutor()) {listener.getExecutor().execute(job);} else {job.run();}}... } 復制代碼這里看到,safeNotifyListener是將事件廣播給所有的Listener,然后有一段及其重要的代碼段,它就是導致LinkedList出現并發使用的原因
listener.getExecutor().execute(job); 復制代碼這里還記得剛剛說過的DelegatingEventPublishingListener對象在創建之初有傳入Executor參數嗎?這里Listener調用Executor將上述的任務調入線程池中進行調度,因此,導致了DeferredApplicationEventPublisher可能存在并發的使用
錯誤復現
public class DeferrNPE {private static LinkedList<String> list = new LinkedList<>();private static CountDownLatch latch = new CountDownLatch(3);private static CountDownLatch start = new CountDownLatch(3);private static class MyListener implements Runnable {public void run() {start.countDown();try {start.await();} catch (InterruptedException e) {e.printStackTrace();}list.add(String.valueOf(System.currentTimeMillis()));latch.countDown();}}public static void main(String[] args) {MyListener l1 = new MyListener();MyListener l2 = new MyListener();MyListener l3 = new MyListener();new Thread(l1).start();new Thread(l2).start();new Thread(l3).start();try {latch.await();Iterator<String> iterator = list.iterator();while (iterator.hasNext()) {System.out.println(iterator.next());iterator.remove();}} catch (InterruptedException e) {e.printStackTrace();}}} 復制代碼最終修正
由于是非線程安全使用在并發的場景下,因此只能更改上層nacos-spring-context的容器使用,將原先的非線程安全的LinkedList轉為線程安全的ConcurrentLinkedQueue
轉載于:https://juejin.im/post/5cee66f66fb9a07eeb138b55
總結
以上是生活随笔為你收集整理的记一次Nacos的issue修复之并发导致的NPE异常的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Helm V3 新版本发布
- 下一篇: CentOS搭建FTP