구너드 2023. 6. 6. 19:56

쓰레드의 동기화

 

멀티쓰레드의 프로세스 경우, 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 됨, 이는 제어권이 넘어간 쓰레드가 제어권을 다시 가지고 실행되었을 때 의도했던 것과 다른 결과를 얻을 수 있는 가능성이 존재

이를 방지하기 위해 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 동기화

동기화를 위해서 도입된 개념, 임계영역과 lock(잠금)

공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정
공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 임계 영역내의 코드룰 수행
해당 쓰레드가 영역 내의 모든 코드를 수행하고 나서 lock을 반납
반납된 lock을 대기하던 쓰레드가 획득하여 임계 영역의 코드를 수행

동기화 - 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것


동기화 방법

 

synchronized - 임계영역을 설정하는 데 사용

public synchronized void calcSum() {

}    // 메서드 전체를 임계영역으로 지정


synchronized(객체의 참조변수) {

}   // 특정한 영역을 임계영역으로 지정

두 방법 모두 lock의 획득과 반납이 자동적으로 이루어짐

임계영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 최소화해서 보다 효율적인 프로그래밍 지향

synchronized로 동기화해서 공유 데이터를 보호한 후, 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것도 중요

이를 위해 고안된 wait(), notify()

 

wait()

동기화된 임계영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니라면, 일단 wait()을 호출하여 쓰레드가 락을 반납하고 대기시킴

이후 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행

매개변수가 있으며, 지정된 시간동안만 기다림.

notify()

이후 작업을 진행할 수 있는 상황이었던 쓰레드에게 락을 주고 작업을 재개

wait()에서 지정된 시간이 지나면 자동적으로 notify()호출

 

wati()과 notify(), notifyAll()

Object에 정의, 동기화 블록 내에서만 사용할 수 있음,보다 효율적인 동기화 가능


StringBuilder 와 StringBuffer 

지난 javalang패키지와 유용한 클래스들 챕터에서 배운 StringBuiler와 StringBuffer는 동기화 여부가 차이점이라고 알고 있다. 동기화가 '한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것'을 의미하는 건 알겠지만 이를 StringBuilder와 StringBuffer가 어떠한 구체적인 상황에서 다르게 작동할 수 있는지에 대한 궁금증이 생겼다. 문자열을 작성하는데 있어서 다른 여러 쓰레드가 동기화가 보장되지 않은 StringBuiler 객체를 작업하면 어떤 일이 발생할까?

 

우선 구글링을 통해서 이 둘의 명확한 차이에 대해서 알아보고자 했다. 하지만 아쉽게도 동기화 여부의 차이가 있을 뿐 동기화 여부의 차이에 따라 구체적으로 어떤 점이 달라질 수 있는지에 대한 설명은 찾기 힘들었다. 따라서 Chat GPT를 이용해 이 둘을 좀 더 명확히 알아보고자 했다.

 

Q: java에서 stringbuilder 랑 stringbuffer 의 차이점이 동기화여부인 건 알겠어. 근데 동기화 여부가 무슨 차이를 만들어내?

A
Java에서 StringBuilder와 StringBuffer는 문자열을 동적으로 조작하기 위한 클래스입니다. 둘 다 문자열을 수정하고 조작하는 데 사용됩니다. 그러나 StringBuilder와 StringBuffer의 주요 차이점은 동기화(synchronization) 여부입니다.

StringBuilder는 동기화를 지원하지 않습니다. 따라서 멀티스레드 환경에서 사용될 때 동기화 처리가 이루어지지 않습니다. 이는 StringBuilder가 더 빠르게 동작하도록 하며, 단일 스레드 환경에서 사용할 때 적합합니다. StringBuilder는 스레드 안전(thread-safe)하지 않으므로 여러 스레드에서 동시에 접근하면 예기치 않은 결과가 발생할 수 있습니다.

반면 StringBuffer는 동기화를 지원합니다. 멀티스레드 환경에서 안전하게 사용할 수 있도록 동기화 처리가 되어 있습니다. 따라서 여러 스레드에서 동시에 접근하더라도 데이터 일관성을 유지할 수 있습니다. 하지만 동기화 처리로 인해 성능상의 오버헤드가 발생할 수 있으므로, 단일 스레드 환경에서는 StringBuilder보다는 성능이 떨어질 수 있습니다.

