JDK-8028054 : com.sun.beans.finder.MethodFinder has unsynchronized access to a static Map
  • Type: Bug
  • Component: client-libs
  • Sub-Component: java.beans
  • Affected Version: 7u45,8
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: linux_ubuntu
  • Submitted: 2013-11-04
  • Updated: 2014-09-19
  • Resolved: 2013-11-26
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 7 JDK 8
7u60Fixed 8 b119Fixed
Related Reports
Duplicate :  
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "1.7.0_40"
Java(TM) SE Runtime Environment (build 1.7.0_40-b43)
Java HotSpot(TM) 64-Bit Server VM (build 24.0-b56, mixed mode)


ADDITIONAL OS VERSION INFORMATION :
3.8.0-31-generic #46-Ubuntu SMP Tue Sep 10 20:03:44 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux

A DESCRIPTION OF THE PROBLEM :
com.sun.beans.finder.MethodFinder has a private static instance CACHE, of type com.sun.beans.WeakCache, which is a thin wrapper around java.util.WeakHashMap. As a result, under concurrent usage, it's possible for the underlying map to contain a circular reference in one of its bucket's linked lists. If a thread subsequently attempts to find a record in that bucket which does not yet exist, it will get stuck in the while loop of WeakHashMap's get method; stack traces will continually show the thread as being at line 470 (e = e.next).

While this is in a private com.sun package, it can be exposed to clients only consuming public APIs. As an example, consider the following stack trace:

        at java.util.WeakHashMap.get(WeakHashMap.java:470)
        at com.sun.beans.WeakCache.get(WeakCache.java:55)
        at com.sun.beans.finder.MethodFinder.findMethod(MethodFinder.java:68)
        at java.beans.Statement.getMethod(Statement.java:357)
        at java.beans.Statement.invokeInternal(Statement.java:287)
        at java.beans.Statement.access$000(Statement.java:58)
        at java.beans.Statement$2.run(Statement.java:185)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.beans.Statement.invoke(Statement.java:182)
        at java.beans.Expression.getValue(Expression.java:153)
        ....

One place this bug is showing up in the wild is https://issues.apache.org/jira/browse/HIVE-4574.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Given that this is a concurrency bug, it cannot be reproduced consistently, but the included code triggers it more often than not for me. Compile the code, then run it. Note that rt.jar needs to be on the compile class path.

javac -classpath /usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar MethodFinderBug.java
java MethodFinderBug

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The program will run for awhile, but eventually terminate, having printed "done!".
ACTUAL -
Frequently, the program does not terminate, but ends up printing the following every two seconds:

progress seen in 0 out of 50 threads
50 of 50 threads are in WeakHashMap code


REPRODUCIBILITY :
This bug can be reproduced often.

---------- BEGIN SOURCE ----------

import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLongArray;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.sun.beans.finder.MethodFinder;


public class MethodFinderBug {
  public static void main(String[] args) throws Exception {
    final List<Method> methods = getMethods(grabClasses(5000));

    Thread[] threads = new Thread[50];
    final AtomicLongArray timestamps = new AtomicLongArray(threads.length);

    for (int i = 0; i < threads.length; i++) {
      final int threadNumber = i;
      threads[i] = new Thread() {
        @Override
        public void run() {
          for (Method method: methods) {
            try {
              MethodFinder.findMethod(method.getDeclaringClass(), method.getName(), method.getParameterTypes());
              timestamps.set(threadNumber, System.currentTimeMillis());
            }
            catch (NoSuchMethodException e) {
            }
          }
          timestamps.set(threadNumber, 0); // mark as complete
          System.out.println("Thread " + threadNumber + " complete");
        }
      };
    }
    for (int i = 0; i < timestamps.length(); i++) {
      timestamps.set(i, System.currentTimeMillis());
    }

    for (Thread thread: threads) {
      thread.start();
    }

    final int checkInterval = 2000; // 2 seconds
    while (true) {
      boolean someThreadsNotFinished = false;
      int progressingThreadCount = 0;
      for (int i = 0; i < timestamps.length(); i++) {
        long threadTime = timestamps.get(i);
        if (threadTime > 0) {
          someThreadsNotFinished = true;
          if (System.currentTimeMillis() - threadTime <= checkInterval) {
            progressingThreadCount++;
          }
        }
      }
      if (someThreadsNotFinished) {
        // look for threads that may be stuck in a loop inside WeakHashMap
        int hashMapLoopCount = 0;
        int activeThreadCount = 0;
        for (Thread thread: threads) {
          if (thread.isAlive()) {
            activeThreadCount++;
            StackTraceElement[] stackTrace = thread.getStackTrace();
            if (stackTrace != null
                && stackTrace.length > 0
                && WeakHashMap.class.getName().equals(stackTrace[0].getClassName())) {
              hashMapLoopCount++;
            }
          }
        }
        System.out.println("progress seen in " + progressingThreadCount + " out of " + activeThreadCount + " threads");
        System.out.println(hashMapLoopCount + " of " + activeThreadCount + " threads are in WeakHashMap code");
        Thread.sleep(2000);
      }
      else {
        break;
      }
    }
    System.out.println("done!");
  }

