본문 바로가기

JAVA

[Thread] 동시성 프로그래밍 - 동기화

 락과 동기화에 대한 내용이다. 


멀티스레드는 하나의 자원을 여러 스레드가 공유해서 사용한다. 

보통은 싱글 스레드 보다 멀티스레드를 필요로 하거나 하나의 자원을 공유해야만 하는 프로그램을 작성할때도 있다. 


값이 변하지 않는 공유자원일 특별히 동기화 처리를 할 필요는 없지만 외부에 인해 혹은 설계적으로 자원의 값이 변해야만 하는 상황이 발생한다면 반드시 동기화 처리를 해주어야 한다. 

프로그램이 공유자원을 참조하고 반영하는 절차가 굉장히 빠르고 거의 즉시라고 볼 수 있다면 이런 문제는 흔하게 발생하지 않으나, 보통 어느 실생활을 보아도 그렇지 않다. 우리가 은행 업무를 보는 절차도 ATM기에서 카드를 넣는것 부터 시작해서 돈을 인출하는 것 까지는 적게는 30초 많게는 1분 덜 소요된다. 돈을 인출한다라는 행위에서 마지막 값이 반영된다면 카드를 넣는 순간부터 인출하는 순간까지 다른 그 누구도 그 계좌에 접속하지 못하게 하고 기다리게 하여야 한다는 것이다. 그게 아니라 만약 한 사람이 인출을 해서 인출을 반영하기 전에 어떤 사람이 그 계좌에서 카드를 꼽고 인출을 시작했다고 치자. 1000원이었던 잔고에서 처음 사람이 800원을 뽑고 그 다음 사람은 800원이 차감된 200원을 갖고 다시 인출을 해야하는데 또 다시 800원을 인출한다면 결국 마지막에 통장 잔고는 마이너스가 될 것이다. 

이런 경우와 같이 다수를 동시에 같은 자원을 처리해야 하는 경우가 발생한다면 상황에 따라 반드시 동기화 처리를 해주어야 한다. 

syncronized

이 키워드는 스레드에서 동기화를 처리하기 위한 키워드이다. 클래스 및 메서드에 지정할 수 있고 그게 아니더라도 블록별로 설정할 수 있다. 이 키워드를 통해서 이 블록에 진입하는 다수개의 레이스 컨디션의 스레드들은 순서를 갖고 자기차례를 기다리게 된다.

레이스 컨디션을 가진 3개 중 하나의 스레드가 공유자원을 통해 작업중이라고 하였다면, lock 풀에서 대기하게 된다. 부여된 우선순위나 레이스 컨디션중인 스레드 중에서 먼저 비어있는 공유자원을 득하여 다음 스레드가 작업을 수행한다. 

그렇다고 하여 무조건 syncronized 키워드가 만병통치약인건 아니다. 스레드는 사용할때 잘된 설계 즉, 디자인 패턴을 활용한 설계를 하였을 경우에 유용하게 사용할 수 있지만 여기저기 남발하여 사용하다가는 데드락 현상에 빠지고 만다. 잘못된 사용은 시스템의 멈춰버림을 초래한다. 아무것도 할 수 없게 된다는 말이다. 아래와 같은 경우에 해당한다. 


public class Ex2_DeadLock {

    private Object lock1 = new Object();

    private Object lock2 = new Object();

  

    public void instanceMethod1() {

        synchronized(lock1) { //데드락을 일부러 고차시켜서 발생시킨다.

            synchronized(lock2) {

                System.out.println("First Thread");

            }

        }

    }

    

    public void instanceMethod2() {

        synchronized(lock2) {//데드락을 일부러 고차시켜서 발생시킨다.

            synchronized(lock1) {

                System.out.println("Second Thread");

            }

        }

    }

            

    public static void main(String[] args) {

        Ex2_DeadLock dl = new Ex2_DeadLock();

        new Thread(new Runnable() {

            @Override

            public void run() {

                while(true)

                    dl.instanceMethod1();

            }

        }).start();

                

        new Thread(new Runnable() {

            @Override

            public void run() {

                while(true)

                    dl.instanceMethod2();

            }

        }).start();

    }

    

}


나도 처음에는 이런 경우가 생기면 에러 메세지도 없고 그냥 아무것도 안된다. 그러다가 프로젝트를 지우기도 했던 경험을 몇번하였다. 물론 옛날 얘기긴 하지만 .. 


wait(), notify(), notifyAll()

