Intrinsic : belongs naturally, essential
이전의 Thread 포스트를 통해 자바에서의 스레드 동기화가 왜 필요한지, 또 어떻게 수행하는지에 대해 알아보았다.
자바의 멀티스레딩에서는 여러 스레드가 하나의 공유자원에 접근하여 작업을 수행하게 된다.
스레드 간에 작업을 통해 발생하는 변경사항이 공유자원에 반영되지 않은 채로 작업이 반복되면 데이터의 일관성을 해치고 스레드 간 작업을 통해 원하는 결과와는 다른 결과가 도출되게 된다.
이를 막기 위해서 존재하는 것이 스레드 동기화이다.
Intrinsic Lock
동기화를 위한 자바의 lock의 종류 중 하나인 Intrinsic Lock에 대해 알아보자.
모든 자바 객체는 lock을 지니며, 각 객체가 지니는 lock은 고유 락(intrinsic lock), 모니터, 모니터락 등 다양한 이름으로 부른다. 동기화를 위한 synchronized 블록이 실행되면 블럭을 실행한 스레드는 고유락을 소유하게 되고, 해당 스레드의 작업이 종료되거나 에러가 발생할 경우 lock 소유가 끝난다.
아래의 예제를 보자. 두 개의 서로 다른 스레드에서 synchronized 메서드를 수행하는 코드이다.
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
31
32
33
34
35
36
|
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
class ThreadTest {
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
Thread t1 = new Thread(() -> {
System.out.println("t1 ready to call: " + LocalDateTime.now());
threadTest.syncTest("t1 call time: ");
System.out.println("t1 after call: " + LocalDateTime.now());
});
Thread t2 = new Thread(() -> {
System.out.println("t2 ready to call: " + LocalDateTime.now());
threadTest.syncTest("t2 call time: ");
System.out.println("t2 after call: " + LocalDateTime.now());
});
t1.start();
t2.start();
}
private synchronized void syncTest(String msg) {
System.out.println(msg + LocalDateTime.now());
try { // 실행 후 5초간 sleep
TimeUnit.SECONDS.sleep(5);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
|
cs |
위의 코드를 실행시켰을 때 결과는 아래와 같다.
t1의 경우
16:08:21(준비 완료) ->
16:08:21(시작) ->
16:08:26(종료)으로 총 5초가 소요되었다.
t2의 경우
16:08:21(준비 완료) ->
16:08:26(시작) ->
16:08:31(종료)로 총 10초가 소요되었다.
즉 작업 과정은 t1&t2 준비완료 -> t1 시작 -> t1 종료 -> t2 시작 -> t2 종료 순으로 일어난 것을 확인할 수 있다.
t1 스레드가 준비 완료 시점으로부터 작업을 끝내기까지 5초가 걸렸고, t2는 10초가 걸렸다.
두 스레드가 서로 같은 synchronize 블록 syncTest를 수행할 경우, 먼저 작업을 수행한 t1이 고유락을 소유하여 t2는 고유락 점유가 끝나는 시점인 5초후에 작업을 수행할 수 있게 된다.
그렇다면 하나의 객체 내에서 서로 다른 synchronize 블록을 수행하면 어떻게 될까?
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
class ThreadTest {
public static void main(String[] args) {
ThreadTest threadTest = new ThreadTest();
Thread t1 = new Thread(() -> {
System.out.println("t1 ready to call: " + LocalDateTime.now());
threadTest.syncTest1("t1 call time: ");
System.out.println("t1 after call: " + LocalDateTime.now());
});
Thread t2 = new Thread(() -> {
System.out.println("t2 ready to call: " + LocalDateTime.now());
threadTest.syncTest2("t2 call time: ");
System.out.println("t2 after call: " + LocalDateTime.now());
});
t1.start();
t2.start();
}
private synchronized void syncTest1(String msg) {
System.out.println(msg + LocalDateTime.now());
try { // 실행 후 5초간 lock 점유
TimeUnit.SECONDS.sleep(5);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void syncTest2(String msg) {
System.out.println(msg + LocalDateTime.now());
try { // 실행 후 5초간 lock 점유
TimeUnit.SECONDS.sleep(5);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
|
cs |
두 스레드가 하나의 메서드를 동작시켰을 때와의 결과와 같은 것을 알 수 있다. 왜 그런 것일까?
t1의 경우
16:17:20(준비 완료) ->
16:17:20(시작) ->
16:17:25(종료)로 총 5초가 소요되었다.
t2의 경우
16:17:20(준비 완료) ->
16:17:25(시작) ->
16:17:30(종료)로 총 10초가 소요되었다.
즉 두 개 이상의 스레드가 객체 내에서 같은 synchronize 블럭을 호출하든 서로 다른 synchronize 블럭을 수행하든 결과는 같다는 것을 확인할 수 있다. 서로 다른 synchronize 블럭을 호출했는데 왜 작업이 끝날 때까지 기다려야 하는 걸까?
고유락 개수!= synchronize 블럭 개수의 개념이며 고유락은 객체 당 하나만 존재하기 때문이다. 따라서 어떤 synchronize 블럭을 호출하던 간에, 호출한 스레드는 고유락을 점유하게 되고 점유가 끝날 때까지 다른 스레드의 작업이 불가능한 것이다.
Reentrancy
고유락 재진입을 통해서도 객체 단위의 lock 동작을 확인할 수 있다.
고유락 재진입이란 이미 스레드가 획득한 잠금을 다시 획득할 수 있는 것을 의미한다. 즉, 동일한 스레드가 이미 보유한 잠금을 다시 획득하는 것이다.
재귀적 함수 호출 혹은 아래와 같이 synchronize 블럭 내부에서 synchronize 블럭에 진입할 경우, 고유락 재진입이 이루어진다.
아래 예시의 경우 객체 내에서 고유락을 얻은 후에 다른 synchronize 블록에 재진입하더라도 이미 객체 내의 프리패스와 같은 고유락을 얻은 상태이기 때문에 별다른 문제없이 진입이 가능한 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class Reentrancy {
public static void main(String[] args) {
Reentrancy test = new Reentrancy();
test.syncTest1();
}
// syncTest1이 실행되는 시점에서 Reentrancy 객체의 lock 획득
private synchronized void syncTest1() {
System.out.println("syncTest1 completed");
syncTest2();
}
// 이미 lock을 획득한 상태이기 때문에 수행 가능
private synchronized void syncTest2() {
System.out.println("syncTest2 completed");
}
}
|
cs |
코드를 실행한 결과는 아래와 같다.
Visibility
멀티스레딩 환경에서 또 고려해야 할 한 가지 문제가 있는데, 바로 가시성(Visibility)이다.
당연히 멀티 태스킹 환경에서 데이터를 write하는 것뿐만 아니라 read를 했을 때도 올바른 데이터를 가져올 수 있어야 한다.
가시성은 여러 스레드가 동시에 작동할 때, A 스레드에 의해 변경된 값을 B 스레드에서의 확인 가능 여부이다.
앞서 확인한 Structured Lock(Intrinsic Lock)과 ReentrantLock 모두 가시성을 보장한다.
References
http://happinessoncode.com/2017/10/04/java-intrinsic-lock/
Java의 고유 락(intrinsic lock)에 대해
고유 락과 synchronized 블록자바의 모든 객체는 락(lock)을 갖고 있다. 모든 객체가 갖고 있으니 고유 락(intrinsic lock)이라고도 하고, 모니터처럼 동작한다고 하여 모니터 락(monitor lock) 혹은 그냥 모니
happinessoncode.com
[읽고서] 자바 고유락과 Synchronization
기술 블로그 리뷰 | Java는 크게 3가지 영역의 메모리 영역을 가지고 있습니다. static 영역 Java 클래스 파일은 크게 필드(field), 생성자(constructor), 메소드(method)로 구성됩니다. 그중 필드 부분에서 선
brunch.co.kr
https://gyoogle.dev/blog/computer-language/Java/Intrinsic%20Lock.html
[Java] 고유 락 (Intrinsic Lock) | 👨🏻💻 Tech Interview
[Java] 고유 락 (Intrinsic Lock) Intrinsic Lock / Synchronized Block / Reentrancy Intrinsic Lock (= monitor lock = monitor) : Java의 모든 객체는 lock을 갖고 있음. Synchronized 블록은 Intrinsic Lock을 이용해서, Thread의 접근을 제
gyoogle.dev
'Java' 카테고리의 다른 글
[Java] Reflection (0) | 2024.04.21 |
---|---|
[Java] Composition(컴포지션) (0) | 2024.04.09 |
[Java] Object 클래스 (0) | 2024.04.07 |
[Java] Thread(스레드) (1) | 2024.04.07 |
[Java] 가비지 컬렉션(Garbage Collection) (0) | 2024.04.06 |