자바 Thread_5
원자적
- 모든 레퍼런스 할당은 원자적임
- long 이나 double 을 제외하고는 모두 원자적임.
- 예외인 이유는 64비트라서임
64비트 컴퓨터라도 실제로는 CPU가 두 개의 연산을 통해 32비트씩 읽어서 완료할 가능성이 높음
=> volatile 키워드를 선언시 앞에 붙여주면 원자적으로 계산이 가능함.
- 원자적 연산은 멀티스레드 애플리케이션을 쓰는 이유에 해당하는, 많은 작업을 병렬 실행하면서도 정확한 결과값을 도출할 수 있는 고성능 애플리케이션을 구축가능함.
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public static void main(String[] args) {
Mertics mertics = new Mertics();
BusinessLogic businessLogic1 = new BusinessLogic(mertics);
BusinessLogic businessLogic2 = new BusinessLogic(mertics);
MerticsPrint merticsPrint = new MerticsPrint(mertics);
businessLogic1.start();
businessLogic2.start();
merticsPrint.start();
}
public static class Mertics {
private long count = 0;
private volatile double averge = 0.0;
public synchronized void addSample(long sample) {
double currentSum = averge * count;
count++;
averge = (currentSum + sample) / count;
}
public double getAverge() {
return averge;
}
}
public static class MerticsPrint extends Thread{
private Mertics mertics;
public MerticsPrint(Mertics mertics) {
this.mertics = mertics;
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
}
catch (Exception e) {
}
double currentAverage = mertics.getAverge();
System.out.println("currentAverage = " + currentAverage);
}
}
}
public static class BusinessLogic extends Thread {
private Mertics mertics;
private Random random = new Random();
public BusinessLogic(Mertics mertics) {
this.mertics = mertics;
}
@Override
public void run() {
while (true) {
long start = System.currentTimeMillis();
try {
Thread.sleep(random.nextInt(10));
} catch (Exception e) {
}
long end = System.currentTimeMillis();
mertics.addSample(end - start);
}
}
}
경쟁상태
- 공유 리소스에 접근하는 여러스레드가 있거나 그 중 최소한 한 스레드가 공유 리소스를 수정하는 경우로 스레드 스케줄리의 순서나 시점에 따라 결과가 달라지는 상황
공유 리소스에서 비원자적 연산이 실행되는게 문제
- 우선 경쟁 상태를 파악하고 경쟁 상태가 일어나는 임계영역을 동기화 블록을 넣어 보호해야함
데이터 경쟁
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
48
49
50
51
52
53
54
55
56
57
58
public static void main(String[] args) throws Exception {
SharedClass sharedClass = new SharedClass();
ShardTest shardTest = new ShardTest(sharedClass);
ShardTest2 shardTest2 = new ShardTest2(sharedClass);
shardTest.start();
shardTest2.start();
}
public static class ShardTest2 extends Thread{
private SharedClass sharedClass;
public ShardTest2(SharedClass sharedClass) {
this.sharedClass = sharedClass;
}
@Override
public void run() {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sharedClass.checkDataRace();
}
}
}
public static class ShardTest extends Thread{
private SharedClass sharedClass;
public ShardTest(SharedClass sharedClass) {
this.sharedClass = sharedClass;
}
@Override
public void run() {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sharedClass.increment();
}
}
}
public static class SharedClass {
private int x = 0; // volatile으로 해결가능
private int y = 0; // volatile으로 해결가능
public void increment() {
x++;
y++;
}
public void checkDataRace() {
if(y > x) {
System.out.println("y > x 상태가 일어났습니다.");
}
}
}
=> 컴파일러와 CPU가 성능 최적화와 하드웨어 활용을 위해 비순차적으로 명령을 처리하는 경우가 있음 (논리를 위반하진않음)
비순차적 명령어 처리가 없다면 프로그램 속도는 매우 느려짐
- 컴파일러 엔지니어들은 입력코드를 가져와서 재정렬해 CPU의 분기 예측 능력과 백테화 능력을 향상시키고 캐시 라인을 프리페치해서 미리 사용할 수 있도록 하고 다양한 최적화를 함
- 일부 명령이 사용할 수 없는 하드웨어를 필요로 하면 CPU가 명령을 비순차적으로 하지만 다른 명령은 CPU 주기를 낭비하는 대신에 다른 하드웨어에서 실행될 수 있음
1
2
3
4
5
6
7
// 각각의 윗줄에 종속성이 있어 절대 논리가 위반될 수 없음
public void Funct() {
x = 1;
y = x+2;
z = y+10;
}
1
2
3
4
5
6
7
8
9
10
11
12
/* CPU나 컴파일러 관점에서는 해당 메서드들의 논리가 동일함
*/
public void Funct1() {
x++;
y++;
}
public void Funct2() {
y++;
x++;
}
Java는 일반적으로 다른 스레드를 통해 동시에 일어나는 연산에 대해서는 시맨틱 이전의 순서는 보장하지 않지만, 예외인 경우가 몇 가지 있습니다.
순서를 지키는 솔루션
- synchoronized 키워드를 사용해서 동시 실행에 대응하고 읽기, 쓰기, 공유 변수로부터 보호할 수 있음.
- synchoronized가 선언된 메서드를 재정렬해도 문제가 되지않음 하나의 스레드만 접근이 가능하기에 데이터 경쟁은 일어나지않음
- 공유 변수에 volatile 키워드를 적기
- 잠금 오버헤드를 줄이고 처리 순서를 보장함
1 2 3 4 5 6
volatile int shared; public void func() { // 모든 명령은 이전에 실행 메모리 장벽 Read or Write -> shared 변수 // 모든 명령은 이후에 실행 메모리 장벽 }
- 잠금 오버헤드를 줄이고 처리 순서를 보장함
- synchoronized 키워드를 사용해서 동시 실행에 대응하고 읽기, 쓰기, 공유 변수로부터 보호할 수 있음.
- 경쟁 상태
- 데이터 경쟁
=> 두 상황 모두 스레드가 공유 리소스를 사용하거나 스레드가 리소스를 수정하는 경우 (원인은 다름)
- synchoronized, volatile 키워드로 문제를 해결가능함.
- volatile는 변수 타입의 읽기, 쓰기, long + double 타입에 대한 데이터 경쟁을 해결할 수 있음.
- volatile는 순서가 보장되기에 모든 경우의 데이터 경쟁을 해결해줌
- 한 스레드에 의해 수정된 모든 공유 변수는 보호해야함
출처 - https://kmooc.udemy.com/course/java-multi-threading/ (Java 멀티스레딩, 병행성 및 성능 최적화 - 전문가 되기 )
This post is licensed under CC BY 4.0 by the author.