자바에서 동기화란 여러 스레드가 하나의 자원을 동시에 접근할 때 데이터의 일관성을 유지하고 충돌을 방지하는 기술을 말한다. 즉, 한번에 한 스레드만 특정 코드를 실행할 수 있게 하는 것이라 할 수 있다.
여러 스레드가 데이터를 동시에 수정하려고하면 데이터가 꼬이는 경합 상태 (Race Condition)가 발생한다.
예를 들어, 통장 잔고가 1,000원인데 두 스레드가 동시에 1,000원을 출금하려고 하면, 운이 나쁠 경우 둘 다 출금에 성공하여 잔고가 -1,000원이 되는 상황이 생길 수 있다.
원자성(Atomicity): 공유 자원에 대한 작업이 "더 이상 쪼개질 수 없는 하나의 단위"로 실행됨을 보장한다.
가시성(Visibility): 한 스레드가 변경한 값이 다른 스레드에게 즉시 보이도록 보장한다.
순서성(Ordering): 코드의 실행 순서가 뒤바뀌지 않도록 보장한다.
자바에서는 기본적으로 synchronized 키워드를 통해 객체의 모니터 락을 획득하여 임계 구역을 보호하며, 이를 통해 원자성과 가시성을 보장합니다.
하지만 락 오버헤드가 발생할 수 있으므로, 상황에 따라 volatile, Atomic 클래스 또는 ReentrantLock 등을 적절히 선택하여 성능과 안전성 사이의 균형을 맞추는 것이 중요gkek.
자바에서는 synchronized 키워드를 통해 모니터락을 획득하고 자원을 보호한다. synchronized 는 메서드나 특정 코드 블록에 적용할 수 있으며, 해당 부분의 코드가 한번에 하나의 스레드에서만 실행할 수 있도록 한다.
모니터락 (Monitor)
OS가 아닌 프로그래밍 언어 수준에서 제공하는 동기화 메커니즘이다. 자바의 모든 객체는 각자 고유한 모니터를 가지며, 이를 획득하고 해제하면서 동기화 작업을 수행한다.
synchronized와 static synchronized의 가장 큰 차이는 락을 거는 대상에 있다.
일반 synchronized 메서드는 호출된 인스턴스(객체) 단위로 락이 걸리므로, 서로 다른 객체 간에는 동기화가 적용되지 않는다.
반면 static synchronized는 Class 타입의 객체 단위로 락이 걸린다. 따라서 해당 클래스의 인스턴스가 아무리 많더라도, 모든 인스턴스가 하나의 락을 공유하게 되어 클래스 전체에서 단 하나의 스레드만 해당 메서드에 진입할 수 있다.
실무에서는 인스턴스 변수를 보호할 때는 일반 방식을, 클래스(정적) 변수를 보호할 때는 static 방식을 선택하여 사용한다.
그러면, static synchronized 와 synchronized 는 모니터락이 공유될까?
일단 생각해보면, Class 인스턴스와 직접 생성한 인스턴스는 서로다른 인스턴스다. 따라서 Lock 은 별도로 동작할 것으로 예상할 수 있다.
synchronized 키워드로 가시성 문제도 해결할 수 있을까?
멀티스레드 환경에서는 한 스레드가 변수 값을 변경했을 때 CPU 캐시와 메인 메모리의 불일치로 다른 스레드가 읽었을 때 변경된 값이 아닌 기존 값을 읽을 수도 있다. 이를 가시성 문제라고 한다. JVM 은 happens-before 관계를 보장해 메모리 가시성 문제가 발생하지 않도록 한다. 즉, 한 스레드가 모니터락을 해제하면, 이후 해당 락을 획득한 스레드는 변경된 데이터를 즉시 볼 수 있다.
"synchronized 블록을 사용하면, unlock 동작이 이후 lock 동작보다 먼저 발생하므로, unlock 이전의 변경 사항이 lock 이후의 스레드에 보인다" 로 이해했다.
wait()을 통해 BLOCKED인 스레드를 WAITING으로 변경하고 해당 스레드를 인터럽트 시킬 수는 있다.synchronized 메커니즘은 Java에서 여러 쓰레드가 공유 객체에 접근할 때 동기화를 보장하기 위한 탄생한 최초의 메커니즘이다. 간단한 동기화 처리에는 유용하지만, 위에서처럼 문제점이 존재하기 때문에 Java 5부터는 보다 세밀한 동시성 제어를 지원하는 다른 동시성 유틸리티 클래스들이 도입되었다.