요약하면, StringBuilder는 단일 스레드 환경에서 성능이 뛰어나고, StringBuffer는 멀티스레드 환경에서 스레드 안전성을 제공합니다. 따라서 동기화 여부는 사용하는 환경과 스레드 안전성의 필요성에 따라 선택되어야 합니다.

 

쓰레드 안전성과 데이터 일관성이라는 단어에 대해 물음표가 생겨 이를 좀 더 물어봤다.

스레드 안전성(thread safety)은 멀티스레드 환경에서 여러 스레드가 동시에 공유된 데이터에 접근할 때 
발생할 수 있는 문제를 방지하고, 데이터의 일관성과 정확성을 보장하는 것을 의미합니다.

멀티스레드 환경에서 여러 스레드가 동시에 같은 데이터를 수정하려고 할 때, 예기치 않은 결과가 발생할 수 있습니다. 
이러한 문제들은 주로 공유된 데이터의 일관성을 유지하기 어렵게 만들거나, 
데이터의 중복 수정, 손실 또는 손상을 초래할 수 있습니다.

스레드 안전성을 유지하기 위해서는 동기화(synchronization)가 필요합니다. 
동기화는 스레드가 공유된 데이터에 접근할 때 상호 배제(mutual exclusion)를 보장하여 
스레드 간의 충돌을 방지합니다. 
이를 통해 여러 스레드가 동시에 데이터를 수정하는 것을 막고, 데이터의 일관성을 보장할 수 있습니다.

StringBuilder와 StringBuffer의 차이에서도 스레드 안전성의 차이가 나타납니다. 
StringBuilder는 동기화를 지원하지 않으므로 멀티스레드 환경에서 여러 스레드가 동시에 접근하면 
데이터 일관성이 깨질 수 있습니다. 
반면 StringBuffer는 동기화를 지원하여 멀티스레드 환경에서 스레드 안전성을 제공합니다.

스레드 안전성은 멀티스레드 환경에서 안정적이고 정확한 프로그램 실행을 보장하기 위해 중요한 개념입니다. 
따라서 스레드 안전성을 고려하여 프로그램을 설계하고 동기화를 적절하게 처리해야 합니다.


데이터 일관성(data consistency)은 여러 개의 데이터 요소가 어떤 조건 또는 규칙에 따라
항상 일관된 상태를 유지하는 것을 의미합니다.
데이터 일관성은 데이터의 정확성, 유효성 및 무결성을 보장하기 위해 중요합니다.

멀티스레드 환경에서 데이터를 공유하고 수정하는 경우,
여러 스레드가 동시에 동일한 데이터를 수정하거나 접근할 수 있습니다.
이때 데이터 일관성을 유지하지 않으면 예기치 않은 결과가 발생할 수 있습니다.

예를 들어, 두 개의 스레드가 동시에 같은 데이터를 수정하고 있다고 가정해 봅시다.
스레드 A는 데이터의 값을 10으로 변경하려고 하고, 스레드 B는 데이터의 값을 20으로 변경하려고 합니다.
이때 스레드 간에 동기화 없이 접근하면 데이터 일관성이 깨질 수 있습니다.

만약 스레드 A가 데이터의 현재 값을 읽고, 아직 변경하지 않았는데
스레드 B가 동시에 접근하여 값을 변경한다면,
스레드 A는 예상과 다른 값으로 데이터를 수정할 수 있습니다.
결과적으로 데이터는 예상치 못한 상태로 변하게 되어 일관성이 깨집니다.

데이터 일관성을 유지하기 위해서는 동기화 메커니즘을 사용하여 스레드 간에 상호 배제를 보장해야 합니다.
이를 통해 여러 스레드가 동시에 데이터에 접근하는 것을 제어하고, 데이터를 정확하고 일관된 상태로 유지할 수 있습니다.

스레드 안전성을 갖춘 클래스들은 동기화를 통해 데이터 일관성을 유지하며,
멀티스레드 환경에서 안정적인 동작을 보장합니다.


여러 쓰레드가 공유된 데이터에 접근했을 때 동기화가 제대로 되어있지 않다면 의도한 데이터 값과 다른 값이 출력되는 환경은 쓰레드 안전성이 저하된 환경이고, 이 경우 데이터의 정확성,유효성 및 무결성이 보장되지 않는, 즉 데이터 일관성이 손상될 수 있다.

 

