본문 바로가기

JAVA

쓰레드(3) - Synchronized

1. 쓰레드와 밀접한 연관이 있는 Synchronized


synchronized는 자바의 예약어 중 하나이다. 즉, 변수명, 클래스명으로 사용이 불가능하다.

 

StringBuffer와 StringBuilder의 차이점으로 StringBuffer는 "쓰레드에 안전" 하고 StringBuilder는 좋은 성능을 보여주는 대신 "쓰레드에 안전하지 못함"이 있다.

 

쓰레드 안전(Thread Safe) 에대해 조금더 설명해 보고자 옷가게를 예를 들어보자.

 

  1. 옷가게 A는 오후 7시부터 선착순으로 줄을서서 '순서대로' 신상품을 판매한다.
  2. 옷가게 B는 신상품을 담아두고 '순서와 상관없이' 판매한다.

 

옷가게 A를 이용하는 고객들은 가장 먼저 온 사람이 옷을 제일 먼저 구매할 수 있고, 가장 늦게 온 사람이 옷을 구매하지 못할 수도 있다.

 

하지만 옷가게 B를 이용하는 고객들은 가장 먼저 온 사람이 옷을 구매하지 못하거나, 서로 구매하겠다고 싸움이 발생할 수도 있는것이다. 즉, 여러 쓰레드가 한 객체에 선언된 메소드에 접근하여 데이터를 처리할 때 동시에 연산을 수행하여 값이 꼬이는 경우가 발생할 수 있다.

 

먼저 쓰레드에 안전하지 못한 코드부터 살펴보자

public class CommonCalculate {
    private int amount;

    public CommonCalculate() {
        amount=0;
    }

    public void plus(int value){
        amount+=value;
    }

    public void minus(int value){
        synchronized(this){
            amount-=value;
        }
    }

    public int getAmount(){
        return amount;
    }

}

public class ModifyAmountThread extends Thread{
    private CommonCalculate cal;
    private boolean addFlag;

    public ModifyAmountThread(CommonCalculate cal, boolean addFlag){
        this.cal=cal;
        this.addFlag=addFlag;
    }

    public void run(){
        for (int i = 0; i < 10000; i++) {
            if (addFlag){
                cal.plus(1);
            }else {
                cal.minus(1);
            }
        }

    }
}

public class RunSync {
    public static void main(String[] args) {
        RunSync r = new RunSync();
        for (int i = 0; i < 10; i++) {
            r.runCommonCalculate();
        }
    }

