java 병렬 프로 그래 밍 테마(5)---상세(JUC)ReentrantLock

지난 절우 리 는 Lock 인터페이스의 간단 한 설명 을 알 게 되 었 고 Lock 자물쇠 의 일반적인 형식 을 알 게 되 었 다.그러면 이 절 에서 우 리 는 본 격 적 으로 JUC 자물쇠(java.util.concurrent 가방 에 있 는 자물쇠,JUC 자물쇠 라 고 약칭)에 들 어가 기 시작 했다.락 이 가장 많이 사용 하 는 실현 류 인 ReentrantLock 을 살 펴 보 자.
1.ReentrantLock 안내
단어의 뜻 으로 우 리 는 이것 이 다시 들 어 갈 수 있다 는 뜻 이라는 것 을 알 수 있다.그렇다면 다시 들 어 갈 수 있 는 것 은 자물쇠 에 무엇 을 의미 하 는 것 일 까?쉽게 말 하면 자물쇠 와 관련 된 획득 카운터 가 있 습 니 다.자물쇠 가 있 는 한 스 레 드 가 다시 잠 겨 있 으 면 계수 기 를 얻 으 려 면 1 을 추가 하고 자 물 쇠 는 두 번 풀 려 야 진정 으로 풀 릴 수 있 습 니 다.이것 은 synchronized 의 의 미 를 모방 했다.스 레 드 가 이미 가지 고 있 는 모니터 가 보호 하 는 synchronized 블록 에 들 어가 면 스 레 드 가 계속 진행 할 수 있 습 니 다.스 레 드 가 두 번 째(또는 후속)synchronized 블록 을 종료 할 때 자 물 쇠 를 풀 지 않 고 스 레 드 가 들 어 오 는 모니터 가 보호 하 는 첫 번 째 synchronized 블록 을 종료 할 때 만 자 물 쇠 를 풀 수 있 습 니 다.
1.1 공평 자물쇠 와 불공평 자물쇠
ReentrantLock 의 소스 코드 를 보면 무 참 구조 함수 가 이 렇 습 니 다.

public ReentrantLock() {
 sync = new NonfairSync();
}
NonfairSync()방법 은 불공평 한 자물쇠 의 실현 방법 이 고 Reentrantlock 은 참조 가 있 는 구조 방법 이 있 습 니 다.

public ReentrantLock(boolean fair) {
 sync = fair ? new FairSync() : new NonfairSync();
}
공평(fair)자 물 쇠 를 원 하 는 지,불공평(unfair)자 물 쇠 를 원 하 는 지 선택 할 수 있 습 니 다.공정 한 자 물 쇠 는 스 레 드 를 요청 한 순서대로 자 물 쇠 를 얻 게 합 니 다.불공평 한 자 물 쇠 는 자 물 쇠 를 직접 가 져 올 수 있 습 니 다.이 경우 스 레 드 는 먼저 자 물 쇠 를 요청 한 다른 스 레 드 보다 먼저 자 물 쇠 를 가 져 올 수 있 습 니 다.
왜 우 리 는 모든 자물쇠 가 공평 하지 못 하 게 합 니까?어쨌든 공평 은 좋 은 일이 고 불공평 은 좋 지 않 은 것 이지 않 습 니까?아이들 이 결정 을 내 리 려 고 할 때,항상"이 건 불공평 해"라 고 외친다.우 리 는 공평 이 매우 중요 하 다 고 생각한다.아이들 도 안다.)현실 에서 자 물 쇠 는 매우 건장 한 자물쇠 로 매우 큰 성능 비용 을 보장 한다.공평 을 확보 하기 위해 필요 한 기장(bookeeping)과 동기 화 를 확보 하려 면 불공 정 자물쇠 의 삼투 율 보다 쟁탈 당 하 는 공평 자물쇠 가 더 낮 다 는 뜻 이다.기본 설정 으로서 공정 을 false 로 설정 해 야 합 니 다.공정 이 알고리즘 에 중요 하지 않 으 면 스 레 드 에 따라 줄 을 서 는 순서 로 서 비 스 를 해 야 합 니 다.
다음은 우리 가 먼저 예 를 하나 보 자.

