8.4 동기화

JAVA PROGRAMMING/JAVA 2010/02/21 21:46 by 킨테리 KinTeL



8.4.1 멀티스레드와 문제점



스레드의 정의를 내릴 때 '스레드란 메서드가 동시에 실행되는 것'이라고 했습니다. 이 말은 동시에 메서드의 작업이 진행된다는 의미를 담고 있습니다. 여러 개의 스레드가 동시에 작업을 진행할 때 가장 큰 문제로 제기되는 것은 바로 공유자원(Shared Resource)의 문제입니다.

스레드가 생성되더라도 메서드 내부에 존재하는 자료만을 사용한다면 별다른 문제가 없습니다. 즉 공유자원 자체가 없는 것이죠. 예를 들어 A, B, C라는 세사람이 화장실을 사용하려고 합니다. 각자의 방에 화장실이 하나씩 있다면 별다른 문제 없이 일을 볼 것입니다. 문제는 화장실이 하나밖에 없을 때 발생합니다. 공용 화장실, 그것도 변기가 하나밖에 없는 화장실이라면 사용상의 문제가 발생합니다. A가 일을 보고 있을 때는 B와 C는 대기해야 합니다. 만약 이것을 어기면 상상하기 힘든 문제가 발생합니다. 이러한 문제를 해결하기 위해서 화장실의 문을 잠그는(Lock) 것입니다. 

이와 같은 문제는 스레드에서도 똑같이 발생합니다. A, B, C를 스레드라고 할 때 하나의 공유자원을 세 개의 스레드가 동시에 이용한다고 가정하죠. A가 공유자원을 사용하고 있을 때 B와 C는 대기해야 합니다. 이 때 공유자원(일반적으로 메모리)에 락(Lock)을 거는 방법으로 하나의 스레드가 공유자원을 다 사용할 때까지 다른 스레드들을 대기시키는 것입니다.

순서대로 공유자원을 사용하게끔 하는 기법을 동기화(Synchronization)라고 합니다. 즉 동기화를 순수한 우리말로 바꾸면 '줄서기'라고 말할 수 있습니다.

▣ 동기화(Synchronization)의 정의
◈ 줄서기(번갈아가면서 순서대로 공유자원 사용하기)

이러한 공유자원의 줄서기를 위해서 자바에서 제공하는 기법은 다음과 같습니다. 

▣ 동기화(Synchronization)의 기법
◈ synchronized 블록(자원을 사용할 때 자원에 락(Lock)을 거는 방식)
◈ wait()와 notify()

이 장에서는 synchronized 기법만을 알아본 뒤 10장에서 wait()와 notify()를 학습하도록 하겠습니다. 먼저 공유자원이 만들어지는 경우와 공유자원을 사용할 때 문제가 발생하는 경우의 예제부터 알아본 뒤 synchronized 기법으로 동기화를 해결해 보도록 하겠습니다.




8.4.2 공유자원의 접근



여러 개의 스레드가 생성되었을 때 하나의 자원을 이용하는 상황은 자주 발생합니다. 공유자원을 이용할 때 문제가 발생하게 되는 근본적인 이유는 동시에 작업을 진행하는 스레드의 특성 때문입니다. 공유자원이 되는 경우를 살펴보죠. 

▣ 공유자원이 문제가 되는 이유
◈ 동시에 작업을 진행하는 스레드의 특성 때문

다음은 Bank라는 클래스의 객체를 NotSynMain에서 스태틱으로 생성함으로써 myBank 객체는 프로그램에서 하나의 메모리만 생성되게 하였습니다.

▣ Bank 클래스
◈ class Bank{
◈         //...
◈ }

▣ NotSynMain 클래스의 스태틱 멤버
◈ class NotSynMain{
◈         public static Bank myBank = new Bank();
◈        //...작업
◈ }

그리고 다음과 같이 두 개의 스레드에서 NotSynMain의 myBank 객체를 이용한다면 myBank는 공유자원이 됩니다.

▣ Park 스레드 클래스
◈ class Park extends Thread{
◈         public void run(){
◈             //NotSynMain.myBank 사용
◈         }
◈ }

▣ ParkWife 스레드 클래스
◈ class ParkWife extends Thread{
◈         public void run(){
◈             //NotSynMain.myBank 사용
◈         }
◈ }

두 개의 스레드가 다음과 같이 동시에 실행된다면 공유자원의 문제가 발생합니다.