    public void runCommonCalculate(){
        CommonCalculate cal = new CommonCalculate();
        ModifyAmountThread thread1 = new ModifyAmountThread(cal,true);
        ModifyAmountThread thread2 = new ModifyAmountThread(cal,true);

        thread1.start();
        thread2.start();

        try {
            thread1.join(); //쓰레드가 종료될 때까지 기다리는 메소드
            thread2.join();
            System.out.println("Final value = " + cal.getAmount());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

 

실행 결과

더하기와 빼기 메소드가있는 CommonCalculate 클래스와 더하기 빼기를 사용하는 쓰레드 클래스인 ModifyAmountThread, 쓰레드를 수행하는 RunSync가 있다.

 

RunSync 클래스의 runCommonCalculate() 메소드를 보면 하나의 CommonCalulate 인스턴스를 생성하고 2개의 쓰레드 클래스에 같은 객체를 매개변수로 넘겨 계산한다.

 

그렇다면 쓰레드 클래스인 ModifyAmountThread의 run() 메소드에서 같은 객체로 연산을 진행했으므로 CommonCalculate 클래스의 amount는 20000로 찍혀야 정상이지만 그렇게 찍히지 않는다 이유가 무엇인가?

 

그 이유는 CommonCalculate 클래스의 plus() 메소드에 있다. plus() 메소드는 다른 쓰레드가 작업을 하고 있더라도 새로운 쓰레드에서 온 작업도 같이 처리할 수 있다. 

 

2. synchronized 사용하기


앞서 살펴봤던 문제를 해결하기 위해서 우리는 synchronized 예약어를 사용한다.

눈치가 빠른 사람들은 CommonCalculate 클래스의 minus() 메소드는 synchronized 를 사용해서 구현한 것을 알아차렸을 것이다. 

public class CommonCalculate {
    private int amount;

    public CommonCalculate() {
        amount=0;
    }

    /*
    * 메소드 자체를 synchronized 하게 선언하는 방법
    **/
    public synchronized void plus(int value){
        amount+=value;
    }

    /*
    * 메소드 내의 특정 문장만 synchronized 로 감싸는 방법
    **/
    public void minus(int value){
        synchronized(this){
            amount-=value;
        }
    }

    public int getAmount(){
        return amount;
    }

}

public class ModifyAmountThread extends Thread{
    private CommonCalculate cal;
    private boolean addFlag;

    public ModifyAmountThread(CommonCalculate cal, boolean addFlag){
        this.cal=cal;
        this.addFlag=addFlag;
    }

    public void run(){
        for (int i = 0; i < 10000; i++) {
            if (addFlag){
                cal.plus(1);
            }else {
                cal.minus(1);
            }
        }

    }
}

public class RunSync {
    public static void main(String[] args) {
        RunSync r = new RunSync();
        for (int i = 0; i < 10; i++) {
            r.runCommonCalculate();
        }
    }

    public void runCommonCalculate(){
        CommonCalculate cal = new CommonCalculate();
        ModifyAmountThread thread1 = new ModifyAmountThread(cal,true);
        ModifyAmountThread thread2 = new ModifyAmountThread(cal,true);

        thread1.start();
        thread2.start();

        try {
            thread1.join(); //쓰레드가 종료될 때까지 기다리는 메소드
            thread2.join();
            System.out.println("Final value = " + cal.getAmount());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

 

실행 결과

plus() 메소드에 synchronized 예약어 하나를 추가 하므로써 이 메소드는 동일한 객체를 참조하는 다른 쓰레드에서, 이 메소드를 변경하려고 하면 먼저 들어온 쓰레드가 종료될 때까지 기다리게 된다.

 

CommonCalculate 클래스의 plus() 메소드와 minus() 메소드는 다른 방식으로 synchronized를 사용한 것을 알 수 있다.

만약 여러분이 작성한 메소드가 엄청나게 많은 연산을 수행하는(예를 들면 50줄) 메소드라면 plus() 메소드 처럼 선언하게 되면 나머지(예를 들면 46줄)를 처리할 때 필요없는 대기시간이 발생하게 된다. 이를 방지하기 위해서 minus() 메소드 처럼 synchronized 예약어를 사용할 수 있다.

 

minus() 메소드에서 사용한 this는 하나의 객체를 사용하여 블록 내의 문장을 하나의 쓰레드만 수행하도록 할 수 있게 하는것이다. 즉, 문지기의 역할을 수행하는 것이다.

 

2. StringBuffer vs StringBulider


앞서 StringBuffer 는 쓰레드에 안전하며 StringBuilder는 성능상 앞서지만 쓰레드에 안전하지 않다고 언급하였다.

 

즉 StringBuffer는 성능이 StringBuilder에 비해 떨어질지 모르지만 synchronized 블록으로 주요 데이터 처리 부분을 감싸 두었기 때문에 멀티 쓰레드 환경에서는 StringBuffer를 사용하는것이 안전하고

 

StringBuilder는 synchronized 가 사용되지 않았기 때문에 성능상에 장점은 있지만 멀티 쓰레드 환경에서는 안전하지 못하다. (값이 변경될 수 있음)  

 

String , StringBuilder, StringBuffer 속도차이 비교

 

String , StringBuilder, StringBuffer 속도차이 비교

이전에 쓰레드에 관련하여 포스팅한 자료가 있는데, 마지막에 언급하였던 StringBuilder 와 StringBuffer 의 성능차이가 어느정도로 나는지 궁금하여 포스팅 하게되었다. 실험해보는김에 String도 같이

dev-cool.tistory.com