둘 이상의 쓰레드가 동시에 공유 자원에 접근 시 발생
ex). 첫 번째 thread가 변수를 읽고 두 번째 thread도 변수에서 동일한 값을 읽는다.
그런 다음 첫 번째 thread와 두 번째 thread는 값에 대한 작업을 수행하고 변수에 마지막으로 값을 쓰기 위해 두 thread는 경쟁하게 되고 하나의 thread가 값을 쓰고, 다음 thread가 값을 덮어쓴다면 마지막에 쓴 thread의 값이 저장되어 원하는 결과를 얻지 못하게 될 것이다.
public class SharedCount {
int count;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
// Critical Section(임계 구역)
public void increment() {
setCount(getCount() + 1);
}
}
class SharedCounter extends Thread {
SharedCount sharedCount;
int count;
int maxCount;
public SharedCounter(String name, int maxCount, SharedCount sharedCount) {
setName(name);
this.sharedCount = sharedCount;
this.maxCount = maxCount;
count = 0;
}
@Override
public void run() {
while (count < maxCount) {
count++;
sharedCount.increment();
}
}
}
class Test {
public static void main(String[] args) throws InterruptedException {
SharedCount sharedCount = new SharedCount();
SharedCounter counter1 = new SharedCounter("counter1", 10000, sharedCount);
SharedCounter counter2 = new SharedCounter("counter2", 10000, sharedCount);
counter1.start();
counter2.start();
System.out.println(counter1.getName() + ": started");
System.out.println(counter2.getName() + ": started");
counter1.join();
counter2.join();
System.out.println(counter1.getName() + ": terminated");
System.out.println(counter2.getName() + ": terminated");
System.out.println("sharedCount : " + sharedCount.getCount());
}
}
실행결과
counter1: started
counter2: started
counter1: terminated
counter2: terminated
sharedCount : 12911
결과는 20000이 나와야 하지만 실행할 때마다 다른 결과를 보인다.
이는 counter1이 count를 읽어 1을 추가한 후 count에 업데이트하기 전에, counter2가 count를 읽어 1을 추가하여 count는 2가 되었지만, counter1이 수행할 결과를 덮어 버림으로써 counter2가 수정한 결과를 잃어버리는 문제가 발생한다.
병렬 컴퓨팅에서 두 이상의 process 또는 thread가 동시 접근이 허용되지 않는 공유 자원(자료 구조 또는 장치)에 접근하는 코드의 블록을 말한다.
Mutual exclusion이란 두 개 이상의 process 혹은 thread가 동시에 하나의 공유 자원으로 발생할 수 있는 race condition 문제를 해결하기 위해 어느 시점에서의 공유 자원 접근을 하나의 process 혹은 thread로 제한하는 것을 말한다.
Deadlock은 mutual exclusion 과정에서 자원 접근 권한 획득과 자원 접근 권한 반환 관계의 꼬임으로 발생한다.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockExample {
private Lock lock1 = new ReentrantLock(true);
private Lock lock2 = new ReentrantLock(true);
public static void main(String[] args) {
DeadlockExample deadlock = new DeadlockExample();
new Thread(deadlock::operation1, "T1").start();
new Thread(deadlock::operation2, "T2").start();
}
public void operation1() {
lock1.lock();
System.out.println("lock1 acquired, waiting to acquire lock2.");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock2.lock();
System.out.println("lock2 acquired");
System.out.println("executing first operation.");
lock2.unlock();
lock1.unlock();
}
public void operation2() {
lock2.lock();
System.out.println("lock2 acquired, waiting to acquire lock1.");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock1.lock();
System.out.println("lock1 acquired");
System.out.println("executing second operation.");
lock1.unlock();
lock2.unlock();
}
}
실행 결과
lock1 acquired, waiting to acquire lock2.
lock2 acquired, waiting to acquire lock1.
쓰레드 T1 은 쓰레드 T2 가 보유한 lock2 를 기다리고, 쓰레드 T2 는 쓰레드 T1 이 보유한 lock1 을 서로 무한 대기하고 있다.
점유 대기 상태는 그림과 같이 process 2와 같이 resource 1의 접근 권한을 획득한 상태에서 resource 2의 접근 권한을 기다리고 있는 것을 말한다.
process 2의 수행 과정이 resource 2의 접근 권한을 획득하여 처리한 후 resource 1의 접근 권한을 해제한다면 process 3이 접근 권한을 해제하기 전까지는 무한 대기 상태에 놓이게 된다.
아울러 resource 1의 접근 권한을 요청하고 있는 process 1도 무한 대기 상태가 된다.
순환 대기는 점유 대기와 공유 자원 획득 후 다른 공유 자원 획득 시까지 무한 대기 상태는 동일하지만, 대기 관계가 아래 그림과 같이 순환 구조를 이루고 있다.
process 1은 resource 2에 대한 접근 권한을 가진 상태에서 resource 1에 대한 접근 권한을 기다리고, process 2는 resource 1에 대한 접근 권한을 가진 상태에서 resource 2에 대한 접근 권한을 기다린다.
두 개의 process는 서로가 다른 process가 가지고 있는 접근 권한을 얻기 위해 대기하고 있어, 하나의 process가 먼저 해제하지 않는 이상 대기 상태는 계속해서 유지된다.
기아 상태는 다른 process나 thread가 공유 자원의 접근 권한을 지속적으로 가짐으로써 발생할 수 있다. process나 thread가 공유 자원의 접근 권한을 해제하더라도 운영 방식등의 이유로 인해 해당 process나 thread가 공유 자원의 접근 권한을 획득하지 못하는 경우도 동일하다.
process나 thread의 우선순위가 다를 경우, 우선순위가 낮은 process나 thread는 scheduler에 의해 공유 자원에 대한 접근 권한을 획득할 만큼의 수행 시간을 갖지 못해 무한히 대기 상태에 놓일 수 있다.
Livelock은 deadlock 문제를 해결하기 위해 공유 자원 접근 요청 후 일정 시간 안에 권한 획득에 실패한 경우, 수행 과정을 종료하면서 발생할 수 있다.
두 개의 process나 thread에서 교착 상태를 유지하다 일정 시간 후 자원 접근 요청을 철회할 때, 두 개의 process나 thread가 동시에 수행하여 자신이 확보하고 있던 공유 자원 접근 권한을 반환하여 교착 상태가 해결된다. 하지만, 두 개의 process나 thread는 교착 상태와 같이 아무런 작업을 수행하지 못하는 것은 아니지만, 해당 자원에 대한 접근 권한을 확보하지 못해 관련된 작업을 수행하지 못하는 결과를 가져온다.
교착 상태는 관련된 process나 thread가 대기 상태를 계속 유지함으로써 여타의 작업 수행이 불가능하지만 livelock은 해당 자원에 대한 작업만 처리하지 못할 뿐 나머지 작업은 처리되는 차이를 가지고 있다.
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.*;
public class LivelockExample {
private Lock lock1 = new ReentrantLock(true);
private Lock lock2 = new ReentrantLock(true);
public static void main(String[] args) {
LivelockExample livelock = new LivelockExample();
new Thread(livelock::operation1, "T1").start();
new Thread(livelock::operation2, "T2").start();
}
public void operation1() {
while (true) {
try {
lock1.tryLock(50, TimeUnit.SECONDS);
System.out.println("lock1 acquired, trying to acquire lock2.");
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (lock2.tryLock()) {
System.out.println("lock2 acquired.");
} else {
System.out.println("cannot acquire lock2, releasing lock1.");
lock1.unlock();
continue;
}
System.out.println("executing first operation.");
break;
}
lock2.unlock();
lock1.unlock();
}
public void operation2() {
while (true) {
try {
lock2.tryLock(50, TimeUnit.SECONDS);
System.out.println("lock2 acquired, trying to acquire lock1.");
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (lock1.tryLock()) {
System.out.println("lock1 acquired.");
} else {
System.out.println("cannot acquire lock1, releasing lock2.");
lock2.unlock();
continue;
}
System.out.println("executing second operation.");
break;
}
lock1.unlock();
lock2.unlock();
}
}
실행결과
lock1 acquired, trying to acquire lock2.
cannot acquire lock1, releasing lock2.
lock2 acquired, trying to acquire lock1.
cannot acquire lock2, releasing lock1.
lock1 acquired, trying to acquire lock2.
cannot acquire lock2, releasing lock1.
lock1 acquired, trying to acquire lock2.
cannot acquire lock1, releasing lock2.
lock2 acquired, trying to acquire lock1.
볼 수 있듯이 두 스레드 모두 반복적으로 잠금을 획득하고 해제하고 있다. 이로 인해 어떤 스레드도 작업을 완료할 수 없다.
Java에서는 thread 동기화를 위해 synchronized keyword를 다양한 곳에 적용할 수 있다.
synchronized를 이용한 instance method 동기화 방법은 아래와 같이 method의 접근 제한자
에 키워드 추가만으로 가능
하다.
public synchronized void increment() {...}
공유 자원인 shared count를 static variable
로 선언하여 instance와 관계없이 접근할 수 있도록 한다.
shared count를 수정할 수 있도록 increment()도 static method
로 정의한다.
Instance method 동기화는 동적으로 생성된 instance variable
에 적용하고, Static method 동기화는 class loading 시점에 생성되는 static variable
에 적용되는 것이 다르다.
그렇다면, 동기화를 위해서는 반드시 해당 object의 class에서 적용되어야 하는가?
그렇지는 않다. synchronized는 method 뿐만 아니라 별도의 code block에도 적용 가능하다.
다만, code block 생성시 lock을 설정할 object는 필요하다.
// Critical Section(임계 구역)
public void increment() {
synchronized(this) {
setCount(getCount() + 1);
}
}
Java에서는 synchronized method
또는 block
에서의 제어를 위해 wait()
와 notify()
를 지원한다.
wait()
는 syncrhonized 영역에서 lock을 소유한 thread가 어떠한 이유에서 자신의 제어권을 양보하고 WAITING
또는 TIMED_WAITING
상태에서 대기하기 위해서 사용된다.
notify()
와 notifyAll()
은 syncrhonized 영역에서 WAITING
상태에 있는 다른 thread를 다시 RUNNABLE
상태로 변경시키는 역할을 한다.
한 가지 착각하기 쉽지만, wait, notify, notifyAll은 Thread의 static method
가 아닌 instance method
라는 점이다.
wait()
는 synchronized 영역 내에서 소유하고 있는 lock을 양보하고, WAITING
또는 TIMED_WAITING
상태로 전환되어 notify가 올때 까지 timeout이 될때까지 기다리도록 사용된다. 이는 다른 스레드에서 notify()
나 notifyAll()
을 호출함으로써 WAITING
또는 TIMED_WAITING
상태의 스레드가 RNNNABLE
상태로 변경된다.
스레드에서 wait()
를 호출하기 위해서는 lock을 소유한 상태이어야 하고, wait()
호출은 자신이 가지고 있던 lock 권한을 풀어버림으로써 다른 스레드가 임계 구역에 진입할 수 있도록 한다.
wait() 메소드
는 InterruptedException
을 발생시킬 수 있으므로, try-catch 블록
내에서 호출해야 합니다.
notify()
함수는 wait()
함수와 마찬가지로 lock을 소유한 상태에서 호출할 수 있다. notify()
함수가 호출되면, wait()
함수를 이용해 대기 상태에 있던 스레드 중 임의의 하나가 깨어난다. 깨어난 스레드는 WAITING
또는 TIMED_WAITING
상태에서 RUNNABLE
상태로 변경되어 실행할 수 있는 상태가 된다.
object 수준 잠금
을 획득하고 synchronized static method 또는 block에 들어가면 class 수준 잠금
을 획득한다.sychronized block
에 사용된 object가 null
인 경우 null point exception
를 발생시킵니다.wait()
, notify()
및 notifyAll()
은 syncrhonization에 사용되는 중요한 방법이다.public class Message {
private String msg;
private boolean hasMessage = false;
// 소비자 스레드가 호출하는 메소드
public synchronized String take() {
// 메시지가 없다면 대기
while (!hasMessage) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 메시지를 가져온 후 상태 변경 및 모든 대기 스레드에게 알림
hasMessage = false;
notifyAll();
return msg;
}
// 생산자 스레드가 호출하는 메소드
public synchronized void put(String msg) {
// 이미 메시지가 있다면 대기
while (hasMessage) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 메시지 저장 후 상태 변경 및 모든 대기 스레드에게 알림
hasMessage = true;
this.msg = msg;
notifyAll();
}
}
class Exam01 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
long count = 0;
while(count<Integer.MAX_VALUE) {
count++;
}
});
System.out.println(thread.getState());
thread.start();
while(thread.isAlive()) {
System.out.println(thread.getState());
// RUNNING이 아닌 Runnerble이 나오는 이유 :
// 타 스레드가 running 상태이므로, 다른 스레드는 runnable 임
Thread.sleep(100);
}
System.out.println(thread.getState());
}
}
실행 결과
NEW
RUNNABLE
TERMINATED
Thread object가 생성은 되었지만, 실행은 되지 않은 상태를 말한다.
실행 준비 상태로 scheduler에 의해 실행되기를 기다리는 상태이다.
언제든지 실행 상태가 될 수 있다.
processor에서 실행중인 상태로서, 다른 thread에서 확인이 불가능하다.
다른 thread는 RUNNABLE, WAITING 등 실행이 아닌 다른 상태중 하나를 갖는다.
Synchronized code block을 다른 thread가 점유하고 있는 경우, 해당 code block의 점유 상태가 해지 될때까지 기다린다.
public class Exam2 {
private static class SharedObject {
// 동기화된 메소드
public synchronized void syncMethod(String threadName) {
System.out.println(threadName + " is in syncMethod.");
try {
Thread.sleep(2000); // 스레드를 2초간 대기시킴
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + " is leaving syncMethod.");
}
}
public static void main(String[] args) {
final SharedObject sharedObject = new SharedObject();
// 스레드 1
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
sharedObject.syncMethod("Thread 1");
}
});
// 스레드 2
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
sharedObject.syncMethod("Thread 2");
}
});
thread1.start();
thread2.start();
}
}
실행 결과
Thread 1 is in syncMethod.
Thread 1 is leaving syncMethod.
Thread 2 is in syncMethod.
Thread 2 is leaving syncMethod.
스스로 대기 상태가 된 후 다른 thread에서 알림을 줄때까지 기다린다.
Synchronized block에서 wait(), 다른 thread가 종료되길 기다리는 join() 등 사용한 경우 적용될 수 있다.
Thread가 종료된 상태이다.
앞서 예제에서 확인 가능하다.
WAITING과 동일하지만, 제한 시간 설정이 가능하다.
제한 시간내에 알림을 받지 못하면 WAITING을 해제하고 RUNNABLE로 변경된다.
thread.sleep(long millis)
wait(int timeout) or wait(int timeout, int nanos)
thread.join(long millis)
LockSupport.parkNanos
LockSupport.parkUntil
class TimeWaitingThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public class Exam2 {
public static void main(String[] args) throws InterruptedException {
TimeWaitingThread timedWaiting = new TimeWaitingThread();
timedWaiting.start();
Thread.sleep(1000);
System.out.println(timedWaiting.getState());
}
}
Thread가 WAITING 상태로 있을 경우, InterruptedException을 발생시켜 RUNNABLE 상태로 변경됨
Synchonized block
에서 thread를 WAITING
상태로 만듦. Parameter로 주어진 시간이 지나면 RUNNABLE
상태가 됨. 시간이 주어지지 않은 경우, 다른 thread에서 notify()
, notifyAll()
호출에 의해 RUNNABLE
상태로 변경됨.
java.lang.Object class
의 instance method
이다. 즉, 이 method는 Java로 생성하는 모든 object
에서 사용할 수 있다.
Synchronized block 상태에서 wait()
에 의해 WAITING
상태에 있는 thread를 RUNNABLE
상태로 변경됨
WAITING
상태에서 BLOCKED
상태로 이동하고, 우선순위에 따라 object의 잠금을 얻을 수 있다.
Object의 잠금을 얻은 thread는 RUNNING
상태로 이동하고, 나머지 thread는 object 잠금을 얻을 때까지 BLOCKED
상태로 유지된다.
주어진 시간 동안 thread를 TIMED_WAITING
상태로 만듦. 주어진 시간이 지나면 자동으로 RUNNABLE
상태로 변경됨
sleep()
는 java.lang.Thread class
의 class method
이다. 즉, thread에서만 사용할 수 있습니다.
WAITING
상태(wait() 호출 후 상태)에 있는 thread는 동일한 잠금에서 notify( )
또는 notifyAll()
함수를 호출하여 다른 thread에 의해 깨울 수 있다. 그러나, TIMED_WAITING
상태(sleep() 호출 후 상태)에 있는 thread는 깨울 수 없다. Thread가 잠자는 thread를 interrupt 하면 InterruptedException이 발생한다.
join()
을 호출한 thread는 join을 요청한 thread가 종료될때 까지 대기함. Time parameter가 주어질 경우에는 해당 시간 동안만 대기하고, 시간 내에 thread가 종료되지 않으면 thread의 종료와 상관없이 RUNNABLE
상태로 변경됨.
try {
System.out.println("메인 스레드는 worker1의 작업 완료를 기다립니다.");
worker1.join(); // worker1의 작업이 끝날 때까지 메인 스레드가 기다림
System.out.println("메인 스레드는 worker2의 작업 완료를 기다립니다.");
worker2.join(); // worker2의 작업이 끝날 때까지 메인 스레드가 기다림
} catch (InterruptedException e) {
e.printStackTrace();
}
일반적으로는 scheduler에 의해 thread간 상태가 전환되지만, yield()의 호출에 의해 RUNNING 상태의 thread는 RUNNABLE 상태로 변경되어 다른 thread가 동작할 수 있도록 함
yield() 메소드는 현재 실행 중인 스레드가 실행을 일시 중단하고 다른 스레드에게 실행 기회를 주기 위해 사용됩니다. 이는 현재 스레드가 가지고 있는 자원을 다른 동일 우선순위의 스레드와 공유하고자 할 때 유용합니다.
하나의 thread가 프로세서를 과도하게 점유하지 않도록 조절할 수 있다.
class ThreadA extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
// 현재 실행 중인 스레드가 CPU 사용을 양보(yield)
Thread.yield();
}
}
}
class ThreadB extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " : " + i);
// 현재 실행 중인 스레드가 CPU 사용을 양보(yield)
Thread.yield();
}
}
}
public class YieldExample {
public static void main(String[] args) {
ThreadA t1 = new ThreadA();
ThreadB t2 = new ThreadB();
// 스레드 시작
t1.start();
t2.start();
}
}
실행 결과
Thread-0 : 0
Thread-1 : 0
Thread-1 : 1
Thread-1 : 2
Thread-0 : 1
Thread-1 : 3
Thread-0 : 2
Thread-0 : 3
Thread-1 : 4
Thread-0 : 4