🎯 목표 : Thread의 동기화의 개념과 관련된 메소드의 활용
📒 Thread의 동기화
- 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는것.
- 멀티쓰레드 프로세스에서 다른 쓰레드의 작업에 영향을 미칠수가 있으며,
- 진행중인 작업이 다른 쓰레드에게 간섭받지 않게 하려면 동기화가 필요하다.
- 동기화를 하려면 간섭받지 않아야 하는 문장들을 임계영역으로 설정하고 임계영역은 락을 얻은 단 하나의 쓰레드만 출입 가능하다(객체 1개마다 1개의 락을 가지고 있다.)
📌 synchronized
public synchronized void sum() {
// 임계영역
}
synchronized(객체의 참조변수) {
//임계영역
}
- 위 코드와 같이 두가지 방법으로 임계영역을 설정할수 있다.
- 아래 예제는 잔고에서 출금하는 것을 예시로 들어서 작성했다.
public class SynchronizationStudy {
public static void main(String args[]) {
Runnable r = new RunnableEx();
new Thread(r).start(); // 출금을 하는 쓰레드 2개를 실행한다.
new Thread(r).start();
}
}
class Account2 {
private int balance = 1000;
public int getBalance() {
return balance;
}
public void withdraw(int money){
// 1초동안 money값을 받아 balance에서 빼고 출력한다.
if(balance >= money) { // balance가 money보다 크거나 같을때 까지 실행한다.
try { Thread.sleep(1000);} catch(InterruptedException e) {}
balance -= money;
}
}
}
class RunnableEx implements Runnable {
Account2 a = new Account2(); // Account2의 객체 생성
public void run() {
while(a.getBalance() > 0) {
int money = (int)(Math.random() * 3 + 1) * 100; // 100~300까지를 정의해준다.
a.withdraw(money); // 메소드 호출
System.out.println("balance:"+a.getBalance());// balance 출력
}
}
}
- 잔고를 계산해주는 Account2 클래스와 출금하는 쓰레드 클래스 RunnableEx를 만들었다.
- 동기화 충돌을 재현하기위해 main 메소드에서는 두개의 쓰레드를 실행시켰고,
- 실행시 1000값을 가진 balance는 두개의 쓰레드에 의해 출금이 된다.
balance:700
balance:700
balance:400
balance:400
balance:100
balance:-100
- 코드에서 쓰레드 클래스 run의 while 조건에는 balance의 값이 0보다 클때까지 동작하도록 구현되어 있는데,
- 출력값을 보면 balance의 값이 -100 으로 출력되어 있다.
- 1초동안 withdraw 메소드에서 money값을 받아 balance의 값에 적용을 하고 있는 도중에 쓰레드 클래스의 run메소드에서 Account2클래스의 balance 값을 읽어들여 계산을하고 출력을 했기 때문에 발생한 현상이다.
- withdraw 메소드에 임계영역을 설정해줘서 출력값을 보며 동기화에 대한 학습을 할수 있다.
public class SynchronizationStudy {
public static void main(String args[]) {
Runnable r = new RunnableEx();
new Thread(r).start(); // 출금을 하는 쓰레드 2개를 실행한다.
new Thread(r).start();
}
}
class Account2 {
private int balance = 1000;
public int getBalance() {
return balance;
}
public synchronized void withdraw(int money){
// 1초동안 money값을 받아 balance에서 빼고 출력한다.
if(balance >= money) { // balance가 money보다 크거나 같을때 까지 실행한다.
try { Thread.sleep(1000);} catch(InterruptedException e) {}
balance -= money;
}
}
}
class RunnableEx implements Runnable {
Account2 a = new Account2(); // Account2의 객체 생성
public void run() {
while(a.getBalance() > 0) {
int money = (int)(Math.random() * 3 + 1) * 100; // 100~300까지를 정의해준다.
a.withdraw(money); // 메소드 호출
System.out.println("balance:"+a.getBalance());// balance 출력
}
}
}
balance:800
balance:600
balance:300
balance:100
balance:100
balance:100
balance:100
balance:100
balance:100
balance:100
balance:0
balance:0
- 임계영역을 설정 후에 출력값을 보면, balance 의 값이 100에서 계속 출력된 것을 볼수 있다. 이것은 쓰레드 클래스의 run 메소드에 있는 while 반복문이 돌고는 있지만 withdraw 메소드의 조건에 부합하지 못하여 balance 값이 초기화 되지 않고 그대로 출력되서 발생한 것이다.
- 즉 money의 값이 100이 될때까지 =(락을 얻어 임계영역의 데이터를 읽을수 있을때까지) 반복해서 동작했다는 것이다.
- withdraw메소드에 임계영역을 설정 해 줌으로써 멀티 쓰레드중 하나의 쓰레드만 withdraw에서 초기화 된 balance값을 읽어 들일수 있게 만들었다.
📌 wait(), notify()
- synchronized로 동기화 해서 공유 데이터를 보호하는 방법을 확인 했다.
- 하지만, 바로 위 출력값 처럼 money의 값이 100이 될때까지 쓰레드는 반복해서 동작하고 있었다
- 즉, 락을 얻어 임계영역에 들어가 필요한 데이터를 읽어오고 동작을 하기위해 쓰레드가 기다리고 있었다는 말과 같다.
- 무의미한 작업을 하고 있었으며 이것은 다른 쓰레드의 작업에 문제를 발생 시킬수도 있다.
- 위와 같은 문제를 해결하기 위해 wait()와 notify()를 활용할수 있다.
- Object 클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
- wait() = 객체의 lock(락)을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.
- notify() = waiting pool 에서 대기중인 쓰레드 중의 하나를 깨운다.
- notifyAll() = waiting pool 에서 대기중인 모든 쓰레드를 깨운다.
👉예제
- 아래 예제는 두개의 쓰레드로 작업을 수행하는 예제다.
- synchronized로 임계영역만 설정후에 실행 시켰을때, 번갈아 가며 작업 수행이 아닌, 하나의 쓰레드가 몰아서 작업을 했다가, 다시 다른 쓰레드가 작업을 몰아서 했다가 반복을 하고 있다. 제일 밑의 출력 값을 보고 확인할수 있다.
public class WaitNotify_1 {
public static void main(String[] args) throws InterruptedException {
WorkPool work = new WorkPool();
Thread a = new Thread(new WorkThread01(work));
Thread b = new Thread(new WorkThread02(work));
a.start();
b.start();
Thread.sleep(10000);
System.out.println("End of Work");
System.exit(0);
}
}
class WorkPool {
public synchronized void work_A() {
System.out.println("Thread01 Working....");
try { Thread.sleep(400); } catch (Exception e) {}
}
public synchronized void work_B() {
System.out.println("Thread02 Working....");
try { Thread.sleep(400); } catch (Exception e) {}
}
}
class WorkThread01 implements Runnable{
private WorkPool work;
WorkThread01(WorkPool work) {
this.work = work;
}
public void run(){
for(int i=0; i<10;i++){
work.work_A();
}
}
}
class WorkThread02 implements Runnable{
private WorkPool work;
WorkThread02(WorkPool work) {
this.work = work;
}
public void run(){
for(int i=0; i<10;i++){
work.work_B();
}
}
}
출력값
Thread01 Working....
Thread02 Working....
Thread02 Working....
Thread01 Working....
Thread01 Working....
Thread02 Working....
Thread02 Working....
Thread02 Working....
Thread02 Working....
Thread02 Working....
Thread02 Working....
Thread02 Working....
Thread02 Working....
Thread01 Working....
Thread01 Working....
Thread01 Working....
Thread01 Working....
Thread01 Working....
Thread01 Working....
Thread01 Working....
End of Work
- synchronized로 임계영역만 설정하고 작업하면, 서로의 작업 영역에 영향을 미칠수는 없지만, 작업의 효율이 떨어지는 단점이 있다. 이런부분을 보완하기 위해 wait()와 notify()를 활용한다. 아래 예제와 출력값을 보면, 효과적으로 작업을 반복하며 프로그램이 실행된 것을 확인 할수 있다.
public class WaitNotify_1 {
public static void main(String[] args) throws InterruptedException {
WorkPool work = new WorkPool();
Thread a = new Thread(new WorkThread01(work));
Thread b = new Thread(new WorkThread02(work));
a.start();
b.start();
Thread.sleep(10000);
System.out.println("End of Work");
System.exit(0);
}
}
class WorkPool {
public synchronized void work_A() {
System.out.println("Thread01 Working....");
try { Thread.sleep(400); } catch (Exception e) {}
notify();
try { wait(); } catch (InterruptedException e){}
}
public synchronized void work_B() {
System.out.println("Thread02 Working....");
try { Thread.sleep(400); } catch (Exception e) {}
notify();
try { wait(); } catch (InterruptedException e){}
}
}
class WorkThread01 implements Runnable{
private WorkPool work;
WorkThread01(WorkPool work) {
this.work = work;
}
public void run(){
for(int i=0; i<10;i++){
work.work_A();
}
}
}
class WorkThread02 implements Runnable{
private WorkPool work;
WorkThread02(WorkPool work) {
this.work = work;
}
public void run(){
for(int i=0; i<10;i++){
work.work_B();
}
}
}
출력값
Thread01 Working....
Thread02 Working....
Thread01 Working....
Thread02 Working....
Thread01 Working....
Thread02 Working....
Thread01 Working....
Thread02 Working....
Thread01 Working....
Thread02 Working....
Thread01 Working....
Thread02 Working....
Thread01 Working....
Thread02 Working....
Thread01 Working....
Thread02 Working....
Thread01 Working....
Thread02 Working....
Thread01 Working....
Thread02 Working....
End of Work