Problem
在寫plugin相關程式時,會去override classloader做成自己想要的載入形式。而這陣子,偶爾會發生如以下Error:
java.lang.LinkageError: loader (instance of org/tonylin/FunnyClassLoader): attempted duplicate class definition for name: "org/apache/commons/cli/BasicParser"
而我們的實做是去extend java.net.URLClassLoader,其中關鍵程式碼如下:
public Class<?> loadClass(String className) throws ClassNotFoundException { Class<?> clazz = findLoadedClass(className); if (clazz != null) { return clazz; } // load locally try { clazz = findClass(className); return clazz; } catch (ClassNotFoundException e) { // ignore } // use the standard URLClassLoader (which follows normal parent // delegation) try { return super.loadClass(className); } catch (ClassNotFoundException e) { return null; } }
目的是為了先讀取已載入的class,若尚未載入則優先搜尋classpath中是否有此class,最後才去找parent。
Solutions
我寫了一個簡單的單元測試去trace這個問題,而內容為:
- 新增一個jar檔到FunnyClassLoader的classpath中。
- 產生5個thread去load某個jar檔中的class。
- 驗證正確完成的工作數量為5。
@Test public void testConcurrentLoad() throws Exception{ mClassLoader = new FunnyClassLoader(Thread.currentThread().getContextClassLoader()); mClassLoader.addURL(new File("./testdata/commons-cli-1.2.jar")); List<Thread> threads = new ArrayList<>(); List<String> passThread = new ArrayList<>(); int size = 5; CountDownLatch latch = new CountDownLatch(size); for( int i = 0 ; i < size ; i ++ ){ Thread t = new Thread(()->{ try { mClassLoader.loadClass("org.apache.commons.cli.BasicParser"); System.out.println("pass: " + Thread.currentThread().getName()); passThread.add(Thread.currentThread().getName()); } catch (Exception e) { e.printStackTrace(); } finally { latch.countDown(); } }); threads.add(t); } threads.parallelStream().forEach(t->t.start()); latch.await(); assertEquals(threads.size(), passThread.size()); }
最後發現問題是由於: 當兩個thread同時存取loadclass時,若class已被define過,就會產生此錯誤。最後寫法是:
@Override public Class<?> loadClass(String className) throws ClassNotFoundException { synchronized (getClassLoadingLock(className)) { // original code } }
留言
張貼留言