brunch

You can make anything
by writing

C.S.Lewis

by 강관우 May 03. 2020

[읽고서] 자바 고유락과 Synchronization

기술 블로그 리뷰


Java는 크게 3가지 영역의 메모리 영역을 가지고 있습니다.  


static 영역

Java 클래스 파일은 크게 필드(field), 생성자(constructor), 메소드(method)로 구성됩니다. 그중 필드 부분에서 선언된 변수(전역 변수)와 정적 멤버변수(static이 붙은 자료형)은 static 영역에서 관리됩니다.  


stack 영역 

스레드마다 할당받는 영역으로 메소드 내에서 정의하는 기본형 지역 변수와 메소드들의 콜스택을 저장하는 영역입니다. 


heap 영역 

참조형 데이터 타입을 갖는 객체(인스턴스), 배열 등을 저장하는 영역입니다.  


자바 멀티 스레드 환경에서는 스레드들끼리 static 영역과 heap 영역을 공유합니다. 때문에 공유 자원에 대한 동기화 문제를 신경 써야 하는데요, 오늘의 기술 아티클 리뷰는 동기화 문제에 대한 솔루션 중 하나인 Synchronization에 대한 궁금증에서 출발했습니다. 


https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/java-intrinsic-locks.html

logicbig 이라는 개발 튜토리얼을 모아놓은 사이트입니다.


Intrinstic lock과 synchronized 블록 


자바의 모든 객체는 락(lock)을 가지고 있다고 합니다. 모든 객체가 갖고 있으니 고유락(intrinsic lock)이라고 한다는데요, 처음에는 이게 뭐지? 싶었는데. synchronized 블록이 이 고유락을 사용해서 락을 다룬다고 하니 이해가 됐습니다.  


자바의 모든 객체(인스턴스, 클래스)는 락을 가지고 있다. synchronized 블록은 이 락을 다룬다. 

synchronized 블록은 객체 단위로 락을 다룬다.


아래 예제를 통해서 고유락을 다루는 Synchronized를 확인해볼 수 있습니다. 


예제 코드 1


(synchronized 키워드를 무시하고 생각하면) syncMethod 안에 5초 동안 멈추는 코드 때문에 각 스레드의 로그는 before call 이 찍힌 후 5초 뒤 after call이 찍힐 것으로 예상할 수 있습니다.  


output: 


thread1 before call 2020-05-03T17:20:05.493925 

thread2 before call 2020-05-03T17:20:05.493920 

in the sync method from thread1 2020-05-03T17:20:05.494494 

in the sync method from thread2 2020-05-03T17:20:10.498293 

thread1 after call 2020-05-03T17:20:10.498293 

thread2 after call 2020-05-03T17:20:15.500107 



하지만 실제 로그는 처음 syncMethod를 실행한 thread1만 5초 안에 모든 로그를 찍고 thread2는 10초 정도 걸렸네요. synchronized 블록이 잘 동작해서 해당 영역이 특정 스레드에서 실행되는 동안 다른 스레드가 접근하지 못하게 막은 것으로 보이죠? 


하지만 이 예제만으로는 synchonized 블록이 어떤 단위로 락을 가지고 가는지 확인할 수 없습니다.  


예제 코드 2


Output: 


thread1 before call 2020-05-03T17:21:42.514032 

thread2 before call 2020-05-03T17:21:42.513778 

in the syncMethod1 from thread1 2020-05-03T17:21:42.514289 

in the syncMethod2 from thread2 2020-05-03T17:21:47.518675 

thread1 after call 2020-05-03T17:21:47.518668 

thread2 after call 2020-05-03T17:21:52.519938 



반면 이 예제에서는 확실히 알 수 있습니다. thread1, thread2가 서로 다른 메소드를 호출함에도 불구하고 thread1은 5초 만에 끝나고 thread2는 또 10초가 걸렸습니다. 인스턴스 lock을 스레드가 갖고 있기 때문이겠죠?


고유락 재진입 


고유락 재진입이란 특성을 통해서도 이 '객체 단위 Lock'을 다시 한번 확인할 수 있습니다. 특정 스레드가 Synchronized 블록을 통해서 객체에 대한 lock을 획득했다면 해당 객체에 다른 메소드의 lock도 획득합니다. 

예제 코드 3


만약 고유락 재진입 특성이 없다면, a 메서드와 b 메서드가 서로 상호 호출하거나 공유 자원에 대한 선점이 있다면 데드락이 발생할 가능성이 있겠죠!?



static method synchronization 


인스턴스 생성 없이도 클래스의 메소드를 사용할 수 있는 방법이 있습니다. 바로 static인데요, static 메소드에  synchronized 블록이 있다면 class 단위로 락을 겁니다. 이 부분에서 객체는 인스턴스 혹은 클래스를 나타낸다는 걸 인식할 수 있었는데요, 락의 단위가 따로 놀기 때문에 혼용하여 사용한다면 동기화 이슈가 발생할 수 있겠네요..! 


예제 코드 4


Output: 


thread2 before call 2020-05-03T17:14:51.126421 

in the sync method from thread2 2020-05-03T17:14:51.126983 

thread1 before call 2020-05-03T17:14:51.126665 

in the sync method from thread1 2020-05-03T17:14:51.127132 

thread2 after call 2020-05-03T17:14:56.130658 

thread1 after call 2020-05-03T17:14:56.130677 



TMI

Lock의 단위는 멀티스레드 환경의 성능 측면에서 굉장히 중요한 이슈인데요. 단순히 메소드 단위로 Synchronized 블록을 적용시켰을 때 해당 객체 전체에 락이 걸린다는 이 개념은 HashTable과 같은 레거시 콜렉션 프레임워크에서 이슈가 됐고 ConcorrentHashMap에서 개선됐습니다.


HashTable의 Put 메소드 일부, 메소드 전체에 synchronized 블록이 걸려있다. 멀티 스레드 환경에서 put이 실행될 때 해시 충돌이 발생하면 해시 충돌을 해결하기 위한 코드가 성능 이슈를 발생시킨다.


ConcurrentHashMap의 put 메소드는 해쉬 충돌이 발생했을 때 해시 충돌 발생 노드에 대해서만 lock을 획득한다. 다른 노드들에 접근하는 스레드들은 blocking이 발생하지 않으므로 성능 이슈가 개선된다.


브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari