Wielowątkowość sposobem na wolny odczyt z plików

Dziś zmierzyłem się z całkiem ciekawym problemem. Prosta Javova aplikacja miała odczytać zawartość plików csv, policzyć wskaźniki, uzupełnić obiekty Hibernate i zapisać wszystko do bazy danych. Wszystko zrobiłem szybciutko ale powstał pewien problem… Aplikacja działała wolno ale procesor był obciążony tylko w 50%. To nie byłby problem przy małej ilości danych, natomiast ja musiałem zaimportować 864 pliki o łącznej wadze 36,3 MB! Niby niewiele, ale przypomina… to są pliki tekstowe.

Gdzie szukać problemu? Sama aplikacja była wydajna, bo operacja matematyczne to błahostka. Nie implementowałem też żadnych skomplikowanych reguł. Może baza danych? Faktycznie interfejs sieciowy miał słabą responsywność. Zwiększyłem cache dla bazy danych do 1GB, zaalokowałem więcej pamięci dla aplikacji Javy (-XX:MaxPermSize=512m -Xms1024m -Xmx2048m) i aplikacja chodzi szybciej, ale nadal za wolno.

A co jest najwolniejsze? Czytanie danych z dysku… tak więc sobie wymyśliłem, że każdy plik będzie odczytywany i dodawany w oddzielnym wątku. Ale aby wszystko działało poprawnie musiałem zrobić kila sztuczek.

public class TestNotowan {

	private static final Logger logger = Logger.getLogger(TestNotowan.class);
	private static MainDao mainDao = MainDao.getInstance();

	/**
	 * @param args
	 * @throws InterruptedException
	 */
	public static void main(String[] args) throws InterruptedException {
		logger.info("begin transaction");

		mainDao.getEntityManager().getTransaction().begin();

		ImportNotowan importNotowan = new ImportNotowan(mainDao);
		importNotowan.deleteAllRows();
		importNotowan.importNotowan(new File("/home/kirkor/mstall"));

		while (importNotowan.nT != 0) {
			Thread.sleep(1000);
		}

		mainDao.getEntityManager().getTransaction().commit();

		logger.info("commit transaction");
	}

}

A poniżej najważniejsza klasa, to ona odpowiada za dodawanie nowych rekordów.

public class ImportNotowan {

	// ilosc uruchomionych watkow
	public int nT = 0;
	public final int maxNt = 20;

	private static final Logger logger = Logger.getLogger(ImportNotowan.class);
	private MainDao dao;

	public ImportNotowan(MainDao dao {
		this.dao = dao;
	}

	public void deleteAllRows() {
		//
	}

	/**
	 * @param directory
	 * @throws IllegalArgumentException
	 *             id @param directory is not an directory
	 * @throws InterruptedException
	 */
	public void importNotowan(File directory) throws IllegalArgumentException, InterruptedException {
		if (!directory.isDirectory()) {
			throw new IllegalArgumentException("passed argument 'File directory' has to be an folder!");
		}

		for (final File file : directory.listFiles()) {
			if (file.isFile() && file.canRead()) {
				while (maxNt <= nT) {
					logger.debug("sleep");
					Thread.sleep(1000);
				}
				
				new Thread(new Runnable() {

					@Override
					public void run() {
						/* 
						 * tylko jeden wątek może mieć dostęp do tej zmiennej, 
						 * inaczej może dojść do sytuacji kiedy dwa wątki 
						 * równocześnie będą chciały zwiększyć licznik
						 */
						synchronized (this) {
							nT++;
						}

						logger.debug("reading: " + file.getAbsolutePath());

						try {
							CSVReader reader = new CSVReader(new FileReader(file));

							String[] nextLine;
							while ((nextLine = reader.readNext()) != null) {
								// tutaj wczytuję różne dziwne rzeczy...
							}
							// zwalniamy pamięć
							reader.close();
						} catch (FileNotFoundException e) {
							logger.warn(e);
						} catch (IOException e) {
							logger.warn(e);
						}

						logger.debug("file has been read.");
						
						// tak samo jak wyżej
						synchronized (this) {
							nT--;
						}
					}
				}).start();

			}
		}
	}	
}

Ograniczyłem maksymalną ilość wątków, które mogą działać równocześnie. Przeprowadziłem też małe testy wydajnościowe, oto wyniki:

Wersja bez ograniczenia ilości wątków: 350 629 ms
Ograniczenie do 10 wątków: 257 415 ms
Ograniczenie do 20 wątków: 259 623 ms
Ograniczenie do 30 wątków: 261 179 ms

Ogólne wnioski, więcej nie znaczy lepiej ;)

W pokazanym przykładzie, w ciele metody dodawałem relację jeden do wielu. Napotkałem przy tym pewne problemy, ale o tym kiedy indziej.