Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8296937

Array initialization race condition on aarch64 macOS in multi-threaded code

XMLWordPrintable

    • aarch64
    • os_x

      ADDITIONAL SYSTEM INFORMATION :
      JDK 19 for aarch64 CPU on MacOS on MacBook Pro (16-inch, 2021).

      Note that I cannot reproduce the problem on my x86_86 CPU MacBook Pro 2017.

      A DESCRIPTION OF THE PROBLEM :
      The following code produces the following output, which shouldn't be possible. An array that cannot contain null values (and is never modified) suddenly contains null values, and then doesn't contain any null values when printing the array a few lines later:
      ----------------------------------------------
      Object o1 = seq1[i1];
      Object o2 = seq2[i2];

      try {
      Objects.requireNonNull(o1, "ARRAY ELEMENTS CANNOT BE NULL");
      Objects.requireNonNull(o2, "ARRAY ELEMENTS CANNOT BE NULL");
      } catch (NullPointerException e) {
      System.out.println("o1 was " + o1);
      System.out.println("o2 was " + o2);
      System.out.println("seq1 was " + asList(seq1));
      System.out.println("seq2 was " + asList(seq2));
      throw e;
      }
      ----------------------------------------------
      o1 was java.text.RuleBasedCollationKey@3e88889
      o2 was null
      seq1 was [java.text.RuleBasedCollationKey@3e88889, java.text.RuleBasedCollationKey@3f6a00a, java.text.RuleBasedCollationKey@404b78b, java.text.RuleBasedCollationKey@412cf0c, java.text.RuleBasedCollationKey@420e68d]
      seq2 was [java.text.RuleBasedCollationKey@412cf0c, java.text.RuleBasedCollationKey@412cf0c, java.text.RuleBasedCollationKey@412cf0c, java.text.RuleBasedCollationKey@42efe0e, java.text.RuleBasedCollationKey@43d158f]
      ----------------------------------------------

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      Please run the executable test case below. You may need to try it a few times. On my MacBook Pro (16-inch, 2021) I can reproduce the problem reliably almost every time (but in rare cases it runs through without error).

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      The test should give the same result for every find(query, index) method call, and not randomly error out sometimes, with an NPE where there cannot be an NPE.
      ACTUAL -
      ...
      95-04: [[12345]]
      o1 was java.text.RuleBasedCollationKey@3e88889
      o2 was null
      seq1 was [java.text.RuleBasedCollationKey@3e88889, java.text.RuleBasedCollationKey@3f6a00a, java.text.RuleBasedCollationKey@404b78b, java.text.RuleBasedCollationKey@412cf0c, java.text.RuleBasedCollationKey@420e68d]
      seq2 was [java.text.RuleBasedCollationKey@404b78b, java.text.RuleBasedCollationKey@3f6a00a, java.text.RuleBasedCollationKey@4594491, java.text.RuleBasedCollationKey@412cf0c, java.text.RuleBasedCollationKey@3da7108]
      Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.NullPointerException: ARRAY ELEMENTS CANNOT BE NULL
      at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
      at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
      at test.ArrayInitializerRaceConditionTest.main(ArrayInitializerRaceConditionTest.java:44)
      Caused by: java.lang.NullPointerException: ARRAY ELEMENTS CANNOT BE NULL
      at java.base/java.util.Objects.requireNonNull(Objects.java:259)
      at test.ArrayInitializerRaceConditionTest.equals(ArrayInitializerRaceConditionTest.java:123)
      at test.ArrayInitializerRaceConditionTest.firstCommonSequence(ArrayInitializerRaceConditionTest.java:103)
      at test.ArrayInitializerRaceConditionTest.matchFirstCommonSequence(ArrayInitializerRaceConditionTest.java:85)
      at test.ArrayInitializerRaceConditionTest.find(ArrayInitializerRaceConditionTest.java:67)
      at test.ArrayInitializerRaceConditionTest.lambda$3(ArrayInitializerRaceConditionTest.java:36)
      at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
      at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
      at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
      at java.base/java.lang.Thread.run(Thread.java:1589)


      ---------- BEGIN SOURCE ----------
      package test;

      import static java.util.Arrays.*;
      import static java.util.stream.Collectors.*;

      import java.text.CollationKey;
      import java.text.Collator;
      import java.util.ArrayList;
      import java.util.LinkedHashSet;
      import java.util.List;
      import java.util.Locale;
      import java.util.Map;
      import java.util.Objects;
      import java.util.Set;
      import java.util.concurrent.ConcurrentHashMap;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      import java.util.concurrent.Future;
      import java.util.function.Function;
      import java.util.regex.Pattern;
      import java.util.stream.IntStream;

      public class ArrayInitializerRaceConditionTest {

      public static void main(String[] args) throws Exception {
      for (int k = 0; k <= 100; k++) {
      for (int n = 4; n <= 16; n++) {
      Map<String, CollationKey> cache = new ConcurrentHashMap<String, CollationKey>();
      List<IndexEntry> index = IntStream.range(0, 50000).mapToObj(i -> new IndexEntry(i, o -> tokenize(o, cache))).collect(toList());
      CollationKey[] query = tokenize(12345, cache);

      ExecutorService threadPool = Executors.newFixedThreadPool(n);

      List<Future<List<IndexEntry>>> tasks = IntStream.range(0, n).mapToObj(t -> {
      return threadPool.submit(() -> {
      return find(query, index);
      });
      }).collect(toList());

      threadPool.shutdown();

      Set<String> output = new LinkedHashSet<String>();
      for (Future<List<IndexEntry>> task : tasks) {
      output.add(task.get().toString());
      }
      System.out.format("%02d-%02d: %s%n", k, n, output);
      }
      }
      }

      private static final Collator COLLATOR = Collator.getInstance(Locale.ENGLISH);
      private static final Pattern SPACE = Pattern.compile("");

      private static CollationKey[] tokenize(Object sequence, Map<String, CollationKey> cache) {
      return SPACE.splitAsStream(sequence.toString()).map(s -> getCollationKey(s, cache)).toArray(CollationKey[]::new);
      }

      private static CollationKey getCollationKey(String word, Map<String, CollationKey> cache) {
      return cache.computeIfAbsent(word, COLLATOR::getCollationKey);
      }

      private static List<IndexEntry> find(CollationKey[] query, List<IndexEntry> index) {
      List<IndexEntry> matches = new ArrayList<IndexEntry>();

      for (IndexEntry i : index) {
      CollationKey[] key = i.getKey();
      CollationKey[] commonName = matchFirstCommonSequence(new CollationKey[][] { query, key });

      if (commonName != null && commonName.length >= query.length) {
      matches.add(i);
      }
      }

      return matches;
      }

      private static <E> E[] matchFirstCommonSequence(E[][] names) {
      E[] common = null;
      for (E[] words : names) {
      if (common == null) {
      // initialize common with current word array
      common = words;
      } else {
      // find common sequence
      common = firstCommonSequence(common, words);
      // no common sequence
      if (common == null) {
      return null;
      }
      }
      }
      return common;
      }

      private static <E> E[] firstCommonSequence(E[] seq1, E[] seq2) {
      E[] matchSeq = null;
      for (int i = 0; i < seq1.length; i++) {
      for (int j = 0; j < seq2.length; j++) {
      // common sequence length
      int len = 0;

      // iterate over common sequence
      while (equals(seq1, i + len, seq2, j + len)) {
      len++;
      }

      // check if a common sequence was found
      if (len > 0) {
      return copyOfRange(seq1, i, i + len);
      }
      }
      }
      return matchSeq;
      }

      private static boolean equals(Object[] seq1, int i1, Object[] seq2, int i2) {
      if (i1 < seq1.length && i2 < seq2.length) {
      Object o1 = seq1[i1];
      Object o2 = seq2[i2];

      try {
      Objects.requireNonNull(o1, "ARRAY ELEMENTS CANNOT BE NULL");
      Objects.requireNonNull(o2, "ARRAY ELEMENTS CANNOT BE NULL");
      } catch (NullPointerException e) {
      System.out.println("o1 was " + o1);
      System.out.println("o2 was " + o2);
      System.out.println("seq1 was " + asList(seq1));
      System.out.println("seq2 was " + asList(seq2));
      throw e;
      }

      return o1.equals(o2);
      }

      return false;
      }

      private static class IndexEntry {

      private Object object;
      private Function<Object, CollationKey[]> prepare;

      private CollationKey[] key;

      public IndexEntry(Object object, Function<Object, CollationKey[]> prepare) {
      this.object = object;
      this.prepare = prepare;
      }

      public CollationKey[] getKey() {
      if (key == null && object != null) {
      key = prepare.apply(object);

      // CONFIRM THAT ARRAY ELEMENTS ARE NOT NULL (this is always the case, so an exception is never thrown here)
      for (CollationKey k : key) {
      if (k == null) {
      throw new IllegalStateException("CollationKey is NULL");
      }
      }
      }
      return key;
      }

      @Override
      public String toString() {
      return object.toString();
      }
      }

      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      Adding a .clone() after .toArray(CollationKey[]::new) somehow makes the test case above never fail for unknown reasons:

      private static CollationKey[] tokenize(Object sequence, Map<String, CollationKey> cache) {
      return SPACE.splitAsStream(sequence.toString()).map(s -> getCollationKey(s, cache)).toArray(CollationKey[]::new).clone();
      }

      FREQUENCY : always


            vlivanov Vladimir Ivanov
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            8 Start watching this issue

              Created:
              Updated:
              Resolved: