Java - 6. Thread: 동기화

갓김치·2020년 9월 23일
0

고급자바

목록 보기
19/47

동기화 Synchronization

  • 공유자원(객체)을 상대로 순서대로 작업이 이루어지도록 처리하는 방법
  • 과도한 동기화: 전체적으로 프로그램의 성능을 저하시킴

필요한 이유

  • 프로세스 내 자원을 여러개의 스레드가 공유하여 작업 진행시 문제 방지
  • 임계영역(Critical Section, 문제 발생 여지가 있는 영역)에 동기화 처리를 해줌으로써 예방

동기화 처리 방법

  • synchronized
    • 메서드 자체에 동기화 처리
      • public synchronized void add() { ~ }
      • 여러개의 Thread들이 공유객체의 메소드를 사용할 때 메소드에 synchronized가 붙어 있을 경우 먼저 호출한 메소드가 객체의 사용권(Monitoring Lock)을 얻는다.
      • 메소드 앞에 synchronized 를 붙혀서 실행해 보면, 메소드 하나가 모두 실행된 후에 다음 메소드가 실행된다.
      • 해당 모니터링 락은 메소드 실행이 종료되거나, wait()와 같은 메소드를 만나기 전까지 유지된다.
      • 다른 쓰레드들은 모니터링 락을 놓을때까지 대기한다.
    • 동기화 블럭
      • synchornized (this) { ~ }
        - synchronized를 메소드에 붙혀서 사용 할 경우, 메소드의 코드가 길어지면, 마지막에 대기하는 쓰레드가 너무 오래 기다리는것을 막기위해서 블럭을 사용
      • 해당 this가 동기화 처리가 됨.
      • this만 동기화 블럭에 접근 가능
      • 블럭빠져나오기전까지는 동기화 블럭 내 애들만 공유객체에 접근 가능
      • 이때는 blocked상태이며 블럭을 빠져나오면서 runnable로 바뀌게됨
      • blocked상태 = waiting상태처럼 일시정지상태
  • lock 객체 이용
    • 객체 생성시 private final로 생성
    • 장점: 범위를 마음대로 정해줄 수 있음
    • 단점: unlock을 잘 해줘야함 안해주면 끝장난다(?)
    • lock에도 여러 종류가 있지만 제일 심플한건 ReentrantLock
    • lock(): 동기화 시작
    • unlock(): 동기화 끝
    • try ~ catch 블럭을 사용할 경우, unlock()은 finally 블럭에서

예시

T15: synchronized

1. 공유객체 클래스

class ShareObject {
  private int sum = 0;
  
// 동기화 방법1: 메서드 자체에 동기화 처리
//public synchronized void add() {

  public void add() {
    for (int i = 0; i < 1000000000; i++) {} // 동기화 처리 전까지 시간벌기용

    // 동기화 방법2: 동기화 블럭으로 설정하기
    synchronized (this) {
      int n = sum;
      n += 10;
      sum = n;
      System.out.println(Thread.currentThread().getName()
                        + " 합계: " + sum);
    }
  }
}

2. 작업을 수행하는 쓰레드 클래스

class WorkerThread extends Thread {
  ShareObject sObj;

  public WorkerThread(String name, ShareObject sObj) {
    super(name); // 원래는 Thread-0, Thread-1 ...자동 생성
    this.sObj = sObj;
  }

  @Override
  public void run() {
    for (int i = 1; i < 10; i++) {
      sObj.add();
    }
  }
}

3. 프로그램이 실행될 main()

  ShareObject sObj = new ShareObject();

  WorkerThread th1 = new WorkerThread("1번 쓰레드", sObj);
  WorkerThread th2 = new WorkerThread("2번 쓰레드", sObj);

  th1.start();
  th2.start();

4. 결과

  • 동기화 안할때
  • 동기화 할때

T16: synchronized 예제

1. 은행의 입출금을 관리하는 클래스 (공유객체)

class SyncAccount {
  private int balance; // 잔액이 저장될 변수

  public synchronized int getBalance() { // 동기화 영역에서 호출당하므로 sync처리
    return balance;
  }

  public synchronized void setBalance(int balance) {
    this.balance = balance;
  }

  // 입금처리를 수행하는 메서드
  public synchronized void deposit(int money) {
    balance += money;
  }

  // 출금처리를 수행하는 메서드 (출금성공: true, 출금실패: false반환)
  // 동기화 영역에서 호출하는 메서드도 동기화 처리를 해 주어야 한다.
  public synchronized boolean withdraw(int money) {
    if(balance >= money) { // 잔액 충분
      for (int i = 1; i < 1000000000; i++) {} // 시간때우기용

      balance -= money;
      System.out.println("메서드 안에서 balance = " + getBalance());
                   // getBalance()도 동기화 처리 해줘야함
      return true;
    }else {
      return false;
    }
  }
}