▣ 스레드의 생성 및 실행
◈ Park p = new Park(); //Park 스레드 생성
◈ ParkWife w = new ParkWife(); //ParkWife 스레드 생성
◈ p.start(); //스레드 시작
◈ w.start(); //스레드 시작

Park과 ParkWife의 클래스가 스레드로 동작할 때 각각의 run()이 호출되고 둘 다 동시에 실행될 것입니다. 이 때 NotSynMain의 스태틱 멤버 변수인 NotSynMain.myBank는 공유자원이 되며, 동기화의 문제가 제기될 수 있습니다. 프로그램적으로 동기화가 발생하는 상황은 특수한 경우입니다. 다음은 동기화의 문제가 발생하는 상황을 고위로 만든 경우입니다.

『chap08\NotSyncMain.java』
ⓙ───────────────────────────────────────
/**
동기화의 문제 발생
**/
class Bank{
    private int money = 10000; //예금 잔액
    public int getMoney(){
        return this.money;
    }
    public void setMoney(int money){
        this.money = money;
    }
    public void saveMoney(int save){
        int m = this.getMoney();
        try{
            Thread.sleep(3000);
        }catch(InterruptedException e){e.printStackTrace();}
        this.setMoney(m + save);
    }
    public void minusMoney(int minus){
        int m = this.money;
        try{
            Thread.sleep(200);
        }catch(InterruptedException e){e.printStackTrace();}
        this.setMoney(m - minus);
    }
} //end of Bank class

class Park extends Thread{
    public void run(){
        NotSyncMain.myBank.saveMoney(3000); 
        System.out.println("saveMoney(3000):" + NotSyncMain.myBank.getMoney());
    }
} //end of Park class

class ParkWife extends Thread{
    public void run(){
        NotSyncMain.myBank.minusMoney(1000); 
        System.out.println("minusMoney(1000):" + NotSyncMain.myBank.getMoney());
    }
} //end of ParkWife class

public class NotSyncMain{
    public static Bank myBank = new Bank();
    public static void main(String[] args) throws Exception{
        Park p = new Park();
        ParkWife w = new ParkWife();
        p.start();
        try{
            Thread.sleep(200);
        }catch(InterruptedException e){e.printStackTrace();}
        w.start();
    } //end of main
} //end of NotSyncMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap08>javac NotSyncMain.java
C:\javasrc\chap08>java NotSyncMain
minusMoney(1000):9000
saveMoney(3000):13000
***/
───────────────────────────────────────ⓑ

위의 예에서 Park과 ParkWife 클래스의 run()에서 NotSyncMain의 myBank를 다음과 같이 이용하고 있습니다.

▣ 공유자원 Bank myBank의 이용
◈ NotSyncMain.myBank.saveMoney(3000); //3000원 입금 
◈ NotSyncMain.myBank.minusMoney(1000); //1000원 출금 

Bank myBank 객체는 통장과 같은 역할을 하며, myBank는 기본적으로 10000원이 예금된 상태입니다. 그리고 Park의 스레드에서 3000원을 입금하려고 하며, ParkWife 스레드에서 1000원을 출금하려 합니다. 이 때 입금을 위해서는 3초의 시간이 걸리며, 출금을 위해서는 0.2초의 시간이 소요됩니다. 일반적인 계산으로는 다음과 같이 계산되어야 합니다.

▣ 동기화가 보장된 상태에서의 입출금 금액
◈ 12000(결과금액) = 10000(원금) + 3000(입금) - 1000(출금)

하지만 위의 예제의 실행결과는 13000원이 출력됩니다. 그 이유는 Park의 run()에서 예금된 금액을 가져온 후 3초의 시간이 지연된 후 입금이 처리되기 때문입니다. 

◈ public void saveMoney(int save){
◈         int m = this.getMoney(); //예금되어 있는 금액 확인
◈         try{
◈             Thread.sleep(3000);
◈         }catch(InterruptedException e){e.printStackTrace();}
◈         this.setMoney(m + save); //입금처리
◈ }

3초의 시간이 지연되는 동안 다른 곳에서 작업이 이루어진다면 문제가 발생할 것입니다. 즉 출금할 때 minusMoney()에서 0.2초의 시간으로 출금작업이 중간에 처리되어 버리기 때문에 계산은 다음의 절차대로 됩니다.