public class TestReentrantLock implements Runnable{

 ReentrantLock lock = new ReentrantLock();

 public void get() {
  lock.lock();
  System.out.println(Thread.currentThread().getId());
  set();
  lock.unlock();
 }

 public void set() {
  lock.lock();
  System.out.println(Thread.currentThread().getId());
  lock.unlock();
 }

 @Override
 public void run() {
  get();
 }

 public static void main(String[] args) {
  TestReentrantLock ss = new TestReentrantLock();
  new Thread(ss).start();
  new Thread(ss).start();
  new Thread(ss).start();
 }
 }
실행 결과:
10
10
12
12
11
11
Process finished with exit code 0
결과적으로 우 리 는 같은 라인 이 같은 ReentrantLock 자물쇠 에 두 번 들 어 갔 음 을 알 수 있다.
2.condition 조건 변수
루트 Object 는 스 레 드 의 wait(),notify(),notify All()사이 에서 통신 하 는 특수 한 방법 을 포함 하고 있다 는 것 을 알 고 있 습 니 다.대상 에 wait 나 notify 를 위해 서 는 대상 의 자 물 쇠 를 가지 고 있어 야 합 니 다.Lock 이 동기 화 된 요약 인 것 처럼 Lock 프레임 워 크 는 wait 와 notify 에 대한 요약 을 포함 하 는데 이 요약 을 조건(Condition)이 라 고 한다.Condition 의 방법 은 wait,notify,notify All 방법 과 유사 하 며,각각 await,signal,signal All 이 라 고 명명 되 어 있 으 며,Object 의 대응 방법 을 덮어 쓸 수 없 기 때 문 입 니 다.
우선 우리 가 한 문 제 를 계산 해 보 자.
우 리 는 1 부터 9 까지 9 개의 숫자 를 인쇄 하려 고 합 니 다.A 스 레 드 에서 1,2,3 을 먼저 인쇄 한 다음 에 B 스 레 드 에서 4,5,6 을 인쇄 한 다음 에 A 스 레 드 에서 7,8,9 를 인쇄 합 니 다.이 문 제 는 여러 가지 해법 이 있 습 니 다.우 리 는 먼저 Object 의 wait,notify 방법 으로 이 루어 집 니 다.

public class WaitNotifyDemo {
 private volatile int val = 1;

 private synchronized void printAndIncrease() {
  System.out.println(Thread.currentThread().getName() +"prints " + val);
  val++;
 }

 // print 1,2,3 7,8,9
 public class PrinterA implements Runnable {
  @Override
  public void run() {
  while (val <= 3) {
   printAndIncrease();
  }

  // print 1,2,3 then notify printerB
  synchronized (WaitNotifyDemo.this) {
   System.out.println("PrinterA printed 1,2,3; notify PrinterB");
   WaitNotifyDemo.this.notify();
  }

  try {
   while (val <= 6) {
   synchronized (WaitNotifyDemo.this) {
    System.out.println("wait in printerA");
    WaitNotifyDemo.this.wait();
   }
   }
   System.out.println("wait end printerA");
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
  while (val <= 9) {
   printAndIncrease();
  }
  System.out.println("PrinterA exits");
  }
 }
 // print 4,5,6 after printA print 1,2,3
 public class PrinterB implements Runnable {

  @Override
  public void run() {
  while (val < 3) {
   synchronized (WaitNotifyDemo.this) {
   try {
    System.out
     .println("printerB wait for printerA printed 1,2,3");
    WaitNotifyDemo.this.wait();
    System.out
     .println("printerB waited for printerA printed 1,2,3");
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
   }
  }
  while (val <= 6) {
   printAndIncrease();
  }

  System.out.println("notify in printerB");
  synchronized (WaitNotifyDemo.this) {
   WaitNotifyDemo.this.notify();
  }
  System.out.println("notify end printerB");
  System.out.println("PrinterB exits.");
  }
 }
 public static void main(String[] args) {
  WaitNotifyDemo demo = new WaitNotifyDemo();
  demo.doPrint();
 }

