본문 바로가기
Java

Java Synchronized vs ReentrantLock

by TheUphill 2020. 3. 18.
Synchronized
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class MultiThreadTest {
 
   private static Logger logger = LoggerFactory.getLogger(MultiThreadTest.class);
 
   public static void main(String[] args) {
      Target target = new Target();
      new Thread(() -> target.criticalSection()).start();
      new Thread(() -> target.criticalSection()).start();
 
      Target2 target2 = new Target2();
      new Thread(() -> target2.criticalSection()).start();
      new Thread(() -> target2.criticalSection()).start();
   }
 
   static class Target {
      public synchronized void criticalSection() {
         logger.info("Enter Critical Section");
         try {
            Thread.sleep(3000);
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
         logger.info("Leave Critical Section");
      }
   }
 
   static class Target2 {
      public void criticalSection() {
         synchronized (this) {
            logger.info("Enter Scoped Critical Section");
            try {
               Thread.sleep(3000);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
            logger.info("Leave Scoped Critical Section");
         }
      }
   }
}
 
18:19:07.776 [Thread-0] INFO  MultiThreadTest - Enter Critical Section
18:19:07.776 [Thread-2] INFO  MultiThreadTest - Enter Scoped Critical Section
18:19:10.780 [Thread-0] INFO  MultiThreadTest - Leave Critical Section
18:19:10.780 [Thread-2] INFO  MultiThreadTest - Leave Scoped Critical Section
 
18:19:10.780 [Thread-1] INFO  MultiThreadTest - Enter Critical Section
18:19:10.780 [Thread-3] INFO  MultiThreadTest - Enter Scoped Critical Section
18:19:13.784 [Thread-3] INFO  MultiThreadTest - Leave Scoped Critical Section
18:19:13.784 [Thread-1] INFO  MultiThreadTest - Leave Critical Section
 
ReentrantLock
- 함수
  • lock() : 호출한 스레드가 해당 객체를 Lock. 만약 이미 Lock이 걸려있으면 무한정 기다린다.
  • boolean tryLock() : 호출한 스레드가 해당 객체를 Lock 시도하고 그 결과를 리턴한다. 만약 다른 Thread가 Lock했다면 호출 즉시 false를 리턴한다.
  • boolean trylock(long timeout, TimeUnit timeUnit) : 일정 시간동안 Lock을 얻기위해 기다린다.
  • getHoldCount() : 해당 스레드가 가지고 있는 lock 갯수 리턴
  • lockInterruptibly() :
  • isHeldByCurrentThread() : 현재 스레드가 Lock을 소유하고 있는지 여부 리턴.
  • unlock() : Lock을 반환
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.util.concurrent.locks.ReentrantLock;
 
public class ReentrantLockTest {
 
   private static Logger logger = LoggerFactory.getLogger(ReentrantLockTest.class);
 
   public static void main(String[] args) {
      ReentrantLock lock = new ReentrantLock();
      Program program = new Program(lock);
      new Thread(program).start();
      new Thread(program).start();
   }
 
   static class Program implements Runnable {
 
      private ReentrantLock lock;
 
      public Program(ReentrantLock lock) {
         this.lock = lock;
      }
 
      @Override
      public void run() {
         lockTest();
      }
 
      private void lockTest() {
         try {
            lock.lock();
 
            logger.info("Acquired Lock. lockCount={}", lock.getHoldCount());
            Thread.sleep(3000);
 
         } catch (Exception e) {
            //
         } finally {
            if (lock.isHeldByCurrentThread()) {
               lock.unlock();
               logger.info("Lock is released. lockCount={}", lock.getHoldCount());
            }
         }
      }
 
      private void tryLockTest() {
         try {
            if (lock.tryLock()) {
               logger.info("Acquired Lock. lockCount={}", lock.getHoldCount());
               Thread.sleep(3000);
            } else {
               logger.info("failed to get lock");
            }
         } catch (Exception e) {
            //
         } finally {
            if (lock.isHeldByCurrentThread()) {
               lock.unlock();
               logger.info("Lock is released. lockCount={}", lock.getHoldCount());
            }
         }
      }
   }
}
 
 
- tryLockTest 수행 결과
18:52:27.169 [Thread-1] INFO  ReentrantLockTest - failed to get lock
18:52:27.169 [Thread-0] INFO  ReentrantLockTest - Acquired Lock. lockCount=1
18:52:30.173 [Thread-0] INFO  ReentrantLockTest - Lock is released. lockCount=0
 
수행 결과 Thread1은 Lock을 획득하지 못하자 바로 종료됐다. 그리고 Thread0은 Lock을 획득해 Thread.sleep()까지 호출되고 종료됐다.
 
- lockTest 수행결과
18:51:38.951 [Thread-0] INFO  ReentrantLockTest - Acquired Lock. lockCount=1
18:51:41.959 [Thread-0] INFO  ReentrantLockTest - Lock is released. lockCount=0
18:51:41.959 [Thread-1] INFO  ReentrantLockTest - Acquired Lock. lockCount=1
18:51:44.960 [Thread-1] INFO  ReentrantLockTest - Lock is released. lockCount=0
 
먼저 실행된 Thread0이 Lock을 획득. 뒤에 실행된 Thread1은 Lock을 획득하기위해 대기.
 
- Lock 중복 획득
하나의 스레드가 Lock을 중복해서 얻을 수 있다. 위에 예제중 lockTest() 함수를 아래와 같이 고쳐서 lock을 두번 획득하도록 했다.
private void lockTest() {
   try {
      lock.lock();
      lock.lock();
      logger.info("Acquired Lock. lockCount={}", lock.getHoldCount());
      Thread.sleep(3000);
 
   } catch (Exception e) {
      //
   } finally {
      if (lock.isHeldByCurrentThread()) {
         lock.unlock();
         logger.info("Lock is released. lockCount={}", lock.getHoldCount());
      }
   }
}
 
Lock을 반납 하는 코드가 한번밖에 없기 때문에 2번 획득, 1번 반납 하기 때문에 결국 첫번째 수행한 스레드가 아직 Lock을 가지고 있다. 때문에 후에 실행된 스레드는 무한정 대기하기하게 된다.
 
- lockInterruptably() 
Lock을 획득하기 위해 대기하다가 Interrupt가 발생하면 대기를 중단한다. 
public static void main(String[] args) throws InterruptedException {
   ReentrantLock lock = new ReentrantLock();
   Program program = new Program(lock);
   Thread thread = new Thread(program);
   thread.start();
 
   Thread thread2 = new Thread(program);
   thread2.start();
 
   Thread.sleep(2000);
   thread2.interrupt();
}
 
private void lockTest() {
   try {
      lock.lockInterruptibly();
      if (lock.isHeldByCurrentThread()) {
         lock.lock();
      }
      logger.info("Acquired Lock. lockCount={}", lock.getHoldCount());
      Thread.sleep(3000);
 
   } catch (Exception e) {
      logger.error("Interrupted", e);
   } finally {
      if (lock.isHeldByCurrentThread()) {
         lock.unlock();
         logger.info("Lock is released. lockCount={}", lock.getHoldCount());
      }
   }
}
 
강조한 구문처럼 lockInterruptibly()를 호출했다. 그리고 두번째 스레드 시작 후 2초후에 Interrupt를 발생시키면 InterruptedException 발생하면서 두번째 스레드가 종료된다. 만약 lock()을 호출하는 경우 Interrupt를 발생시키더라도 계속 대기상태로 유지된다. Interrupt를 발생시키고 스레드 덤프를 남겨보면 Thread-1이 계속 WAITING 상태로 남아있다.
 
"Thread-1" #11 prio=5 os_prio=31 tid=0x00007f82310c2000 nid=0x5103 waiting on condition [0x0000700009941000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x0000000795e85aa8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
    at java.util.concurrent.locks.ReentrantLock$NonfairSync.lock(ReentrantLock.java:209)
    at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
    at ReentrantLockTest$Program.lockTest(ReentrantLockTest.java:38)
    at ReentrantLockTest$Program.run(ReentrantLockTest.java:33)
    at java.lang.Thread.run(Thread.java:748)
 
- Synchronized vs ReentrantLock
  • ReentrantLock은 Lock에 대한 더 많은 컨트롤을 지원한다. 다른 스레드가 이미 모니터를 가지고 있는 경우 일정시간 동안만 기다리거나, 아예 기다리지 않고 interrupt될 수 있다.
  • ReentractLock은 lockInterruptably() 함수를 통해 WAITING 상태의 스레드를 interrupt할 수 있다. 하지만 synchronized로 인해 WAITING 상태인 스레드는 interrupt가 불가능하다.
  • ReentrantLock은 Fairness가 가능하다. Fairness란 하나의 스레드가 자원을 독점하는것이 아닌 여러 스레드와 공유하는 개념.
 

댓글