Synchronized

eunsiver·2023년 6월 10일
0

쓰레드의 동기화

synchronized를 이용한 동기화

동기화 방법에는 몇가지가 있지만 가장 간단한 synchronized키워드를 이용한 동기화에 대해 살펴보면 사용법은 2가지가 있다.

1. 메서드 전체를 임계 영역으로 지정

메서드 앞에 키워드를 붙이게 되면 메서드 자체가 임계 영역으로 설정이되며, 메서드 호출 시점부터 영역내의 모든 객체에 lock을 얻어 작업을 수행하게 되며, 메서드 종료시 lock을 반환한다.

// 1. 메서드 전체를 임계 영역으로 지정
public synchronized void A() {

}

2. 특정한 영역을 임계 영역으로 지정

두번째로는 메서드 내의 코드 일부를 블럭{}으로 감싸고 블록 앞에 synchronized(참조변수)를 붙이는 것이다.

참조변수는 락을 걸고자하는 객체를 참조하는 것이어야 한다. 이 블록을 synchronized블록이라고 부르며, 이 블록의 영역 안으로 들어가면서부터 쓰레드는 지정된 객체의 lock을 얻게 되고 이 블럭을 벗어나면 lock을 반납한다.

public void A() {
    // 2. 특정한 영역을 임계 영역으로 지정
    synchronized(객체의 참조변수) {...}

}

class Account{
	private int balance = 1000; // private으로 해야 동기화가 의미가 있다.
    ...
	public synchronized void withdraw(int money){
		if(balance >= money){
    		try{ Thread.sleep(1000); } catch(InterruptException e) {}
        	balance -= money;
    	}
	} // withdraw
}

여기서 주의할 점은 동시화 시 사용하는 변수를 private로 선언했다는 점이다.

private이 아닌 변수는 외부에서 직접 접근할 수 있기 때문에 아무리 동기화를 해도 이 값의 변경을 막을 길이 없다. synchronized를 이용한 동기화는 지정된 영역의 코드를 한 번에 하나의 쓰레드가 수행하는 것을 보장하는 것일 뿐이기 떄문이다.

한 쓰레드에 의해서 먼저 synchronized메서드/블럭이 호출되면, 이 메서드/블럭이 종료되어 lock이 반남될 때까지 다른 쓰레드는 특정 블럭을 호출하더라도 대기 상태에 머물게 된다.

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


wait()과 notify()

synchronized를 통해 동기화하여 공유 자원을 보호하는 것은 좋지만 lock을 잡고 오랫동안 반환하지 않게 되면 다른 쓰레드는 무한정 대기하게 되는 상황이 발생하게 된다. 이러한 상황을 개선하기위해 개발된 것이 wait()과 notify()이다.

1, 동기화된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면
2. 일단 wait() 호출하여 쓰레드가 락을 반납하고 기다리게 함
3. 그러면 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행
4. 나중에 작업을 진행할 수 있는 상황이 되면 notify() 호출
5. 작업을 중단했던 쓰레드가 다시 락을 얻어 작업 진행

  • 동기화 블럭(synchronized블록)내에서만 사용할 수 있다.

  • 오래 기다린 쓰레드가 락을 얻는다는 보장이 없다.

  • wait()가 호출되면, 실행 중이던 쓰레드는 해당 객체의 대기실(waiting pool)에서 통지를 기다린다.

  • notify()가 호출되면, 해당 객체의 대기실에 있던 모든 쓰레드 중에 임의의 쓰레드만 통지를 받는다.

  • notifyAll()은 기다리고 있는 모든 쓰레드에게 통보를 하지만, 그래도 lock을 얻을 수 있는 것은 하나의 쓰레드다. 따라서 나머지 쓰레드는 통보를 받긴 했지만 lock을 얻지 못 하면 다시 lock을 기다리는 신세가 된다.


wait()와 notify()는 특정 객체에 대한 것이므로 Object클래스에 정의되어있다.
-> 모든 객체에서 호출이 가능하다.

void wait()
void wait(long timeout)
void wait(long timeout, int nanos)
void notify()
void notifyAll()
// wait()와 notify() 예제
class Account{
    int balance = 1000;
 
    public synchronized void withdraw(int money){
        /* 잔고가 부족할 경우 wait()를 호출하여 lock을 풀고 waiting pool에
           들어가면서 제어권을다른 쓰레드에게 양보하게 됩니다. */
        while(balance < money){ 
            try{
                wait();
            }catch(InterruptedException e){ }
        }
 
        balance -= money;
    }
    
