멀티 쓰레드 프로그램에서 한 자원을 여러 쓰레드가 같이 사용할 수 있습니다. 이때 A가 쓰던 데이터를 CPU 스케쥴링에 의해 A가 잠시 두고 B가 작업을 하는 경우, B가 데이터의 내용을 수정할 수도 있습니다. 그리고 다시 A가 해당 데이터를 사용하려고 보면 데이터의 내용이 바뀌어서 의도하던 것과 다른 결과를 내보낼수도 있겠죠?
따라서 이런 경우를 막기위해 동기화
를 구현해서 한 쓰레드가 작업 중인 데이터는 해당 쓰레드의 작업이 완전히 종료될 때까지 다른 쓰레드에 의해 해당 데이터가 변경될 수 없도록 잠금(lock)을 겁니다.
자바에서는 synchronized
키워드를 이용해서 해당 작업에 사용되는 데이터에 lock을 거는 식으로 동기화를 구현할 수 있습니다.
synchronized
는 메소드, 객체 앞에 붙일 수 있습니다. 이 중 객체보다는 메소드에 붙이는 것이 일반적으로 많이 사용됩니다.
synchronized 반환값타입 메소드명 {}
synchronized (객체 참조변수) {}
이렇게 synchronized
가 지정된 메소드, 객체 블록은 해당 블록 내부에서 synchronized
를 사용하는 쓰레드만이 접근할 수 있고 다른 쓰레드는 synchronized
메소드 호출, 객체 접근이 불가능합니다.
synchronized
를 사용할 때 주의해야할 점이 하나 있습니다. 바로 교착상태(Dead lock)
인데요. 교착상태
란 두 쓰레드드가 사용하는 자원이 synchronized
로 lock된 상태에서 서로의 lock된 자원을 이용하고자 요청할 때 발생합니다. 서로 접근을 할 수 없기 때문에 발생하는 문제죠.
따라서 쓰레드의 동기화를 구현할 때 교착상태에 빠지지 않도록 주의하면서 코드를 작성해야합니다.
또한 교착 상태 외에도 작업 도중에 interrupt()
등을 이용해서 쓰레드를 종료시키는 경우에도 잠궈놨던 자원을 다시 해제하거나, 변경 했던 사항을 되돌리는 등의 요소도 추가로 고려해야합니다.
실제로 동기화 메소드를 구현해서 사용해보겠습니다.
Data 객체를 두 개의 쓰레드가 공유합니다.
public class Data {
public int data;
public int getData() {
return data;
}
public synchronized void setData(int data) {
this.data = data;
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
System.err.println(e);
}
System.out.println(Thread.currentThread().getName() + ": " + this.data);
}
}
public class UserThread1 extends Thread {
private Data data;
public UserThread1() {
setName("UserThread1");
}
public void setData(Data data) {
this.data = data; //외부에서 공유 객체를 받아 data에 저장
}
@Override
public void run() {
data.setData(1000); //동기화 메소드 호출
}
}
public class UserThread2 extends Thread {
private Data data;
public UserThread2() {
setName("UserThread2");
}
public void setData(Data data) {
this.data = data; //외부에서 공유 객체를 받아 data에 저장
}
@Override
public void run() {
data.setData(5000); //동기화 메소드 호출
}
}
public class Main {
public static void main(String[] args) {
Data data = new Data();
UserThread1 userThread1 = new UserThread1();
UserThread2 userThread2 = new UserThread2();
userThread1.setData(data);
userThread1.start();
userThread2.setData(data);
userThread2.start();
}
}
우리가 볼 때는 그냥 순차적으로 실행된 것 처럼 보이지만 그림으로 그려보면 다음과 같습니다.
동기화 메소드를 제거하면 순서와 데이터의 값이 보장되지 않습니다.
public void setData(int data) { this.data = data; try { Thread.sleep(1000); } catch (InterruptedException e) { System.err.println(e); } System.out.println(Thread.currentThread().getName() + ": " + this.data); }
synchronized
를 사용하면 다른 쓰레드의 객체, 메소드의 접근을 허용하지 않는다고 했습니다. 하지만 때에 따라서는 교대로 작업을 할 필요도 생기게됩니다. 교대 작업을 위해서는 쓰레드를 일시 정지 상태로 만들었다 실행 대기 상태로 보냈다를 반복할 필요가 있습니다. 이러한 일을 도와주는 메소드가 wait()와 notify() (notifyAll())
메소드입니다.
wait()
는 실행중인 쓰레드를 일시 정지 상태로 만듭니다. notify(), notifyAll()
은 일시 정지 상태의 쓰레드를 실행 대기 상태로 만들어줍니다.
마찬가지로 Work는 공유객체입니다. 숫자를 받아와서 출력하는 동기화 메소드를 가지고 있습니다.
public class Work {
public synchronized void printNumber(int n) {
System.out.println(n);
notify(); //다른 쓰레드를 실행 대기 상태로 만듦
try {
wait(); //현재 쓰레드를 일시 정지 상태로 만듦
}
catch (InterruptedException e) {
System.err.println(e);
}
}
}
public class OddThread extends Thread {
private Work work;
public OddThread(Work work) {
this.work = work; //공유 객체 설정
}
@Override
public void run() {
for (int i = 1; i < 10; i += 2) {
work.printNumber(i); //홀수만 공유 객체 메소드에 전달
}
}
}
public class EvenThread extends Thread {
private Work work;
public EvenThread(Work work) {
this.work = work;
}
@Override
public void run() {
for (int i = 2; i <= 10; i += 2) {
work.printNumber(i); //짝수만 공유 객체 메소드에 전달
}
}
}
public class Main {
public static void main(String[] args) {
Work work = new Work();
OddThread oddThread = new OddThread(work);
EvenThread evenThread = new EvenThread(work);
oddThread.start();
evenThread.start();
}
}
번갈아 가면서 1부터 10까지 잘 출력되었죠?