spin lock 구현
- 락 획득 까지 반복문 수행해 CPU 타임 계속 사용함으로써 스레드가 RUNNABLE 상태를 유지해 동기화
- 스레드 스위칭에 따른 오버헤드 제러 하는 방식으로 향상된 성능 얻는 방식
- 임계 영역 코드 짧고 스레드 개수 적을수록 유리
- 구현하기에 따라 오히려 성능 저하 될 수 있음
- CAS로 구현
LockSupport.parkNanos()이걸 도입하면 스핀락에서 더 나은성능을 뽐낸다. 반복이 과도한것도 성능저하의 원인이 될 수 있기 때문…
클로드가 작성해준 코드 예제
AtomicBoolean
import java.util.concurrent.atomic.AtomicBoolean;
/**
* AtomicBoolean을 이용한 간단한 스핀락 구현
*
* 재진입을 지원하지 않는 가장 기본적인 형태의 스핀락입니다.
* 구현이 단순하고 이해하기 쉬우며, 오버헤드가 최소화됩니다.
*/
public class SimpleSpinLock {
/**
* 락의 상태를 나타내는 AtomicBoolean
* false: 락이 해제된 상태 (사용 가능)
* true: 락이 사용 중인 상태
*/
private final AtomicBoolean locked = new AtomicBoolean(false);
/**
* 락을 획득합니다.
*
* 동작 원리:
* 1. CAS를 통해 locked를 false에서 true로 변경 시도
* 2. 성공하면 락 획득 완료
* 3. 실패하면 스핀하면서 계속 시도
*/
public void lock() {
// 스핀 루프: 락을 획득할 때까지 계속 시도
while (true) {
// CAS 연산: locked가 false이면 true로 변경
// 성공하면 true 반환, 실패하면 false 반환
if (locked.compareAndSet(false, true)) {
// 락 획득 성공!
return;
}
// 락 획득 실패 - 다른 스레드에게 실행 기회 양보
// CPU 사용률을 줄이고 시스템 응답성을 개선
Thread.yield();
}
}
/**
* 논블로킹 방식으로 락 획득을 시도합니다.
*
* @return 락 획득 성공 시 true, 실패 시 false
*/
public boolean tryLock() {
// 한 번만 CAS 시도, 실패하면 즉시 false 반환
return locked.compareAndSet(false, true);
}
/**
* 락을 해제합니다.
*
* AtomicBoolean.set(false)는 volatile write로 동작하여
* 다른 스레드에게 즉시 변경사항을 알립니다.
*/
public void unlock() {
// 락 해제: 단순히 false로 설정
locked.set(false);
}
/**
* 현재 락이 사용 중인지 확인합니다.
*
* @return 락이 사용 중이면 true, 그렇지 않으면 false
*/
public boolean isLocked() {
return locked.get();
}
@Override
public String toString() {
return "SimpleSpinLock[locked=" + locked.get() + "]";
}
}
/**
* 더욱 최적화된 버전의 스핀락
*
* 백오프(backoff) 전략을 사용하여 CPU 사용률을 더욱 줄입니다.
*/
class BackoffSpinLock {
private final AtomicBoolean locked = new AtomicBoolean(false);
/**
* 백오프 전략을 사용한 락 획득
*
* 연속된 실패 시 대기 시간을 점진적으로 증가시켜
* CPU 사용률을 줄이고 경합을 완화합니다.
*/
public void lock() {
int backoff = 1; // 초기 백오프 값
while (true) {
if (locked.compareAndSet(false, true)) {
return; // 락 획득 성공
}
// 백오프 대기: 실패할 때마다 대기 시간 증가
for (int i = 0; i < backoff; i++) {
Thread.yield();
}
// 백오프 값을 점진적으로 증가 (최대 64까지)
backoff = Math.min(backoff * 2, 64);
}
}
public boolean tryLock() {
return locked.compareAndSet(false, true);
}
public void unlock() {
locked.set(false);
}
public boolean isLocked() {
return locked.get();
}
}
/**
* 간단한 스핀락 사용 예제 및 테스트
*/
class SimpleSpinLockExample {
private static final SimpleSpinLock spinLock = new SimpleSpinLock();
private static int sharedCounter = 0;
public static void main(String[] args) throws InterruptedException {
System.out.println("=== 간단한 스핀락 테스트 ===");
// 동시성 테스트
testConcurrency();
// 성능 비교 테스트
performanceComparison();
// tryLock 테스트
testTryLock();
}
/**
* 동시성 테스트: 여러 스레드가 동시에 카운터를 증가
*/
private static void testConcurrency() throws InterruptedException {
System.out.println("\n--- 동시성 테스트 ---");
sharedCounter = 0;
Thread[] threads = new Thread[10];
int iterationsPerThread = 1000;
long startTime = System.currentTimeMillis();
for (int i = 0; i < threads.length; i++) {
final int threadId = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < iterationsPerThread; j++) {
spinLock.lock();
try {
// 임계구역: 공유 자원 접근
sharedCounter++;
} finally {
spinLock.unlock();
}
}
System.out.println("스레드 " + threadId + " 완료");
});
}
// 모든 스레드 실행
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
int expectedValue = threads.length * iterationsPerThread;
System.out.println("실행 시간: " + (endTime - startTime) + "ms");
System.out.println("최종 카운터: " + sharedCounter);
System.out.println("예상 값: " + expectedValue);
System.out.println("결과: " + (sharedCounter == expectedValue ? "성공" : "실패"));
}
/**
* 성능 비교: SimpleSpinLock vs BackoffSpinLock
*/
private static void performanceComparison() throws InterruptedException {
System.out.println("\n--- 성능 비교 테스트 ---");
// SimpleSpinLock 테스트
long simpleTime = measurePerformance(new SimpleSpinLock(), "SimpleSpinLock");
// BackoffSpinLock 테스트
long backoffTime = measurePerformance(new BackoffSpinLock(), "BackoffSpinLock");
System.out.println("성능 차이: " + Math.abs(simpleTime - backoffTime) + "ms");
}
private static long measurePerformance(Object lock, String lockType) throws InterruptedException {
final int THREAD_COUNT = 4;
final int ITERATIONS = 10000;
final AtomicBoolean isSimple = new AtomicBoolean(lock instanceof SimpleSpinLock);
Thread[] threads = new Thread[THREAD_COUNT];
long startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < ITERATIONS; j++) {
if (isSimple.get()) {
SimpleSpinLock sl = (SimpleSpinLock) lock;
sl.lock();
try {
// 짧은 작업 시뮬레이션
} finally {
sl.unlock();
}
} else {
BackoffSpinLock bl = (BackoffSpinLock) lock;
bl.lock();
try {
// 짧은 작업 시뮬레이션
} finally {
bl.unlock();
}
}
}
});
}
for (Thread thread : threads) {
thread.start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
System.out.println(lockType + " 실행 시간: " + duration + "ms");
return duration;
}
/**
* tryLock 기능 테스트
*/
private static void testTryLock() {
System.out.println("\n--- tryLock 테스트 ---");
SimpleSpinLock testLock = new SimpleSpinLock();
// 첫 번째 tryLock - 성공해야 함
boolean firstTry = testLock.tryLock();
System.out.println("첫 번째 tryLock: " + (firstTry ? "성공" : "실패"));
// 두 번째 tryLock - 실패해야 함 (이미 락이 사용 중)
boolean secondTry = testLock.tryLock();
System.out.println("두 번째 tryLock: " + (secondTry ? "성공" : "실패"));
// 락 해제
testLock.unlock();
// 세 번째 tryLock - 다시 성공해야 함
boolean thirdTry = testLock.tryLock();
System.out.println("세 번째 tryLock: " + (thirdTry ? "성공" : "실패"));
testLock.unlock();
System.out.println("tryLock 테스트 완료");
}
}AtomicReference
import java.util.concurrent.atomic.AtomicReference;
/**
* AtomicReference를 이용한 스핀락 구현
*
* 스핀락(SpinLock)은 락을 획득할 때까지 계속해서 체크하는 방식의 동기화 메커니즘입니다.
* CPU 자원을 소모하지만 컨텍스트 스위칭 오버헤드가 없어 짧은 임계구역에 적합합니다.
*/
public class SpinLock {
/**
* 락의 상태를 나타내는 AtomicReference
* null: 락이 해제된 상태 (사용 가능)
* Thread 객체: 해당 스레드가 락을 소유한 상태
*/
private final AtomicReference<Thread> owner = new AtomicReference<>(null);
/**
* 재진입(reentrant) 지원을 위한 카운터
* 같은 스레드가 여러 번 락을 획득할 수 있도록 합니다.
*/
private volatile int reentrantCount = 0;
/**
* 락을 획득합니다.
*
* 동작 원리:
* 1. 현재 스레드가 이미 락을 소유하고 있다면 reentrant count만 증가
* 2. 그렇지 않다면 CAS를 통해 락 획득을 시도
* 3. 실패하면 계속 스핀(반복 체크)하여 대기
*/
public void lock() {
Thread currentThread = Thread.currentThread();
// 재진입 체크: 현재 스레드가 이미 락을 소유하고 있는가?
if (owner.get() == currentThread) {
reentrantCount++;
return;
}
// 스핀 루프: 락을 획득할 때까지 계속 시도
while (true) {
// CAS 연산: owner가 null이면 currentThread로 변경
// 성공하면 true 반환, 실패하면 false 반환
if (owner.compareAndSet(null, currentThread)) {
// 락 획득 성공!
reentrantCount = 1; // 첫 번째 획득이므로 1로 설정
return;
}
// 락 획득 실패 - 짧은 시간 대기 후 다시 시도
// Thread.yield()는 다른 스레드에게 실행 기회를 양보
// CPU 사용률을 약간 줄이는 효과가 있습니다.
Thread.yield();
}
}
/**
* 논블로킹 방식으로 락 획득을 시도합니다.
*
* @return 락 획득 성공 시 true, 실패 시 false
*/
public boolean tryLock() {
Thread currentThread = Thread.currentThread();
// 재진입 체크
if (owner.get() == currentThread) {
reentrantCount++;
return true;
}
// 한 번만 CAS 시도, 실패하면 즉시 false 반환
if (owner.compareAndSet(null, currentThread)) {
reentrantCount = 1;
return true;
}
return false;
}
/**
* 락을 해제합니다.
*
* 동작 원리:
* 1. 현재 스레드가 락의 소유자인지 확인
* 2. reentrant count를 감소시킴
* 3. count가 0이 되면 실제로 락을 해제 (owner를 null로 설정)
*
* @throws IllegalMonitorStateException 락을 소유하지 않은 스레드가 해제를 시도할 때
*/
public void unlock() {
Thread currentThread = Thread.currentThread();
// 락의 소유자 확인
if (owner.get() != currentThread) {
throw new IllegalMonitorStateException(
"현재 스레드가 락을 소유하고 있지 않습니다: " + currentThread.getName()
);
}
// 재진입 카운트 감소
reentrantCount--;
// 모든 재진입이 해제되었으면 락을 완전히 해제
if (reentrantCount == 0) {
// volatile write를 통해 다른 스레드에게 즉시 변경사항을 알림
owner.set(null);
}
}
/**
* 현재 락이 사용 중인지 확인합니다.
*
* @return 락이 사용 중이면 true, 그렇지 않으면 false
*/
public boolean isLocked() {
return owner.get() != null;
}
/**
* 현재 스레드가 락을 소유하고 있는지 확인합니다.
*
* @return 현재 스레드가 락을 소유하고 있으면 true
*/
public boolean isHeldByCurrentThread() {
return owner.get() == Thread.currentThread();
}
/**
* 현재 재진입 횟수를 반환합니다.
*
* @return 재진입 횟수 (락을 소유하지 않으면 0)
*/
public int getHoldCount() {
return owner.get() == Thread.currentThread() ? reentrantCount : 0;
}
/**
* 락을 소유한 스레드의 정보를 반환합니다.
*
* @return 락을 소유한 스레드, 락이 해제되어 있으면 null
*/
public Thread getOwner() {
return owner.get();
}
@Override
public String toString() {
Thread ownerThread = owner.get();
return String.format("SpinLock[owner=%s, reentrantCount=%d]",
ownerThread != null ? ownerThread.getName() : "null",
reentrantCount);
}
}
/**
* 스핀락 사용 예제 및 테스트 클래스
*/
class SpinLockExample {
private static final SpinLock spinLock = new SpinLock();
private static int sharedCounter = 0;
public static void main(String[] args) throws InterruptedException {
System.out.println("=== 스핀락 테스트 시작 ===");
// 5개의 스레드가 동시에 카운터를 증가시키는 테스트
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; i++) {
final int threadId = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
// 스핀락을 사용한 임계구역
spinLock.lock();
try {
// 공유 자원에 대한 안전한 접근
int temp = sharedCounter;
Thread.yield(); // 컨텍스트 스위칭 유도
sharedCounter = temp + 1;
// 재진입 테스트
testReentrant();
} finally {
// 반드시 finally 블록에서 락 해제
spinLock.unlock();
}
}
System.out.println("스레드 " + threadId + " 완료");
}, "Worker-" + i);
}
// 모든 스레드 시작
long startTime = System.currentTimeMillis();
for (Thread thread : threads) {
thread.start();
}
// 모든 스레드 완료 대기
for (Thread thread : threads) {
thread.join();
}
long endTime = System.currentTimeMillis();
System.out.println("=== 테스트 결과 ===");
System.out.println("최종 카운터 값: " + sharedCounter);
System.out.println("예상 값: " + (threads.length * 1000));
System.out.println("실행 시간: " + (endTime - startTime) + "ms");
System.out.println("동기화 " + (sharedCounter == threads.length * 1000 ? "성공" : "실패"));
}
/**
* 재진입(reentrant) 기능을 테스트합니다.
*/
private static void testReentrant() {
// 이미 락을 소유한 상태에서 다시 락을 획득
spinLock.lock();
try {
// 재진입된 상태에서의 작업
// (실제로는 의미있는 작업을 수행)
} finally {
spinLock.unlock();
}
}
}