java concurreny 는 어떻게 동기화를 구현하고 있을까!?

sun.misc.Unsafe

  • 자바는 safe 한 프로그래밍 언어이고 프로그래가 메모리와 관련된 이상한 실수를 하지 않도록 해준다. 하지만 의도적으로 그런 실수들을 하도록 만드는 방법이있다. 그것은 Unsafe 클래스를 사용하는 것이다.
    • JDK 내부적으로만 사용하지만 안전하지 않기 때문에 Userland 에서는 사용하지 않도록 권장하는 api 인것 같다.
  • sun.misc.Unsafe 는 public API 이다.

Unsafe instantiation

  • Unsafe object 는 private 생성자를 가지고 있으므로 new Unsafe() 를 통해 생성할 수가 없다.
  • getUnsafe() 라는 static 생성함수가 있지만 getUnSafe 를 호출 하려고 하면 SecurityException 이 일어나게 되있다. 이 메소드는 오직 trusted code 에서만 사용 가능하도록 되어있다.
1 public static Unsafe getUnsafe() {
2     Class cc = sun.reflect.Reflection.getCallerClass(2);
3     if (cc.getClassLoader() != null)
4         throw new SecurityException("Unsafe");
5     return theUnsafe;
6 }
  • bootclasspath 를 지정해줘서 Unsafe 를 사용하도록 해줄수 있다.

java -Xbootclasspath:/usr/jdk1.7.0/jre/lib/rt.jar:. test

Unsafe API

  • 105 가지의 함수들이 들어있다.
  • Info : low-level 의 메모리 정보를 리턴한다.
    • addressSize
    • pageSize
  • Objects : object 들의 메소드와 필드를 수정할수 있도록 해준다.
    • allocateInstance
    • objectFieldOffset
  • Classes : class 들의 메소드와 static 필드를 수정할수 있도록 해준다
    • staticFieldOffset
    • defineClass
    • defineAnonymousClass
    • ensureClassInitialized
  • Arrays. Arrays 수정
    • arrayBaseOffset
    • arrayIndexScale
  • Synchronization : low-level 의 원자성 도구제공
    • monitorEnter
    • tryMonitorEnter
    • monitorExit
    • compareAndSwapInt
    • putOrderedInt
  • Memory : 직접적으로 메모리에 접근하는 메소드
    • allocateMemory
    • copyMemory
    • freeMemory
    • getAddress
    • getInt
    • putInt

재미있는 사용 케이스

Avoid initialization

  • 생성자 호출 없이 object 생성
 1 class A {
 2     private long a; // 초기화 안된 변수
 3 
 4     public A() {
 5         this.a = 1; // 생성자에서 초기화
 6     }
 7 
 8     public long a() { return this.a; }
 9 }
10 
11 A o1 = new A(); // 생성자 호출
12 o1.a(); // prints 1
13 
14 A o2 = A.class.newInstance(); // reflection 으로 생성 -> 생성자 호출됨
15 o2.a(); // prints 1
16 
17 A o3 = (A) unsafe.allocateInstance(A.class); // unsafe -> 생성자 호출 안됨
18 o3.a(); // prints 0

Memory Conccuption

  • 직접적으로 메모리 수정을 통해 필드값 변경
 1 class Guard {
 2     private int ACCESS_ALLOWED = 1;
 3 
 4     public boolean giveAccess() {
 5         return 42 == ACCESS_ALLOWED; // 무조건 false 를 리턴하는 메소드
 6     }
 7 }
 8 Guard guard = new Guard();
 9 guard.giveAccess();   // false
10 
11 // bypass
12 Unsafe unsafe = getUnsafe();
13 Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
14 unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // 강제로 memory corruption
15 
16 guard.giveAccess(); // true
  • unsafe.putInt 를 통해 직접 메모리에 지정된 offset 을 통해 값을 수정할수 있다. ACCESS_ALLOWED 뒤에 추가적인 필드가 있다면 unsafe.putInt(guard, 16+unsafe.objectFieldOffset(f), 42) 를 통해 똑같이 수정 가능하다.

Concurrency

