[자바 무료 강의] 임계 구역 문제 - 스레드 - 코드라떼
Lesson List button
코스자바로 배우는 프로그래밍
hamburger button
강의임계 구역 문제 - 스레드최종수정일 2021-09-08
아이콘약 9분

스레드에서 굉장히 중요한 개념인 임계 구역(critical section)에 대해서 배웁니다. 스레드를 이용하여 프로그래밍하다가 예상치 못한 결과를 얻게 된다면 9할은 임계 구역 문제입니다. 그래서 서버 애플리케이션 프로그래밍을 할 때도 항상 임계 구역에 대해 인지하고 있어야 합니다.

추가 노트

목차


  1. 임계 구역(Critical Section)
  2. 동기화(synchronized method, synchronized statement)




임계 구역(Critical Section)


멀티 스레드 환경에서 여러 스레드가 접근할 수 있는 공유 자원에 대한 구역을 임계 구역(Critical Section) 이라고 하며 임계 구역에서 발생하는 문제를 임계 구역의 문제라고 부릅니다.

image

자바 메모리 모델 강의Call By Value 강의를 잘 들었으면 좀 더 수월하게 이해할 수 있습니다.
각 스레드는 자신만의 스택을 가지고 있으며 Heap 메모리와 Method Area 구역은 여러 스레드가 자원을 공유합니다.

예시로 메서드의 로컬 변수는 각 스레드의 스택 내에서 할당되고 사라지기 때문에 스레드 간에 서로 영역을 침범할 수 없지만 인스턴스 변수나 정적 변수는 자원을 공유하기 때문에 여러 스레드가 접근하여 값을 읽거나 쓰거나 할 수 있습니다.

하나의 인스턴스 변수를 여러 스레드가 접근하여 읽기만 한다면 문제가 발생하지 않으나 여러 스레드가 값을 읽거나 변경할 수 있는 경우 임계 구역의 문제가 발생합니다.

예시를 하나 들어보겠습니다.





i를 0에서 10,000까지 반복하며 sum을 1씩 증가시키는 코드

public class Task { private long sum = 0; public void calculate() { for (long i = 0; i <10000; i++) { ++sum; } } public long getSum() { return sum; } }
copy

Task 객체의 calculate()는 i를 0에서부터 10,000까지의 sum을 1씩 증가시키는 메서드입니다. 그리고 두 스레드를 만들어서 Task 인스턴스를 공유하여 calculate() 메서드를 호출할 겁니다. 그리고 작업이 끝나면 sum의 값을 출력할 겁니다. 정상적인 결과라면 20,000의 값이 나오겠죠.

try { Task task = new Task(); Thread threadA = new Thread(() -> { task.calculate(); }); Thread threadB = new Thread(() -> { task.calculate(); }); threadA.start(); threadB.start(); threadA.join(); threadB.join(); System.out.println(task.getSum()); } catch (InterruptedException e) { e.printStackTrace(); }
copy

그러나 실제로 코드를 실행해보면 실행할 때마다 다른 값이 출력됩니다.

출력

12385 // 다른 값이 나올 수도 있음
copy



스레드는 종잡을 수 없다 강의에서는 비동기적 문제를 겪었으나, 이번에는 두 스레드가 하나의 인스턴스 변수에 접근하여 생기는 임계 구역의 문제를 겪습니다.

public class Task { // sum 인스턴스 변수에 두 스레드가 접근하여 값을 변경하는 것이 문제 private long sum = 0; public void calculate() { for (long i = 0; i <10000; i++) { ++sum; } } public long getSum() { return sum; } }
copy

해당 코드를 다시 살펴봅시다. 두 스레드는 calculate 메서드에서 ++sum 명령어 부분을 통해 sum 변수의 값을 읽고 변경합니다. 이렇게만 보면 도대체 뭐가 문제인지 알 수 없으니 ++sum 명령어를 다시 자세히 살펴봅시다.

연산자는 컴퓨터에게 내리는 명령이다 강의를 기억할지는 모르겠지만 깊게 공부했다면 ++sum 연산자는 단순히 하나의 원자적인(atomic action) 명령어가 아님을 알 수 있습니다. (그냥 심심해서 넣은 그림이 아닙니다.)

image

++선행 증감 연산자는

