Java sun.misc.Unsafe
java concurreny 는 어떻게 동기화를 구현하고 있을까!?
- sun.misc.Unsafe 의 CAS(Compare-And-Swap) 을 통해 동기화를 구현하고 있다.
- CAS - http://en.wikipedia.org/wiki/Compare-and-swap
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
2
3
4
5
6
public static Unsafe getUnsafe() {
Class cc = sun.reflect.Reflection.getCallerClass(2);
if (cc.getClassLoader() != null)
throw new SecurityException("Unsafe");
return theUnsafe;
}
- 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
private long a; // 초기화 안된 변수
public A() {
this.a = 1; // 생성자에서 초기화
}
public long a() { return this.a; }
}
A o1 = new A(); // 생성자 호출
o1.a(); // prints 1
A o2 = A.class.newInstance(); // reflection 으로 생성 -> 생성자 호출됨
o2.a(); // prints 1
A o3 = (A) unsafe.allocateInstance(A.class); // unsafe -> 생성자 호출 안됨
o3.a(); // prints 0
Memory Conccuption
- 직접적으로 메모리 수정을 통해 필드값 변경
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Guard {
private int ACCESS_ALLOWED = 1;
public boolean giveAccess() {
return 42 == ACCESS_ALLOWED; // 무조건 false 를 리턴하는 메소드
}
}
Guard guard = new Guard();
guard.giveAccess(); // false
// bypass
Unsafe unsafe = getUnsafe();
Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // 강제로 memory corruption
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
interface Counter {
void increment();
long getCounter();
}
// CounterClient.java 간단한 Counter Thread
class CounterClient implements Runnable {
private Counter c;
private int num;
public CounterClient(Counter c, int num) {
this.c = c;
this.num = num;
}
@Override
public void run() {
for (int i = 0; i < num; i++) {
c.increment();
}
}
}
// main.java
int NUM_OF_THREADS = 1000;
int NUM_OF_INCREMENTS = 100000;
ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
Counter counter = ... // 테스트할 counter
long before = System.currentTimeMillis();
for (int i = 0; i < NUM_OF_THREADS; i++) {
service.submit(new CounterClient(counter, NUM_OF_INCREMENTS));
}
service.shutdown();
service.awaitTermination(1, TimeUnit.MINUTES);
long after = System.currentTimeMillis();
System.out.println("Counter result: " + c.getCounter());
System.out.println("Time passed in ms:" + (after - before));
- 동기화가 구현안된 Counter 를 구현한다면 -> 빠르지만 틀린결과가 나온다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class StupidCounter implements Counter {
private long counter = 0;
@Override
public void increment() {
counter++;
}
@Override
public long getCounter() {
return counter;
}
}
// Counter result: 99542945 -> 100000000 값이 나와야 하는데 틀린값이 나온다. 동기화를 안했기 때문
// Time passed in ms: 679 -> 빠른 결과
- synchroized 로 동기화 함수 만들면 -> 올바른 결과, 느린속도
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 언어 차원에서 제공되는 synchrozied 키워드 사용
class SyncCounter implements Counter {
private long counter = 0;
@Override
public synchronized void increment() {
counter++;
}
@Override
public long getCounter() {
return counter;
}
}
// Counter result: 100000000 -> 올바른 결과
// Time passed in ms: 10136 -> 동기화 하지 않았을때보다 15배정도 느리다.
- lock 를 통해 동기화를 조절한다면 -> 올바른 결과, 느르지만 좀더 빠름
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// lock 은 AbstractQueuedSynchronizer Queue 를 사용하고있고 AbstractQueuedSynchronizer는 내부적으로 Unsafe 로 구현됨
class LockCounter implements Counter {
private long counter = 0;
private WriteLock lock = new ReentrantReadWriteLock().writeLock();
@Override
public void increment() {
lock.lock();
counter++;
lock.unlock();
}
@Override
public long getCounter() {
return counter;
}
}
// Counter result: 100000000
// Time passed in ms: 8065 -> 좀더 빨리짐
- Concrruent Atomic 자료 구조인 AtomicLong 사용 -> 좀더 빠름
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// AtomicLong 도 내부적으로 Unsafe 를 사용하고 있다.
class AtomicCounter implements Counter {
AtomicLong counter = new AtomicLong(0);
@Override
public void increment() {
counter.incrementAndGet();
}
@Override
public long getCounter() {
return counter.get();
}
}
// Counter result: 100000000
// Time passed in ms: 6454
- 직접 CompareAndSwap 사용 -> 가장 빠르다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class CASCounter implements Counter {
private volatile long counter = 0; // 무한 루프에 빠지는것을 방지, 컴파일 체적화를 수행하지않음
private Unsafe unsafe;
private long offset;
public CASCounter() throws Exception {
unsafe = getUnsafe();
offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
}
@Override
public void increment() {
long before = counter;
while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
before = counter;
}
}
@Override
public long getCounter() {
return counter;
}
}
// Counter result: 100000000
// Time passed in ms: 6454
- compareAndSwapLong > Atomic > Lock > synchroized
- compareAndSwapLong (CAS) 의 동작 방식
- 어떤 상태값을 가지고있다
- 그것의 복사본을 생성한다.
- 그것을 수정하려고한다.
- 이미 수정이 되어있지 않다면 수정한다. 수정이 되어있다면 실패한다.
- 실패했다면 반복한다.
- 좀더 직접적으로 CAS 를 사용할수록 성능 향상을 가져올수있다.
결론
- 좀더 성능향상을 가져올수 있지만 Unsafe 는 직접적으로 사용하지 말자
- 컴파일 하더라도 warning 메세지가 나온다.