0.학습할 것
- Thread 클래스와 Runnable 인터페이스
- Main 쓰레드
- 쓰레드의 우선순위
- 동기화
- 쓰레드의 상태
- 데드락
0-1. 용어(개념) 정리
프로세스(Process)
- 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램
- 메모리에 올라와 실행되고 있는 프로그램의 인스턴스(독립적인 개체)
- 운영체제로부터 시스템 자원을 할당받는 작업의 단위(동적인 개념으로는 실행된 프로그램을 의미)
특징
- 프로세스는 각각 독립된 메모리 영역(Code, Data, Stack, Heap의 구조)을 할당받는다.
- 기본적으로 프로세스당 최소 1개의 스레드(메인 스레드)를 가지고 있다.
- 각 프로세스는 별도의 주소 공간에서 실행되며, 한 프로세스는 다른 프로세스의 변수나 자료구조에 접근할 수 없다.
- 한 프로세스가 다른 프로세스의 자원에 접근하려면 프로세스 간의 통신(IPC, inter-process communication)을 사용해야 한다.
스레드(Thread)
- “프로세스 내에서 실행되는 여러 흐름의 단위”
- 프로세스의 특정한 수행 경로
- 프로세스가 할당받은 자원을 이용하는 실행의 단위
특징
- 스레드는 프로세스 내에서 각각 Stack만 따로 할당받고 Code, Data, Heap 영역은 공유한다.
- 스레드는 한 프로세스 내에서 동작되는 여러 실행의 흐름으로, 프로세스 내의 주소 공간이나 자원들(힙 공간 등)을 같은 프로세스 내에 스레드끼리 공유하면서 실행된다.
- 같은 프로세스 안에 있는 여러 스레드들은 같은 힙 공간을 공유한다.
- 반면에 프로세스는 다른 프로세스의 메모리에 직접 접근할 수 없다.
- 각각의 스레드는 별도의 레지스터와 스택을 갖고 있지만, 힙 메모리는 서로 읽고 쓸 수 있다.
- 한 스레드가 프로세스 자원을 변경하면, 다른 이웃 스레드(sibling thread)도 그 변경 결과를 즉시 볼 수 있다.
0-2. JAVA의 Thread
다음은 인텔리제이에 명시되어 있는 Thread에 관한 설명이다.
- 스레드는 하나의 프로그램에서의 실행 흐름이다.
- JVM은 병렬적으로 작동하는 여러개의 스레드 실행을 허용한다.
- 모든 스레드는 우선순위가 있다.
- 우선순위가 높은 스레드는 우선순위가 낮은 스레드보다 먼저 실행된다.
- 어떤 스레드는 데몬스레드가 되거나 되지 않을 수 있다.
- 일부 스레드에서 실행중인 코드가 새 스레드 객체를 생성할 때, 새 스레드는 처음에 생선된 스레드의 우선순위와 동일하게 설정된 우선순위를 가지며, 생성스레드가 데몬인 경우에만 데몬스레드가 된다.
- JVM이 시작될 때 일반적으로 메인메서드의 호출로 발생한 단일 비데몬 스레드가 있다.
- JVM은 다음과 같은 상황이 발생할 때 까지 지속된다.
- Runtime 클래스의 exit() 메서드가 호출되고 security manager가 종료 조작을 허가한 경우.
- 데몬 스레드가 아닌 모든 스레드가 run()메서드의 호출로 return되었거나, run()메서드를 넘어서 전파되는 예외를 throw하여 죽은경우.
- 스레드는 두 가지의 실행방식이 있다. 첫 번째는 Thread 클래스의 서브클래스로 선언되는것이다. 이 서브클래스는 반드시 Thread클래스의 run()메서드를 오버라이딩 해야한다. 그런 다음에야 서브클래스의 인스턴스를 할당하고 시작할 수 있다.
- 그 후 인스턴스의 start()메서드를 호출하면 스레드를 실행할 수 있다.
- 또 다른 방법은 Runnable 인터페이스를 구현하는 클래스를 작성하는 것이다.
- 그 클래스는 run()메서드를 구현해야한다.
- 새로운 스레드의 인수로 Runnable인스턴스를 인자로 넘긴 후, 해당 스레드를 실행하면 스레드를 실행할 수 있다.
- 모든 스레드는 식별을 위한 이름을 가지고 있다.
- 둘 이상의 스레드가 동일한 이름을 가질 수 있다.
- 스레드가 생성될 때 이름이 지정되지 않으면 새 이름이 생성된다.
- 달리 명시되지 않는 한, 이 클래스의 생성자, 또는 메서드에 null 인수를 전달하면 NullPointerException이 throw된다.
1. Thread 클래스와 Runnable 인터페이스
자바에서 Thread를 생성하는 방법은 크게 두가지가 있다.
- Thread 클래스를 상속받아서 사용
- Runnable 인터페이스를 구현
1.1 Thread 클래스를 상속받아서 사용
- 클래스를 Thread의 자식 클래스를 선언하여 사용한다.
- 상속받은 자식 클래스는 run()메서드를 재정의하여 사용해야 한다.
아래는 Thread클래스의 상속을 받은 MyThread클래스를 통해 Thread를 호출한 예제이다.
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
class MyThread extends Thread {
@Override
public void run(){
for (int i = 0; i < 3; i++) {
System.out.println("Thread클래스를 상속 받아 만든 MyThread 클래스 입니다.");
}
}
}
출력
Thread클래스를 상속 받아 만든 MyThread 클래스 입니다.
Thread클래스를 상속 받아 만든 MyThread 클래스 입니다.
Thread클래스를 상속 받아 만든 MyThread 클래스 입니다.
1.2 Runnable 인터페이스를 구현하여 사용
- 클래스를 Runnable 인터페이스를 구현하는 클래스로 선언하여 사용한다.
- 구현한 해당 클래스는 run() 메소드를 구현한다.
- run() 메소드를 구현했다면 클래스의 인스턴스를 할당하고 Thread를 만들 때 인수로 전달하고 시작할 수 있다.
- Runnable 은 Thread의 작업 내용을 가지고 있는 객체일 뿐이지 실제 쓰레드는 아니다.
public class ThreadTest2 {
public static void main(String[] args) {
Runnable myThread2 = new MyThread2();
Thread thread = new Thread(myThread2);
thread.start();
}
}
class MyThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("Runnable 인터페이스를 구현하여 만든 MyThread2 클래스 입니다.");
}
}
}
출력
Runnable 인터페이스를 구현하여 만든 MyThread2 클래스 입니다.
Runnable 인터페이스를 구현하여 만든 MyThread2 클래스 입니다.
Runnable 인터페이스를 구현하여 만든 MyThread2 클래스 입니다.
그렇다면 이 두가지 방법의 차이점은 무엇일까?
public class Thread implements Runnable { ...}
Thread 클래스는 Runnable 인터페이스를 상속받는다.
즉 Thread 클래스를 상속받아 사용해도 상위에는 Runnable이 있으며 Thread 클래스는 run() 메서드만 선언된 Runnable 인터페이스를 받아 재정의 해주고 추가적인 함수와 변수를 더 정의해준 것이다.
Thread 클래스를 상속받게 되면 함수 재정의가 필수는 아니다.
하지만 Runnable 인터페이스를 상속받으면 반드시 재정의 해줘야한다. (인터페이스의 특징)
또한 Runnable 인터페이스를 상속받게 되면 다중 상속이 가능해진다.
만약 다른 클래스를 상속해야하는데 다중 스레도도 필요한 경우 해당 인터페이스를 구현하여 다중 상속을 할 수 있다
Thread 클래스가 다른 클래스를 확장할 필요가 있는 경우에는 Runnable 인터페이스를 구현하면 된다.
그렇지 않은 경우 Thread 클래스를 사용하는 것이 좋다.
1-3. 쓰레드의 실행 start(), run()
A thread is a program that starts with a method() frequently used in this class only known as the start() method. This method looks out for the run() method which is also a method of this class and begins executing the bod of the run() method.
쓰레드를 실행하기 위해서는 start 메서드를 통해 해당 쓰레드를 호출해야 한다.
start 메서드는 쓰레드가 작업을 실행할 호출 스택을 만들고 그 안에 run 메서드를 올려주는 역할을 한다.
Thread 의 상태에 대한 자세한 설명은 2. 쓰레드의 상태 를 참고.
public void run() : 쓰레드가 실행되면 run() 메소드를 호출하여 작업을 한다.
public synchronized void start() : 쓰레드를 실행시키는 메소드. start 메소드가 호출되었다고 해서 바로 실행되는 것이 아니라, 일단 실행 대기 상태에 있다가 자신의 차례가 되어야 실행된다.
1-4. Thread 주요 메서드
Thread 클래스의 생성자와 메서드에 대한 자세한 설명과 예제는 다음 링크를 참조.
https://www.geeksforgeeks.org/java-lang-thread-class-java/?ref=gcse
쓰레드의 스케줄링과 관련된 메소드
메서드 | 설 명 |
static void sleep(long millis) static void sleep(long millis, int nanos) |
지정된 시간(1/1000초 단위)동안 쓰레드를 일시정지 시킨다. 지정한 시간이 지나고 나면, 자동적으로 다시 실행 대기 상태가 됩니다. |
void join() void join(long millis) void join(long millis, int nanos) |
지정된 시간동안 쓰레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속합니다. |
void interrupt() | 쓰레드에게 작업을 멈추라고 요청합니다. 쓰레드의 interrupted상태를 false에서 true로 변경합니다. |
static boolean interrupted() | sleep()이나 join()에 의해 일시정지 상태인 쓰레드를 깨워서 실행대기상태로 만듭니다. 해당 쓰레드에서는 InterruptedException이 발생함으로써 일시정지상태를 벗어나게 됩니다. |
@Deprecated void stop() |
쓰레드를 즉시 종료시킵니다. |
@Deprecated void suspend() |
쓰레드를 일시정지 시킵니다. resume()을 호출하면 다시 실행 대기상태가 됩니다. |
@Deprecated void resume() |
suspend()에 의해 일시정지 상태에 있는 쓰레드를 실행대기 상태로 만듭니다. |
static void yield() | 실행 중에 다신에게 주어진 실행시간을 다른 쓰레드에게 양보(yield)하고 자신은 실행 대기상태가 됩니다. |
resume(), stop(), suspend()는 쓰레드를 교착상태(dead-lock)로 만들기 쉽기 때문에 deprecated되었다.
이외의 메소드들
- currentThread() : 현재 실행중인 thread 객체의 참조를 반환합니다.
- destroy() : clean up 없이 쓰레드를 파괴합니다. @Deprecated 된 메소드로 suspend()와 같이 교착상태(deadlock)을 발생시키기 쉽습니다.
- isAlive() : 쓰레드가 살아있는지 확인하기 위한 메소드 입니다. 쓰레드가 시작되고 아직 종료되지 않았다면 살아있는 상태입니다.
- setPriority(int newPriority) : 쓰레드의 우선순위를 새로 설정할 수 있는 메소드입니다.
- getPriority() : 쓰레드의 우선순위를 반환합니다.
- setName(String name) : 쓰레드의 이름을 새로 설정합니다.
- getName(String name) : 쓰레드의 이름을 반환합니다.
- getThreadGroup() : 쓰레드가 속한 쓰레드 그룹을 반환합니다. 종료됐거나 정지된 쓰레드라면 null을 반환합니다.
- activeCount() : 현재 쓰레드의 쓰레드 그룹 내의 쓰레드 수를 반환합니다.
- enumerate(Thread[] tarray) : 현재 쓰레드의 쓰레드 그룹내에 있는 모든 활성화된 쓰레드들을 인자로 받은 배열에 넣습니다. 그리고 활성화된 쓰레드의 숫자를 int 타입의 정수로 반환합니다.
- dumpStack() : 현재 쓰레드의 stack trace를 반환합니다.
- setDaemon(boolean on) : 이 메소드를 호출한 쓰레드를 데몬 쓰레드 또는 사용자 쓰레드로 설정합니다.
JVM은 모든 쓰레드가 데몬 쓰레드만 있다면 종료됩니다. 이 메소드는 쓰레드가 시작되기 전에 호출되야합니다. - isDaemon() : 이 쓰레드가 데몬 쓰레드인지 아닌지 확인하는 메소드입니다. 데몬쓰레드면 true, 아니면 false 반환
- getStackTrace() : 호출하는 쓰레드의 스택 덤프를 나타내는 스택 트레이스 요소의 배열을 반환합니다.
- getAllStackTrace() : 활성화된 모든 쓰레드의 스택 트레이스 요소의 배열을 value로 가진 map을 반환합니다. key는 thread 입니다.
- getId() : 쓰레드의 고유값을 반환합니다. 고유값은 long 타입의 정수 입니다.
- getState() : 쓰레드의 상태를 반환합니다.
예제..
package ThreadTest;
// Java program Demonstrating Methods of Thread class
// Class 1
// RunnableSampleClass class implementing Runnable interface
class RunnableSampleClass implements Runnable {
public void run() {
try {
// Thread2 상태출력
System.out.println("thread2 5000ms 초 대기");
// Thread를 0.5초 stop
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("Thread2 interrupted 상태임");
}
}
}
// Class 2
// Test class extending Runnable interface
public class Test implements Runnable {
// Method 1
// run() method of this class
public void run() {}
// Method 2
// Main driver method
public static void main(String[] args) {
// 2개의 클래스의 객체 생성
Test obj = new Test();
RunnableSampleClass obj2 = new RunnableSampleClass();
// main()메서드에서 2개의 스레드 생성
Thread thread1 = new Thread(obj);
Thread thread2 = new Thread(obj2);
// 2개의 객체(thread) 시작.
thread1.start();
thread2.start();
// Loading thread 1 in class 1 (이 스레드에 대한 컨텍스트 ClassLoader를 반환)
ClassLoader loader = thread1.getContextClassLoader();
// Creating 3rd thread in main() method
Thread thread3 = new Thread(new RunnableSampleClass());
// Getting number of active threads (활성화된 thread 개수 출력)
System.out.println("Thread.activeCount() : " + Thread.activeCount());
thread1.checkAccess();
// Fetching an instance of this (현재 실행 중인 스레드 개체에 대한 참조를 반환)
Thread t = Thread.currentThread();
// Print and display commands
System.out.println("t.getName() : "+t.getName());
System.out.println("Thread1 name: " + thread1.getName());
System.out.println("Thread1 ID: " + thread1.getId());
// Fetching the priority and state of thread1
System.out.println("Thread1의 우선순위 = " + thread1.getPriority());
// Getting the state of thread 1 using getState() method
// and printing the same
System.out.println("Thread1의 상태 : " + thread1.getState());
thread2 = new Thread(obj2);
thread2.start();
thread2.interrupt();
System.out.println("thread2가 interrupted 되었는가? : " + thread2.interrupted() );
System.out.println("thread2가 alive한가? " + thread2.isAlive());
thread1 = new Thread(obj);
System.out.println("thread1 demon thread 설정");
thread1.setDaemon(true);
System.out.println("Is thread1 a daemon thread? " + thread1.isDaemon());
System.out.println("Is thread1 interrupted? " + thread1.isInterrupted());
// Waiting for thread2 to complete its execution
System.out.println();
System.out.println("thread1 waiting for thread2 to join");
try {
thread2.join();
}
catch (InterruptedException e) {
// Display the exception along with line number
// using printStackTrace() method
e.printStackTrace();
}
// Now setting the name of thread1
System.out.println("thread1 이름설정.");
thread1.setName("child thread xyz");
// Print and display command
System.out.println("thread1의 이름은 다음과 같이 설정되었음 : " + thread1.getName());
// Setting the priority of thread1
System.out.println("thread1의 우선순위 설정");
thread1.setPriority(5);
System.out.println("thread2 양보됨.");
thread2.yield();
// Fetching the string representation of thread1
System.out.println("thread1의 스레드의 이름, 우선 순위 및 스레드 그룹을 포함하여 이 스레드의 문자열 표현을 반환합니다.");
System.out.println(thread1.toString());
// Getting list of active thread in current thread's group
Thread[] tarray = new Thread[3];
Thread.enumerate(tarray);
// Display commands
System.out.println("List of active threads:");
System.out.printf("[");
// Looking out using for each loop
for (Thread thread : tarray) {
System.out.println(thread);
}
// Display commands
System.out.printf("]\n");
System.out.println(Thread.getAllStackTraces());
ClassLoader classLoader = thread1.getContextClassLoader();
System.out.println(classLoader.toString());
System.out.println(thread1.getDefaultUncaughtExceptionHandler());
thread2.setUncaughtExceptionHandler(thread1.getDefaultUncaughtExceptionHandler());
thread1.setContextClassLoader(thread2.getContextClassLoader());
thread1.setDefaultUncaughtExceptionHandler(thread2.getUncaughtExceptionHandler());
thread1 = new Thread(obj);
StackTraceElement[] trace = thread1.getStackTrace();
System.out.println("Printing stack trace elements for thread1:");
for (StackTraceElement e : trace) {
System.out.println(e);
}
ThreadGroup grp = thread1.getThreadGroup();
System.out.println("ThreadGroup to which thread1 belongs " + grp.toString());
System.out.println(thread1.getUncaughtExceptionHandler());
System.out.println("Does thread1 holds Lock? " + thread1.holdsLock(obj2));
// Thread.dumpStack();
}
}
결과
thread2 5000ms 초 대기
Thread.activeCount() : 4
t.getName() : main
Thread1 name: Thread-0
Thread1 ID: 13
Thread1의 우선순위 = 5
Thread1의 상태 : TERMINATED
thread2가 interrupted 되었는가? : false
thread2가 alive한가? true
thread1 demon thread 설정
Is thread1 a daemon thread? true
Is thread1 interrupted? false
thread1 waiting for thread2 to join
thread2 5000ms 초 대기
Thread2 interrupted 상태임
thread1 이름설정.
thread1의 이름은 다음과 같이 설정되었음 : child thread xyz
thread1의 우선순위 설정
thread2 양보됨.
thread1의 스레드의 이름, 우선 순위 및 스레드 그룹을 포함하여 이 스레드의 문자열 표현을 반환합니다.
Thread[child thread xyz,5,main]
List of active threads:
[Thread[main,5,main]
Thread[Monitor Ctrl-Break,5,main]
Thread[Thread-1,5,main]
]
{Thread[main,5,main]=[Ljava.lang.StackTraceElement;@28feb3fa, Thread[Monitor Ctrl-Break,5,main]=[Ljava.lang.StackTraceElement;@675d3402, Thread[Common-Cleaner,8,InnocuousThreadGroup]=[Ljava.lang.StackTraceElement;@51565ec2, Thread[Attach Listener,5,system]=[Ljava.lang.StackTraceElement;@482f8f11, Thread[Signal Dispatcher,9,system]=[Ljava.lang.StackTraceElement;@1593948d, Thread[Reference Handler,10,system]=[Ljava.lang.StackTraceElement;@1b604f19, Thread[Thread-1,5,main]=[Ljava.lang.StackTraceElement;@7823a2f9, Thread[Finalizer,8,system]=[Ljava.lang.StackTraceElement;@4cc0edeb}
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
null
Printing stack trace elements for thread1:
ThreadGroup to which thread1 belongs java.lang.ThreadGroup[name=main,maxpri=10]
java.lang.ThreadGroup[name=main,maxpri=10]
Does thread1 holds Lock? false
2. Main 쓰레드
Java는 다중 스레드 프로그래밍을 지원한다.
다중 스레드 프로그램은 동시에 실행할 수 있는 두 개 이상의 스레드를 의미한다.
각 스레드는 별도의 실행 경로를 정의한다.
Java에서는 프로그램이 시작되면 하나의 스레드가 즉시 실행되기 시작한다.
프로그램이 시작될 때 실행되는 스레드이기 때문에 일반적으로 프로그램의 메인 스레드라고 한다.
다른 쓰레드를 생성해서 실행하지 않으면, 메인 메서드, 즉 메인 쓰레드가 종료되는 순간 프로그램도 종료된다.
하지만 여러 쓰레드를 실행하면, 메인 쓰레드가 종료되어도 다른 쓰레드가 작업을 마칠 때까지 프로그램이 종료되지 않는다.
쓰레드는 '사용자 쓰레드(user thread)'와 '데몬 쓰레드(daemon thread)'로 구분되는데,
실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램이 종료된다.
2-1. 데몬스레드
- Java의 데몬 스레드는 가비지 수집과 같은 작업을 수행하기 위해 백그라운드에서 실행되는 우선 순위가 낮은 스레드.
- Java의 데몬 스레드는 사용자 스레드에 서비스를 제공하는 서비스 제공자 스레드
- 데몬스레드 수명은 사용자 스레드에 달려 있습니다. 즉, 모든 사용자 스레드가 죽을 때 JVM은 이 스레드를 자동으로 종료한다.
- 간단히 말해서 백그라운드 지원 작업을 위해 사용자 스레드에 서비스를 제공한다고 말할 수 있다. 사용자 스레드에 서비스를 제공하는 것 외에 다른 역할은 없다.
- 기본적으로 기본 스레드는 항상 데몬이 아니지만 나머지 모든 스레드에 대해 데몬 특성은 부모에서 자식으로 상속된다.
- 즉, 부모가 데몬이면 자식도 데몬이고 부모가 데몬이 아니면 자식도 데몬이 아닙니다.
Deamon Thread 사용
- Main 쓰레드가 Daemon 이 될 쓰레드의 setDaemon(true)를 호출해주면 Daemon 쓰레드가 된다
public final void setDaemon(boolean on)
예제
public class DaemonThread extends Thread
{
public DaemonThread(String name){
super(name);
}
public void run()
{
// Checking whether the thread is Daemon or not
if(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 Daemon
t1.setDaemon(true);
// starting first 2 threads
t1.start();
t2.start();
// Setting user thread t3 to Daemon
t3.setDaemon(true);
t3.start();
}
}
출력
t1 is Daemon thread
t3 is Daemon thread
t2 is User thread
3. 쓰레드의 우선순위
- 쓰레드는 우선순위(priority)라는 멤버 변수를 갖고 있다.
- 각 쓰레드별로 우선순위를 다르게 설정해줌으로써 어떤 쓰레드에 더 많은 작업 시간을 부여할 것인가를 설정해줄 수 있다.
- 우선순위는 1 ~ 10 사이의 값을 지정해줄 수 있으며 기본값으로 5가 설정되어 있다.
- 하지만 쓰레드의 우선순위는 비례적인 절댓값이 아닌 어디까지나 상대적인 값일 뿐이다.
- 우선순위가 10인 쓰레드가 우선순위가 1인 쓰레드보다 10배 더 빨리 수행되는 것이 아니다. (반드시 보장되는 것이 아니다)
- 단지 우선순위가 10인 쓰레드는 우선순위가 1인 쓰레드 보다 좀 더 많이 실행 큐에 포함되어, 좀 더 많은 작업 시간을 할당받을 뿐이다.
쓰레드의 우선순위를 지정하는 필드와 메소드
- public final static int MIN_PRIORITY = 1 : 쓰레드가 가질 수 있는 우선 순위의 최소값
- public final static int NORM_PRIORITY = 5 : 쓰레드가 가지는 기본 우선 순위 값.
- public final static int MAX_PRIORITY = 10 : 쓰레드가 가질 수 있는 우선 순위의 최대값
- setPriority(int newPriority) : 쓰레드의 우선순위를 지정한 값으로 변경
- getPriority() : 쓰레드의 우선순위를 반환
예제.
import java.lang.*;
// Main class
class ThreadDemo extends Thread {
// Method 1
// run() method for the thread that is called
// as soon as start() is invoked for thread in main()
public void run()
{
// Print statement
System.out.println("Inside run method");
}
// Main driver method
public static void main(String[] args)
{
// Creating random threads
// with the help of above class
ThreadDemo t1 = new ThreadDemo();
ThreadDemo t2 = new ThreadDemo();
ThreadDemo t3 = new ThreadDemo();
// Thread 1
// Display the priority of above thread
// using getPriority() method
System.out.println("t1 thread priority : "
+ t1.getPriority());
// Thread 1
// Display the priority of above thread
System.out.println("t2 thread priority : "
+ t2.getPriority());
// Thread 3
System.out.println("t3 thread priority : "
+ t3.getPriority());
// Setting priorities of above threads by
// passing integer arguments
t1.setPriority(2);
t2.setPriority(5);
t3.setPriority(8);
// t3.setPriority(21); will throw
// IllegalArgumentException
// 2
System.out.println("t1 thread priority : "
+ t1.getPriority());
// 5
System.out.println("t2 thread priority : "
+ t2.getPriority());
// 8
System.out.println("t3 thread priority : "
+ t3.getPriority());
// Main thread
// Displays the name of
// currently executing Thread
System.out.println(
"Currently Executing Thread : "
+ Thread.currentThread().getName());
System.out.println(
"Main thread priority : "
+ Thread.currentThread().getPriority());
// Main thread priority is set to 10
Thread.currentThread().setPriority(10);
System.out.println(
"Main thread priority : "
+ Thread.currentThread().getPriority());
}
}
출력
t1 thread priority : 5
t2 thread priority : 5
t3 thread priority : 5
t1 thread priority : 2
t2 thread priority : 5
t3 thread priority : 8
Currently Executing Thread : main
Main thread priority : 5
Main thread priority : 10
순환할당 (Round Robin) 방식
스케줄링 방식에는 우선순위방식 말고도 RR방식도 있는데 이 방식은 스레드마다 사용할 시간을 할당(Time slice)해서 정해진 시간만큼 돌아가며 실행하는 방식으로 JVM에 의해 결정되기 때문에 개발자가 임의로 수정할 수 없다.
4. 스레드의 동기화
싱글쓰레드 프로세스의 경우 프로세스 내에서 단 하나의 쓰레드만 작업하기 때문에 프로세스의 자원을 가지고 작업하는데는 별 문제가 없다.
멀티 쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다. 아래는 두개의 쓰레드에서 공유 자원인 데이터의 값을 1을 증가하는 작업을 할 때 발생할 수 있는 문제다.
class MyData{
int data = 3;
public void plusData(){
int mydata = data;
try{Thread.sleep(2000);} catch (InterruptedException e){}
data = mydata + 1;
}
}
class PlusThread extends Thread{
MyData myData;
public PlusThread(MyData myData){
this.myData = myData;
}
@Override
public void run(){
myData.plusData();
System.out.println(getName()+"실행결과:"+ myData.data);
}
}
public class TheNeedForSynchronized {
public static void main(String[] args) {
// 공유 객체 생성
MyData myData = new MyData();
// plusThread1
Thread plusThread1 = new PlusThread(myData);
plusThread1.setName("plusThread1");
plusThread1.start();
try{Thread.sleep(1000);} catch (InterruptedException e){}
// plusThread2
Thread plusThread2 = new PlusThread(myData);
plusThread2.setName("plusThread2");
plusThread2.start();
}
}
출력
plusThread1실행결과:4
plusThread2실행결과:4
이러한 이유는 두번째 쓰레드가 data필드를 증가시키는 시점에 아직 첫번째 쓰레드 실행이 끝나지 않았기 때문이다.
멀티쓰레드를 사용할 때 동기화는 필요하다. 동기화를 하지 않았을 경우 왼쪽의 step3도중 오른쪽 step1이 먼저 발생하면 결과는 5가 아닌 4가 나올 것이다. 그러므로 동기화를 사용하지 않았을 때 문제가 발생되어 동기화를 해주어야 한다.
사용자가 원하는 로직은 다음과 같다.
위의 그림처럼 하나의 쓰레드가 MyData객체 내의 data 필드값을 증가시키는 연산을 완전히 끝내고 난 뒤에 다음 쓰레드가 동일한 작업을 수행하면 data의 필드값은 5의 결과를 가질 것이다.
위와 같이 하나의 쓰레드가 객체를 모두 사용 후 다음 쓰레드가 사용할 수 있도록 설정하는 것을 '동기화' 라고 한다.
공유 자원 접근 순서에 따라 실행 결과가 달라지는 프로그램의 영역을 임계구역(critical section)이라고 한다.
동기화를 하려면 다른 쓰레드가 간섭해서는 안 되는 부분을 임계 영역(critical section)으로 설정해 주어야 한다.
임계 영역 설정은 synchronized 키워드를 사용한다.
4-1. Mutual Exclusion (상호배제)
Race Condition을 방지하고 예산한 값을 도출하기 위한 방법
1,2,3을 실행하는 동안 다른 프로세스가 접근하지 못하게 하면 된다.
(CS(critical Section)에 누군가 있으면 다른 프로세스(스레드가)가 접근하지 못한다)
용어 정리
- Shared data (공유 데이터) or Critical data : 여러 프로세스들이 공유하는 데이터
- Critical section (임계 영역) : 공유 데이터를 접근하는 코드 영역 (code segment)
- Mutual exclusion (상호배제) : 둘 이상의 프로세스가 동시에 critical section 에 진입하는 것을 막는 것
Mutual exclusion primitives 를 만족해야 할 조건들
- Mutual exclusion (상호배제)
- Critical section (CS) 에 프로세스가 있으면 , 다른 프로세스의 진입을 금지
- Progress (진행)
- CS 안에 있는 프로세스 외에는, 다른 프로세스가 CS 에 진입하는 것을 방해 하면 안됨
- (수행하고 있는 프로세스가 아닌데 다른 프로세스가 수행되는 것을 막는 것)
- Bounded waiting (한정대기)
- 프로세스의 CS 진입은 유한시간 내에 허용되어야 (계속 기다리면 안된다)
임계구역(critical section)과 잠금(lock)의 개념을 활용해서 한 쓰레드가 특정 작업을 마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 할 수 있다.
공유 데이터를 사용하는 코드 영역을 임계구역으로 지정해놓고, 공유 데이터(객체)가 가지고 있는 lock(key)을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다. 그리고 해당 쓰레드가 임계 구역내의 모든 코드를 수행하고 벗어나서 lock(key)을 반납해야만 다른 쓰레드가 반납된 lock(key)을 획득하여 임계구역의 코드를 수행할 수 있게 됩니다.
-> 마치 공공 장소의 화장실을 사용할 때 문을 잠그고 들어가서 일을 본 뒤 화장실 문을 열고 다음사람에게 차례를 넘겨주는것을 떠올리면 lock에 대한 이해가 쉽습니다.
이처럼 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 쓰레드의 동기화(synchronization)라고 합니다.
- 여러 개의 쓰레드가 한 개의 리소스를 사용하려고 할 때 사용 하려는 쓰레드를 제외한 나머지들을 접근하지 못하게 막는 것이다.
- 이것을 쓰레드에 안전하다고 하다 (Thread-safe)
- 자바에서 동기화 하는 방법은 3가지로 분류된다
- Synchronized 키워드
- Atomic 클래스
- Volatile 키워드
4-2. 스레드 동기화 방법
자바에서 동기화 방법은 크게 메서드 동기화, 블록 동기화 로 나눌 수 있다.
메서드 동기화는 2개이상의 쓰레드가 동시에 메서드를 실행할 수 없으며, 블록 동기화도 마찬가지로 동시에 해당 블록을 실행할 수 없다.
메서드 동기화
메서드를 동기화할 때는 도익화하고자 하는 메서드의 리턴 타입 앞에 synchronized 키워드를 넣으면 된다.
접근지정자 synchronized 리턴타입 메서드명(입력매개변수) {
// 동기화가 필요한 코드
}
이렇게 되면 동시에 2개이상의 쓰레드에서 해당 메서드를 실행할 수 없다.
예제 코드를 보면 plusData()메서드 앞에 synchronized 키워드를 추가하였다.
class MyData{
int data = 3;
//synchronized 추가
public synchronized void plusData(){
int mydata = data;
try{Thread.sleep(2000);} catch (InterruptedException e){}
data = mydata + 1;
}
}
class PlusThread extends Thread{
MyData myData;
public PlusThread(MyData myData){
this.myData = myData;
}
@Override
public void run(){
myData.plusData();
System.out.println(getName()+"실행결과:"+ myData.data);
}
}
public class SynchronizedMethod {
public static void main(String[] args) {
// 공유 객체 생성
MyData myData = new MyData();
Thread plusThread1 = new PlusThread(myData);
plusThread1.setName("plusThread1");
plusThread1.start();
try{Thread.sleep(1000);} catch (InterruptedException e){}
Thread plusThread2 = new PlusThread(myData);
plusThread2.setName("plusThread2");
plusThread2.start();
}
}
plusThread1실행결과:4
plusThread2실행결과:5
블럭동기화
메서드 중 일부분만 동기화가 필요한 부분만 있다면 전체 메서드를 동기화를 할 필요가 없다.
성능 면에서 많은 손해를 보기 때문이다.
해당 부분만 동기화 할 수 있는데 이를 '블록 동기화' 라고 한다.
synchronized (임의의 객체){
//동기화가 필요한 코드
}
블럭동기화 예제..
class MyData{
int data = 3;
public void plusData(){
synchronized(this) {
int mydata = data;
try {Thread.sleep(2000);} catch (InterruptedException e) {}
data = mydata + 1;
}
}
}
class PlusThread extends Thread{
MyData myData;
public PlusThread(MyData myData){
this.myData = myData;
}
@Override
public void run(){
myData.plusData();
System.out.println(getName()+"실행결과:"+ myData.data);
}
}
public class SynchronizedBlock {
public static void main(String[] args) {
// 공유 객체 생성
MyData myData = new MyData();
// plusThread1
Thread plusThread1 = new PlusThread(myData);
plusThread1.setName("plusThread1");
plusThread1.start();
try{Thread.sleep(1000);} catch (InterruptedException e){}
Thread plusThread2 = new PlusThread(myData);
plusThread2.setName("plusThread2");
plusThread2.start();
}
}
출력
plusThread1실행결과:4
plusThread2실행결과:5
4-3. 동기화의 원리 정리
- 모든 객체는 자신만의 key값을 가지고 있다.
- 블록 동기화 코드에서 (synchronized(this{...}) 블록이 this객체가 가진 key로 잠긴다.
- 실행하는 쓰레드가 그 key를 갖게 된다
- 해당 쓰레드가 동기화 블록의 실행을 완료하기 전까지 다른 열쇠는 key를 얻을 수 없어 해당 블록을 실행할 수 없다.
- 열쇠는 모든 객체가 소유하지만 메서드를 동기화 하는 경우에 this객체의 key를 사용한다.
- 하나의 객체 내부에 여러 개의 동기화 메서드가 있다면 이 메서드 모두가 this의 key로 잠겨있어 1개의 쓰레드가 이들 중 1개의 메서드를 실행하고 있다면 다른 쓰레드는 나머지 2개의 메서드도 같이 잠겨서 사용할 수 없다.
- 즉 다른 쓰레드는 먼저 사용 중인 쓰레드가 작업을 완료하고 key를 반납할 때까지 대기한다.
동기화 영역이 동일한 열쇠로 동기화 되었을 때 예제.
class MyData {
// this 객체가 갖고 있는 하나의 key를 함께 사용
synchronized void aaa() {
for(int i=0; i<3; i++) {
System.out.println(i + "sec");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
// this 객체가 갖고 있는 하나의 key를 함께 사용
synchronized void bbb() {
for(int i=0; i<3; i++) {
System.out.println(i + "초");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
// this 객체가 갖고 있는 하나의 key를 함께 사용
void ccc() {
synchronized(this) {
for(int i=0; i<3; i++) {
System.out.println(i + "번째");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
}
}
public class KeyTest1 {
public static void main(String[] args) {
//공유객체
MyData myData = new MyData();
//세개의 쓰레드가 각각의 메서드 호출
new Thread() {
public void run() {
myData.aaa();
};
}.start();
new Thread() {
public void run() {
myData.bbb();
};
}.start();
new Thread() {
public void run() {
myData.ccc();
};
}.start();
}
}
출력
0sec
1sec
2sec
0번째
1번째
2번째
0초
1초
2초
동기화 메서드와 동기화 블록이 다른 key를 사용할 때 예제.
class MyData {
// this 객체가 갖고 있는 하나의 key를 함께 사용
synchronized void aaa() {
for(int i=0; i<3; i++) {
System.out.println(i + "sec");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
// this 객체가 갖고 있는 하나의 key를 함께 사용
synchronized void bbb() {
for(int i=0; i<3; i++) {
System.out.println(i + "초");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
// Object 객체가 갖고 있는 key를 사용
void ccc() {
synchronized(new Object()) {
for(int i=0; i<3; i++) {
System.out.println(i + "번째");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
}
}
}
public class KeyObject_2 {
public static void main(String[] args) {
//공유객체
MyData myData = new MyData();
//세개의 쓰레드가 각각의 메서드 호출
new Thread() {
public void run() {
myData.aaa();
};
}.start();
new Thread() {
public void run() {
myData.bbb();
};
}.start();
new Thread() {
public void run() {
myData.ccc();
};
}.start();
}
}
출력
0번째
0sec
1번째
1sec
2번째
2sec
0초
1초
2초
5. 쓰레드의 상태
쓰레드는 객체가 생성, 실행, 종료되기까지 다양한 상태를 가진다.
각 쓰레드는 Thread.State 상태로 정의되었다.
Thread의 인스턴스 메서드인 getState()로 쓰레드의 상태값을 가져올 수 있다.
Thread.State getState()
Thread.State의 내부에는 6개의 문자열 상수(NEW, RUNNABLE, TERMINATED, TIMED_WAITING, BLOCKED, WAITING)가 저장되어 있다.
5-1. 쓰레드가 가질 수 있는 6가지 상태
NEW | new 키워드로 쓰레드의 객체가 생성된 상태(start()전) |
RUNNABLE | start() 이후 CPU를 사용할 수 있는 상태 다른 쓰레드들과 동시성에 따라 실행과 실행 대기를 교차함 |
TERMINATED | run()메서드의 작업 내용이 모두 완료돼 쓰레드가 종료된 상태 (한 번 실행(start())된 쓰레드는 재실행이 불가능하며 객체를 새롭게 생성해야 함) |
TIME_WAITING | 일정시간 동안 일시정지 된 상태. 일정시간이 지나거나 중간에 interrupt()메서드가 호출되면 다시 RUNNABLE상태가 됨 |
BLOCKED | 동기화 메서드 또는 동기화 블록을 실행하기 위해 먼저 실행 중인 쓰레드의 실행 완료를 기다리는 상태 = 먼저 실행중인 쓰레드가 key를 반납할 때까지 기다리고 있는 상태 쓰레드의 동기화 영역 수행이 완료되면 BLOCKED상태의 쓰레드는 RUNNABLE상태가 돼 해당 동기화 영역을 실행하게 된다. |
WAITING | 일정시간이 지정되지 않은 정지 상태즉, 한없이 일시정지된 상태 |
쓰레드가 각 상태에서 어떻게 작동되는지 알아보자.
- 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행 대기열에 저장되어 자신의 차례가 될 때까지 기다려야 한다. (실행 대기열은 큐(queue)와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.
- 자기 차례가 되면 실행상태가 된다.
- 할당된 실행시간이 다되거나 yield() 메소드를 만나면 다시 실행 대기상태가 되고 다음 쓰레드가 실행상태가 된다.
- 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 위해 일시정지 상태가 된다. (I/O block은 입출력 작업에서 발생하는 지연상태를 말합니다. 사용자의 입력을 받는 경우를 예로 들수 있습니다.)
- 지정된 일시정지시간이 다되거나, notify(), resume(), interrupt()가 호출되면 일시정지상태를 벗어나 다시 실행 대기열에 저장되어 자신의 차례를 기다린다.
- 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다. (다시 호출할 수 없다)
5-2. NEW, RUNNABLE, TERMINATED 실습
public class ThreadStateTest1 {
public static void main(String[] args) {
// 쓰레드 상태 저장 클래스
Thread.State state;
// 1. 객체 생성 (NEW)
Thread myThread = new Thread() {
@Override
public void run() {
for(long i=0; i<1000000000L ; i++) {} //시간지연하기 위한 반복문
}
};
state = myThread.getState();
System.out.println("myThread state = "+ state); // NEW 상태
// 2. myThread 시작
myThread.start();
state = myThread.getState();
System.out.println("myThread state = "+ state); //Runnable 상태
// 3. myThread 종료
try {
myThread.join();
} catch (InterruptedException e) { }
state = myThread.getState();
System.out.println("myThread state = "+ state); //TERMINATED 상태
}
}
.join() 은 해당 쓰레드가 완료될 때까지 mian쓰레드는 기다리겠다는 의미이다.
또한 RUNNABLE상태에서 다른 쓰레드에게 CPU를 인위로 양보하여 실행대기 상태로 전환할 수 있다.
static void Thread.yield();
5-3. yield()메서드를 이용한 CPU사용양보
class MyThreadSample extends Thread {
boolean yieldFlag;
@Override
public void run() {
while(true) {
if(yieldFlag) {
Thread.yield(); //yieldFlag가 true이면 다른 쓰레드에게 CPU 사용권 양보함.
} else {
System.out.println(getName() + " 실행");
for(long i=0; i<1000000000L ; i++) {} //시간지연 반복문
}
}
}
}
public class YieldInRunnableState {
public static void main(String[] args) {
MyThreadSample thread1 = new MyThreadSample();
thread1.setName("thread1");
thread1.yieldFlag=false;
thread1.setDaemon(true);
thread1.start();
MyThreadSample thread2 = new MyThreadSample();
thread2.setName("thread2");
thread2.yieldFlag=true;
thread2.setDaemon(true);
thread2.start();
// 1. 6초 지연 (1초마다 한번씩 양보)
for(int i=0; i<6; i++) {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
thread1.yieldFlag=!thread1.yieldFlag;
thread2.yieldFlag=!thread2.yieldFlag;
}
}
}
5-4. TIMED_WAITNG <-> RUNNABLE
RUNNABLE상태에서 TIMED_WAITING상태로 전환하는 방법은 두가지가 있다.
첫번째로 정적메서드인 Thread.sleep(log millis) 호출, 두번째로 인스턴스 메서드인 join(long millis)가 호출되면 쓰레드는 TIMED_WAITING 상태가 된다.
RUNNABLE -> TIMED_WAITING 상태로 전환하기
// sleep() 호출
static void Thread.sleep(long times)
// join() 호출
synchronized void join(long times)
5-4-1. Thread.sleep(log millis) 으로 인한 TIMED_WAITING -> RUNNABLE 상태로 전환하기
1. 일시정지 시간이 종료됨
2. void interrupt()
class MyThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
System.out.println(" -- sleep() 진행중 interrupt() 발생");
for(long i=0; i<1000000000L ; i++) {} //시간지연
}
}
}
public class TimedWaiting_Sleep {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
try {Thread.sleep(100);} catch (InterruptedException e) {} //쓰레드 시작 준비시간
System.out.println("MyThread State = " + myThread.getState()); //TIMED_WAITING
myThread.interrupt(); // TIME_WAITING -> RUNNABLE 상태 전환
try {Thread.sleep(100);} catch (InterruptedException e) {} //인터럽트 준비시간
System.out.println("MyThread State = " + myThread.getState()); //RUNNABLE
}
}
결과
MyThread State = TIMED_WAITING
-- sleep() 진행중 interrupt() 발생
MyThread State = RUNNABLE
5-4-2. join(long times)를 이용한 일시정지 및 RUNNABLE상태 전환
class MyThread1 extends Thread {
@Override
public void run() {
for(long i=0; i<1000000000L ; i++) {} //시간지연
}
}
class MyThread2 extends Thread {
MyThread1 myThread1;
public MyThread2(MyThread1 myThread1) {
this.myThread1 = myThread1;
}
@Override
public void run() {
try {
myThread1.join(3000); //myThread1에게 최대 3초당안 CPU 우선 사용권 부여
} catch (InterruptedException e) {
System.out.println(" -- join(...) 진행중 interrupt() 발생");
for(long i=0; i<1000000000L ; i++) {} //시간지연
}
}
}
public class TimedWaiting_Join {
public static void main(String[] args) {
//#1. 객체 생성
MyThread1 myThread1 = new MyThread1();
MyThread2 myThread2 = new MyThread2(myThread1);
myThread1.start();
myThread2.start();
try {Thread.sleep(100);} catch (InterruptedException e) {} //쓰레드 시작 준비 시간
System.out.println("MyThread1 State = " + myThread1.getState()); //RUNNABLE
System.out.println("MyThread2 State = " + myThread2.getState()); //TIMED_WAITING
myThread2.interrupt(); //TIMED_WAITING -> RUNNABLE 상태 전환
try {Thread.sleep(100);} catch (InterruptedException e) {} //인터럽트 준비 시간
System.out.println("MyThread1 State = " + myThread1.getState()); //RUNNABLE
System.out.println("MyThread2 State = " + myThread2.getState()); //RUNNABLE
}
}
결과
MyThread1 State = RUNNABLE
MyThread2 State = TIMED_WAITING
-- join(...) 진행중 interrupt() 발생
MyThread1 State = RUNNABLE
MyThread2 State = RUNNABLE
5-5. BLOCKED
- BLOCKED는 동기화 메서드 또는 동기화 블록을 실행하고자 할 때 이미 다른 쓰레드가 해당 영역을 실행하고 있는 경우 발생
- 해당 동기화 영역이 잠겼을 때는 이미 실행하고 있는 스레드가 실행을 완료하고, 해당 동기화 영역의 key를 반납할 때까지 기다려야 하는 상태
- t1쓰레드 실행 완료 후 key를 동기화 영역에 반납하는데 t2, t3, t4가 key를 가지기 위해 경쟁을 한다. 즉, 먼저 도착하는 쓰레드가 실행권한을 가지게 된다.
class MyBlockTest {
//1. 공유객체
MyClass mc = new MyClass();
//2. 4 개의 쓰레드 필드 생성
Thread t1 = new Thread("thread1") {
public void run() {
mc.syncMethod();
};
};
Thread t2 = new Thread("thread2") {
public void run() {
mc.syncMethod();
};
};
Thread t3 = new Thread("thread3") {
public void run() {
mc.syncMethod();
};
};
Thread t4 = new Thread("thread4") {
public void run() {
mc.syncMethod();
};
};
void startAll() {
t1.start();
t2.start();
t3.start();
t4.start();
}
class MyClass {
synchronized void syncMethod() {
try {Thread.sleep(100);} catch (InterruptedException e) {} //쓰레드 시작 준비 시간
System.out.println("===="+Thread.currentThread().getName()+"====");
System.out.println("thread1->" +t1.getState());
System.out.println("thread2->" +t2.getState());
System.out.println("thread3->" +t3.getState());
System.out.println("thread4->" +t4.getState());
for(long i=0; i<1000000000L ; i++) {} //시간지연
}
}
}
public class BlockedState {
public static void main(String[] args) {
MyBlockTest mbt = new MyBlockTest();
mbt.startAll();
}
}
결과
====thread1====
thread1->RUNNABLE
thread2->BLOCKED
thread3->BLOCKED
thread4->BLOCKED
====thread3====
thread1->TERMINATED
thread2->BLOCKED
thread3->RUNNABLE
thread4->BLOCKED
====thread4====
thread1->TERMINATED
thread2->BLOCKED
thread3->TERMINATED
thread4->RUNNABLE
====thread2====
thread1->TERMINATED
thread2->RUNNABLE
thread3->TERMINATED
thread4->TERMINATED
5-6. WAITING
- 일시정지하는 시간의 지정없이 쓰레드 객체 .join()메서드를 호출하면 조인된 쓰레드 객체의 실행이 완료될 때까지 이를 호출한 쓰레드는 WAITING 상태가 된다.
- 조인된 쓰레드가 완료되거나 inturrupt()메서드 호출로 예외를 인위적으로 발생시켰을 때만 다시 RUNNABLE상태로 돌아갈 수 있다.
- wait()메서드로 WAITING된 쓰레드는 다른 쓰레드에서 notify(), notifyAll()을 호출해야만 RUNNABLE 상태가 될 수 있다. (스스로 WAITING상태를 벗어날 수 없음)
- WAITING된 쓰레드는 일시정지했던 지점부터 다시 이어서 실행된다.
- wait(), notify(), notifyAll()은 반드시 동기화 블록에서만 사용할 수 있다.
동기화만 사용했을 때 임의적인 두 쓰레드의 실행 순서
class DataBox {
int data;
synchronized void inputData(int data) {
this.data = data;
System.out.println("입력데이터 : "+data);
}
synchronized void outputData() {
System.out.println("출력데이터 : "+data);
}
}
public class Waiting_WaitNotify_1 {
public static void main(String[] args) {
DataBox dataBox = new DataBox();
Thread t1 = new Thread() {
public void run() {
for(int i=1; i<9; i++) {
dataBox.inputData(i);
}
};
};
Thread t2 = new Thread() {
public void run() {
for(int i=1; i<9; i++) {
dataBox.outputData();
}
};
};
t1.start();
t2.start();
}
}
출력
입력데이터 : 1
출력데이터 : 1
출력데이터 : 1
출력데이터 : 1
출력데이터 : 1
출력데이터 : 1
출력데이터 : 1
출력데이터 : 1
출력데이터 : 1
입력데이터 : 2
입력데이터 : 3
입력데이터 : 4
입력데이터 : 5
입력데이터 : 6
입력데이터 : 7
입력데이터 : 8
- 쓰기 쓰레드가 동기화 메서드 실행 후 key가 반납됨
- 읽기 쓰레드가 실행되지 않고 key를 얻기위한 쓰레드는 쓰기와 읽기 쓰레드임
- 매번 호출할 때마다 key를 얻기 위해 경쟁함
- 번갈아가면서 출력되는것이 아닌 자유 경쟁에서 승리한 쓰레드가 랜덤하게 나옴
어떻게 해결하면 좋을까?
먼저, wait(), notify()메서드를 이용하여 쓰레드를 일시정지 및 호출하며 boolean변수를 이용해 쓰기 동작을 할 것인지 읽기 동작을 할 것인지 순서를 정하면 된다.
아래와 같은 방법으로 작동할 것이다.
- 쓰게쓰레드 동작(데이터 쓰기)
- 읽기 쓰레드 깨우기(notify())
- 쓰기 쓰레드 일시정지(wait())
- 읽기 쓰레드 동작(데이터 읽기)
- 쓰기 쓰레드 깨우기(notify())
- 읽기 쓰레드 일시정지(wait())
- 1~6과정 반복
wait(), notify()를 이용한 쓰레드의 교차 실행
class DataBox {
boolean isEmpty = true;
int data;
synchronized void inputData(int data) {
if(!isEmpty) {
try { wait(); } catch (InterruptedException e) {} //WAITING
}
this.data = data;
isEmpty=false;
System.out.println("입력데이터 : "+data);
notify();
}
synchronized void outputData() {
if(isEmpty) {
try { wait(); } catch (InterruptedException e) {} //WAITING
}
isEmpty = true;
System.out.println("출력데이터 : "+data);
notify();
}
}
public class Waiting_WaitNotify_2 {
public static void main(String[] args) {
DataBox dataBox = new DataBox();
Thread t1 = new Thread() {
public void run() {
for(int i=1; i<9; i++) {
dataBox.inputData(i);
}
};
};
Thread t2 = new Thread() {
public void run() {
for(int i=1; i<9; i++) {
dataBox.outputData();
}
};
};
t1.start();
t2.start();
}
}
출력
입력데이터 : 1
출력데이터 : 1
입력데이터 : 2
출력데이터 : 2
입력데이터 : 3
출력데이터 : 3
입력데이터 : 4
출력데이터 : 4
입력데이터 : 5
출력데이터 : 5
입력데이터 : 6
출력데이터 : 6
입력데이터 : 7
출력데이터 : 7
입력데이터 : 8
출력데이터 : 8
6. 데드락 (Deadlock)
각 방향의 자동차들은 서로 다른 곳으로 이동하고 싶지만 꽉 막혀 있기에 이동하지 못한다.
프로세스도 마찬가지로 자신들이 원하는 자원을 가져가고싶은데 그러지 못하는 상태가 된다.
이를 Deadlock (교착상태) 이라고 한다.
자바 예제 코드로 deadlock을 발생시켜보자.
// Java program to illustrate Deadlock
// in multithreading.
class Util
{
// Util class to sleep a thread
static void sleep(long millis)
{
try
{
Thread.sleep(millis);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
// This class is shared by both threads
class Shared
{
// first synchronized method
synchronized void test1(Shared s2)
{
System.out.println("test1-begin");
Util.sleep(1000);
// taking object lock of s2 enters
// into test2 method
s2.test2();
System.out.println("test1-end");
}
// second synchronized method
synchronized void test2()
{
System.out.println("test2-begin");
Util.sleep(1000);
// taking object lock of s1 enters
// into test1 method
System.out.println("test2-end");
}
}
class Thread1 extends Thread
{
private Shared s1;
private Shared s2;
// constructor to initialize fields
public Thread1(Shared s1, Shared s2)
{
this.s1 = s1;
this.s2 = s2;
}
// run method to start a thread
@Override
public void run()
{
// taking object lock of s1 enters
// into test1 method
s1.test1(s2);
}
}
class Thread2 extends Thread
{
private Shared s1;
private Shared s2;
// constructor to initialize fields
public Thread2(Shared s1, Shared s2)
{
this.s1 = s1;
this.s2 = s2;
}
// run method to start a thread
@Override
public void run()
{
// taking object lock of s2
// enters into test2 method
s2.test1(s1);
}
}
public class Deadlock
{
public static void main(String[] args)
{
// creating one object
Shared s1 = new Shared();
// creating second object
Shared s2 = new Shared();
// creating first thread and starting it
Thread1 t1 = new Thread1(s1, s2);
t1.start();
// creating second thread and starting it
Thread2 t2 = new Thread2(s1, s2);
t2.start();
// sleeping main thread
Util.sleep(2000);
}
}
출력
test1-begin
test1-begin
내부에서 서로의 lock을 얻으려고 호출하기 때문에 무한정 락대기 데드락에 빠지게 된다.
데드락 발생 조건(이유)
자원의 특성
- Exclusive use of resources (자원의 배타적 사용)
- Non preemptible resources (비선점 자원)
프로세스의 특성
- Hold and wait (Partial allocation) (자원을 하나 hold 하고 다른 자원 요청)
- Circular wait
데드락 해결 방법
- 데드락 예방 : 데드락 발생 조건 4가지를 원천 봉쇄하는 방법
- 데드락의 회피 : 자원이 어떻게 요청될지에 대한 추가정보를 제공하도록 요구하는 것으로 자원 할당상태를 검사하는 방법
- 데드락 탐지 & 회복 : 데드락이 발생했을때 해결하는 방법
- 데드락 무시 : 아주 적은 확률로 데드락이 발생한다면, 무시하는 방법
무시하거나 재실행하는 편이 자원적으로 더 이득을 볼 수도 있기 때문에 수행하는 방법
자바에서 데드락 방지 방법들
- 락 정렬 : 모든 락이 항상 같은 순서로 획득된다면 데드락은 발생하지 않기 때문에 효과적이나 락의 순서를 알고 있는 상태에서만 사용이 가능하다. (데드락 예방)
- 락 타임 아웃 : 락을 획득하기 위해 기다리는 시간을 정해놓고 시간이 지난 후에는 락을 다시 시도하는 방법 (데드락 탐지&회복)
같은 락을 획득하려는 다른 모든 쓰레드에게 기회를 주는 것이기 때문에 공정성 문제가 발생하게되고, 데드락 뿐만이 아닌 작업을 처리중에도 타임아웃이 발생할 수 도 있다.
- 데드락 감지 : 자료구조를 이용하여 쓰레드가 락을 획득하면 저장하고 이를 이용해 데드락을 감지하고 데드락이 발생했다면, 락을 해제하는 방법
- Atomic Variable : volatile 키워드나 java.util.concurrent.atomic 클래스를 이용하여 원자 변수를 사용하는 방법
참고 및 인용
https://gmlwjd9405.github.io/2018/09/14/process-vs-thread.html
https://leemoono.tistory.com/26
https://www.geeksforgeeks.org/implement-runnable-vs-extend-thread-in-java/?ref=gcse
https://www.geeksforgeeks.org/java-threads/?ref=gcse
DO it 자바 완전 정복 - 저. 김동형
https://www.notion.so/Thread-5fdb5d603a6a473186bf2738f482cedc
https://parkadd.tistory.com/48
'Language > Java-Weekly-study' 카테고리의 다른 글
[Java] W12: 자바 어노테이션 (annotation) (0) | 2022.08.14 |
---|---|
[Java] W11: 자바 Enum (0) | 2022.08.13 |
[Java] W09: 자바 예외 처리 (0) | 2022.07.13 |
[Java] W08: 자바 인터페이스 (0) | 2022.07.12 |
[Java] W07: 자바 패키지 (0) | 2022.07.10 |