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;
}
}



Proponuję byś zapytał na liście mailingowej TestNG.
–
pozdrawiam
Tomek