life is egg
Java의 synchronized 본문
Note
- 스프링 복습하다가 스프링 컨테이너에서 싱글톤이 나왔는데 그와 관련해서 공부하다가 나온 synchronized 정리할겸 ..
스레드 안정성이 보장 안됨
package M11;
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
class RaceConditionExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("Final count: " + counter.getCount());
}
}
실행시 결과를 보면 예상했던 2000이 나올 수도 있고 안나올 수 도있다 ..
만약에 안나왔다면 이건 경쟁상태에 대해서 동시에 처리가 되서 그런건데 ..
단순한 count++ 지만 실제로 메소드가 호출된 후 동작은
1. count 값을 읽음 (load)
2. 읽은 값에 1을더함
3. 더한 값을 count에 저장(store)
이렇게 3가지 단계로 나누어져 실행 되기 때문에 thread1 , thread2 가 동시에 increment 메소드를 호출할때 각 스레드는 count 값ㅇ르 읽어서 1일 더한뒤 다시 저장하려 고하기때문에
이러한 과정에서 한 스레드가 아직 값을 저장하기 전에 다른 스레드가 값을 읽으면, 두 스레드는 모두 같은 원래값을 기반으로 연산을 수행하기 때문에 동시성이 보장이 안된다 ..
이러한 경우 해결 하기 위한 방법중에 하나가 synchronized 키워드를 붙여주는 것인데 ..
public synchronized void increment() {
count++;
}
synchronized 키워드는 한 번에 하나의 스레드만 increment 메소드에 접근 하도록 보장해준다 . ( 특정 코드 블럭에도 사용가능)
* synchronized 이외에 단순 연산의 경우 Atomic을 이용하거나 ReentrantLock 기능을 이용해 세밀한 제어가가능
그러면 어떻게 synchronized 를 명시하면 원자성을 보장해주게 할까 ?
synchronized는 내부적으로 lock을 사용하는데 그 중에서 Monitor Lock을 구현해 사용
- JAVA 코드 레벨에서 알아보자면
- synchronized 메서드나 블록은 해당 객체의 모니터 락(Monitor Lock)을 획득
- 락을 획득한 스레드만 동기화된 코드에 접근
- 락을 해제하면 다른 대기 중인 스레드가 락을 획득하고 실행
- JVM 레벨에서 보자면 ..
- 모니터(Monitor)의 개념
- 모든 Java 객체는 Monitor를 가지고 있음..
- Monitor는 헤더 영역에 있는 메타데이터로 관리. 객체가 락상태인지.. 누가 락을 소유하고 있는지 관리 ..
- 객체 헤더(Object Header) : Mark Word , Class Pointer 로 구성 Array Length (optional)
- Mark Word에 객체의 lock 상태 및 해쉬코드 , GC정보등을 저장함..!.. 나중에 좀 더 공부해보자 ...!
- Unlocked 상태 : 객체에 락이 걸려있지 않음
- Biased 상태 : 특정 스레드가 락을 소유함
- Lightweight 상태 : CAS(Compare-And-Swap) 기반의 경량 락 , 여러 스레드 경쟁시 Heavyweight 로 승격 ( CAS 실패시)
- Heavyweight 상태 : OS 수준의 모니터락 -> Wait Queue ..
- synchronized 실행과정
- 모니터 락 획득 시도 :
- 스레드는 먼저 객체의 Mark Word 를 확인하여 락 상태를 확인
- 락이 비어 있다면 스레드가 Mark Word 를 갱신하여 락을 획득 (CAS 연산 사용 -> 원자성을 보장해주기 위해 성공한 스레드는 Lightweight Lock 상태로 됨 .. !)
- javap -c -v Counter.class 입력해서 바이트 코드를 봤을때 ..
- CAS(Compare-And-Swap)
- 현재 값(Mark Word) 읽기 - > 기대하는 값인지 확인 -> 새값 교체
-
더보기CAS(Compare-And-Swap) 은 하드웨어 수준의 원자적 연산!
메모리 값을 조건 부로 변경 CAS는 다음 3가지 입력 값을 요구
1. 메모리주소 ( 업데이트 하려는 값이 저장된 메모리 주소)
2. 기대값( 현재 값이 우리가 예상한 값과 일치하는지 확인)
3. 새값 ( 예상 값이 일치하면 메모리에 설정할 새로운 값 )
CAS 연산은 한 번에 수행되며, 다른 스레드가 중간에 개입하지 못함 .. !
<---- 원자성이 보장되는 이유 --->
하드웨어 수준에서 실행되고, CPU의 특별한 명령어를 사용하며, 중간에 인터럽트되거나 다른 스레드가 개입할수없음 ..
CPU 명령어 중 CMPXCHG 또는 그와 유사한 명령어로 구현됨.. 이명령어는 다음을 보장
1. 읽기 - 비교 - 교체 가 단일 명령어로 처리
2. 다른 스레드가 같은 메모리 주소를 동시에 접근하더라도, 하드웨어가 이를 직렬화(Serialize)함
메모리 베리어 (Memory Barrier)
CAS 연산은 메모리 베리어를 포함.. 메모리 베리어는 CPU 와 메모리간의 순서를 보장하며
1. CAS 연산 이전의 모든 연산이 먼저 완료됨을 보장
2. CAS 연산 이후의 모든 연산이 나중에 실행됨을 보장 ..
캐시 일관성 프로토콜
CAS 연산이 여러 CPU 코어 환경에서도 일관성을 유지하는 이유는 .. CPU의 캐시 일관성 프로토콜 덕분 .. CAS는 메모리에 직접 쓰기작업을 수행하며, 모든 코어의 캐시를 동기화함 ..
-
- CAS가 성공하면 스레드가 락을 소유 , 실패하면 Heavyweight Lock 으로 승격 ..
- 여러 스레드가 동시에 락을 획득하려고 경쟁한다면 JVM은 Heavyweight Lock 상태로 전환..
- Heavyweight Lock 은 OS의 커널 수준에서 구현되며.. 모니터는 스레드를 대기 큐로 이동시켜 대기하도록 관리 ..
- 현재 값(Mark Word) 읽기 - > 기대하는 값인지 확인 -> 새값 교체
- 락 해제
- 작업이 완료되면 락을 해제, Mark Word를 초기 상태로 되돌림.. -> 대기중인 스레드 중 하나가 락을 획득 ..
- 모니터 락 획득 시도 :
- 바이트 코드에서 살펴볼시 ...
javap -v -c 를 이용해서 바이트코드를 보면
메소드에 synchronzied 선언시..
어피치카 바라보는 방향에 ACC_SYNCHRONIZED 플래그가 설정 된걸 볼 수있음 .. !
이플래그는 JVM에게 해당 메소드가 동기화 되어야 한다는 정보를 전달해서, JVM이 런타임에 자동으로 moniterenter 와 monitorexit를 삽입해줌 ..
그렇다면 블록수준에 synchronzied를 사용하면 명시적으로 바이트 코드 수준에서도 moniterenter 와 monitorexit를 볼 수 있다는데 ..
monitorexite 가 21번에 한번 더 명시된건 예외발생시에도 락을 해제 해야하기 때문인데 나는 예외발생을 따로 명시 안했지만 ..
JVM이 아래와 같은 형태로 변환해서 처리하기 때문이다~!
결론
synchronized는 스레드 안정성을 보장하기 위해 Java에서 제공하는 중요한 도구다. 내부적으로 Monitor Lock과 CAS 연산을 통해 원자성을 보장하고, 데이터의 무결성을 유지하며 동시성 문제를 해결할 수 있다.
하지만 동기화는 성능 저하를 동반할 수 있는 만큼, 무조건적으로 사용하는 것은 지양해야 한다. 특히 여러 스레드가 경쟁 상황에 놓여 Heavyweight Lock으로 전환될 경우 OS 커널 수준의 관리가 추가되면서 성능이 급격히 저하될 수 있다.
그래서 상황에 따라 아래와 같은 대안을 고려할 수도 있다:
- 간단한 카운터 증가나 값 변경에는 AtomicInteger 같은 Atomic 클래스 사용.
- 세밀한 동기화 제어가 필요할 땐 ReentrantLock 사용.
'개인공부 > JAVA' 카테고리의 다른 글
[JAVA] G..C (2) | 2023.10.19 |
---|---|
[JAVA] 배열의 최대 크기 (0) | 2023.07.30 |
[JAVA] sort... 그 무언가.. (0) | 2023.06.19 |