BLOCKED & WAITING은 모두 스레드가 대기하며 CPU 실행 스케줄링에 들어가지 않기 때문에 CPU에서 보면 실행하지 않는 비슷한 상태 이지만 큰 차이가 있다.
synchronized 키워드는 동기화를 제공하지만, 무한 대기와 비공정성이라는 단점이 있다.
이러한 문제를 해결하기 위해 Java 5부터 ReentrantLock이 도입되었다. ReentrantLock은 LockSupport를 활용하여 위 단점을 극복한다.
1. 무한 대기 극복
LockSupport는 concurrent 패키지에 위치하며, 저수준의 객체입니다. LockSupport는 스레드가 락을 획득하려고 대기할 때, 스레드의 상태를 BLOCKED 대신 WAITING으로 변경해서 인터럽트를 허용하게 해줍니다. 이를 통해 synchronized의 무한 대기 문제를 간단하게 해결합니다.
2. 공정성 극복
ReentrantLock은 공정 모드와 비공정 모드를 제공하며, 필요에 따라 선택할 수 있습니다.
2-1. 공정 모드 (Fair Mode)
2-2. 비공정 모드 (Non-Fair Mode)
3. 모니터락을 사용하지 않음
AQS(AbstractQueuedSynchronizer)라는 별도의 프레임워크를 기반으로 동작한다. 이 차이가 synchronized와 ReentrantLock을 가르는 결정적인 지점이다.
이처럼 모니터 락이라는 JVM의 제약에서 벗어나 별도의 로직으로 구현되었기 때문에, 공정성 설정이나 타임아웃 같은 synchronized가 제공하지 못하는 고급 기능을 제공할 수 있는 것이다.
동기화는 멀티 스레드 환경에서 여러 스레드가 하나의 자원에 접근하려 할 때 데이터가 충돌되지 않고 일관성을 유지할 수 있도록 하는 기술입니다. 자바에서는 기본적으로
synchronized키워드로 모니터락을 획득하고 자원을 보호합니다. 이synchronized는 메서드나 특정 코드 블록에 적용할 수 있고 해당 부분의 코드가 하나의 스레드에서만 실행될 수 있도록 합니다. 이 키워드를 사용하면 데이터 정합성을 보호하고 한 스레드에서 데이터를 수정하면 다른 스레드에서도 볼수 있는 가시성을 보장합니다. 하지만 과도하게 사용하면 성능 병목이나 (두 스레드가 서로가 가진 락을 대기하며 무한히 멈추는 현상인 )데드락을 유발할 수 있으므로 사용에 주의해야합니다. 따라서 상황에 따라 ReentrantLock을 사용할 수 있습니다. ReentrantLock은 모니터락이 아닌 AQS라는 동기화 프레임워크 기반으로 동작하기 때문에 synchronized 보다 고급 기능을 제공합니다. 스레드가 락을 획득하려고 할때 BLOCKED가 아닌 WAITING 상태로 변경하여 인터럽트를 허용합니다. 이를 통해 synchronized의 무한대기 문제를 해결합니다. ReentrantLock를 공정모드, 비공정 모드를 제공하여 상황에 따라 스레드 락 획득의 순서를 보장할수 도 있습니다.
비관적 락은 데이터를 수정하기 전에 해당 데이터에 대한 접근을 미리 제한하는 방식입니다. 왜냐하면 이 방식은 동시에 데이터를 수정하려는 여러 요청 사이의 충돌을 방지하기 위함이기 때문입니다.
낙관적 락은 데이터를 수정할 때 충돌이 발생하지 않을 것이라고 '낙관적'으로 가정하는 방식입니다. 왜냐하면 이 방식은 충돌이 발생할 확률이 낮다고 판단될 때 유용하기 때문입니다.낙관적 락은 데이터를 수정하는 과정에서 버전 번호나 타임스탬프를 사용하여 데이터의 변경 여부를 확인합니다. 만약 수정 과정에서 데이터가 변경되었다면, 충돌이 발생했다고 판단하고, 해당 트랜잭션을 롤백합니다.
낙관적 락의 장점은 동시성을 높이고 시스템의 성능에 덜 부정적인 영향을 미친다는 것입니다. 왜냐하면 데이터에 대한 접근을 미리 제한하지 않기 때문입니다.