 private void doPrint() {
  PrinterA pa = new PrinterA();
  PrinterB pb = new PrinterB();
  Thread a = new Thread(pa);
  a.setName("printerA");
  Thread b = new Thread(pb);
  b.setName("printerB");
  //    b     ,  b         ,    wait, a       ,  notify 
  b.start();
  a.start();
 }
 }
실행 결 과 는:
printerB wait for printerA printed 1,2,3
printerA prints 1
printerA prints 2
printerA prints 3
PrinterA printed 1,2,3; notify PrinterB
wait in printerA
printerB waited for printerA printed 1,2,3
printerB prints 4
printerB prints 5
printerB prints 6
notify in printerB
notify end printerB
wait end printerA
printerA prints 7
printerA prints 8
printerA prints 9
PrinterA exits
PrinterB exits.
Process finished with exit code 0
우 리 는 위의 절 차 를 분석 해 보 자.
먼저 main 방법 에서 우 리 는 B 스 레 드 를 먼저 시작 한 것 을 보 았 습 니 다.B 스 레 드 는 wait()대상 을 가지 고 있 고 A 스 레 드 는 notify()를 가지 고 있 기 때문에 A 를 먼저 시작 하면 잠 금 상태 가 될 수 있 습 니 다.
B 라인 시작 후 run()에 들 어 가 는 방법:

 while (val < 3) {
 synchronized (WaitNotifyDemo.this) {
  try {
  System.out.println("printerB wait for printerA printed 1,2,3");
  WaitNotifyDemo.this.wait();
  System.out.println("printerB waited for printerA printed 1,2,3");
  } catch (InterruptedException e) {
  e.printStackTrace();
  }
 }
 }
 while (val <= 6) {
 printAndIncrease();
 }
여기에 while 순환 이 있 습 니 다.val 의 값 이 3 보다 작 으 면 Wait Notify Demo 의 인 스 턴 스 동기 화 블록 에서 Wait Notify Demo.this.wait()방법 을 호출 합 니 다.wait 든 notify 든 notify,notify All 방법 은 인 스 턴 스 대상 의 동기 화 블록 에서 실행 되 어야 현재 스 레 드 가 동기 화 인 스 턴 스 의 동기 화 제어 권 을 얻 을 수 있 습 니 다.동기 블록 에서 wait 나 notify 방법 을 실행 하지 않 으 면 자바.lang.IllegalMonitor State Exception 이상 이 발생 합 니 다.또한 wait 방법 양쪽 의 동기 화 블록 은 wait 실행 이 끝 난 후에 대상 자 물 쇠 를 풀 수 있 음 을 주의해 야 합 니 다.
이렇게 PrinterB 는 대기 상태 에 들 어 갔 습 니 다.PrinterA 의 run 방법 을 살 펴 보 겠 습 니 다.

while (val <= 3) {
 printAndIncrease();
 }

// print 1,2,3 then notify printerB
synchronized (WaitNotifyDemo.this) {
 System.out.println("PrinterA printed 1,2,3; notify PrinterB");
 WaitNotifyDemo.this.notify();
}

try {
 while (val <= 6) {
 synchronized (WaitNotifyDemo.this) {
  System.out.println("wait in printerA");
  WaitNotifyDemo.this.wait();
 }
 }
 System.out.println("wait end printerA");
} catch (InterruptedException e) {
 e.printStackTrace();
}
여기 서 먼저 1,2,3 을 인쇄 한 다음 동기 블록 에서 Wait Notify Demo 인 스 턴 스 의 notify 방법 을 호출 했 습 니 다.그러면 PrinterB 는 계속 실행 하 라 는 통 지 를 받 은 다음 PrinterA 는 대기 상태 에 들 어가 PrinterB 통 지 를 기다 리 고 있 습 니 다.
PrinterB run 방법 나머지 코드 를 다시 보 겠 습 니 다.

while (val <= 6) {
 printAndIncrease();
}

System.out.println("notify in printerB");
synchronized (WaitNotifyDemo.this) {
 WaitNotifyDemo.this.notify();
}
System.out.println("notify end printerB");
System.out.println("PrinterB exits.");
PrinterB 는 먼저 4,5,6 을 인쇄 한 다음 동기 블록 에서 notify 방법 을 호출 하여 PrinterA 에 게 실행 을 시작 하 라 고 알 렸 다.
PrinterA 가 통 지 를 받 은 후 기다 리 는 것 을 멈 추고 나머지 7,8,9 세 개의 숫자 를 인쇄 합 니 다.다음은 PrinterA run 방법 에 남 은 코드 입 니 다.

while (val <= 9) {
 printAndIncrease();
}
전체 프로그램 이 분석 되 었 습 니 다.다음은 Condition 을 사용 하여 이 문 제 를 풀 겠 습 니 다.

public class TestCondition {
 static class NumberWrapper {
 public int value = 1;
 }

