2018년 11월 6일 화요일

[java][thread][unsafe] 자바가 숨겨놓은 악마의 열매 sun.misc.Unsafe - 1

sun.misc.Unsafe

이름부터 강력함을 뽐내는 이 클래스는 아무나 쓸 수 없는 녀석이다.
사실 이 강력함은 우리를 다치게 할 수 있다. 그 어떤 강력한 것도 항상 제물이 필요하다.

열정을 더 불태우기 위해 내 몸을 불 속에 몸을 던지는 행위와 같이
이 세상 모든 뜨거운 것에는 장작이 필요하다.

Unsafe가 그러하다. 자바를 더욱 핫하게 만들어줄 이 녀석은 사실 우리는 쓰지 못하도록 되어 있다.
이 강력한 힘은 사용자가 쓰지는 못하도록 만든 것이다.
더 나아가, 필자의 Eclipse에서는 아예 존재하지 않는 것처럼 보인다.

너무 강력하고 위험하기 때문에, 아예 import조차 못하도록 막은 것이다. (해당 설정을 바꾸면 가능하다)

자바에서도 C와 같은 강력한 기능을 사용하려면 JNI를 이용하여 C언어 개발을 해야 할 것이다.
하지만 C를 모르고 알고 싶지도 않은 사람이 있을 것이다. 하지만 java.misc.Unsafe를 이용하면 Java API를 이용하여
low-level 프로그래밍을 할 수 있게 해준다.

일단 Unsafe를 내가 찾게 된 연유를 보자. 나는 AtomicInteger라는 녀석의 구현을 보고 싶었다.
어느때와 같이 AtomicInteger에서 숫자를 변경하는 메소드를 보았으나. 내 눈앞에 보이는건 Unsafe클래스였다.
안으로 들어가도 바이트코드만 보이는 것을 보고. 뭔가 심각한 녀석임을 눈치챘다.

녀석은 생성자가 private으로 만들어져 있다. 그리고 싱글턴 인스턴스가 private으로 존재한다. 그러므로 getUnsafe라는 녀석이 있는 것으로 봐서
녀석을 실행시키면 된다고 생각했다. 하지만 예외만 던져질 뿐이었다. 왜 그러한지 한 번 둘러보자.
public static Unsafe getUnsafe() {
  Class cc = sun.reflect.Reflection.getCallerClass(2);
  if (cc.getClassLoader() != null)
    throw new SecurityException("Unsafe");
  return theUnsafe;
}
여기서 볼 수 있듯이 일단 theUnsafe라는 녀석이 싱글턴 인스턴스다.
그리고 눈에 보이는 이상한 것들이 있는데 바로 sun.reflection.Reflection 이다.
이녀석은 현재 스레드의 메소드 스택을 확인하여 주어진 숫자 만큼 call-stack-frame 밑으로 내려가서 해당 메소드의 클래스를 리턴한다.
(이 구현을 변경될 수도 있다) 지금 숫자에는 2가 들어있으니까 콜스택의 3번째 값을 가져온다.

index 0. Reflection (getCallerClass)
index 1. Usnafe (getUnsafe)
index 2. Unsafe를 실행하는 나의 어플 내에 있는 메소드