  1. 변수의 값을 읽고
  2. 읽은 값에 1을 더하고
  3. 연산한 값을 변수에 다시 저장합니다.
  4. 그리고 변수의 값을 반환합니다.

우리는 여기서 변수의 값을 반환하는 과정은 상관없으나 중요한 건 변수의 값을 읽고 연산 후 다시 변수에 값을 저장하는 과정이 세 개의 과정으로 나뉜다는 점입니다.

image

두 스레드는 임계 구역인 인스턴스 변수에 아무런 제약 없이 언제든지 접근할 수 있기 때문에 특정 시점에 동시에 변수의 값을 읽는다면 서로 동일한 값을 읽어서 1을 증가시키고 변수에 저장하기 때문에 반복문을 통한 sum의 증감 연산이 무의미하게 사라지게 됩니다. 그래서 20,000의 값을 출력하는 것이 아니라 더 적은 값을 출력하게 되는 거지요.




동기화(synchronized method, synchronized statement)


두 스레드가 변수의 변경되지 않은 값을 읽거나 변경 전 값을 덮어 씌우는 것이 문제가 됐으니, 임계 구역에 접근하는 방법을 동기화하여 하나의 스레드만 접근할 수 있도록 할 수 있습니다. 동기화의 의미는 둘 이상의 활동을 공존하도록 만드는 것입니다.

동기화할 수 있는 방법은 다양하나 가장 간단한 두 가지 방법에 대해서 소개합니다.


synchronized method

메서드에 synchronized 키워드를 작성하여 동기화하는 방법입니다. sum 변수에 접근하는 메서드가 calculcate() 메서드이므로 해당 메서드를 동기화하면 특정 스레드가 이 메서드를 실행하는 동안 다른 스레드는 block 상태가 되어 기다리다가 메서드 실행이 완료되면 다른 스레드가 접근하여 해당 메서드를 실행할 수 있습니다.

public class Task { // sum 인스턴스 변수에 두 스레드가 접근하여 값을 변경하는 것이 문제 private long sum = 0; // synchronized method 방식 사용 public synchronized void calculate() { for (long i = 0; i <10000; i++) { ++sum; } } public long getSum() { return sum; } }
copy

결론적으로 synchronized 키워드가 선언된 후 프로그램을 실행해보면 몇 번을 실행하든 우리가 원하는 정상적인 결과를 확인할 수 있습니다.


synchronized statement

synchronized method 방식이 메서드 전체 내용을 동기화한다고 한다면 synchronized statement 방식은 메서드 내에서 동기화가 필요한 작업에만 중괄호로 감싸서 동기화할 수 있습니다.

public class Task { // sum 인스턴스 변수에 두 스레드가 접근하여 값을 변경하는 것이 문제 private long sum = 0; // synchronized statement 방식 사용 public void calculate() { for (long i = 0; i <10000; i++) { synchronized(this) { // 해당 내용만 동기화 ++sum; } } } public long getSum() { return sum; } }
copy

synchronized statement 방식은 명시적으로 Object Key를 선언해 주어야 하는데 여기서 this는 Task 인스턴스가 동기화의 Key(Lock)가 됩니다. (동기화는 이후의 강의에서 더 깊게 들어갑니다)


synchronized method와 synchronized statement의 차이

  1. 자바 가상 머신이 실행하는 방식이 다름
  2. synchronized statement는 메서드 전체가 아닌 동기화의 범위를 중괄호를 통해 지정할 수 있음
  3. synchronized statement는 명시적인 key를 지정하여 설정할 수 있음
도전자 질문
작성된 질문이 없습니다
이용약관|개인정보취급방침
알유티씨클래스|대표, 개인정보보호책임자 : 이병록
이메일 : cs@codelatte.io
사업자등록번호 : 824-06-01921
통신판매업신고 : 2021-성남분당C-0740
주소 : 경기도 성남시 분당구 대왕판교로645번길 12, 9층 24호
파일
파일파일
Root
파일

Sink 클래스의 washingDishes() 메서드에 선언된 synchronized를 삭제해보세요

Output
root$
Lesson List button
코스자바로 배우는 프로그래밍
hamburger button
강의임계 구역 문제 - 스레드최종수정일 2021-09-08
아이콘약 9분

스레드에서 굉장히 중요한 개념인 임계 구역(critical section)에 대해서 배웁니다. 스레드를 이용하여 프로그래밍하다가 예상치 못한 결과를 얻게 된다면 9할은 임계 구역 문제입니다. 그래서 서버 애플리케이션 프로그래밍을 할 때도 항상 임계 구역에 대해 인지하고 있어야 합니다.

추가 노트

목차