 public static void main(String[] args) {
 //       
 final Lock lock = new ReentrantLock();

 //            3
 final Condition reachThreeCondition = lock.newCondition();
 //            6
 final Condition reachSixCondition = lock.newCondition();

 //NumberWrapper          ,           ,      final
 //       Integer, Integer       
 final NumberWrapper num = new NumberWrapper();
 //   A  
 Thread threadA = new Thread(new Runnable() {
  @Override
  public void run() {
  //      
  lock.lock();
  try {
   System.out.println("threadA start write");
   //A      3  
   while (num.value <= 3) {
   System.out.println(num.value);
   num.value++;
   }
   //   3  signal,  B       
   reachThreeCondition.signal();
  } finally {
   lock.unlock();
  }
  lock.lock();
  try {
   //    6   
   reachSixCondition.await();
   System.out.println("threadA start write");
   //      
   while (num.value <= 9) {
   System.out.println(num.value);
   num.value++;
   }

  } catch (InterruptedException e) {
   e.printStackTrace();
  } finally {
   lock.unlock();
  }
  }

 });


 Thread threadB = new Thread(new Runnable() {
  @Override
  public void run() {
  try {
   lock.lock();

   while (num.value <= 3) {
   //  3       
   reachThreeCondition.await();
   }
  } catch (InterruptedException e) {
   e.printStackTrace();
  } finally {
   lock.unlock();
  }
  try {
   lock.lock();
   //      ,    4,5,6
   System.out.println("threadB start write");
   while (num.value <= 6) {
   System.out.println(num.value);
   num.value++;
   }
   //4,5,6    ,  A  6    
   reachSixCondition.signal();
  } finally {
   lock.unlock();
  }
  }

 });


 //      
 threadB.start();
 threadA.start();
 }
}
기본 적 인 사고방식 은 먼저 A 라인 이 1,2,3 을 써 야 한 다 는 것 이다.이때 B 라인 은 reach ThredCondition 신 호 를 기 다 려 야 한다.그리고 A 라인 이 3 을 다 쓴 후에 signal 을 통 해 B 라인 에 게'내 가 3 을 썼 으 니 네 차례 야'라 고 알려 야 한다.이때 A 라인 은 Sixcondition 신호 에 도달 할 때 까지 기 다 려 야 한다.이 동시에 B 라인 은 통 지 를 받 고 4,5,6 을 쓰기 시작한다.4,5 를 다 쓰 고 한다.6.그 후에 B 스 레 드 는 A 스 레 드 reach Sixcondition 조건 이 성립 되 었 다 고 알 렸 다.이때 A 스 레 드 는 나머지 7,8,9 를 쓰기 시작 했다.
우 리 는 상기 사례 에서 두 개의 Condition 을 만 들 었 고 서로 다른 상황 에서 서로 다른 Condition 을 사용 할 수 있 으 며 wait 와 notify 에 비해 더욱 세밀 한 통 제 를 제공 하 는 것 을 볼 수 있다.
3.스 레 드 차단 원 어 CLockSupport
우 리 는 스 레 드,자물쇠 등 개념 을 거듭 제기 하지만 자 물 쇠 는 실현 된다 면?현재 스 레 드 를 막 고 있 는 또 어떤 대상 인지 어떻게 압 니까?LockSupport 는 JDK 의 하위 클래스 로 자물쇠 와 다른 동기 화 도구 류 의 기본 스 레 드 차단 원 어 를 만 드 는 데 사 용 됩 니 다.
자바 잠 금 및 동기 화 프레임 워 크 의 핵심 AQS:AbstractQueued Synchronizer 는 LockSupport.park()와 LockSupport.unpark()를 호출 하여 스 레 드 의 차단 과 깨 우 는 것 입 니 다.LockSupport 는 이원 신 호 량(1 개의 허가증 만 사용 가능)과 유사 합 니 다.이 허가 가 아직 점용 되 지 않 았 다 면 현재 스 레 드 는 허 가 를 받 고 계속 실 행 됩 니 다.허가 가 점용 되 었 다 면 현재 스 레 드 가 막 혀 허 가 를 기다 리 고 있 습 니 다.
LockSupport 는 특정 스 레 드 를 대상 으로 차단 과 차단 해제 작업 을 합 니 다.Object 의 wait()/notify()/notify All()은 특정 대상 의 대기 집합 을 조작 하 는 데 사 용 됩 니 다.
LockSupport 의 두 가지 주요 방법 은 Park()와 Unpark()입 니 다.그들의 실현 을 살 펴 보 겠 습 니 다.

public static void park(Object blocker) {
 Thread t = Thread.currentThread();
 setBlocker(t, blocker);
 unsafe.park(false, 0L);
 setBlocker(t, null);
 }

public static void park() {
 unsafe.park(false, 0L);
 }

public static void unpark(Thread thread) {
 if (thread != null)
  unsafe.unpark(thread);
 }
원본 코드 에서 볼 수 있 듯 이 Park 방법 내부 에서 먼저 현재 스 레 드 를 얻 은 다음 에 현재 스 레 드 를 막 습 니 다.unpark 방법 은 설정 가능 한 스 레 드 로 전송 하여 이 스 레 드 의 잠 금 을 풀 수 있 습 니 다.'스 레 드'를 방법의 매개 변수 로 하여 의미 가 더욱 뚜렷 하고 사용 하기에 도 편리 하 다.한편,wait/notify 의 실현 으로 인해'스 레 드'의 차단/깨 우 는 것 은 스 레 드 자체 에 수 동적 입 니 다.어떤 스 레 드 를 정확하게 제어 하고 언제 차단/깨 우 는 것 이 어렵 습 니 다.무 작위 로 스 레 드(notify)를 깨 우지 않 으 면 모든(notify All)를 깨 우지 않 습 니 다.
다음은 예 를 들 어 보 겠 습 니 다.

public class TestLockSupport {