  private static List<Method> getMethods(List<Class<?>> classes) {
    List<Method> methods = new ArrayList<>();
    for (Class<?> clazz: classes) {
      for (Method method: clazz.getMethods()) {
        int modifiers = method.getModifiers();
        if (Modifier.isPublic(modifiers) && !Modifier.isAbstract(modifiers)) {
          methods.add(method);
        }
      }
    }
    return methods;
  }

  private static List<Class<?>> grabClasses(int classCount) throws IOException {
    Pattern pattern = Pattern.compile("jar:file:(.*)!.*");
    Matcher matcher = pattern.matcher(
      ClassLoader.getSystemClassLoader().getResource("java/lang/Object.class").toString());
    matcher.matches();
    String jarFileName = matcher.group(1);

    final List<Class<?>> classes = new ArrayList<>();
    try(JarFile rtJar = new JarFile(jarFileName)) {
      int foundClasses = 0;
      for (Enumeration<JarEntry> entries = rtJar.entries(); entries.hasMoreElements(); ) {
        JarEntry jarEntry = entries.nextElement();
        String name = jarEntry.getName();

        if (name.startsWith("java") && name.endsWith(".class")) {
          try {
            String className = name.substring(0, name.indexOf(".")).replace('/', '.');
            classes.add(Class.forName(className));
            foundClasses++;
            if (foundClasses > classCount) {
              break;
            }
          }
          catch (ClassNotFoundException e) {
            e.printStackTrace();
          }
        }
      }
    }
    return classes;
  }
}

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

CUSTOMER SUBMITTED WORKAROUND :
There is no workaround that I am aware of, short of avoiding use of the java.beans API entirely.
Comments
SQE is ok to take the fix in 7u60.
10-12-2013

The new tests covered pretty all scenarios that we could imagine, to be on the safe side please run the regression tests from the jdk/test/java/beans folder (we also checked them - no problems found)
10-12-2013

Please add recommendations on testing besides running two new regression tests.
06-12-2013

Release team: Approved for fixing
25-11-2013

webrev: http://cr.openjdk.java.net/~malenkov/8028054.8.0/ review: http://mail.openjdk.java.net/pipermail/swing-dev/2013-November/003139.html issue impact: it is possible to hang a thread of another application under the same VM risk level: low, the fix does not affect other areas testing: 2 automatic regression tests are added
22-11-2013

SQE: OK to push in jdk8
22-11-2013

http://cr.openjdk.java.net/~malenkov/8028054.8.0/
14-11-2013

The 8023310 issue shows how is slow a simple synchronization. I've created the Cache class that is similar to WeakIdentityMap.
14-11-2013

Yes, it is affected jdk8. I think it would be better to fix.
08-11-2013

This is regression since 7b46. See 4864117. Need to rewrite the WeakCache class.
08-11-2013

is it affected jdk8? if Yes, is it critical or defer case?
08-11-2013