  1. 임계 구역(Critical Section)
  2. 동기화(synchronized method, synchronized statement)




임계 구역(Critical Section)


멀티 스레드 환경에서 여러 스레드가 접근할 수 있는 공유 자원에 대한 구역을 임계 구역(Critical Section) 이라고 하며 임계 구역에서 발생하는 문제를 임계 구역의 문제라고 부릅니다.

image

자바 메모리 모델 강의Call By Value 강의를 잘 들었으면 좀 더 수월하게 이해할 수 있습니다.
각 스레드는 자신만의 스택을 가지고 있으며 Heap 메모리와 Method Area 구역은 여러 스레드가 자원을 공유합니다.

예시로 메서드의 로컬 변수는 각 스레드의 스택 내에서 할당되고 사라지기 때문에 스레드 간에 서로 영역을 침범할 수 없지만 인스턴스 변수나 정적 변수는 자원을 공유하기 때문에 여러 스레드가 접근하여 값을 읽거나 쓰거나 할 수 있습니다.

하나의 인스턴스 변수를 여러 스레드가 접근하여 읽기만 한다면 문제가 발생하지 않으나 여러 스레드가 값을 읽거나 변경할 수 있는 경우 임계 구역의 문제가 발생합니다.

예시를 하나 들어보겠습니다.





i를 0에서 10,000까지 반복하며 sum을 1씩 증가시키는 코드

public class Task { private long sum = 0; public void calculate() { for (long i = 0; i <10000; i++) { ++sum; } } public long getSum() { return sum; } }
copy

Task 객체의 calculate()는 i를 0에서부터 10,000까지의 sum을 1씩 증가시키는 메서드입니다. 그리고 두 스레드를 만들어서 Task 인스턴스를 공유하여 calculate() 메서드를 호출할 겁니다. 그리고 작업이 끝나면 sum의 값을 출력할 겁니다. 정상적인 결과라면 20,000의 값이 나오겠죠.

try { Task task = new Task(); Thread threadA = new Thread(() -> { task.calculate(); }); Thread threadB = new Thread(() -> { task.calculate(); }); threadA.start(); threadB.start(); threadA.join(); threadB.join(); System.out.println(task.getSum()); } catch (InterruptedException e) { e.printStackTrace(); }
copy

그러나 실제로 코드를 실행해보면 실행할 때마다 다른 값이 출력됩니다.

출력

12385 // 다른 값이 나올 수도 있음
copy



스레드는 종잡을 수 없다 강의에서는 비동기적 문제를 겪었으나, 이번에는 두 스레드가 하나의 인스턴스 변수에 접근하여 생기는 임계 구역의 문제를 겪습니다.

public class Task { // sum 인스턴스 변수에 두 스레드가 접근하여 값을 변경하는 것이 문제 private long sum = 0; public void calculate() { for (long i = 0; i <10000; i++) { ++sum; } } public long getSum() { return sum; } }
copy

해당 코드를 다시 살펴봅시다. 두 스레드는 calculate 메서드에서 ++sum 명령어 부분을 통해 sum 변수의 값을 읽고 변경합니다. 이렇게만 보면 도대체 뭐가 문제인지 알 수 없으니 ++sum 명령어를 다시 자세히 살펴봅시다.

연산자는 컴퓨터에게 내리는 명령이다 강의를 기억할지는 모르겠지만 깊게 공부했다면 ++sum 연산자는 단순히 하나의 원자적인(atomic action) 명령어가 아님을 알 수 있습니다. (그냥 심심해서 넣은 그림이 아닙니다.)

image

++선행 증감 연산자는

