JAVA - 동기화

이전 포스팅에서 쓰레드의 메모리 구성에 대해 알아봤다.

실제 쓰레드 프로그래밍에서는 하나의 인스턴스에 둘 이상의

쓰레드가 접근하는 형태의 구현을 많이 볼수 있다. 하지만

이러한 경우에 동기화를 해주지 않으면 문제가 발생한다.

우선 어떤 문제가 발생되는지 알아보자.


예를 들어 변수A에 100이 저장 되어있고,

저장된 값을 1씩 증가시키는 연산을

두 개의 쓰레드가 한다고 하자.


이 상황에서 쓰레드1이 변수 A에 저장된 값을101로

증가시켜놓은 다음 쓰레드2가 변수 A에 접근을하면

예상되듯 변수 A에는 101이 저장된다.


그런데 여기서 중요한것은 값의 증가 방식이다.

값의 증가는 CPU를 통해 연산이 필요한 작업이므로

그냥 변수A에 저장된 값이 변수 A에 저장된 상태로 증가하지 않는다.

이 변수 A에 저장된 값은 쓰레드1에 의해 참조된다. 그리고 쓰레드1은

이 값을 CPU에 전달해서 1이 증가된 값 101을 얻는다.

마지막으로 연산이 완료된 값을 변수 A에 다시 저장한다.

이렇게 해서 변수 A에는 101이 저장되는 것이다.


쓰레드2도 1을 증가시키려면 위와 같은 방식으로

변수 A에 102가 저장될 것이다.

하지만 쓰레드1이 변수 A에 저장된 값을 성공적으로 증가시키기

전이라도 얼마든지 쓰레드2로 CPU의 실행이 넘어갈 수 있다.


그럼 처음부터 다시 생각해보자.

쓰레드1이 변수 A에 접근해 저장된 값을 참조해서 1을

증가시킨다고하자 1을 증가시킨후 101을 변수A에 저장해야하는데,

이 작업이 끝나기전에 쓰레드2로 실행이 넘어가 버렸다.

그런데 다행이 쓰레드2는 증가연산을 완전히 완료해서

증가된 값101을 변수 A에 저장했다고 가정하자.


현재 변수 A에 저장된 값 101은 쓰레드1에 의해 증가된 상태가 아니기

때문에 쓰레드2가 참조한 변수A의 값은 100이다. 결국 쓰레드2에의해

변수 A의 값은 101이 되었다.

이제 남은일은 쓰레드1이 증가시킨 값을 변수A에 저장하는 일만 남았다.

하지만 이미 101로 증가된 변수A에 다시 101을 저장하는 일이 발생된다.


결과적으로 변수 A는 101이 된다. 비록 쓰레드1과 쓰레드2가 각각 1씩

증가시켰지만, 이렇게 엉뚱한 값이 될 수 있다.


이러한 문제를 막기위해서 한 쓰레드가 변수 A에 접근해서

연산을 완료할 때까지 다른 쓰레드가 변수A에 접근하지 못하도록

막아야한다. 이것이 '동기화(Synchronization)' 이다. 


이제 간단하게 동기화 사용에 대해 알아보자.

먼저 사용해볼 동기화 기법은 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
37
38
39
40
41
42
package com.jsp.ex;
 
class Increment{
    int num = 0;
    public void increment() {  num++; }
    public int getNum() { return num; }
}
 
class IncThread extends Thread{
    Increment inc;
    
    public IncThread(Increment inc){
        this.inc = inc;
    }
    public void run(){
        for(int i=0; i<10000; i++)
            for(int j=0; j<10000; j++)
                inc.increment();
    }
}
 