이게 index 2 값을 가져와서 getClassLoader를 실행시킨다. 소스 상에서는 null이어야 한다.
null 레퍼런스는 HotSpot virtual machine 위에 bootstrap class loader를 가리킨다. (이 내용은 {@link Class#getClassLoader()}에 문서화 되어 있다.

아니면 예외를 던진다. (좀 더 자세한 내용은 참고 깃허브 참고)


하지만 트릭은 언제나 존재한다.

바로 이렇게 콜스택으로 체크 하는 방식이 아주 대충 디자인 된 것이라고 한다.
바로 리플렉션의 사용을 막지 않았기 때문에 바로 가져오면 된다.
public void getUnsafe() throws Exception {
  Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
  theUnsafe.setAccessible(true);
  Unsafe unsafe = (Unsafe) theUnsafe.get(null);
  System.out.println(unsafe);
}
이렇게 하면 되지만 안드로이드에서는 Unsafe클래스의 이름이 "theUnsafe"가 아니라. THE_ONE이라는 이름으로 불린다고 하니 이런 코드는 플랫폼에 의존적이다.
다르게 가져와보자.
public Unsafe getUnsafeInstance() throws Exception {
  Constructor unsafeConstructor = Unsafe.class.getDeclaredConstructor();
  Unsafe unsafe = unsafeConstructor.newInstance();
  System.out.println(unsafe);
  return unsafe;
}

좋다. 이렇게 가져와 봤다.
이게 얼마나 위험하기에 이렇게 힘겹게(?) 가져와야 하는 것일까?

해당 내용은 다음에 만들 이야기

[java][thread][unsafe] 자바가 숨겨놓은 악마의 열매 sun.misc.Unsafe - 2

에서 확인해보자.

2018년 11월 5일 월요일

[java][thread] volatile 대체 어디서 쓸만한 걸까?

참고 사이트 (내 깃허브): https://github.com/ssisksl77/java-demo/tree/master/src/main/java/concurrent/volatile_test

volatile은 멀티스레드에서 값들을 캐시하지않고 변경된 내용을 바로바로 가져올 수 있게 해준다.
오! 그렇다면 아무 곳에나 volatile만 넣어준다면 마법처럼 동기화가 되면서 공유상태를 사용할 수 있는 것일까?
아래 소스코드와 결과를 보면 그렇제 않은 것 같다.
package volatile_test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VolatileTest1 {
 private volatile static int counter = 0;
 
 public static void main(String[] args) {
  Runnable r1 = () -> { System.out.println(counter); counter++;};
  
  ExecutorService es = Executors.newCachedThreadPool();
  
  for(int i = 0; i < 10; i++) {
   es.execute(r1);
  }
  
  es.shutdown();
//              OUTPUT
//  0
//  0
//  0
//  3
//  4
//  5
//  6
//  7
//  8
//  9

 }
}
0이 3번이나 나왔다. 왜그럴까? 특이한 것은 결과적으로 +1이 잘 되었지만 조회가 잘 안되었다는 것인데 다시 한 번 해보자.
0
0
0
0
0
0
6
6
8
8
전혀 다른 값이 나왔다. 그 말은 값이 달라졌다는 것이다. volatile을 사용한다고 동기화가 되는 것이 아니다. 읽기-수정-쓰기 가 동기화가 되야 하는 것이다. 그렇다면 volatile은 언제 써야 할까? 아래와 같은 경우를 보자.
package volatile_test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VolatileTest2 {
 // 아래 내용을 보자. 과연 잘 돌아가는 경우가 얼마나 있을까?
 private static boolean flag = true;
 
 public static void main(String[] args) {
  Runnable r1 = () -> { 
   while(flag) {
     // 여기에 막히게 된다. 
   }
   System.out.println("bye");
  };
  
  Runnable r2 = () -> { System.out.println("flag false start"); flag = false;};
  ExecutorService es = Executors.newCachedThreadPool();
  
  for(int i = 0; i < 10; i++) {
   es.execute(r1);
  }
  es.execute(r2);
  
  es.shutdown();

 }
}
여기서는 전혀 돌아가지는 않는다. 이것은 volatile의 부재일 수도 있지만, 컴파일러 마다 최적화를 하면서 결과값이 다르게 나올 수도 있다고 한다. 여튼 위 내용은 현재 flag를 false로 변경했지만 먹히지 않는다. 이때 volatile을 넣어보자.
package volatile_test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class VolatileTest3 {
 // 이럴때 volatile로 바꾸는 것만으로 효과가 있다.
 private volatile static boolean flag = true;
 
 public static void main(String[] args) {
  Runnable r1 = () -> { 
   while(flag) {
    // System.out.println("hi");
   }
   System.out.println("bye");
  };
  
  Runnable r2 = () -> { System.out.println("flag false start"); flag = false;};
  ExecutorService es = Executors.newCachedThreadPool();
  
  for(int i = 0; i < 10; i++) {
   es.execute(r1);
  }
  es.execute(r2);
  
  es.shutdown();

 }
}