  1. 변수의 값을 읽고
  2. 읽은 값에 1을 더하고
  3. 연산한 값을 변수에 다시 저장합니다.
  4. 그리고 변수의 값을 반환합니다.

우리는 여기서 변수의 값을 반환하는 과정은 상관없으나 중요한 건 변수의 값을 읽고 연산 후 다시 변수에 값을 저장하는 과정이 세 개의 과정으로 나뉜다는 점입니다.

image

두 스레드는 임계 구역인 인스턴스 변수에 아무런 제약 없이 언제든지 접근할 수 있기 때문에 특정 시점에 동시에 변수의 값을 읽는다면 서로 동일한 값을 읽어서 1을 증가시키고 변수에 저장하기 때문에 반복문을 통한 sum의 증감 연산이 무의미하게 사라지게 됩니다. 그래서 20,000의 값을 출력하는 것이 아니라 더 적은 값을 출력하게 되는 거지요.




동기화(synchronized method, synchronized statement)


두 스레드가 변수의 변경되지 않은 값을 읽거나 변경 전 값을 덮어 씌우는 것이 문제가 됐으니, 임계 구역에 접근하는 방법을 동기화하여 하나의 스레드만 접근할 수 있도록 할 수 있습니다. 동기화의 의미는 둘 이상의 활동을 공존하도록 만드는 것입니다.

동기화할 수 있는 방법은 다양하나 가장 간단한 두 가지 방법에 대해서 소개합니다.


synchronized method

메서드에 synchronized 키워드를 작성하여 동기화하는 방법입니다. sum 변수에 접근하는 메서드가 calculcate() 메서드이므로 해당 메서드를 동기화하면 특정 스레드가 이 메서드를 실행하는 동안 다른 스레드는 block 상태가 되어 기다리다가 메서드 실행이 완료되면 다른 스레드가 접근하여 해당 메서드를 실행할 수 있습니다.

public class Task { // sum 인스턴스 변수에 두 스레드가 접근하여 값을 변경하는 것이 문제 private long sum = 0; // synchronized method 방식 사용 public synchronized void calculate() { for (long i = 0; i <10000; i++) { ++sum; } } public long getSum() { return sum; } }
copy

결론적으로 synchronized 키워드가 선언된 후 프로그램을 실행해보면 몇 번을 실행하든 우리가 원하는 정상적인 결과를 확인할 수 있습니다.


synchronized statement

synchronized method 방식이 메서드 전체 내용을 동기화한다고 한다면 synchronized statement 방식은 메서드 내에서 동기화가 필요한 작업에만 중괄호로 감싸서 동기화할 수 있습니다.

public class Task { // sum 인스턴스 변수에 두 스레드가 접근하여 값을 변경하는 것이 문제 private long sum = 0; // synchronized statement 방식 사용 public void calculate() { for (long i = 0; i <10000; i++) { synchronized(this) { // 해당 내용만 동기화 ++sum; } } } public long getSum() { return sum; } }
copy

synchronized statement 방식은 명시적으로 Object Key를 선언해 주어야 하는데 여기서 this는 Task 인스턴스가 동기화의 Key(Lock)가 됩니다. (동기화는 이후의 강의에서 더 깊게 들어갑니다)


synchronized method와 synchronized statement의 차이

  1. 자바 가상 머신이 실행하는 방식이 다름
  2. synchronized statement는 메서드 전체가 아닌 동기화의 범위를 중괄호를 통해 지정할 수 있음
  3. synchronized statement는 명시적인 key를 지정하여 설정할 수 있음
도전자 질문
작성된 질문이 없습니다
이용약관|개인정보취급방침
알유티씨클래스|대표, 개인정보보호책임자 : 이병록
이메일 : cs@codelatte.io|운영시간 09:00 - 18:00(평일)
사업자등록번호 : 824-06-01921|통신판매업신고 : 2021-성남분당C-0740
주소 : 경기도 성남시 분당구 대왕판교로645번길 12, 9층 24호(경기창조혁신센터)
파일
파일파일
Root
파일

Sink 클래스의 washingDishes() 메서드에 선언된 synchronized를 삭제해보세요

Output
root$