최근 Practical 모던 자바 책을 재미있게 읽고 있었는데, 그 중 6장 병렬 프로그래밍을 보면서 이전에 얼핏 회사 이사님께서 ReentrantLock 에 대해 설명해주신 것이 생각나 정리해보려고 한다.
자바 5버전의 concurrent 패키지가 나오기 전에는 synchronized block 을 이용하여 다음과 같은 방식으로 락을 거는 방식을 사용했었다.
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
...
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
이와같이 sychronized 키워드만으로도 thread-safe 한 작업은 가능했다. 하지만 synchronized block 에 진입 및 탈출 자체에 비용이 소모되기 때문에 남발하게 될 경우 성능 상에 문제가 될 수 있고 세세한 작업 또한 어려움이 있었다.
위와같은 한계와 개발자들이 일일이 구현하기 어려운 문제가 있었지만, jdk 5 부터는 concurrent API 를 지원하게 되면서 비교적 간단하면서도 효율적이고 세세하게 스레드를 다룰 수 있게 되었다.
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
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
...
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
...
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
기존 synchronized block
을 사용하여 쓰레드의 락을 걸어줬을 때처럼 writeLock
의 lock()
, 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() 등의 여러 메서드들이 있으니 천천히 공부해봐야겠다.