▣ 동기화가 유지되지 않은 경우 입출금의 계산 절차
◈ Park이 10000을 읽어감
◈ Park는 3초 대기
◈ Park이 대기하는 동안 ParkWife 또한 10000을 읽어감
◈ Park이 대기하는 동안 ParkWife 0.2초 대기
◈ Park이 대기하는 동안 ParkWife는 1000원을 출금
◈ Park이 대기하는 동안 ParkWife는 작업 완료(남은 돈은 9000원)
◈ Park은 3초 대기한 후 읽어온 10000으로 3000원 입금
◈ 결과는 13000원

Thread.sleep()과 데이터를 읽어온 후 작업을 하는 방법을 이용해서 고의로 동기화의 에러가 발생하도록 만든 예제입니다. 위의 예제에서 Park p라는 스레드가 작업을 끝내기 전까지는 ParkWife w는 대기하도록 만들어야 합니다.

▣ 동기화의 유지
◈ Park이 Bank myBank를 사용할 때 ParkWife는 Bank myBank를 사용하기 위해서 대기해야 한다.

즉 Park p가 Bank myBank를 사용하고 있을 때 다른 곳에서 myBank를 사용하려 한다면 다른 스레드는 대기하도록 만들어야 합니다. 위의 예에서 실제 공유자원은 Bank의 money입니다. 즉 Bank myBank의 멤버 변수인 money의 메모리를 락(Lock)으로 봉쇄한다면 다른 스레드는 이를 이용하지 못할 것입니다. 공유자원에 락(Lock)을 걸기 위해서 synchronized 키워드를 이용하면 됩니다.



8.4.3 synchronized



synchronized는 메서드와 블록 형태로 사용할 수 있으며, synchronized 메서드로 사용될 경우 해당 메서드 내에서 사용되는 모든 멤버 변수들은 락(Lock)이 걸리게 됩니다. 

▣ synchronized 메서드의 예
◈ public synchronized void saveMoney(int save){
◈        //....공유자원 - 멤버의 사용
◈ }

만약 블록 형태로 사용된다면 블록에서 명시한 객체의 멤버들은 모두 락(Lock)에 걸리게 됩니다.

▣ synchronized 블록의 예
◈ public void saveMoney(int save){
◈         synchronized(this){
◈             //....공유자원 - 멤버의 사용
◈         }
◈ }
◈ //synchronized 메서드나 블록 내에서 사용되는 공유자원은 무조건 동기화가 보장된다.

앞에서 배운 예제에서 saveMoney()와 minusMoney() 메서드 부분에 synchronized만 붙인다면 동기화의 문제는 해결될 것입니다. 다음은 동기화 처리를 한 실제 예를 보여주고 있습니다.

『chap08\SyncMain.java』
ⓙ───────────────────────────────────────
/**
synchronized를 이용한 동기화의 보장
**/
class Bank{
    private int money = 10000; //예금 잔액
    public int getMoney(){
        return this.money;
    }
    public void setMoney(int money){
        this.money = money;
    }
    public synchronized void saveMoney(int save){
        int m = this.getMoney();
        try{
            Thread.sleep(3000);
        }catch(InterruptedException e){e.printStackTrace();}
        this.setMoney(m + save);
    }
    public void minusMoney(int minus){
        synchronized(this){
            int m = this.money;
            try{
                Thread.sleep(200);
            }catch(InterruptedException e){e.printStackTrace();}
            this.setMoney(m - minus);
        }
    }
} //end of Bank class

class Park extends Thread{
    public void run(){
        SyncMain.myBank.saveMoney(3000); 
        System.out.println("saveMoney(3000):" + SyncMain.myBank.getMoney());
    }
} //end of Park class

class ParkWife extends Thread{
    public void run(){
        SyncMain.myBank.minusMoney(1000); 
        System.out.println("minusMoney(3000):" + SyncMain.myBank.getMoney());
    }
} //end of ParkWife class

public class SyncMain{
    public static Bank myBank = new Bank();
    public static void main(String[] args) throws Exception{
        Park p = new Park();
        ParkWife w = new ParkWife();
        p.start();
        try{
            Thread.sleep(200);
        }catch(InterruptedException e){e.printStackTrace();}
        w.start();
    } //end of main
} //end of SyncMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap08>javac SyncMain.java
C:\javasrc\chap08>java SyncMain
saveMoney(3000):13000
minusMoney(3000):12000
***/
───────────────────────────────────────ⓑ

