[Java] Multithreading - Thread Synchronization (1)

반응형

이번 글에서는 multi threading 환경에서 synchronization을 사용하는 방법에 대해 알아보도록 하겠습니다.

1. Thread Synchronization

multi threading 환경에서 synchronization을 이해하는 것은 굉장히 중요합니다.

예를 들어 입력받은 text를 slice하는 작업을 수행하는 StringReverseThread가 있다고 가정해보겠습니다.

public class StringReverseThread extends Thread {
    private List<Character> names;
    private String message;

    public StringReverseThread(List<Character> names, String message){
        this.names = names;
        this.message = message;
    }

    @Override
    public void run() {
       for(int i=0; i<message.length(); i++){
           try {
               // HELLO -> H, E, L, L, O
               names.add(message.charAt(i));
               Thread.sleep(50);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
       System.out.println(names);
    }
}

위 Thread는 "HELLO" 가 들어오면 [ H , E , L , L , O ] 의 형태로 names list에 저장합니다.

위와 같은 상황에서, 만약 2개의 thread가 동시에 동일한 list를 사용해 작업을 수행하면 어떻게 될까요? 😅

public static void main(String[] args) throws InterruptedException {
    List<Character> list = new ArrayList<>();

    Thread t1 = new StringReverseThread(list, "HELLO");
    t1.start();

    Thread t2 = new StringReverseThread(list, "WORLD");
    t2.start();

    t1.join();
    t2.join();
}

이때의 결과값은 우리가 의도했던 [ H , E , L , L , O, W , O , R , L , D ] 가 아닌 이상한 결과값을 return 하게 됩니다.

이는 동시에 2개의 thread가 '동일한' list를 사용해 작업을 수행하기 때문입니다.

따라서 이를 방지하고 list 객체를 한번에 하나의 thread 만 점유해서 사용할 수 있도록 하는 방법이 'Synchronization' 입니다.

synchronization 을 사용하면 하나의 thread가 점유중인 객체에 또다른 thread가 접근하게되면, 해당 thread는 해당 객체의 점유가 해제될 때 까지 기다렸다가 작업을 수행하게 됩니다.

method을 synchronization을 선언해 사용할 수 도 있습니다. 😎

synchronization을 선언하는 방법은 아래와 같이 2가지 방법이 있습니다.

1-1) synchronization block

먼저 synchronization block을 사용하는 방법이 있습니다.

public class StringReverseThread extends Thread {
    private List<Character> names;
    private String message;

    public StringReverseThread(List<Character> names, String message){
        this.names = names;
        this.message = message;
    }

    @Override
    public void run() {
       synchronized (names){
           for(int i=0; i<message.length(); i++){
               try {
                   // HELLO -> H, E, L, L, O
                   names.add(message.charAt(i));
                   Thread.sleep(50);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
           System.out.println(names);
       }
    }
}

위와 같이 메서드 수행전에 list 을 synchronized block안에 넣어주고, 해당 block 내부에서 작업을 수행하면 이 작업이 수행되는 동안에는 다른 thread가 해당 객체에 접근할 수 없게 됩니다.

이후 실행해보면 위와 같이 결과값이 의도한대로 나오는 것을 확인할 수 있습니다.

1-2) lock

두번째 방법은 shared lock을 사용하는 것 입니다.

public class StringReverseThread extends Thread {
    private List<Character> names;
    private String message;
    // shared lock
    private static Lock lock = new ReentrantLock();

    public StringReverseThread(List<Character> names, String message) {
        this.names = names;
        this.message = message;
    }

    @Override
    public void run() {
        lock.lock();
        try {
            for (int i = 0; i < message.length(); i++) {
                try {
                    // HELLO -> H, E, L, L, O
                    names.add(message.charAt(i));
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(names);
        }finally {
            lock.unlock();
        }
    }
}

ReenterantLock 객체를 static으로 생성해 모든 thread instance가 공유하도록 선언합니다. 이후 작업수행전에 lock 객체를 lock(잠금)한뒤 작업이 끝난후 lock 객체를 unlock(해제) 하도록 구현하면 됩니다.

synchronization block이 아닌 lock을 사용하면 아래와 같이 여러 메서드에 걸쳐서 lock을 사용할 수 있습니다.

public void run() {
    // call lock method
    doLock();
    try {
        for (int i = 0; i < message.length(); i++) {
            try {
                // HELLO -> H, E, L, L, O
                names.add(message.charAt(i));
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(names);
    }finally {
        lock.unlock();
    }
}

// lock method
private void doLock() {
    lock.lock();
}

반대로 synchronization block은 반드시 하나의 메서드 내부에서만 사용할 수 있습니다.


추천서적

 

이것이 자바다:신용권의 Java 프로그래밍 정복

COUPANG

www.coupang.com

파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음


반응형

댓글

Designed by JB FACTORY