728x90

Java Thread Synchronized(동기화)란 여러 개의 Thread가 한 개의 자원을 사용하고자 할 때,
해당 Thread만 제외하고 나머지는 접근을 못하도록 막는 것이다.


Multi-Thread로 인하여 동기화를 제어해야하는 경우가 생긴다.
synchronized 키워드를 사용하면, Multi-Thread 상태에서 동일한 자원을 동시에 접근하게 되었을 때 동시 접근을 막게 된다.
synchronized를 사용하는 방법은 아래와 같다.
1. 메서드에 synchronized 하기

    - synchronized 를 붙이면 메소드 전체가 임계 영역으로 설정된다.

    - 쓰레드는 synchronized 메소드가 호출된 시점부터 해당 메소드가 포함된 객체의 Lock을 얻어 작업을 수행하다가 메소드가 종료되면 Lock 을 반환한다.
2. 블록에 synchronized 하기

   - 메소드 내의 코드 일부를 블럭{} 으로 감싸고 블럭 앞에 synchronized를 붙이는 것이다.

   - 이때 참조변수는 Lock 을 걸고자 하는 객체를 참조하는 것이어야 한다.

   - 이 영역 안으로 들어가면서부터 쓰레드는 지정된 객체의 Lock 을 얻게 되고, 이 블럭을 벗어나면 Lock 을 반납한다.


synchronized : 단 하나의 쓰레드만 실행할 수 있는 메소드 또는 블록을 말한다.
- 다른 쓰레드는 메소드나 블록이 실행이 끝날 때까지 대기해야 한다.
- wait(), notify(), notifyAll() 은 동기화 메소드 또는 블록에서만 호출 가능한 Object의 메소드
  두개의 쓰레드가 교대로 번갈아 가며 실행해야 할 경우에 주로 사용한다.
- wait() 호출한 쓰레드는 일시 정지가 된다.
- notify() or notifyAll()을 호출하면
다른 쓰레드가 실행 대기 상태가 된다.

- synchronized를 대충 사용하면 퍼포먼스 저하, 예상치 못한 동작이 발생할 수 있다.

- 임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 메소드 전체에 Lock을 거는 것보다 synchronized 블럭으로 임계 영역을 최소화해서 보다 효율적인 프로그램이 되도록 노력해야 한다.

- 쓰레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료된다.




아래 예제를 실행해서 결과가 나오는 걸 이해해야 한다.

synchronized 를 붙인 경우와 붙이지 않은 경우에 결과를 비교해보면 명확하게 이해가 될 것이다.


예제1

public class SynchThread extends Thread {
    int total = 0;

    @Override
    public void run() {
        synchronized (this) { // 해당 객체(this)에 Lock 이 걸린다.
            for (int i = 0; i < 5; i++) {
                System.out.println(i + "를 더한다.");
                total += i;

                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            notify(); // 위 작업이 모두 끝나면 notify()를 호출하여 다른 쓰레드를 실행 대기 상태로 만든다.
        }
    }
}

public class SyncMainThread {

    public static void main(String[] args) {
        SynchThread syncThread = new SynchThread();
        syncThread.start();
       
        synchronized (syncThread) {
            System.out.println("syncThread 가 완료될 때까지 기다린다.");
            try {
                syncThread.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Total Sum : " + syncThread.total);
        }
    }

}

실행결과

syncThread 가 완료될 때까지 기다린다.
0를 더한다.
1를 더한다.
2를 더한다.
3를 더한다.
4를 더한다.
Total Sum : 10



예제2 (동기화 개념을 명확히 이해하고자 한다면 아래 동영상 강좌를 꼭 들어라)

두개의 쓰레드가 교대로 번갈아 가며 실행해야 하는 경우에 주로 사용한다.

"이것이 자바다" 유투브 강좌 https://www.youtube.com/watch?v=hao05jNL2m8 35분부터 참조하면 설명이 잘 되어 있다. 아래 코드는 https://www.youtube.com/watch?v=7pNZr8cmjis 강좌에 나온 내용이다.


- notify() 는 현재 waiting 된 다른 쓰레드를 실행대기 상태로 만든다.

- wait() 를 호출해서 자기 자신은 일시 정지가 된다.


public class DataBox {
    private String data;

    public synchronized String getData() { // 소비자 쓰레드가 호출
        if (this.data == null) { // 데이터를 읽을 수 없으니까 wait 으로 만든다.
            try {
                wait(); // 생성자 쓰레드가 데이터를 넣어줄 때가지 일시 정지된다.

            } catch (InterruptedException e) {
            }
        }
        String returnValue = data;
        System.out.println("ConsummerThread가 읽은 데이터: " + returnValue);
        data = null; // 데이터를 읽었으니까 null 로 만든다.
        notify(); //
notify()를 호출해서 현재 일시정지 상태에 있는 생성자 쓰레드를 실행 대기 상태로 만든다.
        return returnValue;
    }

    public synchronized void setData(String data) { // 생성자 쓰레드가 호출
        if (this.data != null) { // 소비자 쓰레드가 아직 데이터를 읽지 않았다면
            try {
                wait(); //

            } catch (InterruptedException e) {
            }
        }
        this.data = data;
// 데이터를 공유 객체에 저장한다.

        System.out.println("ProducerThread가 생성한 데이터: " + data);
        notify(); // notify()를 호출해서 소비자 쓰레드를 실행 대기 상태로 만들어 data를 읽어갈 수 있게 한다.

    }
}

public class ProducerThread extends Thread {
    private DataBox dataBox;

    public ProducerThread(DataBox dataBox) {
        this.dataBox = dataBox;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            String data = "Data-" + i;
            dataBox.setData(data);
        }
    }

}

public class ConsumerThread extends Thread {
    private DataBox dataBox;

    public ConsumerThread(DataBox dataBox) {
        this.dataBox = dataBox;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            String data = dataBox.getData();
        }
    }

}

public class WaitNotifyExam {

    public static void main(String[] args) {
        DataBox dataBox = new DataBox();

        ProducerThread producerThread = new ProducerThread(dataBox);
        ConsumerThread consumerThread = new ConsumerThread(dataBox);

        producerThread.start();
        consumerThread.start();
    }
}

실행 결과

ProducerThread가 생성한 데이터: Data-1
ConsummerThread가 읽은 데이터: Data-1
ProducerThread가 생성한 데이터: Data-2
ConsummerThread가 읽은 데이터: Data-2
ProducerThread가 생성한 데이터: Data-3
ConsummerThread가 읽은 데이터: Data-3


자바의 정석 13장 예제

public class SyncEx {

    public static void main(String[] args) {
        Runnable runnable = new RunnableSync();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}

class Account {
    private int balance = 1000; // private으로 해야 동기화 의미가 있다.

    public  int getBalance() {
        return balance;
    }

    public synchronized void withdraw(int money){ // synchronized로 메서드를 동기화
        if(balance >= money) {
            try {
                Thread.sleep(1000);
            } catch(InterruptedException e) {}
            balance -= money;
        }
    } // withdraw
}

class RunnableSync implements Runnable {
    Account acc = new Account();

    public void run() {
        while(acc.getBalance() > 0) {
            // 100, 200, 300중의 한 값을 임으로 선택해서 출금(withdraw)
            int money = (int)(Math.random() * 3 + 1) * 100;
            acc.withdraw(money);
            System.out.println("balance:"+acc.getBalance());
        }
    } // run()
}
 


728x90
블로그 이미지

Link2Me

,