위의 예제는 앞에서 테스트한 NotSyncMain의 예제에서 saveMoney()와 minusMoney() 메서드에 syncrhonzied 단 한단어만 추가되었습니다.

▣ synchronized 메서드
◈ public synchronized void saveMoney(int save){
◈        //메서드 내에 사용된 멤버 변수들의 동기화가 보장된다.
◈ }

◈ public synchronized void minusMoney(int minus){
◈        //메서드 내에 사용된 멤버 변수들의 동기화가 보장된다.
◈ }

synchronized는 synchronized 메서드가 위치한 클래스의 멤버 변수를 사용할 때, 해당 멤버 변수의 메모리를 잠그는(Lock) 방법으로 다른 스레드가 해당 자원에 접근하면 자동으로 대기시키게 만듭니다. synchronized 키워드를 메서드 앞에 붙여주면 해당 메서드 내에 존재하는 모든 멤버 변수를 모두 동기화시키게 됩니다. 하지만 동기화할 필요가 없는 자원도 있을 수 있습니다. 이를 위해서 메서드 내에 특정 구간만 동기화시키고자 할 때는 다음과 같이 synchronized 블록을 이용하면 됩니다. 

▣ synchronized 블록을 사용한 saveMoney() 메서드
◈ public void saveMoney(int save){
◈         synchronized(this){
◈            //블록 내에 사용된 this 즉 현재 클래스의 멤버 변수들에 대한 동기화가 보장된다.
◈         }
◈ }

▣ synchronized 블록을 사용한 minusMoney() 메서드
◈ public void minusMoney(int minus){
◈         synchronized(this){
◈            //블록 내에 사용된 this 즉 현재 클래스의 멤버 변수들에 대한 동기화가 보장된다.
◈         }
◈ }

이 때 주의하셔야 하는 것은 synchronized의 괄호에 들어가는 this입니다. syncrhonized 블록을 사용할 때 괄호안에 사용된 객체의 의미는 공유자원이 어디에 존재하는가의 문제입니다. 공유자원이 현재의 클래스에 존재하기 때문에 synchronized 괄호안에 this가 사용된 것입니다. 현재 클래스의 멤버가 공유자원이면 현재 클래스를 의미하는 this를 사용하시면 됩니다. 위의 예는 synchronized 메서드와 synchronized 블록을 둘 다 보여주고 있습니다. Park p의 run() 내에 존재하는 synchronized된 부분이 끝나기 전까지는 ParkWife w의 run()은 대기하게 될 것입니다.

synchronized 메서드와 sychronized 블록 내에서 사용되는 공유자원은 무조건 동기화가 보장됩니다. Bank myBank의 메모리를 공유하지만 synchronized를 사용했기 때문에 순서대로 Bank myBank의 메모리를 사용하게 되는 것입니다.



8.4.4 synchronized의 활용



synchronized 블록을 이용하실 때 괄호 안에 들어가는 객체의 멤버 변수에 대해서 동기화가 보장된다고 했습니다. 그렇다면 다음과 같이 saveMoney()와 minusMoney()의 메서드에서 synchronized 블록을 사용해도 됩니다. 

◈ public void run(){
◈         syncrhonized(SyncMain2.myBank){
◈             SyncMain2.myBank.saveMoney(3000); 
◈         }
◈ }

◈ public void run(){
◈         syncrhonized(SyncMain2.myBank){
◈             SyncMain2.myBank.minusMoney(1000); 
◈         }
◈ }

이것은 synchronized가 객체의 멤버를 상대하기 때문에 해당 객체만 명시해 주면 어디서든 사용해도 됩니다. 이것을 예제로 만들어 보면 다음과 같습니다.

『chap08\SyncMain2.java』
ⓙ───────────────────────────────────────
/**
synchronized 블럭을 동기화 예제(또다른 방법)
**/
class Bank{
    private int money = 10000; //예금 잔액
    public int getMoney(){
        return this.money;
    }
    public void setMoney(int money){
        this.money = money;
    }
    public void saveMoney(int save){
        int m = this.getMoney();
        try{
            Thread.sleep(3000);
        }catch(InterruptedException e){e.printStackTrace();}
        this.setMoney(m + save);
    }
    public void minusMoney(int minus){
        int m = this.money;
        try{
            Thread.sleep(200);
        }catch(InterruptedException e){e.printStackTrace();}
        this.setMoney(m - minus);
    }
} //end of Bank class

