synchronized
。여러스레드가 동시에임계 영역의단일 자원에 접근하지 못하도록Lock을 설정하는자바 키워드
▶Race Condition방지 목적
。키워드가 선언된코드는 오직 하나의스레드만 접근하도록 제한
。Lombok의@Synchronized 어노테이션을 통해 별도의Lock 객체에서 간단하게 사용이 가능
▶this에서synchronized를 설정할 필요가 없다.
메서드를동기화하는 경우public synchronized void method() { // 동기화 영역 }public class BankAccount { // private int balance = 1000; // // synchronized 추가 → 한 번에 하나의 스레드만 이 메서드에 진입 가능 public synchronized void withdraw(int amount) { if (balance >= amount) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } balance -= amount; System.out.println(Thread.currentThread().getName() + " 출금 완료. 남은 잔액: " + balance); } else { System.out.println(Thread.currentThread().getName() + " 출금 실패. 잔액 부족"); } } }
- 특정
코드 블록만 동기화하는 경우
。가장 많이 사용되는 형태public void method() { synchronized(this) { // 동기화 영역 } }
클래스전체를동기화하는 경우
。클래스 단위로Lock을 설정
▶클래스.class기준으로 잠금을 수행public static synchronized void method() {}
synchronized를 적용하는 이유?
。아래코드에서공용 변수 : count에스레드들이 동시 접근 시무결성을 보장하지 못한다.class Counter { int count = 0; // public void increase() { count++; } }。
synchronized를 선언 시 각스레드들이동기식으로 작동하도록 보장하여 안전class Counter { int count = 0; // public synchronized void increase() { count++; } }
synchronized를 잘못 사용하는 경우 데드락이 발생할 수 있으므로 주의synchronized (resource1) { // A 지점 - resource1 잠금 } // // 전혀 다른 클래스, 전혀 다른 메서드 synchronized (resource1) { // B 지점 - 같은 resource1 얻으려 하면 A가 풀 때까지 대기 }
Race Condition구현 OS - Race Condition
。User Thread로Race Condition구현하기
。생성된스레드는Round Robin 스케쥴링을 통해 각각문맥교환을 수행하면서 작업처리.class UserThread1 implements Runnable { static int count = 0; // 스레드에 의해 접근될 공유자원 @Override public void run() { for (int i = 0; i < 10000; i++) count++; } } public class RaceCondition { public static void main(String[] args) throws Exception { // Java Thread 생성 Thread t1 = new Thread(new UserThread1()); Thread t2 = new Thread(new UserThread1()); // 각각 Round Robin 스케쥴링으로 // 스레드의 문맥교환을 수행하면서 작업처리 t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Result 공유자원 : " + UserThread1.count); } }
。Race Condition에 의해프로세스 / 스레드에 의한공유자원( =static int count) 접근순서에 따라 결과값이 각각 변화
▶run()의iter = 100이면 이러한데이터 불일치문제는 발생하지않고,iter = 1000이면 간헐적으로,iter = 10000이면 자주발생.
생산자-소비자 문제에서임계영역의Race Condition해결을 위한Peterson's Solution구현 OS - 동기화문제 해결
。임계영역의Race Condition문제를 어느정도 해결이 가능하나 완전하게데이터 불일치를 없앨 수 없다.
。Static Nested Class을 통해생산자 / 소비자 스레드를중첩 클래스로서 구현
。while ( !( !flag[1] || turn != 1 ) ) ;구문을 통해스레드간상호배제구현
▶turn = 1또는flag[1] == false인 경우Critical Section진입 , 아니면무한루프로 진입대기
▶ 다른 코드에서는while ( flag[1] && turn == 1 ) ;로 표현됨public class peterson { static int count = 0; static int turn = 0; // 스레드의 임계영역 진입권한 static boolean[] flag = new boolean[2]; // 각 스레드 임계영역 진입의사 저장 // 생산자 스레드 static class Producer implements Runnable { @Override public void run(){ for( int i = 0 ; i < 10000 ; i++ ){ // Entry Section flag[0] = true; turn = 1; // turn = 1 또는 flag[1] == false 인 경우 Critical Section 진입 while ( !( !flag[1] || turn != 1 ) ) ; // Critical Section count++; // Exit Section flag[0] = false; // 소비자 스레드 임계영역 진입 // Remainder Section } } } // 소비자 스레드 static class Consumer implements Runnable { @Override public void run(){ for( int i = 0 ; i < 10000 ; i++ ){ // Entry Section flag[1] = true; turn = 0; while ( !( !flag[0] || turn != 0 ) ) ; // Critical Section count--; // Exit Section flag[1] = false; // 생산자 스레드 임계영역 진입 // Remainder Section } } } public static void main(String[] args) throws Exception { Thread t1 = new Thread(new Producer()); Thread t2 = new Thread(new Consumer()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
。데이터 불일치발생빈도는 적어졌지만Entry Section과Exit Section에서 발생하는Context Switch문제 때문에피터슨 알고리즘이 정상작동하지않아데이터 불일치가 여전히 발생.
▶iter = 100000이면 더 자주발생.
Atomic Variable활용하여피터슨 알고리즘개선 Atomic Variable
。피터슨 알고리즘의Entry Section과Exit Section에서 발생하는Context Switch를 없애고자Atomic Instruction으로 구현한Atomic Variable을 활용
。flag역할의boolean배열을AtomicVariable객체 배열로 변경
JAVA - AtomicVariablestatic AtomicBoolean[] flag; static { flag = new AtomicBoolean[2]; for (int i = 0; i < flag.length; i++) { flag[i] = new AtomicBoolean(false); } }▶ 기존
boolean배열을AtomicVariable객체 배열로서 설정 및Static 생성자를 통해 초기화 및 내부 로직 교체import java.util.concurrent.atomic.AtomicBoolean; public class peterson { static int count = 0; static int turn = 0; // boolean[] 을 AtomicBoolean[]으로 교체 static AtomicBoolean[] flag; static { // 객체배열 초기화 flag = new AtomicBoolean[2]; for (int i = 0; i < flag.length; i++) { flag[i] = new AtomicBoolean(false); } } static class Producer implements Runnable { @Override public void run(){ for( int i = 0 ; i < 10000 ; i++ ){ // Entry Section flag[0].set(true); turn = 1; while ( !( !flag[1].get() || turn != 1 ) ) ; // 수정 // Critical Section count++; // Exit Section flag[0].set(false); // 수정 // Remainder Section } } } // 소비자 스레드 static class Consumer implements Runnable { @Override public void run(){ for( int i = 0 ; i < 10000 ; i++ ){ // Entry Section flag[1].set(true); turn = 0; while ( !( !flag[0].get() || turn != 0 ) ) ; // 수정 // Critical Section count--; // Exit Section flag[1].set(false); // 수정 // Remainder Section } } } public static void main(String[] args) throws Exception { Thread t1 = new Thread(new Producer()); Thread t2 = new Thread(new Consumer()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
。피터슨 알고리즘의boolean[]을Atomic Variable을 구현한AtomicBoolean[]으로 교체
▶Entry Section과Exit Section에서문맥교환없이원자적으로 작용하도록 하여피터슨 알고리즘이 정상작동하면서데이터 불일치가 발생하지 않는다.
JAVA Monitor OS - Monitor
。JAVA에 내장된Lock이라는 의미로monitor-lockorintrinsic-lock이라는 이름으로Monitor-like한 기능을 제공
▶사용자 스레드를Concurrency하게Execute할 경우의동기화 메카니즘
synchronizedkeyword
。임계영역에 해당하는코드블록을 선언 시 활용하는JAVA keyword
▶ 해당코드블록에 단일사용자 스레드만 진입하도록하여동기화를 수행하여Race Condition방지
▶상호배제문제 해결
。코드블록에는공유자원에 접근하여동기화가 필요한 코드영역만 넣는다
▶동기화가 필요없는 코드까지 넣으면 비효율적.
。기존entry section과exit section에서 수행하던Lock & Unlock과정을JVM에서 자동으로 수행synchronized (객체){ // critical section }。 해당
코드블록은모니터락을 획득해야 진입가능
▶모니터락을 보유한객체를 지정가능.
。Method에 선언 시Method의 전체코드블록이임계영역으로 지정public synchronized void 메소드명(){ // critical section }▶
this객체가모니터락을 획득해야함
Monitor에 사용되는 Operation
。java.lang.ObjectClass에 선언된메소드
▶ 모든자바 객체에서 사용가능
。세마포어에서 사용되는P(),V()와 유사한 역할을 수행
객체.wait()
。스레드가 임의의객체의wait()호출 시 해당객체의모니터락을 획득하기위해스레드는Entry Queue로 전송하여 대기
▶notify()를 통해InterruptedException이 호출되는 경우 실행됨
객체.notify()
。스레드가 임의의객체의notify()호출 시InterruptedException을 호출시켜 해당객체의모니터의Entry Queue에서 대기중인스레드하나를Ready Queue에 전송
▶ 보통 대기열 첫번째스레드를Ready Queue로 전송 및Execute
객체.notifyAll()
。스레드가 임의의객체의notify()호출 시InterruptedException을 호출시켜 해당객체의모니터의Entry Queue에서 대기중인스레드전체를Ready Queue로 전송
▶ 이후스레드끼리 경쟁하여Execute하여 공평한 기회 부여class BufferPool{ private int[] buffer; private int count, in, out; public BufferPool(int bufferSize){ buffer = new int[bufferSize]; count = in = out = 0; } synchronized public void insert(int input){ while ( count == buffer.length ){ try { wait(); } catch (InterruptedException e){} } buffer[in] = input; in = (in + 1) % buffer.length; count++; System.out.printf("생산자스레드 삽입 : %d\n",input); notify(); } }。
Buffer Pool Class의 객체를생성자로 전달하여 생성한 복수의스레드가synchronizedkeyword 메서드로 해당 객체의 공유자원buffer에 동시접근 시모니터로서동기화유지
▶synchronizedkeyword 메서드에서는this객체를 사용하므로객체.wait()할 필요가 없다.
。 생성된스레드가synchronizedkeywordinsert()실행 시 만약BufferPool내Empty Buffer가 없는경우wait()을 선언하여BufferPool객체의모니터의Entry Queue에서스레드를 대기
。임계영역에 진입 후 작업이 끝난스레드는notify()를 호출하여InterruptedException를 발생시켜 다음Entry Queue에서 대기중인스레드하나를Execute
▶catch문 이후부터 실행
。Buffer를 삭제하는소비자 스레드의remove()생성 시insert()와 대칭관계를 유지해야한다.
▶wait()과notify()의 대칭관계를 유지synchronized public int remove() throws InterruptedException{ while ( count == 0 ){ try { wait(); } catch (InterruptedException e){} } int output = buffer[out]; out = (out + 1) % buffer.length; count--; System.out.printf("소비자스레드 삭제 : %d\n",output); notify(); return output; }
JAVA-Monitor활용 동기화 예제
- 문제 : 단일
공유자원에 여러스레드가Concurrent하게 접근하는 경우
。메인스레드에서 생성된사용자 스레드들을join()을 통해RR 스케쥴링으로문맥교환하면서 실행
。동기화 해결책을 전혀 구현하지않은 상태로서임계영역문제가 그대로 존재
▶SynchExample Class의count++에서 여러사용자 스레드의 동시 접근으로인한Race Condition문제로문맥교환이 발생하여데이터 불일치발생public class SynchExample { static class Counter{ public static int count = 0; public static void increment() { // 복수의 스레드의 동시접근으로 인한 문맥교환 발생으로 데이터 불일치 발생 count++; } } static class MyRunnable implements Runnable{ @Override public void run() { for (int i = 0; i < 10000; i++) { Counter.increment(); } } } public static void main(String[] args) throws Exception { Thread[] threads = new Thread[5]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new MyRunnable()); threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } System.out.println(Counter.count); } }
▶50000이 도출되어야 하나,Race Condition으로 인해데이터 불일치가 심각하게 발생.
synchronizedkeyword를 활용한동기화
。Race Condition문제로 인해공유자원에 접근하여동기화가 필요한 코드(count++) 부분을synchronizedkeyword를 선언하여임계영역으로 지정
▶동기화가 필요없는 코드까지 넣으면 비효율적.
。Lock과Unlock은JVM에서 자동으로 수행하므로Entry Section과Exit Section을 구현할 필요는 없다
synchronizedkeyword -메서드
。this 객체에 동기화된모니터의모니터락을 획득 시메소드내임계영역에 진입하는동기화 메카니즘static class Counter{ public static int count = 0; synchronized public static void increment() { // Critical Section 지정 count++; } }
。count++부분을임계영역으로 지정하여모니터락을 획득한 단일사용자 스레드만 진입하도록동기화
▶상호배제를 충족하여데이터 불일치가 발생하지않음
synchronizedkeyword -코드블록
。특정 객체와 동기화된모니터의모니터락을 획득 시코드블록내임계영역에 진입하는동기화 메카니즘static class Counter{ private static Object object = new Object(); public static int count = 0; public static void increment() { synchronized (object) { // Critical Section 지정 count++; } } }
static 메소드가 아닌멤버 메소드인 경우객체를this로 대체할 경우
。스레드마다 각각의Counter 객체를 생성하여 생성자로 전달 시public class SynchExample { static class Counter{ public static int count = 0; public void increment() { synchronized (this) { // Critical Section 지정 count++; } } } static class MyRunnable implements Runnable{ Counter counter; public MyRunnable(Counter counter) { this.counter = counter; } @Override public void run() { for (int i = 0; i < 10000; i++) { counter.increment(); } } } public static void main(String[] args) throws Exception { Thread[] threads = new Thread[5]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new MyRunnable(new Counter())); threads[i].start(); } for (int i = 0; i < threads.length; i++) { threads[i].join(); } System.out.println(Counter.count); } }。
메인스레드에서 5개의사용자 스레드생성 시 각각스레드 클래스 객체(MyRunnable )생성자에 활용되는Counter 객체도 5개씩 각각 생성됨
。이때 5개의스레드를RR 스케쥴링으로Concurrent하게 실행하는 경우스레드 클래스에 정의된run()에 의해 각각의 5개의Counter 객체의counter.increment()가Concurrent하게 실행
。Counter 클래스의increment()에 정의된synchronizedkeyword의this객체는 각각의Counter 객체를 지시하며 이는 각각의스레드당Counter 객체에 동기화된 5개의Monitor생성
。각스레드는 각각 동기화된Monitor의모니터락을 획득 하여Monitor의임계영역으로Concurrent하게 진입
▶ 5개의모니터간에는동기화되지않으므로 5개의Counter 객체가 공유하는 단일static int count공유자원에 동시에 접근하는Race Condition발생
▶Race Condition로 인한데이터 불일치발생
。다음 코드처럼 단일Counter 객체생성 및 이를 기반으로 5개의스레드를 생성하면 총 1개의Monitor에 대해 5개의스레드가모니터락을 획득 및 반납하면서임계영역에 진입하므로Race Condition을 방지할 수 있다.Thread[] threads = new Thread[5]; Counter counter = new Counter(); for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new MyRunnable(counter)); threads[i].start(); }
▶ 상호배제 충족
- 문제 :
생산자 - 소비자 문제에서Monitor를 활용해 동기화 문제 해결 생산자-소비자 문제 개념
Buffer PoolClass 구현class BufferPool{ private int[] buffer; private int count, in, out; public BufferPool(int bufferSize){ buffer = new int[bufferSize]; count = in = out = 0; } synchronized public void insert(int input){ while ( count == buffer.length ){ // Buffer에 Empty Buffer가 없는경우 Block try { wait(); } catch (InterruptedException e){} } buffer[in] = input; in = (in + 1) % buffer.length; count++; System.out.printf("생산자스레드 삽입 : %d\n",input); notify(); } synchronized public int remove() throws InterruptedException{ while ( count == 0 ){ // Buffer에 Full Buffer가 없는경우 Block try { wait(); } catch (InterruptedException e){} } int output = buffer[out]; out = (out + 1) % buffer.length; count--; System.out.printf("소비자스레드 삭제 : %d\n",output); notify(); return output; } }。
BufferPool역할의Class생성 및synchronizedkeyword메소드로insert와remove구현
。생성된스레드들에서생산자 스레드에서insert()를 호출하여,소비자 스레드에서remove()를 호출하면서 공유자원buffer에Concurrent하게 접근하므로synchronizedkeyword를 활용하여동기화를 설정해야한다.
▶ 해당스레드들이생성자를 통해 생성되어 참조하는ButterPool객체에 대한Monitor Lock을 획득 및 반납을 반복
。insert()에서Buffer Pool에Empty Buffer가 없는경우 Block하여Entry Queue에 전송 및Monitor Lock을 획득할때까지 대기
▶remove()에서Buffer Pool에Full Buffer가 없는경우도 동일
。임계영역에서의 작업이 끝난 경우notify()를 호출해서InterruptedException을 발생시켜 다음스레드에서임계영역으로 진입하여Execute
- 생산자 스레드 Class 구현
class Producer implements Runnable{ BufferPool pool; public Producer(BufferPool pool){ this.pool = pool; } @Override public void run(){ try{ while (true) { Thread.sleep((long)(Math.random()*500)); int input = ((int)(1+Math.random()*9))*10000; pool.insert(input); } } catch (InterruptedException e){} } }
- 소비자 스레드 Class 구현
class Consumer implements Runnable{ BufferPool pool; public Consumer(BufferPool pool){ this.pool = pool; } @Override public void run(){ try{ while(true){ Thread.sleep((long)(Math.random()*500)); int output = pool.remove(); } } catch (InterruptedException e){} } }
- 메인 스레드 구현
。5개Buffer를 가진Buffer Pool을 생성자로 전달하여 각각 5개의생산자 / 소비자 스레드생성 및RR 스케줄링으로Concurrent하게 실행public class BoundedBuffer { public static void main(String[] args) { BufferPool pool = new BufferPool(5); Thread[] producers = new Thread[5]; Thread[] consumers = new Thread[5]; for(int i = 0; i < 5; i++){ producers[i] = new Thread(new Producer(pool)); producers[i].start(); } for(int i = 0; i < 5; i++){ consumers[i] = new Thread(new Consumer(pool)); consumers[i].start(); } } }
。 각각 5개의소비자 스레드와생산자 스레드가 서로동기화를 유지하면서Concurrent하게 실행
- 문제 :
Readers-Writers Problem에서Monitor를 활용해 동기화 문제 해결 독자-작가 문제 개념
。여러명의 독자와 저자들이 하나의 공유자원( ex.DB)에 동시접근 시 발생하는 동기화 문제
。Reader-Writer Lock방식으로Reader Lock과Writer Lock을 각각 구현sharedDB.acquireReaderLock(); sharedDB.read(); sharedDB.releaseReaderLock();▶
DB에서Read시 다음처럼Reader Lock을 획득 및 작업 후 반납하는 방식으로 구현
Reader Lock/Writer Lock구현public class SharedDB { private int readerCount = 0; // 임계영역의 Reader 스레드 갯수 private boolean isWriting = false; public void read(){ // 임계영역으로서 DB에서 데이터를 읽을 코드 작성 } public void write(){ // 임계영역으로서 DB에서 데이터를 작성할 코드 작성 } synchronized public void acquireReaderLock(){ // Writer 스레드가 Execute 중인 경우 해당 SharedDB객체 모니터의 Entry Queue에서 대기 및 // readerCount == 0 을 통한 notify() 호출 시 catch 문 이후 실행됨 while (isWriting == true){ try{ wait(); } catch (InterruptedException e){} } // Reader 스레드의 경우 Count 증가 readerCount++; } synchronized public void releaseReaderLock(){ readerCount--; if (readerCount == 0){ // 임계영역에 Reader 스레드가 없는 경우 Writer 스레드 실행 notify(); } } synchronized public void acquireWriterLock(){ // 임계영역에 Reader 스레드 or Writer 스레드가 존재하는 경우 // 해당 SharedDB객체 모니터의 Entry Queue에서 대기 // 이후 notifyAll() 호출 시 catch 문 이후 실행됨 while ( readerCount > 0 || isWriting == true ){ try{ wait(); }catch (InterruptedException e){} } isWriting = true; } synchronized public void releaseWriterLock(){ isWriting = false; notifyAll(); } }。
read(),write():임계영역으로서 공유자원과 상호작용할 코드 작성
。int readerCount:read()를 통해임계영역에서Execute중인Reader수
。bool isWriting:Writer가Execute중인지 여부
acquireReaderLock()/releaseReaderLock()
。다음Reader 스레드코드를 실행하는 경우sharedDB.acquireReaderLock(); sharedDB.read(); sharedDB.releaseReaderLock();。
readerCount++가 되면서read()실행
。 이때Writer가임계영역에서 작업중인 경우 해당SharedDB객체 모니터의Entry Queue에서 대기
▶ 이후releaseReaderLock()를 통해read()를 수행하는Reader가 없는 경우 (readerCount==0)notify()를 호출하여Entry Queue에서 대기중인스레드를Ready Queue로 전달 및Execute하여catch문 부터 시작하여read()수행
acquireWriterLock()/releaseWriterLock()
。다음Writer 스레드코드를 실행하는 경우sharedDB.acquireWriterLock(); sharedDB.write(); sharedDB.releaseWriterLock();。사전에
read()또는write()를임계영역상에서 수행하고있는스레드가 존재하는 경우 해당SharedDB객체 모니터의Entry Queue에 전송하여 대기
。이후임계영역상에스레드가 없는경우isWriting = false설정 및notifyAll()을 호출하여Entry Queue에서 대기중인 모든스레드를Ready Queue로 전달 및스레드간 경쟁을 통해Execute하여catch문 부터 시작하여write()수행