 public static Object u = new Object();
 static ChangeObjectThread t1 = new ChangeObjectThread("t1");
 static ChangeObjectThread t2 = new ChangeObjectThread("t2");

 public static class ChangeObjectThread extends Thread {
 public ChangeObjectThread(String name) {
  super.setName(name);
 }

 public void run() {
  synchronized (u) {
  System.out.println("in" + getName());
  LockSupport.park();
  }
 }
 }

 public static void main(String[] args) throws InterruptedException {
 t1.start();
 Thread.sleep(2000);
 t2.start();
 LockSupport.unpark(t1);
 LockSupport.unpark(t2);
 t1.join();
 t2.join();
 }
}
"LockSupport.unpark(t1);"이 한 마디 가 떨 어 지면 우 리 는 프로그램 이 자물쇠 에 빠 진 것 을 발견 할 것 이다.그리고 재 main 방법 에서 unpark 는 t1 과 t2 가 시 작 된 후에 야 실 행 된 것 을 보 았 습 니 다.그런데 왜 t1 이 시 작 된 후에 t2 도 시 작 된 것 입 니까?주의,**unpark 함 수 는 Park 보다 먼저 호출 할 수 있 습 니 다.예 를 들 어 스 레 드 B 는 unpark 함 수 를 호출 하여 스 레 드 A 에 게'허가'를 보 냈 습 니 다.그러면 스 레 드 A 가 Park 를 호출 할 때'허가'가 있 는 것 을 발견 하면 바로 다시 실행 할 것 입 니 다.**unpark 함 수 는 스 레 드 에'허가(permit)'를 제공 하고 스 레 드 호출 Park 함 수 는'허가'를 기다 리 고 있 습 니 다.이것 은 약간 신 호 량 과 같 지만 이'허가'는 중첩 할 수 없고'허가'는 일회 성 이다.예 를 들 어 스 레 드 B 는 unpark 함 수 를 세 번 연속 으로 호출 했 습 니 다.스 레 드 A 가 Park 함 수 를 호출 하면 이'허가'를 사용 하고 스 레 드 A 가 다시 Park 를 호출 하면 대기 상태 에 들 어 갑 니 다.
정시 차단 기능 외 에 중단 영향 도 지원 하지만 다른 수신 중단 함수 와 달리 던 지지 않 습 니 다.
Interrupted Exception 이상,그 는 묵묵히 돌아 올 뿐 이지 만,우 리 는 Thread.Interrupted()등 방법 으로 중단 표 시 를 받 을 수 있 습 니 다.
우 리 는 예 를 하나 보 자.

public class TestLockSupport {
 public static Object u = new Object();
 static ChangeObjectThread t1 = new ChangeObjectThread("t1");
 static ChangeObjectThread t2 = new ChangeObjectThread("t2");

