티스토리 뷰
자바는 동시성 문제를 위해 4가지 해결책을 갖고 있다.
- Syncronized 키워드
- Concurrency 라이브러리
- Atomic
- volatile
우리는 그 해결책인 4가지 키워드를 잡고 학습해볼 것이다.
Synchronized
자바는 mult-thread 를 지원하는 언어이기 때문에 여러 thread
가 자원을 공유하는 경우 RaceCondition이 발생하고 데이터 일관성을 지킬 수 없게되는 위험 요소가 존재합니다. 그렇기 때문에 항상 동기화를 통해 임계영역에서 안전하게 작업하는 동안 Lock을 걸어 문제를 줄여야 하는데
이 때 사용하는 키워드가 Synchronized
이다. synchronized 키워드는 공유 자원에 대한 동시접근을 막아준다.
코드는 다음과 같이 사용하면 된다.
- 블록에 거는 방법
public class Hello implements Runnable {
@Override
public void run() {
String str = "hello";
synchronized (str) {
System.out.println(str);
}
}
public static void main(String[] args) {
Hello hello = new Hello();
hello.run();
}
}
첫 번째는 Sychronized 블럭을 이용하는 방법이다. 해당 블록에 스레드가 접근하면 블록 내부가 임계 영역이 되고 그 순간 Lock을 걸어두어 다른 스레드의 접근을 막습니다. 그리고 풀릴 때 까지 다른 스레드는 기다려야 한다.
- 메서드에 거는 방법
public class Hello implements Runnable {
@Override
public synchronized void run() {
String str = "hello"; System.out.println(str);
}
public static void main(String[] args) {
Hello hello = new Hello(); hello.run();
}
}
두 번째 방법은 메서드에 직접 synchronized를 거는 것이다. 이 경우 블록 내부가 임계 영역이 된다.메서드에 들어오는 순간부터 Lock이 걸려 해당 스레드가 메서드에 접근할 수 없다.
Lock의 범위
두 방법은 모두 동시성 문제에서 벗어날 수 있게 Lock을 걸어 접근을 제한하는데, 이 때 범위의 차이점을 알고 있으면 좋다.
- 메서드에 synchronized 키워드를 추가하면 그 함수가 포함된 객체(this)에 잠금(Lock)이 걸린다.
- 블록에 추가한다면 괄호안에 넣는 객체가 Lock의 대상이 된다.
즉, synchronized 키워드가 메서드에 붙는다면 해당 메서드가 있는 객체 자체가 Lock이 걸린다. 그렇기에 this 키워드를 사용할 수 도 있고 다른 객체를 잠금 객체로 사용할 수 있지만, 이런 경우 동기화 작업이 제대로 되지 않을 수 있기 때문에 락 객체는 하나만 사용하는 것이 좋다.
블록을 사용한다면 블록 내부에 접근하기 전까지는 여러 스레드가 해당 객체와 메서드 까지는 동시에 접근이 가능하다. 하지만 synchronized 블록에는 모든 스레드는 동작을 멈추고 자신의 차례까지 대기한다.
Atomic 클래스
저번에 Wrapper 클래스를 설명할 때 다뤘던 내용 중 왜 Wrapper 클래스는 불변인데 동시성 문제를 해결해주는 Atomic 클래스가 있을까?
답은 간단 했다. 바이트 코드에서 봤다 싶이 연산 시 .getValue()로 기본형으로 바꾸고 다시 Wrapper 클래스로 감싸주기 때문에 여기서 동시성 문제가 생기기 쉽다. 그렇기 때문에 등장한 것이 Atomic 클래스이다. 어떻게 해결할 지 알아보자
public class SharedState {
@Test
void sharedState() {
final ExecutorService executorService = Executors.newCachedThreadPool();
final AtomicCounter counter = new AtomicCounter();
executorService.execute(new CounterSetter(counter));
final int value = counter.getNumber().incrementAndGet();
assertEquals(1, value);
}
static class CounterSetter implements Runnable {
private final AtomicCounter counter;
public CounterSetter(AtomicCounter counter) {
this.counter = counter;
}
@Override
public void run() {
while (true) {
counter.getNumber().set(0);
}
}
}
}
class AtomicCounter {
private final AtomicInteger number = new AtomicInteger(0);
public AtomicInteger getNumber() {
return number;
}
}
위 코드를 실행했을 때 increaseAndGet 메서드를 통해 증감 연산자 처럼 동작하게 할 수 있는데 이 때 ++가 컴파일 됨을 인식하면 바이트 코드는 메모리에 저장된 x값을 가져오고 1을 더하고 x를 메모리에 저장하는 세 가지 연산을 한다.
이 때 Atomic은 원자성을 보장하여 동기화 문제없이 동시성 문제를 해결할 수 있다.
Atomic의 경우 CAS(Compare And Swap) 알고리즘 덕분에 NonBlocking이 가능하다. 멀티스레드 환경에서는 CPU 메인 메모리에서 변수 값을 참조하지 않고 캐시영역에서 참조하는데 이 때, 메인 메모리에 저장된 값과 CPU 캐시에 저장된 값이 다른 경우 사용 되는 것이 CAS 알고리즘이다. 현재 스레드에 저장된 값과 메인 메모리 값을 비교하여
- 일치하는 경우 새로운 값으로 교체
- 일치하지 않는다면 실패 후 재시도
해당 방법을 통해 CPU 캐시에 잘못된 값을 참조하는 가시성 문제를 해결할 수 있다.
내부는 너무 복잡하기 때문에 따로 공유는 못할 것 같다... 확실한건 내부에 compareAndSet 메소드가 native로 구현되어있고 이 CAS 알고리즘을 호출하여 그 결과 값이 성공일 때 까지 while을 통해 무한 루프를 돈다. 이 후 찾게 되면 메모리에 저장된 값과 CPU에 캐시된 expect 값을 비교해 동일한 경우에만 update가 실행된다.
volatile
AtomicInteger 내부를 보면 volatile이라는 키워드를 볼 수 있다. 이게 무엇일까?
멀티 코어 프로세서는 각각의 코어가 별도의 캐시를 가지고 있다. 그리고 각각의 코어는 메모리에서 읽어온 값을 캐시에 저장하고 읽어서 작업을 수행하는데 같은 값을 다시 읽을때는 먼저 캐시를 확인 후 없을 때만 메모리에서 읽어온다.
그러다보니 저장된 값과 메모리에 저장된 값이 불일치 하는 경우가 발생한다. 그래서 나온 키워드가 volatile이다.
하지만 volatile 키워드는 변수의 작업을 원자화 하는 것일 뿐 동기화하는 것이 아니니 synchronized를 대체할 수 있는 것은 아니다.
package atomicTest;
public class VolatileEx {
volatile long cnt;
synchronized long getCount() {
return cnt;
}
synchronized void withdraw(int num) {
if (cnt >= num) {
cnt -= num;
}
}
}
위 코드는 volatile이 가시성을 보장해주기 때문에 getCount()에 synchronized가 필요 없을 것 처럼 보이지만 동기화를 해주지 않으면 특정 thread에서 withdraw()가 호출되어 lock이 걸리고 로직이 처리되는 중에도 getCount가 호출이 가능해진다.
물론 synchronized를 사용하면 동시성을 제어할 수 있다. thread가 블럭으로 들어가고 나올 때 캐시와 메모리간 동기화가 이뤄지면서 값 불일치가 해소되기 때문이다.
Concurrent는 다음 챕터에서 자세히 알아보도록하자.
Reference.
https://catsbi.oopy.io/2dbebae1-788e-4e64-98bd-0ecb97441e84
[Volatile
개요
catsbi.oopy.io](https://catsbi.oopy.io/2dbebae1-788e-4e64-98bd-0ecb97441e84)
https://catsbi.oopy.io/0e1bbfe4-9ffc-423c-aea0-01b0daabe9ae
[synchronized
한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 쓰레드의 동기화(synchronization)이라 한다.
catsbi.oopy.io](https://catsbi.oopy.io/0e1bbfe4-9ffc-423c-aea0-01b0daabe9ae)
'Java' 카테고리의 다른 글
Reflection API (리플렉션) (2) | 2022.12.06 |
---|---|
<자바 고급스터디 7주차> 직렬화 역직렬화 (0) | 2022.03.23 |
<자바 고급스터디 5주차 - 2부> 자바는 Synchronized / Blocking 으로 동작한다? (0) | 2022.03.08 |
<자바 고급스터디 5주차 - 1부> 자바는 어떻게 Lock을 걸까? (0) | 2022.03.08 |
<자바 고급 스터디 3주차 - 4부> Enum 타입 2번째 (0) | 2022.02.26 |
- Total
- Today
- Yesterday
- IT
- 면접 준비
- 프로그래밍
- Redis
- MySQL
- 취업
- 게시판
- 개발
- 프로젝트
- 면접
- 자바
- Kotlin
- 코딩
- CS
- 취업준비
- swarm
- JPA
- 코드
- java
- 백엔드
- Spring
- DB
- 개발자
- DevOps
- 인터뷰
- 면접준비
- thread
- 동시성
- 취준
- docker
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |