[자바] ReentrantReadWriteLock lock(), tryLock() 훑어보기

최대한·2021년 3월 31일
2

개요

최근 Practical 모던 자바 책을 재미있게 읽고 있었는데, 그 중 6장 병렬 프로그래밍을 보면서 이전에 얼핏 회사 이사님께서 ReentrantLock 에 대해 설명해주신 것이 생각나 정리해보려고 한다.

1. 기존 lock 방식

자바 5버전의 concurrent 패키지가 나오기 전에는 synchronized block 을 이용하여 다음과 같은 방식으로 락을 거는 방식을 사용했었다.

1) lock 을 걸지 않았을 경우

package com.vitamax;

import java.util.stream.IntStream;

public class OldJavaMain {

    static Integer count = 1;

    public static void main(String[] args) {
        Thread a = new Thread(addCount("A "));
        Thread b = new Thread(addCount("B "));
        Thread c = new Thread(addCount("C "));
        Thread d = new Thread(addCount("D "));
        Thread e = new Thread(addCount("E "));
        a.start();
        b.start();
        c.start();
        d.start();
        e.start();
    }

    static Runnable addCount(String threadName){
        return () -> IntStream.range(0, 20000)
                .forEach(i -> {
                    System.out.println(threadName + count++);
                });
    }
}

...
A 99973
A 99974
Process finished with exit code 0

2) synchronized lock 을 걸었을 경우

...
static Runnable addCount(String threadName){
        return () -> IntStream.range(0, 20000)
                .forEach(i -> {
                    synchronized (OldJavaMain.class){
                        System.out.println(threadName + count++);
                    }
                });
}

...
B 99999
B 100000
Process finished with exit code 0

3) synchronized lock 의 한계

이와같이 sychronized 키워드만으로도 thread-safe 한 작업은 가능했다. 하지만 synchronized block 에 진입 및 탈출 자체에 비용이 소모되기 때문에 남발하게 될 경우 성능 상에 문제가 될 수 있고 세세한 작업 또한 어려움이 있었다.

2. Java 5 Concurrent

위와같은 한계와 개발자들이 일일이 구현하기 어려운 문제가 있었지만, jdk 5 부터는 concurrent API 를 지원하게 되면서 비교적 간단하면서도 효율적이고 세세하게 스레드를 다룰 수 있게 되었다.

1) executorService 를 이용한 쓰레드 사용

package com.vitamax;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

public class NewJavaMain {

    static int count = 1;

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        executorService.execute(addCount("A "));
        executorService.execute(addCount("B "));
        executorService.execute(addCount("C "));
        executorService.execute(addCount("D "));
        executorService.execute(addCount("E "));
        executorService.shutdown();
    }

    static Runnable addCount(String threadName){
        return () -> IntStream.range(0, 20000)
                              .forEach(i -> {
                                        System.out.println(threadName + count++);
                              });
    }
}

...
A 99997
A 99998
Process finished with exit code 0

2) ReentrantReadWriteLock.WriteLock 의 lock() 사용

package com.vitamax;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.IntStream;

public class NewJavaMain {

    static int count = 1;

    static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    static Lock writeLock = lock.writeLock();

    ...

    static Runnable addCount(String threadName){
        return () -> IntStream.range(0, 20000)
                              .forEach(i -> {
                                  try {
                                    writeLock.lock();
                                    System.out.println(threadName + count++);
                                  } finally {
                                    writeLock.unlock();
                                  }
                              });
    }
}

...
E 99999
E 100000
Process finished with exit code 0

3) tryLock() 사용

    ...

    static Runnable addCount(String threadName){
        return 
        () -> IntStream
                .range(0, 20000)
                .forEach(i -> {
                  if (writeLock.tryLock()){
                      try {
                        System.out.println(threadName + count++);
                      } finally {
                        writeLock.unlock();
                      }
                  } else {
                      System.out.println(threadName + "failed to acquire, count = " + count);
                  }
                });
    }
}

...
E failed to acquire, count = 11598
E failed to acquire, count = 11598
B 11597
B 11598
B 11599
B 11600
E failed to acquire, count = 11598
E failed to acquire, count = 11602
...
D failed to acquire, count = 11602
B 11601
B 11602
...
B 19907
B 19908
Process finished with exit code 0

4) tryLock(time, timeUnit) 사용

...
    static Runnable addCount(String threadName){
        return
        () -> IntStream
                .range(0, 20000)
                .forEach(i -> {
                      try {
                          if (writeLock.tryLock(1, TimeUnit.SECONDS)){
                            System.out.println(threadName + count++);
                          } else {
                            System.out.println(threadName + "failed to acquire, count = " + count);
                          }
                      } catch (InterruptedException e ) {
                      } finally {
                        writeLock.unlock();
                      }
                });
    }
}

A 1
A 2
A 3
...
B 15525
B 15526
A 15527
A 15528
...
B 64348
B 64349
C 64350
...
D 99999
D 100000
Process finished with exit code 0

5) 결과

기존 synchronized block 을 사용하여 쓰레드의 락을 걸어줬을 때처럼 writeLocklock(), unlock() 을 이용하여 간단하게 쓰레드를 제어할 수 있음을 확인했다.
메서드 내부의 원하는 로직에만 쓰레드 락을 걸어줄 수도 있었다.

3) 과 4) 의 tryLock() 을 이용했을 때는 상이한 결과가 나왓는데 그 이유는 다음과 같다.

  • tryLock() 의 경우 해당 쓰레드가 tryLock() 이 실행되는 시점에 lock 을 취득할 수 있는지 파악한다.

    Acquires the lock only if it is free at the time of invocation. 문서

  • tryLock(time, timeUnit) 의 경우 주어진 시간 내에 lock 을 취득하거나 interrupt 당하지 않을 경우

    Acquires the lock if it is free within the given waiting time and the current thread has not been interrupted. 문서

3) 은 여러 쓰레드가 lock 을 현 상황에서 바로 취득 가능 여부를 판단하기 때문에, 대부분의 경우 선점중인 lock 이 있어서 false 반환
4) 의 경우 주어진 시간 내에 lock() 을 취득하기 때문에 정상적으로 작업을 수행함

결론

기존 sychronized block 을 이용하여 thread lock 을 제어하는데에 비용이 들어가고 세세한 컨트롤이 어려웠었다. 하지만 Java 5부터 추가된 Concurrent API 을 이용할 경우 비교적 쉽고 세세하게 제어가 가능해졌다. lock(), tryLock() 외에도 lockInterruptibly() 등의 여러 메서드들이 있으니 천천히 공부해봐야겠다.

profile
Awesome Dev!

0개의 댓글