 public static class ChangeObjectThread extends Thread {
 public ChangeObjectThread(String name) {
  super.setName(name);
 }

 public void run() {
  synchronized (u) {
  System.out.println("in " + getName());
  LockSupport.park();
  if (Thread.interrupted()) {
   System.out.println(getName() + "     !");
  }
  }
  System.out.println(getName() + "     ");
 }
 }

 public static void main(String[] args) throws InterruptedException {
 t1.start();
 Thread.sleep(100);
 t2.start();
 t1.interrupt();
 LockSupport.unpark(t2);
 }
}
출력:
in t1
t1 끊 겼 어!
t1 실행 종료
in t2
t2 실행 종료
Process finished with exit code 0
run 방법 에서 터미널 이상 캡 처 를 통 해 스 레 드 가 중단 되 었 을 때 이상 을 던 지지 않 고 정상적으로 실행 되 는 것 을 볼 수 있 습 니 다.
LockSupport 에 대해 사실은 소개 해 야 할 것 이 많 습 니 다.이런 것들 은 밑바닥 의 일부 방법 을 실 현 했 기 때문에 각종 자물쇠 의 실현 은 모두 이 를 바탕 으로 발 전 된 것 입 니 다.앞으로 jdk 내부 의 차단 체 제 를 한 장 으로 배 울 것 입 니 다.앞에서 우 리 는 Object 의 wait 와 notify,Condition 조건,jdk 에서 외부 에 노출 되 지 않 는 Lock Support 가 원 어 를 막 는 것 에 대해 이야기 했다.그러면 JUC 가방 에 또 다른 차단 체제 인 신 호 량 체제(Semaphore)가 있 으 니 다음 절 에 우리 가 함께 토론 해 보 자.
이상 은 자바 병렬 프로 그래 밍 테마(5)-상세(JUC)ReentrantLock 의 상세 한 내용 입 니 다.자바 ReentrantLock 에 관 한 자 료 는 저희 의 다른 관련 글 을 주목 하 세 요!

좋은 웹페이지 즐겨찾기