class Park extends Thread{
    public void run(){
        synchronized(SyncMain2.myBank){
            SyncMain2.myBank.saveMoney(3000); 
        }
        System.out.println("saveMoney(3000):" + SyncMain2.myBank.getMoney());
        
    }
} //end of Park class

class ParkWife extends Thread{
    public void run(){
        synchronized(SyncMain2.myBank){
            SyncMain2.myBank.minusMoney(1000); 
        }
        System.out.println("minusMoney(3000):" + SyncMain2.myBank.getMoney());
    }
} //end of ParkWife class

public class SyncMain2{
    public static Bank myBank = new Bank();
    public static void main(String[] args) throws Exception{
        Park p = new Park();
        ParkWife w = new ParkWife();
        p.start();
        try{
            Thread.sleep(200);
        }catch(InterruptedException e){e.printStackTrace();}
        w.start();
    } //end of main
} //end of SyncMain2 class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap08>javac SyncMain2.java
C:\javasrc\chap08>java SyncMain2
saveMoney(3000):13000
minusMoney(3000):12000
***/
───────────────────────────────────────ⓑ

위의 예에서 사용된 synchronized 블록의 객체는 SyncMain2.myBank입니다. 이것의 의미는 SyncMain2.myBank의 멤버 변수에 동기화를 걸겠다는 의미입니다. 

▣ synchronized 블록을 이용한 동기화
◈ synchronized(SyncMain2.myBank){
◈     //SyncMain2.myBank 멤버의 동기화 보장
◈ }

syncrhonized를 사용하는 곳은 어디라도 상관없습니다. 보통은 공유자원이 있는 클래스에서 사용하는 경우가 일반적입니다. 하지만 그렇지 않은 경우도 많기 때문에 두 가지 기법을 모두 알아두시기 바랍니다. 동기화가 보장되는 것과 보장되지 않은 것은 전혀 다른 의미를 지니고 있습니다. 앞의 예제에서 사용한 동기화는 사소한 것 같지만 동기화가 보장되지 않는 통장을 상상해 보신다면 그 중요성을 짐작할 수 있을 것입니다.



8.4.5 synchronized의 한계



synchronized는 자동 동기화입니다. 즉 공유자원을 사용하면 synchronized 블록에서는 자동으로 동기화가 보장되는 것입니다. 아주 편리하지만 미세한 동기화를 제어하기에는 약간의 한계가 있습니다.

비디오 가게를 생각해 보죠. 비디오 가게에 비디오 테이프가 5개 있고 그리고 손님들이 비디오 테이프를 빌려간다고 가정하죠. 이 때 동기화가 보장되어야 하는 것은 당연한 것입니다. 두 사람이 동시에 하나의 테이프를 빌려가면 안되니까요. 물론 synchronized를 사용하면 동기화는 보장할 수 있습니다. 하지만 비디오를 빌린 후 비디오 테이프를 보는 동안에 동기화를 걸면, 한사람이 비디오를 보는(시청하는) 동안 어떠한 사람도 비디오 테이프를 빌릴 수 없습니다. 다음은 잘못된 동기화의 처리로 인해 비디오 가게에서는 단 하나의 비디오 테이프만을 빌려주는 예를 보여주고 있습니다.

『chap08\VideoShopMain.java』
ⓙ───────────────────────────────────────
/**
잘못된 동기화의 예
**/
import java.util.*;
class VideoShop{
    private Vector buffer = new Vector();
    public VideoShop(){
        buffer.addElement("은하철도999-0");
        buffer.addElement("은하철도999-1");
        buffer.addElement("은하철도999-2");
        buffer.addElement("은하철도999-3");
    }
    public String lendVideo(){
        String v = (String)this.buffer.remove(buffer.size()-1);
        return v;
    }
    public void returnVideo(String video){
        this.buffer.addElement(video);
    }
} //end of VideoShop class

