Skip to content

[아이템 78] 공유 중인 가변 데이터는 동기화해 사용하라 #102

Description

@jupyter471

📌 [아이템 78] 공유 중인 가변 데이터는 동기화해 사용하라

✨ 핵심 내용

  • synchronized 키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장

<동기화의 기능>

  • 배타적 실행, 객체에 락을 걸어 하나의 일관된 상태에서 다른 일관된 상태로 변화시킨다

  • 한 스레드가 만든 변화를 다른 스레드에서 확인할 수 있게 해준다

    즉, 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다

자바 언어 명세는 스레드가 필드를 읽을 때 항상 수정이 완전히 반영된 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 보이는가는 보장하지 않는다

⇒ 동기화는 스레드 사이의 안정적인 통신에 꼭 필요함!

자바의 메모리 모델

스레드가 만든 변화가 다른 스레드에게 언제 어떻게 보이는지를 규정함

공유 중인 가변 데이터를 원자적으로 읽고 쓸 수 있을지라도 동기화에 실패하면 결과가 처참해질 수 있다

  • Thread.stop 메서드는 이미 deprecated 되었다
  • 사용 금지!
public class StopThread {
		private static boolean stopRequested;
		
		public static void main(String[] args) throws InterruptedException {
				Thread backgroundThread = new Thread(() -> {
						int i = 0;
						while (!stopRequested)
							i++;
				});
				backgroundTh read.start();
				
				TimeUnit.SECONDS.sleep(1);
				stopRequested = true;
		}
}

스레드1 : boolean 값을 폴링하면서 true가 되면 멈춤

다른 스레드에서 스레드1을 멈추기 위해 필드를 변경하는 방식

프로그램이 1초 후에 종료될거라 예상하지만, 해당 프로그램은 영원히 수행된다!!!

⇒ why? 동기화하지 않으면 메인 스레드가 수정한 값을 백그라운드 스레드가 언제쯤에나 보게 될지 보증할 수 없다

🔽 수정한 코드 이젠 동기화되어 스레드가 정상 종료된다

public class StopThread {
		private static boolean stopRequested;
		
		private static synchronized void requeststop() {
				stopRequested = true;
		}
		
		private static synchronized boolean stopRequested() {
				return stopRequested;
		}
		
		public static void main(String[] args) throws InterruptedException {
				Thread backgroundThread = new Thread(() -> {
						int i = 0;
						while (!stopRequested())
							i++;
				});
				backgroundThread.start();
				TimeUnit.SECONDS.sleep(1);
				requestStop();
		}
}

읽기와 쓰기 메서드 모두 동기화가 필요하다! 쓰기만 동기화해서는 충분하지 않다

🔽 volatile로 선언하면 동기화 생략 가능하다

public class StopThread {
		private static **volatile** boolean stopRequested;
		public static void main(String [] args) throws InterruptedException {
				Thread backgroundThread = new Thread(() -> {
				int i = 0;
				while (!stopRequested)
					i++;
				});
				backgroundThread.start();
				
				TimeUnit.SECONDS.sleep(1);
				stopRequested = true;
		}
}

volatile 한정자는 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다! 그렇기에 배타적 수행과는 상관없어도 동기화를 생략가능하다

☠️ ☠️  volatile 주의사항!

private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
		return nextSerialNumber++;
}

일련번호 생성 코드

문제는 ++ 연산자이다

++ 연산자는 실제로 nextSerialNumber 해당 필드에 두 번 접근한다

  1. 읽고
  2. 증가한 값을 저장

두번째 스레드가 두 과정 사이를 비집고 들어와 값을 읽어가면, 첫 번째 스레드와 똑같은 값을 돌려받게 된다

프로그램이 잘못된 결과를 계산해내는 오류를 안전 실패라고 함

<해결법>

generateSerialNumber 메서드 그 자체에 synchronized 한정자를 붙이면 된다

→ 동시에 호출해도 서로 간섭하지 않으며 이전 호출이 변경한 값을 읽게 됨

  • 다만! synchronized를 붙으면 volatile 는 제거해야함

AtomicLong (아이템 59)

java.util.concurrent.atomic을 이용한 락-프리 동기화

  • 락 없이도 스레드 safe 프로그래밍 지원
private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
		return nextSerialNum.getAndlncrement();
}

→ 성능도 동기화보다 우수함!

애초에 가변 데이터를 공유하지 말라!

가변 데이터는 단일 스레드에서만 쓰자!

  • 한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유할 때 해당 객체에서 공유하는 부분만 동기화 가능

    → 객체를 다시 수정할 일이 생기기 전까지 다른 스레드들은 동기화 없이 자유롭게 값 읽을 수 있음! 이런 객체를 불변이라하고, 이걸 건네는 행위를 안전 발행(safe publication)이라함

  • 방식

    • 클래스 초기화 과정에서 객체를 정적 필드, volatile 필드, final 필드, 락을 통해 접근하는 필드에 저장가능

💡 새롭게 알게 된 점

  • volatile 한정자를 통해 동기화를 간접적으로 할 수 있다는 것을 알게 되었습니다

📚 정리

여러 스레드가 가변 데이터를 공유한다면 그 데이터를 읽고, 쓰는 동작은 반드시 동기화 해야함

배타적 실행은 필요 없고 스레드끼리 통신만 필요하다면 volatile 한정자만으로 동기화 가능

📢 댓글로 각자의 학습 내용을 공유해주세요!

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions