-
멀티쓰레드 프로그래밍자바 스터디 2021. 2. 2. 22:13
백기선님의 Java 스터디를 진행하며 찾아본 내용입니다.
목표
자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.
학습할 것
- Thread 클래스와 Runnable 인터페이스
- 쓰레드의 상태
- 쓰레드의 우선순위
- Main 쓰레드
- 동기화
- 데드락
Thread 클래스와 Runnable 인터페이스
Thread는 두가지 방법을 통해 구현되어 질 수 있다.
1. Thread class의 상속
2. Runnable interface의 구현
1. Thread class의 상속
Thread class를 상속 받은 뒤에 run() 메소드에서 수행할 작업을 작성 할 수 있다.
1234567891011121314151617181920public class MultithreadingDemo extends Thread{public void run() {try {// Displaying the thread that is runningSystem.out.println("Thread " + Thread.currentThread().getId() + "is running");}catch (Exception e) {System.out.println("Exception is caught");}}public static void main(String[] args) {int n = 8;for (int i = 0; i < n; i++) {MultithreadingDemo object = new MultithreadingDemo();object.start();}}}cs Output:
2. Runnable interface의 구현
Runnable interface를 구현하고 run() 메소드를 override한다.
Runnable Interface
Runnable Interface 구현
12345678910111213141516171819202122class MultithreadingDemo2 implements Runnable {@Overridepublic void run() {try {System.out.println("Thread " + Thread.currentThread().getId() + " is running");}catch(Exception e) {System.out.println("Exception is caught");}}}public class Multithread {public static void main(String[] args) {int n = 8; // Number of threadfor(int i = 0; i < n; i++) {Thread object = new Thread(new MultithreadingDemo2());object.start();}}}cs Output:
Thread Class vs Runnable Interface
- Thread 클래스를 상속하는 방법을 사용한다면, 자바에서는 다중 상속을 지원하지 않기 때문에 다른 클래스를 상속 받을 수 없다. 하지만 Runnable Interface를 구현한다면 다른 클래스를 상속 받을 수 있다.
- Thread Class를 상속하여 사용한다면 yield(), interrupt()와 같은 내장되어 있는 메소드를 상속 받아 사용 할 수 있지만 Runnable interface에서는 사용이 불가능하다.
Java의 멀티스레딩에서 start() function을 사용하는 이유.
스레드를 생성할 떄 멀티 스레딩에서 Run()함수를 사용하는 것이 아닌 start()를 사용하는데 이유는 다음과 같다.
start() 함수를 실행 하면 새로운 call stack을 생성하고 run()함수를 실행한다
하지만 단순히 run() 함수를 override 한 것을 실행하면 call stack은 생성되지 않고 run() 함수만이 실행된다.
Example) start()대신 run()를 사용할 경우
1234567891011121314151617181920212223class ThreadTest extends Thread {@Overridepublic void run() {try {System.out.println("Thread " + Thread.currentThread().getId() + " is running");}catch(Exception e) {System.out.println("Exception is caught");}}}public class Main {public static void main(String args[]) {int n = 8;for (int i = 0; i < n; i++) {ThreadTest object = new ThreadTest();// start() 대신 run()을 사용object.run();}}}cs Output:
스레드의 상태
스레드의 생명주기
1. New Thread
- 스레드가 생성되어졌을 때의 상태. 아직 start()가 호출이 되어지지 않은 상태
2. RUNNABLE State
- 실행 가능한 상태, 이미 run중이거나 run할 준비가 되어있는 상태이다. 시분할 방식으로 스케쥴링되어진다.
3. Blocked / Waiting
- 스레드가 일시적으로 정지 되어 있을 때 스케쥴러에 따라 Blocked 혹은 Waiting과 같은 state를 따른다
- 스레드는 I/O에 의한 인터럽트나, 다른 스레드에 의해 lock 되어진 임계영역에 접근 하려 시도 할 때 Block 되어 질 수 있다.
- 임계 영역이 unlock 되거나 I/O 인터럽트가 끝나면 스케쥴러가 block된 스레드 중 하나를 runnable 스테이트로 옮 긴다.
- 스레드가 어떤 조건을 충족하지 못할 때 Waiting state에 있게 되고 조건이 충족되면 스레드가 스케쥴러에 의해 runnable state로 이동되게 된다.
4. Waiting, Timed Waiting
- 스레드는 Thread 메소드가 timeout 파라미터와 함께 호출될 때 time waiting state에 놓이게 된다.
- 스레드는 timeout이 완료되거나 notification을 받을 때 까지 이 상태에 있게 된다.
- 예를들어 sleep이나 conditional wait을 호출하면 스레드는 timed waiting 상태로 이동되게 된다.
5. Terminated State:
- 다음과 같은 경우에 Terminated 된다.
- thread의 코드가 전부 실행되었을 경우
- segmentation fault나 unhandled exception등의 예측하지 못한 예외의 발생
스레드 상태 구현
스레드의 현재 state를 구하기 위해서 Thread.getState()라는 메소드를 사용한다.
java.lang.Thread.State 클래스에서 스레드의 상태에 대한 ENUM 상수를 정의 하고 있으며 목록은 다음과 같다.
- public static final Thread.State NEW
- public static final Thread.State RUNNABLE
- public static final Thread.State BLOCKED
- public static final Thread.State WAITING
Example)
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768class thread implements Runnable {@Overridepublic void run() {try {Thread.sleep(1500);}catch (InterruptedException e) {e.printStackTrace();}System.out.println("State of thread1 while it called join() method on thread2 - " + Test.thread1.getState());try {Thread.sleep(200);}catch(InterruptedException e) {e.printStackTrace();}}}public class Test implements Runnable{public static Thread thread1;public static Test obj;public static void main(String[] args) {obj = new Test();thread1 = new Thread(obj);// thread1 created and is currently in the NEW state.System.out.println("State of thread1 after creating it - " + thread1.getState());thread1.start();// thread1 moved to Runnable stateSystem.out.println("State of thread1 after calling .staer() method on it - " + thread1.getState());}@Overridepublic void run() {thread myThread = new thread();Thread thread2 = new Thread(myThread);// thread1 created and is currnetly in the NEW state.System.out.println("State of thread2 after creating it - " + thread2.getState());thread2.start();System.out.println("State of thread2 after calling .start() method on it - " + thread2.getState());try {// moving thread1 to timed waiting stateThread.sleep(200);}catch(InterruptedException e) {e.printStackTrace();}System.out.println("State of thread2 after calling .sleep() method on it - " + thread2.getState());try {// waiting for thread2 to diethread2.join();}catch(InterruptedException e) {e.printStackTrace();}System.out.println("State of thread2 when it has finished it's execution: - " + thread2.getState());}}cs Output:
쓰레드의 우선순위
- 스레드가 생성될 때 마다 스레드는 우선순위가 부여된다.
- JVM이나 프로그래머가 명시적으로 우선 순위를 부여 할 수 있다.
- 스레드 스케쥴러는 우선순위에 따라서 스레드에 프로세서를 할당한다.
- 1~10 범위의 스레드 우선순위가 있다.
다음과 같은 우선 순위에 대한 3개의 static 변수가 Thread 클래스에 정의되어있다.
- public static int MIN_PRIORITY: 가장 작은 우선순위를 가진다. '1'의 값을 가진다
- public static int NORM_PRIORITY: 스레드가 생성될 때 기본적으로 가지는 값이다. '5'의 값을 가진다.
- public static int MAX_PRIORITY: 가장 큰 우선순위를 가진다. '10'의 값을 가진다.
Get, Set Method
public final int getPriority():
- java.lang.Thread내의 메소드로써 주어진 스레드의 우선순위를 반환한다.
public final void setPriority(int newPriority):
- java.lang.Thread내의 메소드로써 newPriority 값으로 우선순위를 변경한다.
- 스레드 우선순위 값의 범위(1~10)을 벗어날 경우 IllegalArgumentException을 throw한다.
Example)
12345678910111213141516171819202122232425262728293031323334353637383940414243444546public class ThreadDemo3 extends Thread{public void run() {System.out.println("Inside run method");}public static void main(String[] args) {ThreadDemo3 t1 = new ThreadDemo3();ThreadDemo3 t2 = new ThreadDemo3();ThreadDemo3 t3 = new ThreadDemo3();// Default 5System.out.println("t1 thread priority : " + t1.getPriority());// Default 5System.out.println("t2 thread priority : " + t2.getPriority());// Default 5System.out.println("t3 thread priority : " + t3.getPriority());t1.setPriority(2);t2.setPriority(5);t3.setPriority(8);// t3.setPriority(21); will throw IllegalArgumentException// 2System.out.println("t1 thread priority : " + t1.getPriority());// 5System.out.println("t2 thread priority : " + t2.getPriority());// 8System.out.println("t3 thread priority : " + t3.getPriority());// Main Thread// Display the name of currently executing threadSystem.out.println("Currently Executing Thread : " + Thread.currentThread().getName());// Main thread priority is set to 10Thread.currentThread().setPriority(10);System.out.println("Main thread priority : " + Thread.currentThread().getPriority());}}cs Output
Main Thread
자바는 내장되어있는 멀티스레딩 지원 툴이있다. 멀티 스레드는 두개 혹은 더 많은 파트를 동시에 진행 시킬 수 있게 해준다.
각각의 프로그램의 파트는 스레드라고 불리며 각각의 스레드는 별도의 실행경로를 정의한다.
Main Thread
자바 프로그램이 시작 될 때 시작되는 스레드.
특징
다른 "child" 스레드를 생성 할 수 있다.
프로그램의 'shutdown'시 가장 마지막에 종료되는 스레드다.
* 스레드의 참조를 얻기 위해서 currentThread()를 사용 할 수 있다.
Example)
123456789101112131415161718192021222324252627282930313233343536373839404142434445public class Test2 extends Thread{public static void main(String[] args) {// getting reference to Main threadThread t = Thread.currentThread();// getting name of Main threadSystem.out.println("Current Thread: " + t.getName());// changing the name of Main threadt.setName("KJMain");System.out.println("After name change: " + t.getName());// getting priority of Main threadSystem.out.println("Main thread priority: " + t.getPriority());System.out.println("Main thread new priority: " + t.getPriority());for(int i = 0; i < 5; i++) {System.out.println("Main thread");}// Main thread creating a child threadChildThread ct = new ChildThread();// getting priority of child class// which will be inherited from Main thread// as it is created by Main threadSystem.out.println("Child thread priority: " + ct.getPriority());ct.setPriority(MIN_PRIORITY);System.out.println("Child thread new priority: " + ct.getPriority());// starting child threadct.start();}}class ChildThread extends Thread{public void run() {for(int i = 0; i < 5; i++) {System.out.println("Child thread");}}}cs Output:
main() 메소드와 Main Thread 사이의 관계
자바 프로그램은 실행되면 우선 JVM이 main thread를 만들게 된다. 그러고 나서 main thread가 main() 메소드를 찾아서 클래스로 초기화 시켜주는 역할을 맡는다.
Daemon thread
데몬스레드는 가비지 콜렉터와 같이 '백그라운드'에서 실행되는 낮은 우선순위를 가진 스레드를 말한다.
메소드
void setDaemon(boolean status)
- 현재 스레드를 데몬 메소드인지 유저 메소드인지 표시하는데 사용되어진다.
- 어떠한 스레드 tU가 있다고 가정 할 때 tU.setDaemon(true) 과 같이 설정하면 tU스레드를 데몬 스레드로 만들수 있고, 반대로 인자값을 false로 주게 된다면 유저 스레드로 변경 할 수 있다.
boolean isDaemon()
- 현재 스레드가 데몬 스레드인지 체크해준다. 데몬 스레드라면 true를 리턴한다.
Example)
123456789101112131415161718192021222324252627282930313233public class DaemonThread extends Thread{public DaemonThread(String name) {super(name);}@Overridepublic void run() {// Checking whether the thread is Daemon or notif(Thread.currentThread().isDaemon()) {System.out.println(getName() + " is Daemon thread");}else {System.out.println(getName() + " is User thread");}}public static void main(String[] args) {DaemonThread t1 = new DaemonThread("t1");DaemonThread t2 = new DaemonThread("t2");DaemonThread t3 = new DaemonThread("t3");// Setting user thread t1 to Daemont1.setDaemon(true);// starting first 2 thredat1.start();t2.start();// setting user thread t3 to Daemont3.setDaemon(true);t3.start();}}cs Output:
* 스레드가 start() 된 뒤에 setDaemon()을 사용하면 IllegalThreadStateException을 throw한다
* 프로세스에 데몬 스레드만 남는다면 프로그램은 종료된다.
동기화
임계영역에 있는 공유자원에 접근하고자 할 때 Race condition등의 문제로 인해 에러나 예측하지 못한 문제가 생길 수 있기 때문에 동기화를 해야할 필요성이 생긴다.
자바에서는 이를 Syncronized block을 통해 지원하고 있다.
Syncronized block은 하나의 오브젝트에서 여러 스레드가 수행 되어야 할 때 한 순간에 한 스레드만 수행하도록 하여
스레드간 충돌을 피할 수 있도록 해준다.
Syncronized block이 작업중인 코드에 다른 스레드가 접근하여 작업하려 할 때 다른 스레드는 block되어진다.
'한 순간에 한 스레드만 수행 할 수 있다'는 컨셉은 자바에서 Monitor를 통해 구현되어진다.
lock된 모니터에 들어가기를 원하는 스레드는 작업중인 스레드가 모니터에서 빠져나올때까지 block되어 중지된다.
Example)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354class Sender {public void send(String msg) {System.out.println("Sending \t" + msg);try {Thread.sleep(1000);}catch (Exception e) {System.out.println("Thread interrupted.");}System.out.println("\n" + msg + "Sent");}}public class ThreadedSend extends Thread{private String msg;Sender sender;// Receive a message object and a string// message to be sentThreadedSend(String m, Sender obj) {msg = m;sender = obj;}public void run() {// Only one thread can send a message at a timesynchronized (sender) {// synchronizing the snd objectsender.send(msg);}}}class SyncDemo {public static void main(String args[]) {Sender snd = new Sender();ThreadedSend S1 = new ThreadedSend(" Hi ", snd);ThreadedSend S2 = new ThreadedSend(" Bye ", snd);// start two thread of ThreadedSend typeS1.start();S2.start();// wait for threads to endtry {S1.join();S2.join();}catch(Exception e) {System.out.println("Interrupted");}}}cs Output:
DeadLock
Race condition등을 해결하기 위해 자바에서는 synchronized 키워드를 통한 동기화를 제공한다.
synchronized 키워드는 한 자원에 하나의 스레드가 들어가 lock을 걸어 다른 스레드가 동시에 사용하지 못하게 하는 것인데, 이는 Deadlock을 유발 할 수 있다.
Deadlock은 스레드가 발생 가능성이 없는 이벤트를 기다릴 경우 발생되는데 이는 다음과 같은 상황에서 발생 될 수 있다.
위의 그림은 다음과 같은 과정을 거치는데
1. Thread X가 자원 A를 점유하고 locking한다.
2. Thread Y가 자원 B를 점유하고 locking한다.
3. Thread X가 자원 A를 점유한 상태에서 자원 B를 요청한다.
4. Thread Y가 자원 B를 점유한 상태에서 자원 A를 요청한다.
이때 스레드는 하고 있는 작업이 끝날때까지 자원을 반환하지 않을 것임으로 Thread X가 자원 B를 받을 확률도 없고
Thread Y가 자원 A를 받을 확률도 없다. 이때 이런 상태를 Deadlock(교착상태)라고 한다.
Example)
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798// Java Program to illustrate Deadlock in multithreadingclass Util {// Util class to sleep a threadstatic void sleep(long millis) {try {Thread.sleep(millis);}catch(InterruptedException e) {e.printStackTrace();}}}// This class is shared by both threadsclass Shared {// first synchronized methodsynchronized void test1(Shared s2) {System.out.println("test1-begin");Util.sleep(1000);// taking object lock of s2 enters// into test2 methods2.test2(this);System.out.println("test1-end");}// second synchronized methodsynchronized void test2(Shared s1) {System.out.println("test2-begin");Util.sleep(1000);s1.test1(this);// taking object lock of s1 enters}}class Thread1 extends Thread {private Shared s1;private Shared s2;// constructor to initialize fieldspublic Thread1(Shared s1,Shared s2) {this.s1 = s1;this.s2 = s2;}// run method to start a thread@Overridepublic void run() {// taking object lock of s1 enters// into test1 methods1.test1(s2);}}class Thread2 extends Thread {private Shared s1;private Shared s2;// constructor to initialize fieldspublic Thread2(Shared s1, Shared s2) {this.s1 = s1;this.s2 = s2;}// run method to start a thread@Overridepublic void run() {// taking object lock of s2// enter into test2 methods2.test2(s1);}}public class Deadlock {public static void main(String[] args) {// creating one objectShared s1 = new Shared();// creating second objectShared s2 = new Shared();// creating first thread and starting itThread1 t1 = new Thread1(s1, s2);t1.start();// creating second thread and starting itThread2 t2 = new Thread2(s1, s2);t2.start();// Sleeping main threadUtil.sleep(2000);}}cs Output:
Deadlock으로 인해 두 스레드가 끝나지 않는 것을 볼 수 있다.
Deadlock을 피하는 방법
- 하나의 스레드에 lock을 제공했으면 다른 스레드에 lock을 제공하지 않도록 하기
- 필요하지 않은 Lock 사용하지 않기
- Max-time을 두고 Thread.join()을 활용해 스레드를 종료시키기
Reference
www.geeksforgeeks.org/multithreading-in-java/
www.geeksforgeeks.org/start-function-multithreading-java/?ref=rp
www.geeksforgeeks.org/difference-between-thread-start-and-thread-run-in-java/
www.geeksforgeeks.org/lifecycle-and-states-of-a-thread-in-java/
www.geeksforgeeks.org/java-thread-priority-multithreading/
www.geeksforgeeks.org/main-thread-java/
www.geeksforgeeks.org/daemon-thread-java/
www.geeksforgeeks.org/synchronized-in-java/
www.geeksforgeeks.org/deadlock-in-java-multithreading/