나는 이마트에 쇼핑을 갔다. 대형 마트에 가면 시식코너가 많은데 나는 그 중에서도 삼겹살을 가장 좋아하는데 조각 조각잘라서 사람들이 줄서서 먹으면 금방 동나기 때문에 기다려야만 한다. 여기서 wait(), notify(), notifyAll()을 사용해 볼 수 있는 좋은 예라 할 수 있겠다.

고기는 한번에 구으면 몇개의 조각이 나온다. 그 조각에 비례해서 보통 사람들도 먹을 수 있다. 한번에 두점 먹는 사람도 있긴 하지만..

예를 들어 고기를 5개 구워놨는데 그 고기를 앞서온 5명이 다 먹어버렸다.

그럼 그 고기를 다시 구워서 먹을 수 있을때까진 시간이 걸린다. 

그럼 지금 고기를 먹기 위해 줄 서 있는 사람들은 wait() 상태가 되는 것이다. 

대기해야 하기 때문.. 

고기를 구워주는 직원이 고기를 먹어도 된다면 notify()나 notifyAll()로 고기를 먹어도 됩니다~라고 알려주는 것이다. 


위에 들어온 예시와 같이 wait()는 현재 스레드의 상태를 대기 상태로 전환하는 것을 말한다. 특정 잡이 완료되기 전에 어떤 스레드가 작업을 요청하면 그 스레드는 특정 잡이 완료된 후에 작업할 수 있다고 가정했을때 그 스레드는 wait 대기 상태로 전환해둔다. 

이후, 특정 잡이 끝나면 notify()나 notifyAll()을 통해서 대기 중이던 스레드를 원래 하려던 작업에 임할 수 있게 lock을 얻게 된다. 

이 세가지는 syncronized 키워드 블럭 내부에서만 사용할 수 있다.

/** 자동차 메인 **/

public class CarMain {

    

    public static void main(String[] args) throws InterruptedException {

        

        Car car = new Car();

        

        new Thread(new Producer(car)).start();

        new Thread(new Customer(car)).start();

        

    }

    

}


/** 생산자 **/

import java.util.logging.Level;

import java.util.logging.Logger;


public class Producer implements Runnable{


    private Car car;


    public Producer(Car car) {

        this.car = car;

    }

    

    @Override

    public void run() {

        String carName = "";

        for(int i=0; i<100; i++) {

            carName = car.getCar();

            Thread t = Thread.currentThread();

            car.push(carName, t);

            try {

                Thread.sleep((int)(Math.random()*200));

            } catch (InterruptedException ex) {

                ex.getStackTrace();

            }

        }

    }

}


/** 소비자 **/

import java.util.logging.Level;

import java.util.logging.Logger;



public class Customer implements Runnable{


    private Car car;


    public Customer(Car car) {

        this.car = car;

    }

    

    @Override

    public void run() {

        String carName = "";

        for(int i=0; i<100; i++) {

            carName = car.getCar();

            Thread t = Thread.currentThread();

            try {

                carName = car.pop(t);

                Thread.sleep((int)(Math.random()*100));

            } catch (InterruptedException ex) {

                ex.getStackTrace();

            }

        }

    }

}


/** 자동차 객체 **/

import java.util.ArrayList;


public class Car {

    

    private ArrayList<String> carList;


    public Car() {

            this.carList = new ArrayList<String>();

    }


    public String getCar() {

        String carName = "";

        int carNum = (int) (Math.random()*3);

        switch(carNum) {

            case 0: carName = "SM7"; break;

            case 1: carName = "소나타"; break;

            case 2: carName = "더뉴스포티지"; break;

        }

        return carName;

    }

    

    //wait은 절대적으로 sync가 들어가있는 곳에서만 실행된다

    public synchronized String pop(Thread t) throws InterruptedException {

        String carName = "";

        if(carList.size() == 0) {

            System.out.println("자동차가 없습니다.  생산할 때까지 대기실에서 기다려주세요.");

            System.out.println(t.getName()+"을 대기실로 이동시킵니다.");

            this.wait();

        }

        carName = carList.remove(carList.size()-1);

        System.out.println("손님이 차를 구입하셨습니다.");

        System.out.println("구입한 차 이름 : "+carName+":"+t.getName());

        return carName;

    }

    

    public synchronized void push(String car, Thread t){

        carList.add(car);

        System.out.println("주문하신 "+car+" 자동차가 만들어 졌습니다.");

//        System.out.println("Thread : "+t.getName());

        if(carList.size() > 5) {

            this.notify();

        }

    }

}