class MultiThreadedJLanguageTool extends JLanguageTool {
  
  private final int threadPoolSize;
  private final ExecutorService threadPool;

  public MultiThreadedJLanguageTool(Language language) {
    this(language, null);
  }

  /**
   * @see #shutdown()
   * @param threadPoolSize the number of concurrent threads (use 0 or negative value for a default)
   * @since 2.9
   */
  public MultiThreadedJLanguageTool(Language language, int threadPoolSize) {
    this(language, null, threadPoolSize, null);
  }

  /**
   * @see #shutdown()
   */
  public MultiThreadedJLanguageTool(Language language, Language motherTongue) {
    this(language, motherTongue, getDefaultThreadCount(), null);
  }

  /**
   * @since 4.2
   */
  public MultiThreadedJLanguageTool(Language language, Language motherTongue, UserConfig userConfig) {
    this(language, motherTongue, getDefaultThreadCount(), userConfig);
  }

  /**
   * @see #shutdown()
   * @param threadPoolSize the number of concurrent threads
   * @since 4.2
   */
  public MultiThreadedJLanguageTool(Language language, Language motherTongue, int threadPoolSize,
      UserConfig userConfig) {
    this(language, motherTongue, threadPoolSize, null, userConfig);
  }

  /**
   * @see #shutdown()
   * @param threadPoolSize the number of concurrent threads
   * @since 4.2
   */
  public MultiThreadedJLanguageTool(Language language, Language motherTongue, int threadPoolSize,
                                    GlobalConfig globalConfig, UserConfig userConfig) {
    super(language, Collections.emptyList(), motherTongue, null, globalConfig, userConfig);
    this.threadPoolSize = threadPoolSize <= 0 ? getDefaultThreadCount() : threadPoolSize;
    threadPool = new ForkJoinPool(this.threadPoolSize, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, false);
  }

  /**
   * Call this to shut down the internally used thread pool.
   * @since 3.0
   */
  public void shutdown() {
    threadPool.shutdownNow();
  }

  /**
   * Call this to shut down the internally used thread pool after all running tasks are finished.
   * @since 3.1
   */
  public void shutdownWhenDone() {
    threadPool.shutdown();
  }

  private static int getDefaultThreadCount() {
    return Runtime.getRuntime().availableProcessors();
  }

  /**
   * When no thread pool size is configured, the number of available processors is returned.
   */
  protected int getThreadPoolSize() {
    return threadPoolSize;
  }
  
  /**
   * @return a fixed size executor with the given number of threads
   */
  protected ExecutorService getExecutorService() {
    return threadPool;
  }
  
  @Override
  protected List<AnalyzedSentence> analyzeSentences(List<String> sentences) throws IOException {
    if (sentences.size() < 2) {
      return super.analyzeSentences(sentences);
    }

    List<AnalyzedSentence> analyzedSentences = new ArrayList<>();
    
    ExecutorService executorService = getExecutorService();

    int j = 0;
    
    List<Callable<AnalyzedSentence>> callables = new ArrayList<>();
    for (String sentence : sentences) {
      AnalyzeSentenceCallable analyzeSentenceCallable = 
          ++j < sentences.size() 
            ? new AnalyzeSentenceCallable(sentence)
            : new ParagraphEndAnalyzeSentenceCallable(sentence);
      callables.add(analyzeSentenceCallable);
    }
    
    try {
      List<Future<AnalyzedSentence>> futures = executorService.invokeAll(callables);
      for (Future<AnalyzedSentence> future : futures) {
        AnalyzedSentence analyzedSentence = future.get();
        rememberUnknownWords(analyzedSentence);
        printSentenceInfo(analyzedSentence);
        analyzedSentences.add(analyzedSentence);
      }
      
    } catch (InterruptedException | ExecutionException e) {
      throw new RuntimeException(e);
    }
    
    return analyzedSentences;
  }
  
  
  @Override
  protected CheckResults performCheck(List<AnalyzedSentence> analyzedSentences, List<String> sentenceTexts,
                                         RuleSet ruleSet, ParagraphHandling paraMode,
                                         AnnotatedText annotatedText, RuleMatchListener listener, Mode mode, Level level, boolean checkRemoteRules, Set<ToneTag> toneTags) {<FILL_FUNCTION_BODY>}

