The document discusses various worker thread models and provides examples of using concurrency in Java. It also summarizes the java.util.concurrent package, which provides common high-level concurrency classes like ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue, CountDownLatch and Executor. The package was introduced in Java 5 to make concurrent programming easier and more robust.
Barrier.java public class Barrier { /* * count 와 reset 필드는 busy wait 을 할 때 체크되므로 , 다른 쓰레드가 * 값을 변경한 것이 항상 반영되도록 하기 위해서는 volatile 로 선언해야 한다 . */ private volatile int count; // sync 까지 남은 쓰레드 개수 private volatile boolean reset; // reset 상태인지 여부 private int resetCount; // sync 할 총 쓰레드 개수 private int waiters; // wait 상태의 쓰레드 개수 public Barrier(int count) { this.count = count; resetCount = count; reset = true; waiters = 0; } public void sync() { /* * sync() 루틴을 완료한 쓰레드들 중 아직 Barrier 가 reset 되기 전에 * 다시 sync() 루틴으로 들어온 경우에는 * 마지막으로 깨어난 쓰레드가 Barrier 를 reset 할 때까지 기다려야 한다 . * 이 경우에는 별도의 wait/notify 를 사용하기보다는 * 잠깐동안 busy wait 을 한다 . */ if (!reset && count == 0) { int loop = 0; // busy wait 하는 횟수를 보기 위한 디버깅 변수 while (true) { /* * 일찍 깨어난 쓰레드들은 CPU 를 양보하면서 reset 되길 기다리게 구현할 수도 있다 . * 이때 context switch 되므로 volatile 을 사용할 필요가 없다 . */ // Thread.yield(); // 메시지를 출력하기 위해 별도로 체크하였다 . // 메시지 출력이 필요없으면 loop 변수를 없애고 밖의 if 문을 while 문으로 바꾸면 된다 . if (reset || count != 0) { // reset 과 count 가 동기화 블록 밖에서 busy wait 하면서 체크되므로 volatile 로 선언되어야 한다 . System.out.println(Thread.currentThread().getName() + " end busy waiting after " + loop + " loops."); break; } loop++; } } synchronized (this) { // Barrier 객체에 대해서 동기화 count--; // Barrier 에 들어온 마지막 쓰레드인 경우 // 기다리던 모든 쓰레드들을 깨운다 . if (count == 0) { reset = false; // waking up all waiters System.out.println(Thread.currentThread().getName() + " is the last thread and waking up all waiters..."); notifyAll(); return; } // Barrier 에 들어온 쓰레드들 중 마지막이 아닌 쓰레드들은 // 모두 마지막 쓰레드가 깨워줄 때까지 기다린다 . while (count != 0) { ++waiters; System.out.println(Thread.currentThread().getName() + " waiting"); try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } --waiters; } // 기다리던 쓰레드들 중 마지막으로 깨어난 쓰레드는 // 모두 깨어나서 다음을 실행하도록 Barrier 를 reset 한다 . if (waiters == 0) { System.out.println(Thread.currentThread().getName() + " resets the barrier ..."); count = resetCount; // count 값을 초기화 reset = true; } } // end of synchronized (this) } }
BarrierTest.java public class BarrierTest { private static final int NUM_THREADS = 5; // 방벽 구조를 사용하여 동기화할 쓰레드 개수 public static void main(String[] args) { Thread[] threads = new Thread[NUM_THREADS]; Barrier barrier = new Barrier(NUM_THREADS); // 쓰레드 갯수 크기의 Barrier 를 생성한다 // 테스트 쓰레드 생성 for (int i = 0; i < NUM_THREADS; i++) { threads[i] = new TestThread(i, barrier); } // 각 쓰레드를 실행한다 . for (int i = 0; i < NUM_THREADS; i++) { threads[i].start(); } // 모든 쓰레드가 종료할 때까지 기다린다 . for (int i = 0; i < NUM_THREADS; i++) { try { threads[i].join(); } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * 방벽 구조를 테스트하는 쓰레드 */ class TestThread extends Thread { private static final int SYNC_COUNT = 3; // 방벽을 사용하여 동기화할 횟수 private Barrier barrier = null; TestThread(int id, Barrier barrier) { super(&quot;test-&quot; + id); // 쓰레드 이름 지정 this.barrier = barrier; } public void run() { for (int i = 0; i < SYNC_COUNT; i++) { System.out.println(getName() + &quot; before syncing &quot; + i + &quot;th time.&quot;); barrier.sync(); System.out.println(getName() + &quot; after syncing &quot; + i + &quot;th time.&quot;); } } }
Queue : Producer-Consumer pattern. One shared work queue among all the consumers Deque : double ended queue, Work stealing pattern. A consumer can steal work from the tail of someone else ’ s deque if he exhausts his work usually in a situation when the consumers are also producers. Annoying ConcurrentModificationException!!!
boolean add(java.lang.Object e) Inserts the specified element into this queue if it is possible to do so immediately without violating capacity restrictions, returning true upon success and throwing an IllegalStateException if no space is currently available. java.lang.Object element() throws NoSuchElementException Retrieves, but does not remove, the head of this queue. This method differs from peek only in that it throws an exception if this queue is empty. boolean offer(java.lang.Object e) Inserts the specified element into this queue if it is possible to do so immediately without violating capacity restrictions. When using a capacity-restricted queue, this method is generally preferable to add(java.lang.Object) , which can fail to insert an element only by throwing an exception. java.lang.Object peek() Retrieves, but does not remove, the head of this queue, or returns null if this queue is empty. java.lang.Object poll() Retrieves and removes the head of this queue, or returns null if this queue is empty. java.lang.Object remove() throws NoSuchElementException Retrieves and removes the head of this queue. This method differs from poll only in that it throws an exception if this queue is empty. is empty.
CountDownLatch CountDownLatch 클래스는 CyclicBarrier 클래스와 유사하게 해당 객체에 대해 await() 메쏘드를 호출하면 조건이 충족될 때까지 블로킹되지만 CyclicBarrier 가 await() 를 호출한 쓰레드의 갯수에 따라 블로킹을 풀어주는 반면 , CountDownLatch 는 countDown() 메쏘드가 호출되는 횟수에 따라 블로킹을 풀어주게 된다는 점이 다르다 . 또 CyclicBarrier 객체는 reset() 메쏘드를 제공해 다시 사용할 수 있지만 CountDownLatch 객체는 한번만 사용할 수 있다 . [ 래치 ( 회로 )( 어떤 입력에 대한 출력 상태를 다른 입력이 있을 때까지 유지하는 논리 회로 ).] 세마포어 세마포어는 내부적으로 숫자 값을 유지하고 있는데 이 값은 얼마든지 커질 수 있지만 0 보다 작을 수는 없다 . 세마포어는 acquire 와 release 두 개의 연산을 지원하며 , acquire 시에는 숫자 값을 1 감소시키고 , release 시에는 숫자 값을 1 증가시키되 숫자 값이 0 이 되면 acquire 를 요청한 쓰레드가 무한히 기다리도록 하는 방식으로 동작한다 . 이렇게 함으로써 결과적으로 동시에 접근하는 쓰레드의 갯수를 지정한 숫자 값으로 제한하게 된다 . Barrier 방벽 구조 (Barrier) 는 여러 쓰레드가 서로의 수행을 동기화하기 위해 기다리는 동기화 지점을 나타내는 구조이다 . 서로 협업하는 쓰레드들이 모두 방벽 지점에 이를 때까지 기다렸다가 모두 도착하면 다음 단계로 넘어가는 방식으로 진행된다 . 즉 , 마지막 쓰레드가 방벽 지점에 도착할 때까지 먼저 온 쓰레드는 기다리고 있다가 도착하는 순간 모두 다시 진행을 계속한다 . 즉 , 일정 개수의 쓰레드가 도달할 때까지 방벽이 각 쓰레드의 진행을 막고 있다가 일정 개수의 쓰레드가 도달하는 순간 도달한 쓰레드를 모두 방벽을 지나가도록 통과시키는 구조이다 . 이 구조는 선원 모델 (work crew model 혹은 divide and conquer model) 을 구현하는 데 많이 사용된다 . 선원 모델에서는 하나의 작업을 여러 조각의 부분 작업으로 나누어 여러 개의 쓰레드가 한 조각씩 동시에 수행하는 방식으로 작업을 수행한다 . 각 쓰레드는 다른 쓰레드의 작업 진행 상태에 의존하지 않고 독자적으로 작업을 진행한다 . 선원 모델의 예는 어떤 영역에 걸쳐 자료를 검색하는 작업이 있을 때 영역을 분리하여 각 영역별로 선원 쓰레드를 만들어 검색하는 것을 생각해볼 수 있다 . 각 부분 영역을 맡고 있는 쓰레드들이 모두 작업을 완료하면 부분 작업 결과들을 통합하여 전체 작업 결과가 나온다 . 일반적으로 선원 모델을 사용할 경우 각 선원 쓰레드별로 종료 시간이 다르므로 모든 선원 쓰레드가 종료한 후 각 결과물들을 종합하는 방식으로 진행하게 된다 . CyclicBarrier 클래스는 방벽 구조를 잘 구현한 클래스로 도달한 각 쓰레드는 CyclicBarrier 객체의 await() 메쏘드를 호출하고 지정된 정원이 찰 때까지 블로킹된다 . 정원이 차고 난 다음에는 CyclicBarrier 객체를 생성할 때 Runnable 객체가 지정한 경우 이 객체를 실행하여 결과물들을 종합하는 개념의 일을 할 수 있다 . CyclicBarrier 객체는 이 Runnable 을 수행한 후 블로킹됐던 모든 쓰레드를 풀어준다 . Exchanger Exchanger 클래스는 두 개의 쓰레드가 특정 지점에서 동일한 자료형의 데이터를 서로 교환할 수 있는 특별한 동기화 구조이다 . 두 개의 쓰레드가 동일한 Exchanger 객체에 대해 exchange() 메쏘드를 호출하면 두 쓰레드가 모두 호출할 때까지 블로킹되었다가 서로의 값을 교환한 후 계속해서 쓰레드가 진행하게 된다 .
Executor 인터페이스는 쓰레드를 실행시키는 새로운 방법을 정의하고 있다 . Thread 클래스의 start() 메쏘드를 호출하는 대신 좀더 직관적으로 Runnable 을 실행시키거나 Callable 을 실행시킨다 . Callable 은 Runnable 과 유사하나 리턴값을 가질 수 있으며 예외를 던질 수 있다 . Interface Executor { void execute(Runnable runnable); } interface Callable<V> { V call() throws Exception; } interface ThreadFactory { Thread newThread(Runnable r); } Future 인터페이스는 비동기적으로 쓰레드를 수행하고 다른 일을 수행한 후 , 나중에 그 결과 값을 가져올 수 있는 구조이다 . Executor 와 Future 사용 예제 public static void main(String[] args) throws Exception { FutureTask<String> future = new FutureTask<String>(new Callable<String>() { public String call() throws IOException { … ( 생략 ) // 뭔가 복잡한 일을 한다 . return &quot;Done&quot;; } }); ExecutorService executor = Executors.newSingleThreadExecutor(); for (int i = 0; i < 3; i++) { // future 를 실행한다 . executor.execute(future); // 여기에서 다른 일을 먼저 수행한다 . Thread.sleep(1000L); // future 에 담긴 결과를 가져온다 . 아직 future 의 실행이 종료되지 않은 경우 블럭된다 . System.out.println(&quot;Callable returned : &quot; + future.get()); } // executor 서비스를 종료한다 . executor.shutdown(); }