Pages

2014년 12월 11일 목요일

[JAVA] Thread safe와 none Thread safe의 비교



Thread 관련된 문제는 항상 강조하는 게 "버그 잡기 힘들다." 입니다.

1. 원자성


원자성이란 CPU가 처리하는 하나의 단일 연산을 말합니다.

int a=1; 과 같은 코드는 원자성입니다.
int a=1+1; 도 원자성입니다.
그러나
a++;은 원자성이 아닙니다.
a++은 a= a+1 과 같은 의미인데, (a값을 읽는다) + (읽은 값에 1을 더해서 a에 대입한다.) 라는 2개의 원자성 연산의 조합입니다.
원자성을 가진 연산이 2개 이상 있을 때 그것은 원자성이 보장되지 않습니다. 아래의 코드를 실행시켜보세요.
public class AtomicTest {
    private int a = 0;
    public int incrementAndGet() {
        return a++;
    }
    public static void main(String[] args) {
        final AtomicTest test = new AtomicTest();
        for (int i = 0; i < 100; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        System.out.println(test.incrementAndGet());
                    }
                }
            }.start();
        }
    }
}

100개의 Thread가 1000번씩 증가시켰으면 100,000이 나와야 할 것 같지만 보통 그 보다 좀 덜 나옵니다.

초기값이 0인 a에 대해 a++연산을 Thread A, B가 처리할 경우,
1. A Thread가 a의 값을 읽음. (a=0)
2. B Thread가 a의 값을 읽음. (a=0)
3. A Thread가 a의 값 계산 후 대입. (a=0+1 = 1);
4. B Thread가 a의 값 계산 후 대입. (a=0+1 = 1);
와 같이 흘러가면 결국 a=1이 됩니다. 두번 돌았으니 2가 되어야 하지만 말입니다.

원자성 연산의 조합은 절대 원자성 연산이 아닙니다.




2. Thread safe


Thread safe란 여러 개의 쓰레드에서 동시에 호출해도 문제가 되지 않는 클래스입니다. Thread safe에 대해서 정확히 알려면, 변수의 scope과 자바의 메모리 관리 등에 대해서 좀 알아야 합니다. 자바 기초 책에 나오는 클래스 변수, 지역 변수 등을 명확히 이해해야 하며, 메인 메모리와 워킹 메모리에 대해서 대략적인 가닥은 잡으셔야 합니다.


Thread safe 하게 프로그램을 만들려면 일반적으로 synchronized 구문을 씁니다.

StringBuffer 와 StringBuilder, Vector 와 ArrayList, Hashtable과 HashMap 같은 것들이 synchronized가 걸려있고 안걸려 있고 정도의 차이만을 보이는 대표적인 클래스들입니다.(물론, 그게 전부는 아니지만 핵심입니다.)

StringBuffer, Vector, Hashtable 등이 자바 초기에 등장한 애들입니다. 얘들은 synchronized가 걸려있는 대표적인 애들입니다. synchronized가 걸리면 안 걸린 것에 비해서 당연히 느립니다. 즉, 정말 synchronized 가 걸려있어야 하는지를 보고 그렇다면 그런 애들을 쓰면 됩니다.


먼저 StringBuffer는 일반적으로 쓸 일이 없습니다. 여러 개의 Thread에서 하나의 String을 조합하는 것은 일반적으로 굉장히 이상한 일입니다. 다만 StringBuffer 자체를 인자로 받는 라이브러리를 쓸 때는 어쩔 수 없지만 일반적으로는 쓸 일이 없습니다. 보통은 StringBuilder를 사용하면 됩니다.


Vector와 Hashtable 등을 쓸 수는 있지만, iterator 등을 돌 때는 주의해야 합니다. iterator를 돌면서 추가 삭제를 하는 것은 위험합니다. 다음 프로그램은 에러를 발생시킵니다.(쓰레드 관련 프로그램이 늘 그렇듯... 안 발생할 수도 있습니다.)
public class VectorIteratorTest {
    public static void main(String[] args) {
        final Vector < Integer > a = new Vector < Integer > ();
        for (int i = 0; i < 100; i++) {
            a.add(i);
        }

        new Thread() {
            public void run() {
                for (int i = 100; i < 1000; i++) {
                    a.add(i);
                }
            }
        }.start();

        for (Integer integer: a) {
            System.out.println(integer);
        }
    }
}

Exception in thread "main" java.util.ConcurrentModificationException

 at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
 at java.util.AbstractList$Itr.next(AbstractList.java:343)
 at thread.VectorIteratorTest.main(VectorIteratorTest.java:24)

요런일이 생깁니다. Thread safe 하기 때문에 문제가 안 될 것 같지만, iterator 부분이 문제 입니다. 일단 요기 를 읽어보시고...

전부 단일 연산인 것 같지만 iterator를 도는 게 단일 연산인 것은 아니기 때문에 생기는 문제입니다. 저걸 단일 연산으로 맞추고 싶다면 iterator 바깥부분을 synchronized (a) { } 로 감싸주어야 합니다.


3. 잘못 쓰기 쉬운 라이브러리


date format을 처리하기 위해 SimpleDateFormat 클래스를 씁니다. 이놈은 thread safe 하지 않습니다. new 로 매번 생성하거나 생성된 객체를 clone 떠서 사용하기를 권장합니다.


Jaxb Marshaller는 xsd에서 나온 bean 객체를 이용하여 xml을 만들어낼 때 쓰는 놈인데, 이 놈도 thread safe 하지 않습니다. 게다가 이놈은 생성비용도 굉장히 비쌉니다. 생성 과정을 전부 살펴보진 않아서 정확히는 모르겠지만, 동시에 수백개를 생성하게 되면 생성 속도가 몇 초 단위로 나오더군요. 이건 pool을 사용해서 처리했습니다. jakarta common에 있는 common-pool 을 사용했습니다.


Servlet이나 JSP는 Flyweight 로 구현되었습니다. 즉 instance가 1개이기 때문에 동시에 여러 thread가 접근하면 문제가 생길 수 있습니다. 흔히 디비에서 조회한 결과를 멤버 변수에 저장하는 실수를 범하곤 하는데, 인스턴스가 1개이기 때문에 엉뚱한 사람이 그 결과를 보게 될 수 있습니다. 즉 Servlet이나 JSP는 Thread safe하게 제작되어야 합니다.!
다음 예제는 잘못된 서블릿입니다.

public class WrongServlet extends HttpServlet {
    private static final long serialVersionUID = 1 L;
    private String userName;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        fillUserName(request);
        response.getWriter().write(userName);
    }
    private void fillUserName(HttpServletRequest request) {
        userName = (String) request.getSession().getAttribute("USER_NAME");
    }
}

여러 개의 request가 들어오면 A thread가 set한 userName을 B thread가 읽어 들일 수 있습니다. 


4. Thread 관련된 것은 꼭 테스트를!!

Thread에 관련된 문제는 말씀드렸다시피 잘못 짜도 일반적으로 발생하지 않습니다. 어느 순간 발생하기도 하기 때문에 잡아내기가 정말 힘듭니다. 결과의 정합성에 대해서 "Thread 차원에서" 테스트를 반드시 거치기 바랍니다.


댓글 없음:

댓글 쓰기