JavaRush /Blog Java /Random-PL /Nie możesz zepsuć Javy wątkiem: część I - Wątki
Viacheslav
Poziom 3

Nie możesz zepsuć Javy wątkiem: część I - Wątki

Opublikowano w grupie Random-PL

Wstęp

Wielowątkowość jest wbudowana w Javę od jej początków. Przyjrzyjmy się zatem szybko, na czym polega wielowątkowość. Nie możesz zrujnować Javy za pomocą wątku: Część I - Wątki - 1Za punkt wyjścia przyjmijmy oficjalną lekcję firmy Oracle: „ Lekcja: Aplikacja „Hello World! ”. Zmieńmy trochę kod naszej aplikacji Hello World na następujący:
class HelloWorldApp {
    public static void main(String[] args) {
        System.out.println("Hello, " + args[0]);
    }
}
argsjest tablicą parametrów wejściowych przekazywanych podczas uruchamiania programu. Zapiszmy ten kod do pliku o nazwie odpowiadającej nazwie klasy i rozszerzeniu .java. Skompilujmy przy użyciu narzędzia javac : javac HelloWorldApp.java Następnie wywołaj nasz kod z jakimś parametrem, na przykład Roger: java HelloWorldApp Roger Nie możesz zrujnować Javy za pomocą wątku: Część I - Wątki - 2Nasz kod ma teraz poważną wadę. Jeśli nie przekażemy żadnego argumentu (czyli po prostu uruchomimy Java HelloWorldApp), otrzymamy błąd:
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 0
        at HelloWorldApp.main(HelloWorldApp.java:3)
Wystąpił wyjątek (tzn. błąd) w wątku o nazwie main. Okazuje się, że w Javie są jakieś wątki? Tutaj zaczyna się nasza podróż.

Java i wątki

Aby zrozumieć, czym jest wątek, musisz zrozumieć, w jaki sposób uruchamiana jest aplikacja Java. Zmieńmy nasz kod w następujący sposób:
class HelloWorldApp {
    public static void main(String[] args) {
		while (true) {
			//Do nothing
		}
	}
}
Teraz skompilujmy go ponownie, używając javac. Następnie dla wygody uruchomimy nasz kod Java w osobnym oknie. W systemie Windows możesz to zrobić w następujący sposób: start java HelloWorldApp. Teraz, korzystając z narzędzia jps , zobaczmy, jakie informacje powie nam Java: Nie możesz zrujnować Javy za pomocą wątku: Część I - Wątki - 3Pierwsza liczba to PID lub Process ID, identyfikator procesu. Co to jest proces?
Процесс — это совокупность kodа и данных, разделяющих общее виртуальное adresное пространство.
Za pomocą procesów wykonywanie różnych programów jest odizolowane od siebie: każda aplikacja korzysta z własnego obszaru pamięci, nie zakłócając działania innych programów. Radzę dokładniej przeczytać artykuł: „ https://habr.com/post/164487/ ”. Proces nie może istnieć bez wątków, więc jeśli proces istnieje, istnieje w nim co najmniej jeden wątek. Jak to się dzieje w Javie? Kiedy uruchamiamy program Java, jego wykonanie rozpoczyna się od pliku main. Wchodzimy do programu, więc ta specjalna metoda mainnazywa się punktem wejścia lub „punktem wejścia”. Metoda mainmusi być zawsze public static voidtaka, aby wirtualna maszyna Java (JVM) mogła rozpocząć wykonywanie naszego programu. Więcej szczegółów znajdziesz w artykule „ Dlaczego główna metoda Java jest statyczna? ”. Okazuje się, że program uruchamiający Java (java.exe lub javaw.exe) jest prostą aplikacją (prostą aplikacją w C): ładuje różne biblioteki DLL, które w rzeczywistości są maszyną JVM. Program uruchamiający Java wykonuje określony zestaw wywołań Java Native Interface (JNI). JNI to mechanizm łączący świat wirtualnej maszyny Java i świat C++. Okazuje się, że programem uruchamiającym nie jest JVM, ale jej moduł ładujący. Zna prawidłowe polecenia, które należy wykonać, aby uruchomić maszynę JVM. Wie, jak zorganizować całe niezbędne środowisko za pomocą wywołań JNI. Ta organizacja środowiska obejmuje również utworzenie głównego wątku, który zwykle nazywa się main. Aby lepiej zobaczyć, jakie wątki żyją w procesie Java, używamy programu jvisualvm , który jest zawarty w JDK. Znając pid procesu, możemy od razu otworzyć o nim dane: jvisualvm --openpid айдипроцесса Nie możesz zrujnować Javy za pomocą wątku: Część I - Wątki - 4Co ciekawe, każdy wątek ma swój własny, odrębny obszar w pamięci przydzielony dla procesu. Ta struktura pamięci nazywa się stosem. Stos składa się z ramek. Ramka jest punktem wywołania metody, punktem wykonania. Ramkę można również przedstawić jako StackTraceElement (zobacz Java API dla StackTraceElement ). Więcej o pamięci przydzielonej każdemu wątkowi możesz przeczytać tutaj . Jeśli spojrzymy na API Java i wyszukamy słowo Thread, zobaczymy, że istnieje klasa java.lang.Thread . To właśnie ta klasa reprezentuje strumień w Javie i właśnie z nią musimy pracować. Thread'ом Java не испортишь: Часть I — потоки - 5

java.lang.Wątek