  private class AnalyzeSentenceCallable implements Callable<AnalyzedSentence> {
    private final String sentence;

    private AnalyzeSentenceCallable(String sentence) {
      this.sentence = sentence;
    }

    @Override
    public AnalyzedSentence call() throws Exception {
      return getAnalyzedSentence(sentence);
    }
  }
  
  private final class ParagraphEndAnalyzeSentenceCallable extends AnalyzeSentenceCallable {
    private ParagraphEndAnalyzeSentenceCallable(String sentence) {
      super(sentence);
    }

    @Override
    public AnalyzedSentence call() throws Exception {
      return markAsParagraphEnd(super.call());
    }
  }
}

    List<Rule> allRules = ruleSet.allRules();
    List<SentenceData> sentences = computeSentenceData(analyzedSentences, sentenceTexts);

    Map<Rule, BitSet> map = new HashMap<>();
    for (int i = 0; i < sentences.size(); i++) {
      for (Rule rule : ruleSet.rulesForSentence(sentences.get(i).analyzed)) {
        map.computeIfAbsent(rule, __ -> new BitSet()).set(i);
      }
    }

    AtomicInteger ruleIndex = new AtomicInteger();
    Map<Integer, List<RuleMatch>> ruleMatches = new TreeMap<>();
    List<Range> ignoreRanges = new ArrayList<>();
    List<Future<?>> futures = IntStream.range(0, getThreadPoolSize()).mapToObj(__ -> getExecutorService().submit(() -> {
      while (true) {
        int index = ruleIndex.getAndIncrement();
        if (index >= allRules.size()) return null;

        Rule rule = allRules.get(index);
        BitSet applicable = map.get(rule);
        if (applicable == null) continue;

        // less need for special treatment of remote rules when execution is already parallel
        CheckResults res = new TextCheckCallable(RuleSet.plain(Collections.singletonList(rule)),
          RuleSet.filterList(applicable, sentences),
          paraMode, annotatedText, listener, mode, level, true, toneTags).call();
        if (!res.getRuleMatches().isEmpty()) {
          synchronized (ruleMatches) {
            ruleMatches.put(index, res.getRuleMatches());
          }
          synchronized (ignoreRanges) {
            ignoreRanges.addAll(res.getIgnoredRanges());
          }
        }
      }
    })).collect(Collectors.toList());

    try {
      for (Future<?> future : futures) {
        future.get();
      }
    } catch (InterruptedException | ExecutionException e) {
      throw new RuntimeException(e);
    }

    List<RuleMatch> rm = applyCustomFilters(Lists.newArrayList(Iterables.concat(ruleMatches.values())), annotatedText);
    return new CheckResults(rm, ignoreRanges);
  
