스레드 동기화 필요성
다수의 스레드가 공유 데이터에 동시 접근할 때 예상치 못한 결과를 낳을 수 있다. 스레드 동기화란 공유 데이터에 접근하고자 하는 다수의 스레드가 서로 순서대로 충돌 없이 공유 데이터를 배타적으로 접근하기 위해 상호 협력하는 것을 말한다. 공유 데이터에 대한 접근은 배타적이고 독점적으로 이루어져야 하며, 독점적으로 공유 데이터를 다루는 프로그램 코드를 임계 영역이라고 부른다. 그림 예제는 다음과 같다.
자바 synchronized 키워드
하나씩 순차적으로 실행하도록 제어하는 기법으로, synchronized 키워드는 임의의 코드 블록을 동기화가 설정된 임계 영역으로 지정한다. 임계 영역으로 지정할 수 있는 예제는 다음과 같다.
- 메소드 전체를 임계 영역으로 지정: 다음 코드와 같이 synchronized 키워드를 사용하면 add() 메소드를 임계 영역으로 지정할 수 있다.
synchronized void add() {
int n = getCurrentSum(); // 현재 합을 알아낸다.
n += 10; /// 10만큼 증가시킨다.
setCurrentSum(n); //증가된 결과를 기록한다.
}
- 코드 블록을 임계 영역으로 지정: 다음과 같이 중괄호({})를 이용하여 코드 블록을 synchornized 키워드로 지정할 수 있다.
void execute() {
...
synchornized(this) {
int n = getCurrentSum();
n += 10;
setCurrentSum(n);
}
}
wait(), notify(), notifyAll() 메소드를 이용한 스레드 동기화
스레드들이 synchronized 키워드를 이용하여 공유 데이터에 순차적으로 잘 접근하도록 만들어진 경우라도, 여전히 동기화가 필요한 상황이 있다. 대표적인 경우가 공유 메모리를 통해 두 스레드가 데이터를 주고받을 때, 공유 메모리에 대해 두 스레드가 동시에 접근하는 producer-cunsumer 문제이다.
-
Object wait(), notify() 메소드: java.lang.Object 클래스는 스레드 사이에 동기화를 위한 3개의 wait(), notify(), notifyAll() 메소드를 제공한다. 자바는 모든 객체가 동기화 객체가 될 수 있도록 설계하였다.
-
wait(): 다른 스레드가 이 객체의 notify() 메소드를 불러줄 때까지 대기한다.
-
notify(): 이 객체에 대기 중인 스레드를 깨워 RUNNABLE 상태로 만든다. 2개 이상의 스레드가 대기 중이라도 오직 한 개의 스레드만 깨워 RUNNABLE 상태로 한다.
-
notifyAll(): 이 객체에 대기 중인 모든 스레드를 깨우고 모두 RUNNABLE 상태로 한다.
-
wait(), notify() 메소드를 이용한 바 채우기 예제 코드는 다음과 같다.
package ch13n7;
import java.awt.Color;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.JFrame;
import javax.swing.JLabel;
class MyLabel extends JLabel {
private int barSize = 0; // 현재 그려져야할 바의 크기
private int maxBarSize; // 바의 최대 크기
public MyLabel(int maxBarSize) {
this.maxBarSize = maxBarSize;
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.MAGENTA);
int width = (int)(((double)(this.getWidth())) / maxBarSize * barSize);
if (width == 0) {
return; // 크기가 0 값이기 때문에 바를 그릴 필요가 없다.
}
g.fillRect(0, 0, width, this.getHeight());
}
synchronized public void fill() {
if (barSize == maxBarSize) {
try {
wait(); // 바가 최대이면, ConsumerThread 객체에 의해 바가 줄어들 때까지 대기한다.
}
catch (InterruptedException e) {
return;
}
}
++barSize;
repaint(); // 바의 크기가 변했으니 다시 그린다.
notify(); // 기다리는 ConsumerThread 스레드를 깨운다.
}
synchronized public void consume() {
if (barSize == 0) {
try {
wait(); // 바의 크기가 0 값이면 바의 크기가 0 값보다 커질 때까지 대기한다.
}
catch (InterruptedException e) {
return;
}
}
--barSize;
repaint(); // 바의 크기가 변했으니 다시 그린다.
notify(); // 기다리는 이벤트 스레드를 깨운다.
}
}
class ConsumerThread extends Thread {
private MyLabel bar;
public ConsumerThread(MyLabel bar) {
this.bar = bar;
}
@Override
public void run() {
while (true) {
try {
sleep(200);
bar.consume(); // 0.2초마다 바를 1 값 줄인다.
}
catch (InterruptedException e) {
return;
}
}
}
}
public class TabAndThreadEx extends JFrame{
private MyLabel bar = new MyLabel(100); // 바의 최대 크기를 100 값으로 설정
public TabAndThreadEx(String title) {
super(title);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Container c = getContentPane();
c.setLayout(null);
bar.setBackground(Color.ORANGE);
bar.setOpaque(true);
bar.setLocation(20, 50);
bar.setSize(300, 20); // 300 * 20 크기의 바
c.add(bar);
// 컨텐트팬에 키 이벤트 핸들러를 등록한다.
c.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
bar.fill(); // 키를 누를 때마다 바가 1 값씩 증가한다.
}
});
setSize(350, 200);
setVisible(true);
c.setFocusable(true);
c.requestFocus(); // 컨텐츠팬에 키 처리권을 부여한다.
ConsumerThread th = new ConsumerThread(bar); // 스레드를 생성한다.
th.start(); // 스레드를 시작한다.
}
public static void main(String[] args) {
new TabAndThreadEx("아무키나 빨리 눌러 바를 채우세요.");
}
}