그렇다면 StringBuiler 와 StringBuffer의 동기화 여부에 따라 갈리는 차이점에는 어떤 예시가 있을까? 이를 적절하게 설명하기 위해 주위 사람들에게 설명해보려고 했지만 쉽지 않았다. 이 때, 지네릭스에서 와일드 카드에 대한 설명을 해주셨던 분이 매우 알기 쉽게 얘기해주셨다.

 

첫 번째 설명
예를들어 다음과 같은 기능을 하는 메서드가 있다고 가정해봅니다

1.문자열을 검사한다.
2.문자열이 검사조건에 맞다면 메서드의 인자로 입력해준 문자열을 합친 문자열을 반환한다.

이런 기능이 있는 메서드가 있다고 가정하자면
처음에 “hello” 라는 인자를 전달해준 메서드를 실행해서 
문자열의 길이를 검사하는 도중에 다른 메서드가 “world” 라는 인자를 가진 상태로 한번 더 호출이 이뤄진다면, 

동기화가 되어있는 상태에서는 두번째 호출은 첫번째 호출이 끝나기 전에 해당 값에 접근하지 못하고 있다가 
그 호출이 끝나면 다시 1번과정부터 수행해야합니다

하지만 동기화가 되지 않는 상태라면 
첫번째 호출이 끝나서 “world”가 붙은 문자열이 반환되기도 전에 먼저 검사를 진행하기때문에 
첫번째 실행한 메서드의 실행결과가 보장되지 못한다 요렇게 생각하심 될거같이요



이에 대한 나의 질문
예를 들어 ’stringbuilder로 game over라는 문구를 가져오려는 과정에서
쓰레드 안전성이 저하된 상태로 잘못 프로그래밍하면 game over가 아닌 다른 손상된 문구가 출력이 될 수 있지만, 
stringbuffer로 game over를 가져오면 손상된 문구가 출력될 일이 없다‘이런 느낌일까요? 


두 번째 설명
음 그렇다기보다는 가져오는 것은 호출이라는 개념이잖아요. 
사실은 똑같은 값을 호출만 하는것은 손상될 여지가 거의 없어서 적절하지 않은것같고, 
game over를 할때마다 거기에 추가로 누가 game over 되었는지 이름을 보여주도록 설계를 한다고 가정해보겠습니다.

그럼 처음에는
“game over”

이후에 user1이 죽었다면
“game over” + ” user1" ->  “game over user1”

이후에 user2가 죽었다면
“game over user1" + ” user2" -> “game over user1 user2”

이런식으로 기존 문자열에 유저 이름만 더하기 해주는 로직이라고 친다면 문제가 발생할 수 있죠.

user 3과 user4가 동시에 죽어버린다면 차례대로 user3 이 더해지고 user4가 더해지는게 아니라
“game over user1 user2" + ” user3"
“game over user1 user2" + ” user4"
이렇게 로직이 실행되니까
“game over user1 user2 user4” 이런식으로 실행될 우려가 있다는겁니다.

이게 동기화가 보장되지 않는다는거에요
객체의 값에 동시에 접근을 해서 이런 일이 생겼다고 보심 될거같아요!

정말 알기 쉽고 재밌는 설명이었다.

문자열을 추가, 제거하는 기능을 가진 StringBuiler와 StringBuffer의 동기화 여부에 따라 로직은 깔끔해도 출력되는 값은 설계자의 의도와 다르게 출력될 수 있다. 이를 적용해서 생각해보면 StringBuiler는 쓰레드를 하나만 쓰는 싱글쓰레드에 적합하고, StringBuffer는 멀티쓰레드에 적합하다고 할 수 있다. 동기화를 지원하는 Buffer보다 그렇지 않은 Builder가 처리속도가 더 빠르기 때문에 싱글쓰레드가 많은 코딩테스트 문제에서는 StringBuilder를 사용하는 풀이가 많았던 것도 이런 점에서 알 수 있었다.

 

이 처럼 동기화는 프로그래머가 의도한 값을 명확하게 출력하기 위해서 멀티쓰레드를 사용할 때 필수적으로 고려하고 생각해봐야하는 문제다. 다만 동기화를 남발하는 것은 프로그램의 속도와 효율을 해치기 때문에 신중하게 사용할 필요성이 있다.