class test2{
    public static void main(String[] args){
        Increment inc = new Increment();
        IncThread it1 = new IncThread(inc);
        IncThread it2 = new IncThread(inc);
        IncThread it3 = new IncThread(inc);
        
        it1.start();
        it2.start();
        it3.start();
        
        try{
            it1.join();
            it2.join();
            it3.join();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(inc.getNum());
    }
}
cs


먼저 위 소스에서 16~17line 에서 10000 for 문을 2번 사용해

increment메소드는 100000000씩 증가가 된다.


그리고 24~31line에서 총 세개의 쓰레드를 생성해

하나의 인스턴스에 저장된 값을 증가시키고 있다.

따라서 위 소스만 보면 300000000이 출력되어야 한다.


하지만 소스를 실행할때마다 결과값은 다를것이다.

쓰레드에게 많은 일을 시켜 동기화로 인해 문제가 발생했기 때문이다.


문제는 5line의 num++; 때문이다.

둘 이상의 쓰레드가 동시에 이 문장을 실행하면서

문제가 발생된 것이다.

따라서 위 문제를 해결하기 위해선 synchronized 를 사용해

동기화 메소드를 선언하거나 동기화 블록을 지정하면 된다. 먼저

동기화 메소드의 선언 방법을 보자.


5line의 public 과 void 사이에 synchronized 만 추가해주면된다.



위와같이 동기화 메소드를 만들수 있다.

그리고 동기화 메소드는

한 순간에 하나의 쓰레드만 호출이 가능하다.

따라서 쓰레드 A가 이 메소드를 호출하면 쓰레드B는

쓰레드A가 작업을 완료할때까지 대기하다 완료가 되면

그때 실행한다. 위처럼 동기화 메소드를 생성후

다시 소스를 실행해 보면 300000000 이 정상적으로 출력된다.


하지만 동기화로 인해 성능이 매우 저하 되있을것이다.

둘 이상의 쓰레드가 동시에 접근하지 못하니 성능이 떨어질 수 밖에 없다.

따라서 동기화를 사용할때는 필요한 위치에 제한적으로 사용해 성능에

영향을 주지 않도록 해야한다.


다음으로 동기화 메소드의 다른 예를 보자.


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
package com.jsp.ex;
 
class Calculator{
    int opCnt = 0;
    
    public int add(int n1, int n2){
        opCnt++
        return n1+n2;
    }
    public int min(int n1, int n2){
        opCnt++;
        return n1-n2;
    }
    public void showOpCnt(){
        System.out.println("총 연산 횟수 : "+opCnt);
    }
}
 
class AddThread extends Thread{
    Calculator cal;
    
    public AddThread(Calculator cal) { this.cal=cal; }
    
    public void run(){
        System.out.println("1+2="+cal.add(12));
        System.out.println("2+4="+cal.add(24));
    }
}
 
class MinThread extends Thread{
    Calculator cal;
    public MinThread(Calculator cal) { this.cal=cal; }
    
    public void run(){
        System.out.println("2-1="+cal.min(21));
        System.out.println("4-2="+cal.min(42));
    }
}
 
class test2{
    public static void main(String[] args){
        Calculator cal = new Calculator();
        AddThread at = new AddThread(cal);
        MinThread mt = new MinThread(cal);
        
        at.start();
        mt.start();
        
        try{
            at.join();
            mt.join();
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        cal.showOpCnt();
    }
}
cs


6,10line에 정의된 메소드에서는 4line에서 선언된 변수 opCnt에

접근한다. 따라서 두 메소드는 각각 다른 메소드에 의해 동시에

호출이 되어서는 안된다. 예를 들어 A쓰레드가 add 메소드를 실행중일때

B쓰레드는 add 메소드와 min메소드를 호출하면 안된다.

그리고 14line에 메소드 내에서도 opCnt의 값을 참조한다. 이 메소드는

add와 min메소드의 호출과 겹치지 않는다는 가정하에

(main메소드의 join메소드로 인해 겹치지 않는다)

동기화의 대상에서 제외 된다.


위 소스에서 7,11 line이 동기화의 대상이다.

저 두 메소드를 동기화 시키는 방법은 처음 알아본 소스코드 에서처럼

synchronized 을 추가해주면 된다.



위처럼 synchronized 를 사용함으로써 add메소드와 min메소드는

동기화 처리되었다. 따라서 add메소드와 min메소드는 동시에 호출되지않고

add메소드가 호출될 때 min메소드의 호출을 막을 수 있다.


다음 포스팅에서 동기화 선언에 의한 동기화 블록의 구성에대해 알아보겠다.




<참고>난 정말 JAVA를 공부한 적이 없어요(윤성우 저)

댓글

Designed by JB FACTORY