본문으로 바로가기

자바 synchronized에 대하여

category Java 개발 이야기 2018. 11. 24. 20:20
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.



자바에서 프로그래밍을 한다면 Multi-Thread로 인하여 동기화를 제어해야하는 경우가 생긴다. 그 때 자바에서 제공하는 키워드인 synchronized 키워드를 사용하게 되는데, Multi-Thread 상태에서 동일한 자원을 동시에 접근하게 되었을 때 동시 접근을 막게 된다. 


즉 공유 데이터에 lock을 걸어서 먼저 작업 중이던 쓰레드가 작업을 완전히 끝낼 때까지는 다른 쓰레드에게 제어권이 넘어가더라도 데이터가 변경되지 않도록 보호함으로써 쓰레드의 동기화를 가능하게 한다.


synchronized 외에 volatile을 사용할 수 있고, Atomic 클래스를 사용할 수도 있지만 이번 포스팅에서는 synchronized를 이용한 사용법 그리고 예제를 포스팅해본다.


synchronized를 사용하는 방법은 아래와 같다. 


1. 메서드에 synchronized 하기

2. 블록에 synchronized 하기


먼저 Multi-Thread 환경에서 synchronized를 사용하지 않을 경우 어떤 문제가 발생하는지 아래 예제로 설명해 본다.


아래 예제에 대해 설명을 하자면

1. 계좌에는 1000원이 있다.

2. 100 ~ 300 으로 랜덤하게 계좌에서 출금을 할 수 있다.

3. 계좌에서 출금을 하는데 잔액이 출금하는 돈보다 크다면 출금이 가능하다.

4. Multi-Thread 방식으로 2명이 출금을 한다.


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
class Account {
    int balance = 1000;
    
    public void withDraw(int money) {
        if(balance >= money) {
            try {
                Thread thread = Thread.currentThread();
                System.out.println(thread.getName() + " money : " + money);
                Thread.sleep(1000);
                balance -= money;
                System.out.println(thread.getName() + " balance : " + balance);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
 
class Task implements Runnable {
    Account acc = new Account();
    
    @Override
    public void run() {
        while(acc.balance > 0) {
            int money = (int)((Math.random() * 3+ 1* 100;
            acc.withDraw(money);
        }
    }
}
 
public class ThreadSynchronizedTest {
 
    public static void main(String[] args) {
        Task task = new Task();
        Thread thread1 = new Thread(task);
        thread1.setName("No.1");
        Thread thread2 = new Thread(task);
        thread2.setName("No.2");
        
        thread1.start();
        thread2.start();
    }
}



위의 예제를 실행시켜 보면 아래와 같다.  


1
2
3
4
5
6
7
8
9
10
11
12
No.2 money : 200
No.1 money : 100
No.1 balance : 700
No.1 money : 300
No.2 balance : 700
No.2 money : 200
No.1 balance : 200
No.1 money : 100
No.2 balance : 200
No.2 money : 200
No.1 balance : 100
No.2 balance : -100



잔액이 -100이 되 버렸다. 한 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어서 출금을 먼저 했기 때문이다. 위의 예제에서는 상황 설명을 위해 if문을 실행하고 다른 스레드에게 제어권을 넘겨주기 위해 Thread.sleep(1000)을 주었다. 하지만 Thread.sleep을 사용하지 않더라도 위와 같은 상황은 충분히 발생할 수 있다.


이렇게 동기화를 하지 않으면 의도치 않은 일이 발생할 지도 모른다. 사용자가 의도한 대로 정상적인 결과를 얻으려면 객체, 함수, 또는 변수에 synchronized를 사용해야 한다.



1. 메서드에 synchronized 하기


인스턴스 메서드의 동기화는 이 메서드를 가진 인스턴스를 기준으로 이루어진다. 그러므로 한 클래스에 synchronized를 사용한 메서드를 가진다면, 여기서 동기화는 인스턴스를 기준으로 이루어진다. 그리고 오직 하나의 Thread 만이 동기화된 인스턴스 메서드를 실행할 수 있다. 결론은 synchronized를 사용한 메서드가 존재한다면 인스턴스당 한 개의 Thread만이 접근할 수 있다. 쉽게 생각하면 메서드에 synchronized를 사용하면 그 함수가 포함된 객체(this)가 lock이 걸린 것이다. 


사용 방법은 아래와 같다.


1
2
3
4
public synchronized void synchronizedTest () {
 

}



위의 예제에서 메서드에 synchronized를 사용하려면 아래와 같이 사용하면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
public synchronized void withDraw(int money) {
    if(balance >= money) {
        try {
            Thread thread = Thread.currentThread();
            System.out.println(thread.getName() + " money : " + money);
            Thread.sleep(1000);
            balance -= money;
            System.out.println(thread.getName() + " balance : " + balance);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}



2. 블록에 synchronized 하기


아래 synchronized 키워드 괄호 사이에 락(Lock)이라 부르는 객체를 볼 수 있다. 위에서 설명했듯이 동기화과 공유 자원에 대한 접근을 허락한다고 정의한 바 있다. 즉 synchronized 키워드는 공유 자원에 대한 범위를 지정하는 기능을 하며 synchronized() 괄호 안에 있는락 객체는 다른 스레드의 접근을 차단하거나 접근을 허용하는 일종의 자물쇠 역할을 한다. 


1
2
3
synchronized([락(Lock) 객체]) {
 
}



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void withDraw(int money) {
    synchronized(this) {
        if(balance >= money) {
            try {
                Thread thread = Thread.currentThread();
                System.out.println(thread.getName() + " money : " + money);
                Thread.sleep(1000);
                balance -= money;
                System.out.println(thread.getName() + " balance : " + balance);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}



락 객체는 문지기 역할을 해서 오직 하나의 스레드만이 동기화 블록에 접근할 수 있다.


동기화는 락을 필요로 하며 락은 모든 객체마다 존재한다. 메서드 제어자 뒤에 synchronized 키워드가 위치한 동기화 메서드는 락 객체를 지정하는 부분이 없다. 동기화 메서드는 내부적으로 자신의 객체를 락으로 사용한다. 즉, 객체 스스로 메서드 전체를 감시하는 역할을 한다. 이에 반해서 동기화 블록은 메서드 안의 특정 부분을 동기화할 수 있다. 이런 경우에 락 객체는 자기 자신 객체를 의미하는 this 키워드를 사용할 수도 있지만 다른 객체를 락으로 사용할 수 있다. 단, 락 객체가 여러 개라면 우리가 원하는 동기화 작업을 제대로 실행할 수 없다. 그래서 보통은 락 객체를 하나만 사용하는 경우가 많다. 동기화 블록을 사용하는 경우, 해당 메서드는 여러 스레드가 동시에 점유할 수 있다. 하지만 동기화된 블록에 이르면 락 객체에 의해서 모든 스레드들은 실행을 중단하고 자신의 차례가 될 때까지 대기한다.


참고 

* https://tourspace.tistory.com/54

* http://soulduse.tistory.com/44