JAVA - 동기화 관련 ( synchronized 키워드 )

TopOfTheHead·2025년 8월 1일

자바 ( JAVA )

목록 보기
12/23

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 ThreadRace 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 SectionExit Section에서 발생하는 Context Switch 문제 때문에 피터슨 알고리즘이 정상작동하지않아 데이터 불일치가 여전히 발생.
iter = 100000이면 더 자주발생.

  • Atomic Variable 활용하여 피터슨 알고리즘 개선 Atomic Variable
    피터슨 알고리즘Entry SectionExit Section에서 발생하는 Context Switch를 없애고자 Atomic Instruction으로 구현한 Atomic Variable을 활용

    flag역할의 boolean 배열을 AtomicVariable 객체 배열로 변경
    JAVA - AtomicVariable
 static 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 SectionExit Section에서 문맥교환 없이 원자적으로 작용하도록 하여 피터슨 알고리즘이 정상작동하면서 데이터 불일치가 발생하지 않는다.

JAVA Monitor OS - Monitor
JAVA에 내장된 Lock이라는 의미로 monitor-lock or intrinsic-lock이라는 이름으로 Monitor-like한 기능을 제공
사용자 스레드Concurrency하게 Execute할 경우의 동기화 메카니즘

  • synchronized keyword
    임계영역에 해당하는 코드블록을 선언 시 활용하는 JAVA keyword
    ▶ 해당 코드블록에 단일 사용자 스레드만 진입하도록하여 동기화를 수행하여 Race Condition 방지
    상호배제 문제 해결

    코드블록에는 공유자원에 접근하여 동기화가 필요한 코드영역만 넣는다
    동기화가 필요없는 코드까지 넣으면 비효율적.

    。기존 entry sectionexit section에서 수행하던 Lock & Unlock 과정을 JVM에서 자동으로 수행
synchronized (객체){
          // critical section
}

。 해당 코드블록모니터락을 획득해야 진입가능
모니터락을 보유한 객체를 지정가능.

Method에 선언 시 Method의 전체 코드블록임계영역으로 지정

public synchronized void 메소드명(){
        // critical section
}

this객체모니터락을 획득해야함

  • Monitor에 사용되는 Operation
    java.lang.Object Class에 선언된 메소드
    ▶ 모든 자바 객체에서 사용가능

    세마포어에서 사용되는 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의 객체를 생성자로 전달하여 생성한 복수의 스레드synchronized keyword 메서드로 해당 객체의 공유자원 buffer에 동시접근 시 모니터로서 동기화 유지
    synchronized keyword 메서드에서는 this객체를 사용하므로 객체.wait()할 필요가 없다.

    。 생성된 스레드synchronized keyword insert() 실행 시 만약 BufferPoolEmpty 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 Classcount++에서 여러 사용자 스레드의 동시 접근으로인한 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으로 인해 데이터 불일치가 심각하게 발생.

  • synchronized keyword를 활용한 동기화
    Race Condition 문제로 인해 공유자원에 접근하여 동기화가 필요한 코드( count++ ) 부분을 synchronized keyword를 선언하여 임계영역으로 지정
    동기화가 필요없는 코드까지 넣으면 비효율적.

    LockUnlockJVM에서 자동으로 수행하므로 Entry SectionExit Section을 구현할 필요는 없다
    • synchronized keyword - 메서드
      this 객체에 동기화된 모니터모니터락을 획득 시 메소드임계영역에 진입하는 동기화 메카니즘
    static class Counter{
            public static int count = 0;
            synchronized public static void increment() {
                // Critical Section 지정
                count++;
            }
        }


    count++ 부분을 임계영역으로 지정하여 모니터락을 획득한 단일 사용자 스레드만 진입하도록 동기화
    상호배제를 충족하여 데이터 불일치가 발생하지않음

    • synchronized keyword - 코드블록
      특정 객체와 동기화된 모니터모니터락을 획득 시 코드블록임계영역에 진입하는 동기화 메카니즘
    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()에 정의된 synchronized keyword의 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 Pool Class 구현
    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 생성 및 synchronized keyword 메소드insertremove 구현

    。생성된 스레드들에서 생산자 스레드에서 insert()를 호출하여, 소비자 스레드에서 remove()를 호출하면서 공유자원 bufferConcurrent하게 접근하므로 synchronized keyword를 활용하여 동기화를 설정해야한다.
    ▶ 해당 스레드들이 생성자를 통해 생성되어 참조하는 ButterPool 객체에 대한 Monitor Lock을 획득 및 반납을 반복

    insert()에서 Buffer PoolEmpty Buffer가 없는경우 Block하여 Entry Queue에 전송 및 Monitor Lock을 획득할때까지 대기
    remove()에서 Buffer PoolFull 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 LockWriter 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 : WriterExecute 중인지 여부

  • 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() 수행

profile
공부기록 블로그

0개의 댓글