    /* 다른 쓰레드에 의해서 deposit()메서드가 호출되어 잔고가 증가하면서 notify()를 
       호출하면 객체의 waiting pool에서 기다리고 있던 쓰레드를 깨우게 됩니다. */
    public synchronized void deposit(int money){
        balance += money;
        notify();
    }
}

Lock과 Condition을 이용한 동기화

동기화할 수 있는 방법은 synchronized블럭 외에도 java.util.concurrent.locks 패키지가 제공하는 lock클래스들을 이용하는 방법이 있다.

synchronized블럭으로 동기화를 하면 자동적으로 lock이 잠기고 풀리기 때문에 편리하다.
심지어 synchronized블럭 내에서 예외가 발생해도 lock은 자동적으로 풀린다.
그러나 때로는 같은 메서드 내에서만 lock을 걸 수 있다는 제약이 불편하다.
그럴 때 이 lock클래스를 사용한다.

ReentrantLock: 재진입이 가능한 lock, 가장 일반적인 배타 lock
ReentrantReadWriteLock: 읽기에는 공유적이고, 쓰기에는 배타적인 락
StampedLockㅣ ReeatrantReadWriteLock에 낙관적인 lock의 기능 추가

자바 1.2 버전 부터 제공되고 있는 ThreadLocal 클래스

이해가 잘 안되네요...ㅠ 이 링크 봐주세요
ThreadLocal

ThreadLocal - Thread 단위로 로컬 변수를 할당하는 기능

ThreadLocal 이란?

일반 변수의 수명은 특정 코드 블록(ex)method 범위, for블록 내의 범위) 내에서만 유효하다.

{
    int a = 10;
    ...
   // 블록 내에서 a 변수 사용 가능
}

변수 a는 위 코드 블록이 끝나면 더 이상 유효하지 않다. (즉, 수명을 다한다.)

반면에 ThreadLocal을 이용하면 thread 영역에 변수를 설정할 수 있기 때문에, 특정 쓰레드가 실행하는 모든 코드에서 그 쓰레드에 설정된 변수 값을 사용할 수 있게 됩니다.

ThreadLocal이 활용되는 환경은 해당 컨테이너를 가지고 있는 서비스가 싱글톤 객체로 공유되는 객체임을 가정한다. 그렇기에 각각의 스레드는 동일한 서비스객체에 접근을 하게 되고, 동일한 ThreadLocal이라는 자원에 접근하는 것이다.

ThreadLocal의 기본 사용법

  1. ThreadLocal 객체를 생성합니다.
  2. ThreadLocal.set() 메서드를 이용하여 현재 thread의 로컬 변수에 값을 저장합니다.
  3. ThreadLocal.get() 메서드를 이용하여 현재 thread의 로컬 변수값을 읽어옵니다.
  4. ThreadLocal.remove() 메서드를 이용하여 현재 thread 로컬 변수값을 삭제합니다.
// 현재 쓰레드와 관련된 로컬 변수를 하나 생성한다.
ThreadLocal<UserInfo> local = new ThreadLocal<UserInfo>();

// 로컬 변수에 값 할당
local.set(currentUser);

// 이후 실행되는 코드는 쓰레드 로컬 변수 값을 사용
UserInfo userInfo = local.get();
public class Context {
    public static ThreadLocal<Date> local = new ThreadLocal<Date>();
}


///Context 클래스를 사용하여 thread 로컬 변수를 설정하고 사용하는 코드 예시입니다.

class A {
    public void a() {
        Context.local.set(new Date());
       
        B b = new B();
        b.b();

        Context.local.remove();
    }
}

class B {
    public void b() {
        Date date = Context.local.get();

        C c = new C();
        c.c();
    }
}

class C {
    public void c() {
        Date date = Context.local.get();
    }
}

• Synchronized 키워드가 어디에 붙는지에 따라 의미가 약간씩 변화하는데, 각각 어떤 의미를 갖게 되는지 설명해 주세요.
• 효율적인 코드 작성 측면에서, Synchronized는 좋은 키워드일까요?
• Synchronized 를 대체할 수 있는 자바의 다른 동기화 기법에 대해 설명해 주세요.
• Thread Local에 대해 설명해 주세요.

profile
Let's study!

0개의 댓글