class Person extends Thread{
    public void run(){
        synchronized(VideoShopMain.vShop){
            //5초동안 VideoShopMain.vShop은 락(Lock)에 걸리게 된다.
            try{
                String v = VideoShopMain.vShop.lendVideo();
                System.out.println(this.getName() + ":" + v  + " 대여");
                System.out.println(this.getName() + ":" + v  + " 보는중");
                this.sleep(5000);
                System.out.println(this.getName() + ":" + v  + " 반납");
                VideoShopMain.vShop.returnVideo(v);
            }catch(InterruptedException e){e.printStackTrace();}
        }
    }
} //end of Person class

class VideoShopMain{
    public static VideoShop vShop = new VideoShop();
    public static void main(String[] args){
        System.out.println("프로그램 시작");
        Person p1 = new Person();
        Person p2 = new Person();
        Person p3 = new Person();
        Person p4 = new Person();
        p1.start();
        p2.start();
        p3.start();
        p4.start();
        System.out.println("프로그램 종료");
    }
} //end of VideoShopMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap08>javac VideoShopMain.java
C:\javasrc\chap08>java VideoShopMain
프로그램 시작
프로그램 종료
Thread-1:은하철도999-3 대여
Thread-1:은하철도999-3 보는중
Thread-1:은하철도999-3 반납
Thread-2:은하철도999-3 대여
Thread-2:은하철도999-3 보는중
Thread-2:은하철도999-3 반납
Thread-3:은하철도999-3 대여
Thread-3:은하철도999-3 보는중
Thread-3:은하철도999-3 반납
Thread-4:은하철도999-3 대여
Thread-4:은하철도999-3 보는중
Thread-4:은하철도999-3 반납
***/
───────────────────────────────────────ⓑ

위의 예에서 4개의 Person 스레드를 실행시키고 있습니다.

◈ Person p1 = new Person();
◈ Person p2 = new Person();
◈ Person p3 = new Person();
◈ Person p4 = new Person();
◈ p1.start(); p2.start(); p3.start(); p4.start();

일반적인 경우라면 4개의 스레드가 동시에 작업을 진행할 것이라고 생각합니다. 하지만 다음과 같이 비디오를 빌린 후 반납할 때까지 동기화를 걸어두었다면, 비디오 테이프를 빌린 첫번째 사람이 비디오를 보기 전까지는 어떠한 스레드도 VideoShop에 접근할 수 없습니다.

▣ synchronized 블럭
◈ synchronized(VideoShopMain.vShop){
◈         String v = VideoShopMain.vShop.lendVideo(); //비디오를 빌린다.
◈         try{
◈             this.sleep(5000); //비디오를 보는 시간 5초
◈         }catch(InterruptedException e){e.printStackTrace();}
◈         VideoShopMain.vShop.returnVideo(v); //비디오를 반납한다.
◈ }

비디오를 보는 5초동안 VideoShop vShop은 synchronized로 인해 락(Lock)이 걸린 상태이기 때문에 어떠한 스레드도 접근할 수 없습니다. 이렇게 될 경우 비디오 가게에서는 단 하나의 테이프만을 운영하는 것이 됩니다. 결과를 보시면 '은하철도999-4' 비디오 테이프만을 대여하고 있는 것을 확인할 수 있습니다. 이것은 일반적인 메서드 호출이나 별반 다른 것이 없습니다. 즉 비디오 가게에서는 4개의 테이프를 동시에 빌려 주어야만 장사를 잘하는 것이 될 것입니다.

synchronized를 사용할 때 다음과 같이 비디오 테이프를 빌려주는 곳과 반환하는 곳에 synchronized를 거는 것이 더 정확한 사용 방법입니다.

▣ synchronized를 걸어야 하는 곳
◈ public synchronized String lendVideo(){...}
◈ public synchronized void returnVideo(String video){...}

데이터를 집어넣고 추출할 때에만 동기화를 보장하는 것이죠. 다음은 이러한 방법으로 프로그램한 예는 다음과 같습니다.

『chap08\VideoShopMain2.java』
ⓙ───────────────────────────────────────
/**
데이터를 집어넣고 추출할 때에만 동기화를 보장
**/
import java.util.*;
class VideoShop{
    private Vector buffer = new Vector();
    public VideoShop(){
        buffer.addElement("은하철도999-0");
        buffer.addElement("은하철도999-1");
        buffer.addElement("은하철도999-2");
        buffer.addElement("은하철도999-3");
    }
    public synchronized String lendVideo(){
        String v = (String)this.buffer.remove(buffer.size()-1);
        return v;
    }
    public synchronized void returnVideo(String video){
        this.buffer.addElement(video);
    }
} //end of VideoShop class

