Java 7 w akcjiJava 7 in action Wreszcie znalazłem wolną chwilę by wypróbować w praktyce nowe ficzery Java 7. Zabawy z wersją beta najnowszego jdk ułatwia NetBeans IDE 7.0 Beta 2. Do testów na pierwszy ogień poszły usprawnienia...

Readmore

Monitoring podstawowych parametrów JVM z poziomu web... Problem Monitoring podstawowych parametrów JVM z poziomu web aplikacji - przydatne zwłaszcza wtedy, gdy nasz serwer aplikacji/kontener serwletów nie pokazuje takich informacji w swojej webowej konsoli...

Readmore

Vaadin vs Richfaces i o tym co z tego wyszło [Java... Głośno ostatnio na DWorld i DZone zrobiło się o nowej odsłonie Vaadina - frameworku opartego na GWT. Nigdy wcześniej nie miałem styczności z GWT (prócz kilku tutoriali i paru hellowordów). Pracuję...

Readmore

Jak wyciągnąć kilka pierwszych wyników zapytania... [sql] -- Oracle select a.* from (select rownum row_num, t.* from t_table t ) a where a.row_num <= N -- DB2 select * from t_table fetch first 10 rows only -- Informix select...

Readmore

Vademecum IBM i oraz darmowe konto na iSeriesIBM i... Znalazłem jakiś czas temu 'hosting' oparty o iSeries, na którym można założyć sobie darmowe konto. Gdyby ktoś zatem poczuł nieodpartą pokusę pobawienia się AS/400 Green Screen, to ma taką...

Readmore

twitter

Singleton z double-checked locking oraz problem z wielowątkowością w TestNG

Kategoria : java

Wyczytałem ostatnio w mądrych książkach (tej i tej), że synchronizacja singletonów na poziomie całej metody getInstance może w środowisku wielowątkowym znacznie (25%) spowolnić pobieranie instancji obiektu przechowywanego przez owy singleton. W polskiej wikipedii ktoś napisał, że synchronizacja ta może obniżyć wydajność (w stosunku do metody niesynchronizowanej) o czynnik 100 lub więcej – szczerze, nie rozumiem co oznacza rzeczony ‚czynnik 100’…

W przypadku, gdy wydajność ma dla nas kluczowe znaczenie, do tworzenia singletonów zalecano stosowanie wzorca double-checked locking (blokada z podwójnym zatwierdzaniem). Postanowiłem więc sprawdzić doświadczalnie jak w rzeczywistości wygląda spadek wydajności metod synchronizowanych. Napisałem więc trzy singletony:

tradycyjny

package info.ludera.SingletonPerformance.action.impl;

import info.ludera.SingletonPerformance.action.TestSingleton;

/**
 * Singleton z ZAWSZE synchronizowaną metodą {@link SimpleSingleton#getInstance()}
 * @author darekl
 *
 */
public class SimpleSingleton extends TestSingleton {

	/**
	 * Jedyna instancja obiektu {@link SimpleSingleton}
	 */
	private static SimpleSingleton instance;

	/**
	 * Konstruktor domyślny
	 */
	private SimpleSingleton() {
		super();
	}

	/**
	 * Zwraca instancję obiektu  {@link SimpleSingleton}
	 * @return
	 */
	public static synchronized SimpleSingleton getInstance() {

		if (instance == null) {
			instance = new SimpleSingleton();
		}
		return instance;
	}
}

i dwie różne implementacje double-checked locking:

„podstawowa”

package info.ludera.SingletonPerformance.action.impl;

import info.ludera.SingletonPerformance.action.TestSingleton;

/**
 * Singleton z synchronizowaną metodą {@link SimpleSingleton#getInstance()}. Synchronizacja aktywna jest tylko przy pierwszym uruchomieniu tej metody.
 * @author darekl
 *
 */
public class ThreadSaveSingleton extends TestSingleton {

	/**
	 * Jedyna instancja obiektu {@link SimpleSingleton}
	 */
	private volatile static ThreadSaveSingleton instance = null;

	/**
	 * Konstruktor domyślny
	 */
	private ThreadSaveSingleton() {
		super();
	}

	/**
	 * Zwraca instancję obiektu  {@link SimpleSingleton}
	 * @return
	 */
	public static ThreadSaveSingleton getInstance() {

		if (instance == null) {
			synchronized (ThreadSaveSingleton.class) {
				if (instance == null) {
					instance = new ThreadSaveSingleton();
				}
			}
		}
		return instance;
	}
}

i „rozszerzona

package info.ludera.SingletonPerformance.action.impl;

import info.ludera.SingletonPerformance.action.TestSingleton;

/**
 * Singleton z synchronizowaną metodą {@link SimpleSingleton#getInstance()}. Synchronizacja aktywna jest tylko przy pierwszym uruchomieniu tej metody.
 * @author darekl
 *
 */
public class AdvancedThreadSaveSingleton extends TestSingleton {

	/**
	 * Jedyna instancja obiektu {@link SimpleSingleton}
	 */
	private volatile static AdvancedThreadSaveSingleton instance = null;

	/**
	 * Konstruktor domyślny
	 */
	private AdvancedThreadSaveSingleton() {
		super();
	}

	/**
	 * Zwraca instancję obiektu  {@link SimpleSingleton}
	 * @return
	 */
	public static AdvancedThreadSaveSingleton getInstance() {

		AdvancedThreadSaveSingleton result = instance;
		if (result == null) {
			synchronized (AdvancedThreadSaveSingleton.class) {
				result = instance;
				if (result == null) {
					instance = result = new AdvancedThreadSaveSingleton();
				}
			}
		}
		return result;
	}
}

Przetestowałem ich działanie poniższym testem dla różnej ilości wątków (100-100000):

package info.ludera.SingletonPerformance.test;

...

public class FabricMultiThreadSingletonTest extends AbstractSingletonTest {

	@DataProvider
	public Object[][] ValidJavaVersion() {

		return new Object[][]{
				{ 5 }
		};
	}

	/**
	 * Ilość iteracji
	 */
	protected static Long testCounter;

	/**
	 * Prefix nazwy wątku
	 */
	protected static String threadNamePrefix = "Thread_";

	@BeforeClass
	@Parameters(value="repetitionQuantity")
	public void setTestCounter(long repetitionQuantity) {
		testCounter = repetitionQuantity;
	}

	@Test
	public void getSimpleSingletonNTimes() {

		for (long i=0; i<testCounter; i++) {
			SingletonFactory simpleSingletonFactory = new SimpleSingletonFactory(threadNamePrefix + i);
			simpleSingletonFactory.start();
		}
	}

	@Test(dataProvider = "ValidJavaVersion")
	public void getThreadSaveSingletonNTimes(final int javaVersion) {

		//Assert.assertTrue((Integer.parseInt("" + System.getProperty("java.version").charAt(2)) >= javaVersion), "This test is avialable only with Java 5 or above!");

		for (long i=0; i<testCounter; i++) {
			SingletonFactory threadSaveSingletonFactory = new ThreadSaveSingletonFactory(threadNamePrefix + i);
			threadSaveSingletonFactory.start();
		}
	}

	@Test(dataProvider = "ValidJavaVersion")
	public void getAdvencedThreadSaveSingletonNTimes(final int javaVersion) {

		//Assert.assertTrue((Integer.parseInt("" + System.getProperty("java.version").charAt(2)) >= javaVersion), "This test is avialable only with Java 5 or above!");

		for (long i=0; i<testCounter; i++) {
			SingletonFactory advancedThreadSaveSingletonFactory = new AdvancedThreadSaveSingletonFactory(threadNamePrefix + i);
			advancedThreadSaveSingletonFactory.start();
		}
	}
}

I rzeczywiście. Czasowe wyniki działania tych testów, dla różnej ilości iteracji, rozchodziły się w słuszną stronę. Wraz ze wzrostem iteracji, SimpleSingleton działał coraz wolniej w stosunku do wersji z double-checked locking. Testy przeprowadzałem na JDK 1.6.0_16 oraz JDK 1.5.0_22. Nie testowałem wydajności na Java 1.4. Istnieje bowiem prawdopodobieństwo wystąpienia problemu błędnej implementacji volatile. Oto przykładowe wyniki dla 10000 iteracji na JDK 1.6:

getSimpleSingletonNTimes - 1624ms
getThreadSaveSingletonNTimes - 1209ms
getAdvencedThreadSaveSingletonNTimes - 1153ms

To nie wszystko. Po zastanowieniu, doszedłem do wniosku, że użycie wzorca fabryki do takich teścików to przerost formy nad treścią. Stwierdziłem więc, że wykorzystam wielowątkowości z TestNG.

oto test:

package info.ludera.SingletonPerformance.test;

...

import org.testng.annotations.Test;

/**
 * Test wydajności synchronizacji singletonów {@link info.ludera.SingletonPerformance.test.helper.SimpleSingletonFactory} i {@link info.ludera.SingletonPerformance.test.helper.ThreadSaveSingletonFactory}
 * @author darekl
 *
 */
public class MultiThreadSingletonTest extends AbstractSingletonTest {

	@Test(threadPoolSize=1000, invocationCount = 1000)
	public void getSimpleSingleton() {

		SimpleSingleton.getInstance();
	}

	@Test(dataProvider = "ValidJavaVersion", threadPoolSize=1000, invocationCount = 1000)
	public void getThreadSaveSingleton(final int javaVersion) {

		//Assert.assertTrue((Integer.parseInt("" + System.getProperty("java.version").charAt(2)) >= javaVersion), "This test is avialable only with Java 5 or above!");

		ThreadSaveSingleton.getInstance();
	}

	@Test(dataProvider = "ValidJavaVersion", threadPoolSize=1000, invocationCount = 1000)
	public void getAdvencedThreadSaveSingleton(final int javaVersion) {

		//Assert.assertTrue((Integer.parseInt("" + System.getProperty("java.version").charAt(2)) >= javaVersion), "This test is avialable only with Java 5 or above!");

		AdvancedThreadSaveSingleton.getInstance();
	}
}

i wyniki (tym razem dla 1000 wątków):

getSimpleSingletonNTimes - 110ms
getThreadSaveSingletonNTimes - 97ms
getAdvencedThreadSaveSingletonNTimes - 100ms
getSimpleSingleton - 716ms
getThreadSaveSingleton - 2050ms
getAdvencedThreadSaveSingleton - 2079ms

Zastanawiające. Dlaczego trzy ostatnie testy (te dopisane powyżej) nie układają się tak jak te poprzednie, gdzie wielowątkowość napisałem ręcznie? Dlaczego widać tak duże różnice w wydajności? Przyznam szczerze, że z wielowątkowości w TestNG korzystam po praz pierwszy, czy ktoś bardziej doświadczony w tym temacie, mógłby mi wyjaśnić w czym tkwi problem na który się natknąłem i co robię źle?

Pobierz projekt eclipse (wymagania: Maven2 lub Eclipse z pluginami: m2eclipse i TestNG)

PS. Apropos optymalizacji kodu pod względem jego wydajności, polecam ten artykuł. Po jego przeczytaniu nasunęła mi się analogia dotycząca RDBMS i hintów oraz przesiadki z optymalizatorów regułowych na kosztowe ;)

package info.ludera.SingletonPerformance.action.impl;

import info.ludera.SingletonPerformance.action.TestSingleton;

/**
* Singleton z ZAWSZE synchronizowaną metodą {@link SimpleSingleton#getInstance()}
* @author darekl
*
*/
public class SimpleSingleton extends TestSingleton {

/**
* Jedyna instancja obiektu {@link SimpleSingleton}
*/
private static SimpleSingleton instance;

/**
* Konstruktor domyślny
*/
private SimpleSingleton() {
super();
}

/**
* Zwraca instancję obiektu {@link SimpleSingleton}
* @return
*/
public static synchronized SimpleSingleton getInstance() {

//System.out.println(„SimpleSingleton.getInstance start for ” + Thread.currentThread().getName());
if (instance == null) {
instance = new SimpleSingleton();
}
//System.out.println(„SimpleSingleton.getInstance stop for ” + Thread.currentThread().getName());
return instance;
}
}