2. 은행 업무를 처리하는 쓰레드

class BankThread extends Thread {
  private SyncAccount sAcc;

  public BankThread(SyncAccount sAcc) {
    this.sAcc = sAcc;
  }

  @Override
  public void run() {
    boolean result = sAcc.withdraw(6000); // 6000원 인출
    System.out.println("쓰레드 안에서 result = " + result 
        + ", balance = " + sAcc.getBalance());
  }
}

3. 프로그램이 실행되는 main()

SyncAccount sAcc = new SyncAccount();
sAcc.setBalance(10000); // 입금처리

BankThread th1 = new BankThread(sAcc);
BankThread th2 = new BankThread(sAcc);

th1.start();
th2.start();

T17: Lock 예제

1. 입출금을 담당하는 클래스 (공유객체)

class LockAccount {
  private int balance; // 잔액이 저장될 변수

  // 2. lock객체 생성 => 되도록이면 private final로 만든다.
  private final ReentrantLock lock = new ReentrantLock();

  public int getBalance() {
    return balance;
  }

  public void setBalance(int balance) {
    this.balance = balance;
  }

  // 3. 입금처리 하는 메서드
  public void deposit(int money) {
    lock.lock();	// 동기화 시작
    balance += money; // 동기화 처리 부분
    lock.unlock();	// 동기화 해제 필수
  }
  
  // 4. 출금하는 메서드 (출금 성공: true, 출금 실패: false 반환)
  public boolean withdraw(int money) {
    lock.lock(); // 시작
    boolean chk = false;
    // try ~ catch 블럭을 사용할 경우에는
    // unlock()메서드 호출은 finally 블럭에서 하도록 한다.

    try {
      if(balance >= money) {
        for (int i = 0; i < 1000000000; i++) {} // 시간때우기
        balance -= money;
        System.out.println("메서드 안에서 = " + getBalance());
        chk = true;
      }
    } catch (Exception e) {
      chk = false;
    } finally {
      lock.unlock(); // 예외발생 여부에 관계 없이 동기화 해제
    }

    return chk;
  }
}

2. 은행 업무를 처리하는 쓰레드

class BankThread2 extends Thread {
  private LockAccount lAcc;

  public BankThread2(LockAccount lAcc) {
    this.lAcc = lAcc;
  }

  @Override
  public void run() {
    boolean result = lAcc.withdraw(6000);
    System.out.println("쓰레드 안에서 result = " + result 
        + ", balance = " + lAcc.getBalance());
  }
}

3. 프로그램이 실행되는 main()

LockAccount lAcc = new LockAccount();
lAcc.setBalance(10000);

BankThread2 th1 = new BankThread2(lAcc);
BankThread2 th2 = new BankThread2(lAcc);

th1.start();
th2.start();

두 방법 모두 동일한 결과

Collection 클래스 동기화처리

  • Vector, HashTable등 예전부터 존재하던 Collection 클래스들은 내부에 동기화 처리가 되어있다.
  • 최근에 새로 구성된 Collection들은 동기화 처리가 되어있지 않다.
  • 동기화가 필요한 프로그램에서 이런 Collection들을 사용하려면 동기화 처리를 한 후에 사용해야 한다.
  • 동기화 처리시: Collections의 정적 메서드 중에서 synchronized로 시작하는 메서드 이용.

T18:

// 동기화 처리를 하지 않을 경우
private static List<Integer> list1 = new ArrayList<Integer>();

// 동기화하는 경우
// Collections의 정적 메서드 중에서 synchronized로 시작하는 메서드 이용.
private static List<Integer> list2 = Collections.synchronizedList(new ArrayList<>());

public static void main(String[] args){

  // 익명클래스로 쓰레드 구현
  Runnable r = new Runnable() {

    @Override
    public void run() {
      for(int i=1; i<=10000; i++){
//      list1.add(i); // 동기화 처리를 하지 않은 리스트 사용
        list2.add(i); // 동기화 처리가 된 리스트
      }
    }
  };

  Thread[] ths = new Thread[] {
    new Thread(r), new Thread(r),
    new Thread(r), new Thread(r), new Thread(r)
  };
  // 이놈들이 동시에 어레이에 접근해서 add하려고 하니까 없는 범위에도 접근하고 막 그래..? 먼소리지 

  long startTime = System.currentTimeMillis();

  for (Thread th : ths) {
    th.start();
  }

  for (Thread th : ths) {
    try {
      th.join();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  long endTime = System.currentTimeMillis();

  System.out.println("처리 시간(ms)" + (endTime - startTime));

//System.out.println("list1의 개수 : " + list1.size());
  System.out.println("list2의 개수 : " + list2.size()); // 동기화 처리가 된 리스트
}

동기화 처리 하지않은 리스트를 사용했을 때

동기화 처리가 된 리스트

profile
갈 길이 멀다

0개의 댓글