class Person extends Thread{
    public void run(){
        try{
            String v = VideoShopMain2.vShop.lendVideo();
            System.out.println(this.getName() + ":" + v  + " 대여");
            System.out.println(this.getName() + ":" + v  + " 보는중");
            this.sleep(5000);
            System.out.println(this.getName() + ":" + v  + " 반납");
            VideoShopMain2.vShop.returnVideo(v);
        }catch(InterruptedException e){e.printStackTrace();}
    }
} //end of Person class

class VideoShopMain2{
    public static VideoShop vShop = new VideoShop();
    public static void main(String[] args){
        System.out.println("프로그램 시작");
        Person p1 = new Person();
        Person p2 = new Person();
        Person p3 = new Person();
        Person p4 = new Person();

        p1.start();
        p2.start();
        p3.start();
        p4.start();
        System.out.println("프로그램 종료");
    } //end of main
} //end of VideoShopMain2 class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap08>javac VideoShopMain2.java
C:\javasrc\chap08>java VideoShopMain2
프로그램 시작
프로그램 종료
Thread-1:은하철도999-3 대여
Thread-2:은하철도999-2 대여
Thread-3:은하철도999-1 대여
Thread-4:은하철도999-0 대여
Thread-1:은하철도999-3 보는중
Thread-2:은하철도999-2 보는중
Thread-3:은하철도999-1 보는중
Thread-4:은하철도999-0 보는중
Thread-1:은하철도999-3 반납
Thread-2:은하철도999-2 반납
Thread-3:은하철도999-1 반납
Thread-4:은하철도999-0 반납
***/
───────────────────────────────────────ⓑ

위의 프로그램은 synchronized를 lendVideo()와 returnVideo()에 걸어 두었습니다. 이렇게 되면 4개의 비디오를 동시에 대여할 수 있습니다. 비디오를 보는 동안 synchronized 처리하는 것보다는 훨씬 유연하게 동작하는 것을 확인할 수 있습니다.

하지만 이 경우에는 또 다른 문제가 제기됩니다. 즉 비디오 테이프를 보는 곳에 동기화를 걸지 않았기 때문에 5번째 비디오 테이프를 빌리면 심각한 문제가 발생할 것입니다. 위의 예에서 다음과 같이 5번째 비디오를 빌리는 구문을 삽입하면 에러가 발생합니다.

▣ 문제발생
◈ Person p5 = new Person();
◈ p5.start();

비디오를 보는 곳에 동기화를 걸 수 없으며, 테이프는 4개밖에 없기 때문에 에러가 발생할 것입니다. 물론 비디오가 없으면 빌려주지 않으면 됩니다. 즉 Person p5가 구동되었을 때 비디오 테이프가 없으면 바로 p5를 종료하면 될 것입니다. 즉 여유분이 있을 때만 비디오 테이프를 빌려주면 됩니다. 이를 프로그램으로 작성하면 다음과 같습니다.

『chap08\VideoShopMain3.java』
ⓙ───────────────────────────────────────
/**
여유분이 있을 때만 비디오 테이프를 빌려주기
**/
import java.util.*;
class VideoShop{
    private Vector buffer = new Vector();
    public VideoShop(){
        buffer.addElement("은하철도999-0");
        buffer.addElement("은하철도999-1");
        buffer.addElement("은하철도999-2");
        buffer.addElement("은하철도999-3");
    }
    public synchronized String lendVideo(){
        if(buffer.size()>0){
            String v = (String)this.buffer.remove(buffer.size()-1);
            return v;
        }else{
            return null;
        }
    }
    public synchronized void returnVideo(String video){
        this.buffer.addElement(video);
    }
} //end of VideoShop class

class Person extends Thread{
    public void run(){
        String v = VideoShopMain3.vShop.lendVideo();
        if( v == null){
            System.out.println(this.getName() + "비디오가 없군요. 안봅니다.");
            return;
        }
        try{
            System.out.println(this.getName() + ":" + v  + " 대여");
            System.out.println(this.getName() + ":" + v  + " 보는중\n");
            this.sleep(5000);
            System.out.println(this.getName() + ":" + v  + " 반납");
            VideoShopMain3.vShop.returnVideo(v);
        }catch(InterruptedException e){e.printStackTrace();}
    }
} //end of Person class

