쓰레드의 동기화

by 조쉬 posted Sep 13, 2016
?

단축키

Prev이전 문서

Next다음 문서

ESC닫기

크게 작게 위로 아래로 댓글로 가기 인쇄

쓰레드의 동기화


싱글쓰레드 프로세스의 경우 프로세스 내에 단 하나의 쓰레드만 작업하기 때문에 프로세스의 자원을 가지고 작업하는 데 별문제가 없지만, 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업을 하기 때문에 서로의 작업에 영향을 주게 된다. 만일 쓰레드A가 작업하던 도중에 다른 쓰레드B에게 제어권이 넘어갔을 때, 쓰레드A가 작업하던 공유 데이터를 쓰레드B가 임의로 변경하였다면, 다시 쓰레드A가 제어권을 받아서 나머지 작업을 마쳤을 때 원래 의도했던 것과는 다른 결과를 얻을 수 있다.

이는 마치 한 방의 여러 사람이 방안의 컴퓨터를 함께 나눠 쓰는 상황과 같아서 한 사람이 컴퓨터로 문서작업 도중에 잠시 자리를 비웠을 때 다른 사람이 컴퓨터를 만져서 앞 사람이 작업하던 문서가 지원진다던가 하는 일이 생길 수 있다. 이럴 때는 문서작업이 끝날 때까지는 컴퓨터에 비밀번호를 걸어서 다른 사람이 사용할 수 없도록 해야 한다.

이처럼 멀티쓰레드 프로그래밍에서 동기화는 중요한 요소이다. 얼마만큼 동기화를 잘 처리하는가에 따라서 프로그램의 성능에 많은 영향을 미치게 된다.


1. synchronized를 이용한 동기화

자바에서는 키워드 synchronized를 통해 해당 작업과 관련된 공유 데이터에 lock을 걸어서 먼저 작업 중이던 쓰레드가 작업을 완전히 마칠 때까지는 다른 쓰레드에게 제어권이 넘어가더라도 데이터가 변경되지 않도록 보호함으로써 쓰레드의 동기화를 가능하게 한다.


(1) 특정 객체에 lock을 걸고자 할 때

synchronized(객체의 참조변수){

//.....

}

synchronized블록의 경우 지정된 객체는  synchronized블럭의 시작부터 lock이 걸렸다가 블록이 끝나면 lock이 풀린다. 이 블록을 수행하는 동안은 지정된 객체에 lock이 걸려서 다른 쓰레드가 이 객체에 접근할 수 없게된다.


(2) 메서드에 lock을 걸고자 할 때

public synchronized void calcSum(){

//.....

}

synchronized 메서드의 경우에도 한 쓰레드가 synchronized 메서드를 호출해서 수행하고 있으면, 이 메서드가 종료될 때까지  다른 쓰레드가 이 메서드를 호출하여 수행할 수 없게 된다.


2. 예제

class ThreadEx24 {

public static void main(String args[]) {

Runnable r = new A();

Thread t1 = new Thread(r);

Thread t2 = new Thread(r);


t1.start();

t2.start();

}

}


class Account {

int balance = 1000;


public void withdraw(int money){

if(balance >= money) {

try { Thread.sleep(1000);} catch(Exception e) {}

balance -= money;

}

} // withdraw

}


class A implements Runnable {

Account acc = new Account();


public void run() {

while(acc.balance > 0) {

// 100, 200, 300중의 한 값을 임으로 선택해서 출금(withdraw)

int money = (int)(Math.random() * 3 + 1) * 100;

acc.withdraw(money);

System.out.println("balance:"+acc.balance);

}

} // run()

}

실행결과)

balance:700

balance:500

balance:200

balance:200

balance:0

balance:-100

실행결과를 보면 잔고(balance)가 음수인 것을 알 수 있다. 그 이유는 한 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어서 출금을 먼저 했기 때문이다.

에를 들어 한 쓰레드가 id문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어서 출금을 먼저 했기 때문이다. 예를 들어 한 쓰레드가 if문의 조건식을 계산했을 때는 잔고(balane)가 200이고 출금하려는