<methods>public void <init>(org.languagetool.Language, org.languagetool.Language) ,public void <init>(org.languagetool.Language) ,public void <init>(org.languagetool.Language, org.languagetool.Language, org.languagetool.ResultCache) ,public void <init>(org.languagetool.Language, org.languagetool.ResultCache, org.languagetool.UserConfig) ,public void <init>(org.languagetool.Language, List<org.languagetool.Language>, org.languagetool.Language, org.languagetool.ResultCache, org.languagetool.GlobalConfig, org.languagetool.UserConfig) ,public void <init>(org.languagetool.Language, List<org.languagetool.Language>, org.languagetool.Language, org.languagetool.ResultCache, org.languagetool.GlobalConfig, org.languagetool.UserConfig, boolean) ,public void <init>(org.languagetool.Language, org.languagetool.Language, org.languagetool.ResultCache, org.languagetool.UserConfig) ,public void activateLanguageModelRules(java.io.File) throws java.io.IOException,public void activateRemoteRules(java.io.File) throws java.io.IOException,public void activateRemoteRules(List<org.languagetool.rules.RemoteRuleConfig>) throws java.io.IOException,public void addMatchFilter(org.languagetool.rules.RuleMatchFilter) ,public void addRule(org.languagetool.rules.Rule) ,public static void addTemporaryFile(java.io.File) ,public org.languagetool.rules.RuleMatch adjustRuleMatchPos(org.languagetool.rules.RuleMatch, int, int, int, java.lang.String, org.languagetool.markup.AnnotatedText) ,public List<org.languagetool.AnalyzedSentence> analyzeText(java.lang.String) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> check(java.lang.String) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> check(java.lang.String, org.languagetool.JLanguageTool.Level) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> check(java.lang.String, org.languagetool.RuleMatchListener) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> check(java.lang.String, boolean, org.languagetool.JLanguageTool.ParagraphHandling) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> check(java.lang.String, boolean, org.languagetool.JLanguageTool.ParagraphHandling, org.languagetool.RuleMatchListener) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> check(org.languagetool.markup.AnnotatedText) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> check(org.languagetool.markup.AnnotatedText, org.languagetool.RuleMatchListener) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> check(org.languagetool.markup.AnnotatedText, boolean, org.languagetool.JLanguageTool.ParagraphHandling) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> check(org.languagetool.markup.AnnotatedText, boolean, org.languagetool.JLanguageTool.ParagraphHandling, org.languagetool.RuleMatchListener) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> check(org.languagetool.markup.AnnotatedText, boolean, org.languagetool.JLanguageTool.ParagraphHandling, org.languagetool.RuleMatchListener, org.languagetool.JLanguageTool.Level) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> check(org.languagetool.markup.AnnotatedText, boolean, org.languagetool.JLanguageTool.ParagraphHandling, org.languagetool.RuleMatchListener, org.languagetool.JLanguageTool.Mode, org.languagetool.JLanguageTool.Level) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> check(org.languagetool.markup.AnnotatedText, boolean, org.languagetool.JLanguageTool.ParagraphHandling, org.languagetool.RuleMatchListener, org.languagetool.JLanguageTool.Mode, org.languagetool.JLanguageTool.Level, java.lang.Long) throws java.io.IOException,public org.languagetool.CheckResults check2(org.languagetool.markup.AnnotatedText, boolean, org.languagetool.JLanguageTool.ParagraphHandling, org.languagetool.RuleMatchListener, org.languagetool.JLanguageTool.Mode, org.languagetool.JLanguageTool.Level, java.lang.Long) throws java.io.IOException,public org.languagetool.CheckResults check2(org.languagetool.markup.AnnotatedText, boolean, org.languagetool.JLanguageTool.ParagraphHandling, org.languagetool.RuleMatchListener, org.languagetool.JLanguageTool.Mode, org.languagetool.JLanguageTool.Level, Set<org.languagetool.ToneTag>, java.lang.Long) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> checkAnalyzedSentence(org.languagetool.JLanguageTool.ParagraphHandling, List<org.languagetool.rules.Rule>, org.languagetool.AnalyzedSentence) throws java.io.IOException,public List<org.languagetool.rules.RuleMatch> checkAnalyzedSentence(org.languagetool.JLanguageTool.ParagraphHandling, List<org.languagetool.rules.Rule>, org.languagetool.AnalyzedSentence, boolean) throws java.io.IOException,public void disableCategory(org.languagetool.rules.CategoryId) ,public void disableRule(java.lang.String) ,public void disableRules(List<java.lang.String>) ,public void enableRule(java.lang.String) ,public void enableRuleCategory(org.languagetool.rules.CategoryId) ,public List<org.languagetool.rules.Rule> getAllActiveOfficeRules() ,public List<org.languagetool.rules.Rule> getAllActiveRules() ,public List<org.languagetool.rules.Rule> getAllRules() ,public List<org.languagetool.rules.spelling.SpellingCheckRule> getAllSpellingCheckRules() ,public org.languagetool.AnalyzedSentence getAnalyzedSentence(java.lang.String) throws java.io.IOException,public Map<org.languagetool.rules.CategoryId,org.languagetool.rules.Category> getCategories() ,public static synchronized org.languagetool.broker.ClassBroker getClassBroker() ,public static synchronized org.languagetool.broker.ResourceDataBroker getDataBroker() ,public Set<java.lang.String> getDisabledRules() ,public org.languagetool.Language getLanguage() ,public static java.util.ResourceBundle getMessageBundle() ,public static java.util.ResourceBundle getMessageBundle(org.languagetool.Language) ,public List<org.languagetool.rules.patterns.AbstractPatternRule> getPatternRulesByIdAndSubId(java.lang.String, java.lang.String) ,public org.languagetool.AnalyzedSentence getRawAnalyzedSentence(java.lang.String) throws java.io.IOException,public List<java.lang.String> getUnknownWords() ,public boolean isCategoryDisabled(org.languagetool.rules.CategoryId) ,public static boolean isCustomPasswordAuthenticatorUsed() ,public List<org.languagetool.rules.patterns.AbstractPatternRule> loadFalseFriendRules(java.lang.String) throws javax.xml.parsers.ParserConfigurationException, org.xml.sax.SAXException, java.io.IOException,public List<org.languagetool.rules.patterns.AbstractPatternRule> loadPatternRules(java.lang.String) throws java.io.IOException,public static void removeTemporaryFiles() ,public List<java.lang.String> sentenceTokenize(java.lang.String) ,public void setCheckCancelledCallback(org.languagetool.JLanguageTool.CheckCancelledCallback) ,public static synchronized void setClassBrokerBroker(org.languagetool.broker.ClassBroker) ,public void setCleanOverlappingMatches(boolean) ,public void setConfigValues(Map<java.lang.String,java.lang.Integer>) ,public static synchronized void setDataBroker(org.languagetool.broker.ResourceDataBroker) ,public void setListUnknownWords(boolean) ,public void setMaxErrorsPerWordRate(float) ,public void setOutput(java.io.PrintStream) ,public static void useCustomPasswordAuthenticator(boolean) <variables>public static final java.lang.String BUILD_DATE,public static final java.lang.String CUSTOM_PATTERN_FILE,public static final java.lang.String DICTIONARY_FILENAME_EXTENSION,public static final java.lang.String FALSE_FRIEND_FILE,public static final java.lang.String GIT_SHORT_ID,public static final java.lang.String MESSAGE_BUNDLE,public static final java.lang.String PARAGRAPH_END_TAGNAME,public static final java.lang.String PATTERN_FILE,public static final java.lang.String SENTENCE_END_TAGNAME,public static final java.lang.String SENTENCE_START_TAGNAME,public static final java.lang.String STYLE_FILE,public static final java.lang.String VERSION,private static final java.util.regex.Pattern ZERO_WIDTH_NBSP,private final non-sealed List<org.languagetool.Language> altLanguages,private final non-sealed List<org.languagetool.rules.Rule> builtinRules,private final non-sealed org.languagetool.ResultCache cache,private org.languagetool.JLanguageTool.CheckCancelledCallback checkCancelledCallback,private static org.languagetool.broker.ClassBroker classBroker,private boolean cleanOverlappingMatches,private static org.languagetool.broker.ResourceDataBroker dataBroker,private final non-sealed org.languagetool.ShortDescriptionProvider descProvider,private final Set<org.languagetool.rules.CategoryId> disabledRuleCategories,private final Set<java.lang.String> disabledRules,private final Set<org.languagetool.rules.CategoryId> enabledRuleCategories,private final Set<java.lang.String> enabledRules,private final non-sealed org.languagetool.GlobalConfig globalConfig,private final non-sealed boolean inputLogging,private final non-sealed org.languagetool.Language language,private boolean listUnknownWords,private static final Logger logger,private final List<org.languagetool.rules.RuleMatchFilter> matchFilters,private float maxErrorsPerWordRate,private final non-sealed org.languagetool.Language motherTongue,private final Set<java.lang.String> optionalLanguageModelRules,private java.io.PrintStream printStream,private final Map<org.languagetool.LevelToneTagCacheKey,org.languagetool.rules.patterns.RuleSet> ruleSetCache,private static final List<java.io.File> temporaryFiles,private Set<java.lang.String> unknownWords,private static volatile boolean useCustomPasswordAuthenticator,private final non-sealed org.languagetool.UserConfig userConfig,private final List<org.languagetool.rules.Rule> userRules