public class VideoShopMain3{
    public static VideoShop vShop = new VideoShop();
    public static void main(String[] args){
        System.out.println("프로그램 시작");
        Person p1 = new Person();
        Person p2 = new Person();
        Person p3 = new Person();
        Person p4 = new Person();
        Person p5 = new Person();
        p1.start();
        p2.start();
        p3.start();
        p4.start();
        p5.start();
        System.out.println("프로그램 종료");
    } //end of main
} //end of VideoShopMain3 class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap08>javac VideoShopMain3.java
C:\javasrc\chap08>java VideoShopMain3
프로그램 시작
프로그램 종료
Thread-1:은하철도999-3 대여
Thread-1:은하철도999-3 보는중

Thread-2:은하철도999-2 대여
Thread-2:은하철도999-2 보는중

Thread-3:은하철도999-1 대여
Thread-3:은하철도999-1 보는중

Thread-4:은하철도999-0 대여
Thread-4:은하철도999-0 보는중

Thread-5비디오가 없군요. 안봅니다.
Thread-1:은하철도999-3 반납
Thread-2:은하철도999-2 반납
Thread-3:은하철도999-1 반납
Thread-4:은하철도999-0 반납
***/
───────────────────────────────────────ⓑ

위의 예에서는 비디오 테이프가 없을 때 비디오 테이프를 빌리고자 한다면 null을 리턴하게 되어 있습니다. 

▣ 비디오 테이프가 없을 때 null을 리턴
◈ if(buffer.size()>0){
◈         String v = (String)this.buffer.remove(buffer.size()-1);
◈         return v;
◈ }else{
◈         return null;
◈ }

그리고 Person에서는 비디오 테이프가 없다면 다음과 같이 바로 스레드를 종료하게 됩니다.

◈ if( v == null){
◈         System.out.println(this.getName() + "비디오가 없군요. 안봅니다.");
◈         return null; //비디오 테이프를 빌리는 것 자체를 포기한다.
◈ }

이렇게 한다면 에러 발생은 막을 수 있습니다. 하지만 자원을 요청한 사람은 대기하는 것이 아니라 아예 포기하는 것이 됩니다. 즉 이 비디오 가게는 장사를 잘 못하는 것입니다. 만약 제대로 장사를 한다면 잠깐 기다리라고 한 뒤 비디오 테이프가 들어오면 바로 빌려주면 될 것입니다. 

지금까지 배운 동기화 기법으로 이것을 구현하는 것은 거의 불가능합니다. 물론 구현할 수 없는 것은 아닙니다만 약간 까다롭습니다. 보다 효율적인 스레드 제어의 기법을 구현하기 위해서는 즉 비디오 가게에서 손님의 동기화를 보장하기 위해서는 wait()와 notify()를 사용하면 됩니다. 비디오 테이프가 없다면 wait() 시켰다가 비디오 테이프가 반납되면 notify()해서 빌려가게 한다면 보다 효율적으로 비디오 가게를 운영할 수 있을 것입니다.

wait()와 notify() 예제는 10장에서 다루게 될 것입니다. 이 절의 문제 해결책으로 제시된 10장의 예제를 반드시 확인하시기 바랍니다. 10장에 있는 이유는 10장을 어느 정도 학습한 뒤 wait()와 notify()를 보면 더 효율적이기 때문입니다.




저작자 표시 비영리 변경 금지

'JAVA PROGRAMMING > JAVA' 카테고리의 다른 글

9.3 표준 스트림과 File 클래스  (0) 2010/02/21
9.1 Java Stream  (0) 2010/02/21
8.4 동기화  (0) 2010/02/21
8.3 스레드의 제어  (0) 2010/02/21
8.2 스레드의 기본  (0) 2010/02/21
8.1 Thread and Synchronization  (0) 2010/02/21
1  ... 45 46 47 48 49 50 51 52 53  ... 159 
BLOG main image
(주)KinTeL 회장 김형기

카테고리

분류 전체보기 (159)
My Name Is KinTeL (3)
농구 인생 (1)
사진 이야기 (13)
미디어 (5)
JAVA PROGRAMMING (105)
C# Programming (2)
FLEX (6)
Database (4)
About Eclipse (1)
참조 - Reference (9)
모든 무료 정보 (2)
좋은글 (2)


Statistics Graph
Total : 73,500
Today : 11
Yesterday : 43

최근에 받은 트랙백