금액(money)이 100이라서 조건식(balance >= money)이 true가 되어 출금(balance -= money)을 수행하려는 순간 다른 쓰레드에게 제어권이 넘어가서 다른 쓰레드가 200을 출금하여 잔고가 0이 되었다.

다시 이전의 쓰레드로 제어권이 넘어오면 if문 다음부터 수행하게 되므로 확인하는 if문과 출금하는 문장은 하나로 동기화블록으로 묶어져야 한다.

예제에서는 상황을 보여주기 위해 일부러 Thread.sleep(1000)을 사용해서 if문을 통과하자마자 다른 쓰레드에게 제어권이 넘기도록 하였지만, 굳이 이렇게 하지 않더라도 쓰레드의 작업이 다른 쓰레드에 의해서 영향을 받는 일이 발생할 수 있기 때문에 동기화가 반드시 필요하다.


class ThreadEx24 {

public static void main(String args[]) {

Runnable r = new A();

Thread t1 = new Thread(r);

Thread t2 = new Thread(r);


t1.start();

t2.start();

}

}


class Account {

int balance = 1000;


public synchronized void withdraw(int money){ //synchronized 키워드를 붙이기만 하면 간단히 동기화가 된다.

if(balance >= money) {

try { Thread.sleep(1000);} catch(Exception e) {}

balance -= money;

}

} // withdraw

}


class A implements Runnable {

Account acc = new Account();


public void run() {

while(acc.balance > 0) {

// 100, 200, 300중의 한 값을 임으로 선택해서 출금(withdraw)

int money = (int)(Math.random() * 3 + 1) * 100;

acc.withdraw(money);

System.out.println("balance:"+acc.balance);

}

} // run()

}

실행결과)

balance:800

balance:700

balance:500

balance:300

balance:100

balance:100

balance:0

balance:0

한 쓰레드에 의해서 먼저 withdraw()가 호출되면, 종료될 때까지 다른 쓰레드가 withdraw()를 호출하더라도 대기상태에 머물게 된다. 즉, withdraw()는 한 순간에 단 하나의 쓰레드만 사용할 수 있다는 것이다.


cf.) 만일 withdraw()가 수행되는 동안 객체에 lock을 걸고자 한다면 다음과 같이 할 수도 있다.

public void withdraw(int money){

synchronized (this){

if(balance >= money) {

try { Thread.sleep(1000);} catch(Exception e) {}

balance -= money;

}

    }

  }// withdraw()


아래와 같이 어떠한 하나의 데이터(int x) 를 처리함에 있어 쓰레드로 작성되어 있다면 그 데이터를 관리하는 set, get 메서드는 함께 동기화를 걸어주는 것이 기존적인 사항이다.

class K extends Thread {

private int x = 100;

public synchronized void setX(int x) {

this.x += x;

}

public synchronized int getX() { 

return x;

}

public void run() {

synchronized (this) { // 지역 동기화

setX(200);//300 + 200

System.out.println("x = " + getX()); //500

}

}

}

public class Exam_04 {

public static void main(String[] ar) {

K kp = new K();

kp.start();

}

}

실행결과)

x = 300

여러개의 쓰레드가 동시에 실행되면서 문제가 발생한다. 여러개 쓰레드 수행 도중에 데이터 꼬임 발생한다. 하나의 쓰레드에 대해 만약에 여러 개가 동작해서 문제의 소지가 발생할 경우를 대비하여 동기화를 시켜준다.

누군가 run()이라는 메서드를 처리하는 동안에는 다른 사람은 run()이라는 메서드를 실행할 수 없다.


cf.) 동기화 메서드(메서드 전체)

public synchronized void run() {

// 지역 동기화(특정 범위에 하나의 메서드가 들어있을 경우)

// 지역을 지정하여, setX()나 getX() 메서드 두 가지(동기화 필요한 메서드만)에 대해 동기화를 걸 경우 사용하는 것이다.

synchronized (this) { 

setX(200);

System.out.println("x = " + getX()); 

}

}