Unsafe.compareAndSwap 메소드는 atmoic 하다. 높은 수준에 lock-free 자료구조를 구현할때 사용된다! JDK 안에 ConcurrentHashMap AtomicInteger 등등이 Unsafe 를 통해 구현 되어있다.

  • Test!
 1 interface Counter {
 2     void increment();
 3     long getCounter();
 4 }
 5 
 6 // CounterClient.java 간단한 Counter Thread 
 7 class CounterClient implements Runnable {
 8     private Counter c;
 9     private int num;
10 
11     public CounterClient(Counter c, int num) {
12         this.c = c;
13         this.num = num;
14     }
15 
16     @Override
17     public void run() {
18         for (int i = 0; i < num; i++) {
19             c.increment();
20         }
21     }
22 }
23 
24 // main.java
25 int NUM_OF_THREADS = 1000;
26 int NUM_OF_INCREMENTS = 100000;
27 ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
28 Counter counter = ... // 테스트할 counter
29 long before = System.currentTimeMillis();
30 for (int i = 0; i < NUM_OF_THREADS; i++) {
31     service.submit(new CounterClient(counter, NUM_OF_INCREMENTS));
32 }
33 service.shutdown();
34 service.awaitTermination(1, TimeUnit.MINUTES);
35 long after = System.currentTimeMillis();
36 System.out.println("Counter result: " + c.getCounter());
37 System.out.println("Time passed in ms:" + (after - before));
  • 동기화가 구현안된 Counter 를 구현한다면 -> 빠르지만 틀린결과가 나온다.
 1 class StupidCounter implements Counter {
 2     private long counter = 0;
 3 
 4     @Override
 5     public void increment() {
 6         counter++;
 7     }
 8 
 9     @Override
10     public long getCounter() {
11         return counter;
12     }
13 }
14 // Counter result: 99542945 -> 100000000 값이 나와야 하는데 틀린값이 나온다. 동기화를 안했기 때문
15 // Time passed in ms: 679 -> 빠른 결과
  • synchroized 로 동기화 함수 만들면 -> 올바른 결과, 느린속도
 1 // 언어 차원에서 제공되는 synchrozied 키워드 사용
 2 class SyncCounter implements Counter {
 3     private long counter = 0;
 4 
 5     @Override
 6     public synchronized void increment() {
 7         counter++;
 8     }
 9 
10     @Override
11     public long getCounter() {
12         return counter;
13     }
14 }
15 // Counter result: 100000000 -> 올바른 결과
16 // Time passed in ms: 10136 -> 동기화 하지 않았을때보다 15배정도 느리다.
  • lock 를 통해 동기화를 조절한다면 -> 올바른 결과, 느르지만 좀더 빠름
 1 // lock 은 AbstractQueuedSynchronizer Queue 를 사용하고있고 AbstractQueuedSynchronizer는 내부적으로 Unsafe 로 구현됨
 2 class LockCounter implements Counter {
 3     private long counter = 0;
 4     private WriteLock lock = new ReentrantReadWriteLock().writeLock();
 5 
 6     @Override
 7     public void increment() {
 8         lock.lock();
 9         counter++;
10         lock.unlock();
11     }
12 
13     @Override
14     public long getCounter() {
15         return counter;
16     }
17 }
18 // Counter result: 100000000
19 // Time passed in ms: 8065 -> 좀더 빨리짐
  • Concrruent Atomic 자료 구조인 AtomicLong 사용 -> 좀더 빠름
 1 // AtomicLong 도 내부적으로 Unsafe 를 사용하고 있다.
 2 class AtomicCounter implements Counter {
 3     AtomicLong counter = new AtomicLong(0);
 4 
 5     @Override
 6     public void increment() {
 7         counter.incrementAndGet();
 8     }
 9 
10     @Override
11     public long getCounter() {
12         return counter.get();
13     }
14 }
15 // Counter result: 100000000
16 // Time passed in ms: 6454
  • 직접 CompareAndSwap 사용 -> 가장 빠르다.
 1 class CASCounter implements Counter {
 2     private volatile long counter = 0; // 무한 루프에 빠지는것을 방지, 컴파일 체적화를 수행하지않음
 3     private Unsafe unsafe;
 4     private long offset;
 5 
 6     public CASCounter() throws Exception {
 7         unsafe = getUnsafe();
 8         offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
 9     }
10 
11     @Override
12     public void increment() {
13         long before = counter;
14         while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
15             before = counter;
16         }
17     }
18 
19     @Override
20     public long getCounter() {
21         return counter;
22     }
23 }
24 // Counter result: 100000000
25 // Time passed in ms: 6454
  • compareAndSwapLong > Atomic > Lock > synchroized
  • compareAndSwapLong (CAS) 의 동작 방식
    • 어떤 상태값을 가지고있다
    • 그것의 복사본을 생성한다.
    • 그것을 수정하려고한다.
    • 이미 수정이 되어있지 않다면 수정한다. 수정이 되어있다면 실패한다.
      • 실패했다면 반복한다.
  • 좀더 직접적으로 CAS 를 사용할수록 성능 향상을 가져올수있다.

결론

  • 좀더 성능향상을 가져올수 있지만 Unsafe 는 직접적으로 사용하지 말자
    • 컴파일 하더라도 warning 메세지가 나온다.

Reference