본문 바로가기

Java

이클립스에서 다중 스레드 관리하기

고전적인 병렬 스레드 문제를 하나 가정하자. 아래의 코드는 하나의 List<String>을 여러 스레드가 동시에 접근하는 예이다.


  1. import java.util.ArrayList;
  2. import java.util.List;
  3.  
  4. public class MultiThread  {
  5.    public static void main(String[] args) {
  6.       new MultiThread().run();
  7.    }
  8.  
  9.    // 특정 List<String> 주어진 단어를 10000개 집어 넣는 스레드
  10.    private class MyThread extends Thread {
  11.       private final List<String> queue;
  12.       private final String word;
  13.  
  14.       public MyThread(List<String> queue, String word) {
  15.          this.queue = queue;
  16.          this.word = word;
  17.       }
  18.  
  19.       @Override
  20.       public void run() {
  21.          for (int i = 0; i < 10000; i++) {
  22.             queue.add(word);
  23.          }
  24.       }
  25.    }
  26.  
  27.    // 메인 로직
  28.    private void run() {
  29.       ArrayList<String> queue = new ArrayList<String>();
  30.       MyThread m1 = new MyThread(queue, "테스트1");
  31.       MyThread m2 = new MyThread(queue, "테스트2");
  32.       MyThread m3 = new MyThread(queue, "테스트3");
  33.       m1.start();
  34.       m2.start();
  35.       m3.start();
  36.    }
  37. }


위와 같은 코드를 실행 해보면(전체 선택한 뒤 아무 패키지에나 붙여 넣으면 실행해 볼 수 있다), 바로 다음과 같은 에러를 볼 수 있다.

Exception in thread "Thread-2" java.lang.ArrayIndexOutOfBoundsException: 11621
   at java.util.ArrayList.add(ArrayList.java:352)
   at snippet.MultiThread$MyThread.run(MultiThread.java:23)
Exception in thread "Thread-0" java.lang.ArrayIndexOutOfBoundsException: 2294
   at java.util.ArrayList.add(ArrayList.java:352)
   at snippet.MultiThread$MyThread.run(MultiThread.java:23)
ERROR: JDWP Unable to get JNI 1.2 environment, jvm->GetEnv() return code = -2
JDWP exit error AGENT_ERROR_NO_JNI_ENV(183):  [../../../src/share/back/util.c:820]


운이 좋으면 아예 에러가 나지 않거나, 완전히 다른 종류의 에러가 날 수도있다. 병렬 프로그래밍의 신명나는 백미는 이렇게 재현 불가능한 케이스로 개발자를 미치게 한다는 점이다.

이런 에러가 나는 이유는 하나의 List<String>을 여러 스레드가 동시에 접근하기 때문에, 접근 시점에 따라 문제가 발생하는 것이다.

Java의 synchronized 키워드 사용하기

  1. import java.util.ArrayList;
  2. import java.util.List;
  3.  
  4. public class MultiThread {
  5.    public static void main(String[] args) {
  6.       new MultiThread().run();
  7.    }
  8.  
  9.    private class MyThread extends Thread {
  10.       private final List<String> queue;
  11.       private final String word;
  12.  
  13.       public MyThread(List<String> queue, String word) {
  14.          this.queue = queue;
  15.          this.word = word;
  16.       }
  17.  
  18.       @Override
  19.       public void run() {
  20.          for (int i = 0; i < 10000; i++) {
  21.             synchronized (queue) {
  22.                queue.add(word);
  23.             }
  24.          }
  25.       }
  26.    }
  27.  
  28.    private void run() {
  29.       ArrayList<String> queue = new ArrayList<String>();
  30.       MyThread m1 = new MyThread(queue, "테스트1");
  31.       MyThread m2 = new MyThread(queue, "테스트2");
  32.       MyThread m3 = new MyThread(queue, "테스트3");
  33.       m1.start();
  34.       m2.start();
  35.       m3.start();
  36.    }
  37. }


자바의 synchronized 키워드를 이용하면 이 문제를 꽤 손쉽게 해결 할 수 있다. 이렇게 하면, 각 스레드들은 22번 줄을 실행하기전 queue의 모니터를 먼저 획득하고, 수행후 모니터를 놓아준다. 모니터가 이미 다른 스레드가 가지고 있는 경우라면, 모니터를 획득할 수 있을 때 까지 대기하게 된다.

그런데 특정 스레드가 오랬동안 queue를 점유하며 놓아주지 않는 경우, 어떻게 대처를 해야 할까? 또 배타적으로 접근해야 될 자원이 하나가 아니라 여러개인 경우, 이에 대한 추상화를 어떻게 해야 할까?


Eclipse Job Manager

JFace의 JobManager는 이러한 록 매커니즘을 제공한다. 이를 이용하여 위의 예제를 다시 작성하면...


  1. import java.util.ArrayList;
  2. import java.util.List;
  3.  
  4. import org.eclipse.core.runtime.jobs.ILock;
  5. import org.eclipse.core.runtime.jobs.Job;
  6.  
  7. public class MultiThread {
  8.    public static void main(String[] args) {
  9.       new MultiThread().run();
  10.    }
  11.  
  12.    private class MyThread extends Thread {
  13.       private final List<String> queue;
  14.       private final String word;
  15.       private final ILock lock;
  16.  
  17.       public MyThread(ILock lock, List<String> queue, String word) {
  18.          this.lock = lock;
  19.          this.queue = queue;
  20.          this.word = word;
  21.       }
  22.  
  23.       @Override
  24.       public void run() {
  25.          for (int i = 0; i < 10000; i++) {
  26.             try {
  27.                lock.acquire(100);
  28.                queue.add(word);
  29.             } catch (InterruptedException e) {
  30.                System.out.println("0.1 초간 대기했지만, queue를 확보하지 못했습니다.");
  31.             } finally {
  32.                lock.release();
  33.             }
  34.          }
  35.       }
  36.    }
  37.  
  38.    private void run() {
  39.       ArrayList<String> queue = new ArrayList<String>();
  40.       ILock lock = Job.getJobManager().newLock();
  41.  
  42.       MyThread m1 = new MyThread(lock, queue, "테스트1");
  43.       MyThread m2 = new MyThread(lock, queue, "테스트2");
  44.       MyThread m3 = new MyThread(lock, queue, "테스트3");
  45.       m1.start();
  46.       m2.start();
  47.       m3.start();
  48.    }
  49. }


27 에서 록을 소유하기 위한 제한 시간을 지정할 수 있고 29실패시의 처리를 지정할 수 있다.32에서 록을 명시적으로 릴리즈하는 것도 눈여겨 보자.

이렇게하면, 교착 현상시 발생할 수 있는 기아도 제거할 수 있다. ILock의 기본 구현과 록 매커니즘이 순환 록 상황을 자동으로 검사하여, 해당 스레드에 익셉션을 발생시켜 탈출 시켜주기 때문이다. 따라서 작업이 정상적으로 수행안될지는 몰라도, 사용자가 영원히 끝나지 않는 작업을 기다릴 일은 발생하지 않게 된다.