Wątek w Javie jest reprezentowany jako instancja klasy java.lang.Thread. Warto od razu zrozumieć, że instancje klasy Thread w Javie same w sobie nie są wątkami. Jest to po prostu rodzaj API dla wątków niskiego poziomu zarządzanych przez maszynę JVM i system operacyjny. Kiedy uruchamiamy maszynę JVM za pomocą programu uruchamiającego Java, tworzy ona główny wątek z nazwą maini kilkoma innymi wątkami usług. Jak stwierdzono w dokumencie JavaDoc klasy Thread: When a Java Virtual Machine starts up, there is usually a single non-daemon thread Istnieją 2 typy wątków: demony i inne niż demony. Wątki demona to wątki działające w tle (wątki usługowe), które wykonują pewne prace w tle. To ciekawe określenie jest nawiązaniem do „demona Maxwella”, o którym więcej można przeczytać w artykule na Wikipedii o „ demonach ”. Jak stwierdzono w dokumentacji, JVM kontynuuje wykonywanie programu (procesu) do momentu:
  • Metoda Runtime.exit nie jest wywoływana
  • Wszystkie wątki inne niż demony zakończyły swoją pracę (zarówno bez błędów, jak i ze zgłoszonymi wyjątkami)
Stąd ważny szczegół: wątki demona można zakończyć po wykonaniu dowolnego polecenia. Dlatego nie gwarantuje się integralności zawartych w nich danych. Dlatego wątki demonów nadają się do niektórych zadań serwisowych. Na przykład w Javie istnieje wątek odpowiedzialny za przetwarzanie metod finalizacji lub wątków związanych z modułem Garbage Collector (GC). Każdy wątek należy do jakiejś grupy ( ThreadGroup ). Grupy mogą wchodzić w siebie, tworząc pewną hierarchię lub strukturę.
public static void main(String []args){
	Thread currentThread = Thread.currentThread();
	ThreadGroup threadGroup = currentThread.getThreadGroup();
	System.out.println("Thread: " + currentThread.getName());
	System.out.println("Thread Group: " + threadGroup.getName());
	System.out.println("Parent Group: " + threadGroup.getParent().getName());
}
Grupy pozwalają usprawnić zarządzanie przepływami i śledzić je. Oprócz grup wątki mają własną procedurę obsługi wyjątków. Spójrzmy na przykład:
public static void main(String []args) {
	Thread th = Thread.currentThread();
	th.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
		@Override
		public void uncaughtException(Thread t, Throwable e) {
			System.out.println("Wystąpił błąd: " + e.getMessage());
		}
	});
    System.out.println(2/0);
}
Dzielenie przez zero spowoduje błąd, który zostanie przechwycony przez procedurę obsługi. Jeśli sam nie określisz procedury obsługi, zadziała domyślna implementacja obsługi, która wyświetli stos błędów w StdError. Więcej możesz przeczytać w recenzji http://pro-java.ru/java-dlya-opytnyx/obrabotchik-neperexvachennyx-isklyuchenij-java/ ”. Poza tym wątek ma priorytet. Więcej o priorytetach możesz przeczytać w dziale artykuł „ Priorytet wątku Java w wielowątkowości ”.

Tworzenie wątku

Jak podano w dokumentacji, mamy 2 sposoby na utworzenie wątku. Pierwszym z nich jest stworzenie swojego spadkobiercy. Na przykład:
public class HelloWorld{
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Hello, World!");
        }
    }

    public static void main(String []args){
        Thread thread = new MyThread();
        thread.start();
    }
}
Jak widać w metodzie uruchamiane jest zadanie run, a w metodzie wątek start. Nie należy ich mylić, ponieważ... jeśli uruchomimy metodę runbezpośrednio, żaden nowy wątek nie zostanie uruchomiony. Jest to metoda start, która prosi maszynę JVM o utworzenie nowego wątku. Opcja z potomkiem Thread jest zła, ponieważ uwzględniamy Thread w hierarchii klas. Drugą wadą jest to, że zaczynamy łamać zasadę „Wyłącznej odpowiedzialności” SOLID, ponieważ nasza klasa staje się jednocześnie odpowiedzialna zarówno za zarządzanie wątkiem, jak i za pewne zadanie, które należy wykonać w tym wątku. Który jest poprawny? Odpowiedź kryje się w metodzie, runktórą zastępujemy:
public void run() {
	if (target != null) {
		target.run();
	}
}
Oto targetniektóre z nich java.lang.Runnable, które możemy przekazać do Thread podczas tworzenia instancji klasy. Dlatego możemy to zrobić:
public class HelloWorld{
    public static void main(String []args){
        Runnable task = new Runnable() {
            public void run() {
                System.out.println("Hello, World!");
            }
        };
        Thread thread = new Thread(task);
        thread.start();
    }
}
Jest to także Runnableinterfejs funkcjonalny od wersji Java 1.8. Dzięki temu możesz jeszcze piękniej pisać kod zadań dla wątków:
public static void main(String []args){
	Runnable task = () -> {
		System.out.println("Hello, World!");
	};
	Thread thread = new Thread(task);
	thread.start();
}

Całkowity

Mam więc nadzieję, że z tej historii jasno wynika, czym jest strumień, jak istnieje i jakie podstawowe operacje można na nim wykonać. W dalszej części warto zrozumieć jak wątki oddziałują na siebie i jaki jest ich cykl życia. #Wiaczesław
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION