• [JAVA] 멀티 스레드

    2018. 3. 19. 21:02

    by. 위지원

    멀티스레드 프로그래밍이란?

    프로그램의 명령문이 실행되는 순서를 따라가다보면 길게 연결된 한 가닥 실과 같은 흐름을 스레드(한가닥 실이라는 뜻)라고 합니다.스레드를 한개만 가지고 있으면 싱글 스레드 스레드를 두개 이상 가지고 있으면 멀티 스레드라고 합니다.


    스레드에 대한 이미지 검색결과


    멀티스레드의 작동 방식

    처음 자동으로 시작되는 메인 스레드 외에 다른 스레드는 생성 위치에서 시작하여 끝이난다. 프로그램은 이 모든 스레드가 끝나야 끝이 난다.



    멀티스레드 프로그램의 작성 방법

    스레드 프로그램을 작성하기 위해선 Thread  class와 Runnable interface를 이용할 수 있다.




    Thread Class 이용

    예를 들어서 아래와 같은 영문/숫자 출력 프로그램 코드를 작성한다고 해봅시다




    결론 코드부터 보면 아래와 같습니다. 

    1) Thread class를 상속받는 class를 선언 한 뒤 이 스레드 클래스가 해야 할 일을 run 메서드안에 작성합니다.

    2) Thread class도 결과적으로 보면 객체이기때문에 시작할때에는 객체로 생성하고 start()라는 메서드로 시작을 해줍니다. (


    * DigitThread 객체로 받아도 되지만 Thread 객체로 받는것이 일반적

    * Thread 객체는 한번 생성해서 한번만start() 할 수 있습니다. 두번은lllegalThreadStateException 발생


            


    그런데, 위에 그림에서 보면 출력 순서가 엉망입니다. 위의 영문/숫자 출력을 계획한 그림을 보면 마치 동시에 실행되는 것 같아 보이지만 그렇지 않습니다. 사실은 스레드들이 번갈아가며 실행 되는 것입니다. 스레드에게 시간과 순서를 할당해주는건 자바 가상 기계가 하는 일입니다.


    CPU의 실행시간을 아주 잘게 쪼개서 스레드를 실행하여 마치 동시에 실행되게 하는데 이런 방식을 Round Robin방식이라고 합니다.


    Sleep 메서드

    Thread Class에 있는 메서드로 그냥 시간이 경과되는 것을 기다리며 정적 메서드이기 때문에 Thread.Sleep(); 으로 호출할 수 있습니다. 단 이 메서드는 InterruptedException(checked)이 발생하므로 Try-Catch로 체크를 해주어야 한다.




    여러개의 스레드를 만드는 멀티 스레드 프로그램


    아까 작성한 Digit Class에 이어서 main에 있던 실행문도 thread class로 따로 선언해줍니다.



    그리고 같은 방법으로 스레드 객체를 생성하고 시작해줍니다.






    Rnnable 인터페이스를 사용한 멀티 스레드 프로그램의 작성 방법

    방금 작성한 프로그램을 Runnable을 이용하여 작성해볼 수 있습니다. Thread class를 상속받는 방법과의 큰 차이점은 Runnable 인터페이스를 구현하는 클래스의 객체를 Thread 생성시 파라미터로 넘겨줘야 합니다. (line8)




    Runnable을 이용해야만 하는 경우

    제가 예전에 수업때도 배웠지만,, Thread Class를 상속받는 것보단 Runnable을 구현하는 것이 더 좋다.. 라고 들었는데요. 자바는 다중상속을 허용하지 않기때문에 확장성이 중요한 프로그램에서는 Runnable을 구현하는 것이 더 좋을 것 입니다.


    아래와 같이 두개의 클래스는 다른 패키지에 존재합니다. list 메서드는 protected로 선언 되어있기때문에 상속을 받아 서브클래스가 되어야만 사용할 수 있습니다. 그러기때문에 Numbers라는 클래스도 상속받아야하고 Thread Class도 만족시키기 위해서는 Runnable을 구현하는 방법밖에는 없습니다.




    결과는 다음과 같습니다.



    스레드간의 커뮤니케이션

    예를 들어 아래와 같은 흐름의 프로그램이 있을 때 공유 영역을 만들 수 있는 프로그램을 알아보겠습니다.





    스레드간의 데이터 교환

    공유 영역을 참조 타입으로 생성해서 각 스레드가 참조 값을 이용하여 영역을 참조할 수 있도록 하면 됩니다.


    아래의 코드처럼 SharedArea라는 공유 영역을 생성하여 각 스레드가 값을 공유할 수 있게합니다.




    하지만 이렇게하면 Print하는 메서드에서 공유 데이터가 입력되었나? 하면서 isReady필드를 계속 확인하게 될겁니다. 어차피 다른 스레드가 실행되기 전까지는 계속 같은 값일테니까 말입니다. 그래서 우리는 이전에 배웠던 sleep을 이용하여 다른 스레드로 흐름이 넘어가게끔 유도해서 부담을 조금 줄일 수 있습니다.


    성능을 위해 자바 컴파일러는 알아서 명령문을 줄이거나 생략하기도 한다고합니다. 예를 들어 방금 말한 isReady필드가 변경되지 않는 경우 그냥 그 값을 한번 가져다가 계속해서 체크하게 되기도 하죠. 그러나 이러면 자칫 무한루프가 되어버릴 수 있습니다. 때문에 volatile라는 키워드를 붙여 최적화 대상에서 제외시킬 수 있습니다.


    처음에 그냥 실행했을때는 아래와 같이 결과 값이 나오는데 한참 걸리는 것 같이 아무것도 안나오고 계속 실행 상태더군요 아마 무한루프에 빠진 것 같습니다. 그리고나서 volatile 키워드를 isReady에 붙였더니 바로 결과 값을 출력하였습니다.



    critical section

    하지만 이보다 좋은 방법이 있습니다. 한 스레드가 " 나 끝났어~ " 하고 다른 스레드에게 알려주는 것이지요. 이 방법을 알기전 먼저 critical section의 동기화를 알아보겠습니다.


    Critical section은 한 스레드가 공유 영역을 사용하고 있는데 급작스럽게 B스레드로 넘어가 문제가 생기는 부분을 이야기합니다. (스레드 실행 순서는 자바 가상 기계가 정해줌)


    이런 문제를 해결하기 위해 A가 공유 영역을 사용하는 동안 B가 사용하지 못하게 하는것이 Critical section의 동기화 (synchronization)이라고 합니다.


    멀티스레드에서의 동기화는 공유데이터를 다른 스레드가 사용하지 못하도록 하는것을 의미합니다.예를 들면 화장실에서 볼일 보고 있을 때 다른사람이 들어오면 안되니까 잠금 장치로 잠구어 버리는것과 같습니다.




    아래와 같은 흐름의 프로그램이 있다고 해봅시다.




    코드로 작성하면 아래와 같습니다. 공유 영역과 계좌 클래스를 작성하고



    돈을 이체하는 클래스와 계좌에 잔액을 총합하여 출력하는 클래스를 작성합니다.




    그리고나서 main 클래스에서는 계좌를 생성하여 이체를 해봅시다. 단순히 이체만 하는 코드이기때문에 전체 계좌 잔액은 동일해야겠죠?




    근데 결과는 그렇지 않습니다.  그 이유는 이체 클래스가 완료 되기전에 print 클래스가 실행되어버렸기 때문입니다.




    럴 때 다음과같이 56 line~ 59line을 critical section이라고 할 수 있습니다.



    Print의 73line도 critical section입니다.



    이 크리티컬 세션을 synchronized라는 키워드로 사용하여 동기화를 해줍니다.


    synchronized(공유 객체){}로 동기화 블럭을 생성할 수 있습니다. 자바 가상 기계는 공유 객체로 동기화 블럭을  동기화 블럭을 구분하기 때문에 공유객체를 입력해주어야 합니다. 이렇게 해야 해당 공유객체에 대해 동기화가 됩니다.





    이렇게하면 같은 값이 나옵니다. ( 위에 코드에 이체에서 B계좌에 0을 하나 빼먹었네요..따흣 )




    공유 객체에 메서드가 있다면 this를 이용해서 동기화를 할 수 도 있겠죠?



    아니면 이런 방법도 있습니다. 메서드 자체를 synchronized 키워드를 이용하여 동기화할 수 있게 하는겁니다.



    스레드간의 신호 전송

    데이터말고 신호를 주고받아봅시다.

    [notify() : 신호를 보내는] 와 [wait() : 신호를 기다리는] 를 이용합니다.


    아래가 notify와 wait를 이용한 코드입니다.  주의할 점은

    1) notify와 wait는 공유 객체에 의해 호출됩니다.

    2) notify와 wait는 반드시 동기화 블록안에 있어야합니다.




    아무 문제 없이 결과가 출력됩니다.



    대기하고 있는 모든 스레드에게 신호보내기

    notifyAll()을 사용하여 한 스레드가 다수의 스레드에게 신호를 보낼 수 있습니다.


    SharedArea의 notity를 notyfyAll로 바꾸고 class를 하나 더 추가해봅시다.






    처음에 notify() 였을때에는 한 클래스의 값이 나오고 무한 대..기..

    notifyAll()로 메서드를 변경하니 아무 문제 없이 두 클래스 다 값이 나옵니다.




    스레드의 상태

    자바 가상기계가 스레드 실행 순서나 시간을 정하기 때문에 코드만 보고는 예측 할 수는 없지만 실행 중 상태는 알 수 있습니다.


    스레드의 라이프 사이클




    스레드의 라이프 사이클

    스레드의 라이프 사이클은 다음과 같습니다.



    스레드의 상태는 함수로 알아볼 수 있습니다.Thread의 getSTate라는 메서드를 이용하면 됩니다.


    위의 코드에서 스레드의 상태를 모니터링하는 class를 하나 생성합니다. 그리고 나서 thread1의 상태를 모니터링 해봅시다.




    결과는 다음과 같습니다.



    이때 의미는 다음과 같다.



    책에 있는 코드를 좀만 변경해봅시다.  Runnable을 구현했을때는 Thread.currentThread().getName()으로 현재 스레드의 이름을 알 수 있습니다. Thread Class를 상속받은 경우에는 그냥 getName()을 하면 스레드의 이름을 알 수 있습니다.

    * 이름은 setName메서드로 지정할 수 있습니다.








    *이제 책에 없는 내용을 더 공부해보겠습니다.



    스레드 우선 순위

    아무리 자바 가상머신이 막 정해준다지만 가끔 더 중요한 스레드를 지정하고 싶을 때가 있을 겁니다. 자바 API에 가면 아래와 같은 메서드가 있습니다.



    우선 순위는 다음과 같은 메서드와 필드를 가지고 있습니다.



       



    1~10의 값을 지정하거나 상수값을 넣어 우선 순위를 지정할 수 있습니다. 우선 순위를 지정하지 않은 경우에는 default 우선 순위인 5가 지정이 됩니다.



    스레드를 생성하는 다른 방법(익명 구현 객체,람다식[JAVA8])

    우리는 스레드를 스레드 클래스를 상속받거나 Runnable을 구현하는 두가지뿐이 배우지 않았습니다. 하지만 이것 말고도 다른 방법들이 있습니다.


    아래와 같이 두가지 방법이 있습니다. 첫번째 thread 객체는 익명 구현 객체를 사용했고 두번째는 람다식을 이용하였습니다.


    *익명 구현 객체 : 말그대로 이름이 없는 클래스로 주로 일회용성으로 사용 됨

    *람다식 : 간단히 말하면 함수를 변수처럼 사용하는 것


    찾아보니 익명 구현 객체 대신 람다식을 사용해서 코드를 간결하게 하는거같군요.




    스레드 실행 양보

    오..스레드는 착한 친구입니다. 실행을 양보하여 자신은 Runnable 상태가 되고 우선순위에 따라 다른 스레드가 실행 기회를 얻을 수 있습니다.yield라는 메서드를 사용하면 됩니다.






    다른 스레드를 기다림

    스레드는 정말 낭만적인 친구입니다. 다른 스레드가 종료할때까지 기다려주기도 합니다.



    아래의 코드 결과를 봅시다.






    스레드 죽여버리기

    스레드가 끝날때까지 기다리지 말고 끝내봅시다! stop 메서드가 있지만, 이를 사용하지 말라고 권고한답니다.  이유는 이곳에서 알려주고 있습니다. 안전하지 않다는군요.



    1) flag 변수 이용하기

    flag 변수의 이용은 간단합니다. run()메서드안에서 flag 변수가 true/false인 동안 함수가 실행되게 하고 그렇지않으면 그 함수블럭을 빠져나오게 하는것이지요.



    2) interrupt 메서드 이용하기

    일부로 인터럽트를 발생시켜서 프로그램을 종료 시키는겁니다.



    데몬스레드

    우선 데몬이 뭔지알아봅시다. 데이먼, 디먼이라고도 불리는군요. 한마디로 백그라운드에서 돌면서 여러 작업을 하는 겁니다. 예전에 sparkR할때도 데몬R프로세스가 있었죠.


    위키 백과를 참고하니 재밌는 유래가 있습니다. 저는 단어의 유래를 좋아합니다. 지루한 공부를 즐겁게 해주니까...  MIT의 MAC 프로젝트 프로그래머가 만든거라는 군여 맥스웰의 도깨비 사고실험에서 도깨비가 보이지 않는 곳에서 무엇을 한다는 이것에서 영감을 받은 것이ㅏㄹ고 합니다.  귀엽군여 역학을 공부안해봐서 자세히 모르겠지만..



    그럼 이 개념으로 데몬 스레드는 사용자가 제어하지 않는 보이지 않는 곳에서 일을 하는 친구입니다.  주 스레드의 작업을 도와주며 당연히 주 스레드가 죽으면 같이 죽겠죠? 그럼 자동저장하는 스레드를 데몬 스레드로 만들어보겠습니다.






    스레드 그룹

    스레드를 여러개 묶어서 그룹으로 관리하는 것입니다.



    JVM 운영에 필요한 스레드는 system 스레드 그룹에 포함시키고 그 하위 스레드는 main 스레드 그룹에 포함시키는 형태입니다. 이게 무슨 말인지 코드를 통해 확인해봅시다.




    이번엔 스레드 그룹을 생성해봅시다. 생성자를 보면 2가지가 있는데 부모를 지정해주는것 그렇지 않은 경우입니다.  아래 코드와 같이 생성해주면 됩니다. 스레드 그룹에도 많은 메서드들이 있습니다.  https://docs.oracle.com/javase/7/docs/api/ 참조






    스레드 풀

    스레드를 필요할때마다 생성하고.. 다시 죽이고 하면 CPU가 바빠져서 메모리 사용량이 늘어난다고 합니다. 나중에 한번에 처리해야 할 양이 수백개 수천개가 되어버리면 스레드가 수백개 수천개... (적은건가..?) 아무튼 이렇게 스레드량이 엄첨나게 많아지는 것을 막으려면 생성 될 수 있는 스레드 양을 정해놓는 것이 좋을 것 같습니다.


    아래 그림처럼 스레드 풀에 스레드를 정해놓고(그림상 6개) Task Queue에 들어오는 작업을 완료하는 것이지요


    자바의 concurrent 패키지에 있는 Executore 클래스로 ExcecutoreService 구현 객체를 만들 수 있습니다. 아래와 같이 간단한 두가지 방법과 직접 생성하는 법이 있으며 간단한 생성 법의 차이 점은 다음과 같습니다. 코어 스레드 수 는 최소 유지해야 하는 스레드의 개수입니다.


    직접 생성하는 경우에는 코어 스레드 수/ 최대 스레드 수/생존 시간/타임유닛/작업큐를 설정해줘야합니다.




     

    초기 스레드 수

    코어 스레드 수 

    최대 스레드 수

     newCachedThreadPool

     0

    0

     integer.MAX_VALUE

     newFixedThreadPool

     0

     nThread

    nThread





    스레드 풀의 있는 스레드는 주 스레드가 끝나도 죽지 않기때문에 종료를 시켜줘야합니다. await는 인터럽트 익셉션이 발생하므로 try catch로 검사해야합니다.



    shutdown 

    return void

    작업 큐에 있는 모든 작업을 완료한 후에 종료

     shutdownNow

    return List<Runnable>

    현재 작업은 인터럽트해서 중지 시키고 작업 큐에 처리하지 못한 작업들을 return

     awaitTermination(long timeout,TimeUnit unit)

    return Boolean

     shutdown() 이후에 timeout안에 완료하면 true 그렇지 않으면 현재 작업을 인터럽트 하고 falase


    작업


    자 이제 스레드 풀에서 처리할 작업에 대해 알아보겠습니다.


    작업은 Runnable Callable 구현 클래스로 표현할 수 있습니다. 두개의 차이점은 작업이 끝나고 return 값이 없다/있다의 차이입니다.

    Callable같은 경우는 그림에서 String이라고 적힌 부분에 return type을 작성해주면 됩니다. (generic)



    작업 처리 요청

    이제 생성했으니 처리해달라고 요청해봅시다! execute()와 submit() 두가지가 있지만 submit()메서드가 오버헤드를 줄일 수 있다고 합니다. submit에는 다음과 같이 총 3가지가 있습니다. 



    Future type으로 return을 하고 있습니다.

    이는 submit으로 작업처리해달라고 요철할 때 작업이 큐에 저장되어있다가 가능할 때 처리되어 결과값을 그때서야 return해줄겁니다.


    이렇게 나~중에 리턴되는 결과 값을 구할 때 사용되는 것이 future입니다.(지연 완료 객체라고 한다) future의 메서드인 get() 메서드는 는 실행이 완료될때까지 블로킹 되었다가 완료되면 결과값을 리턴합니다. 그래서 블로킹을 사용하는 작업 완료 통보 방식이라고 합니다.


    그 외에도 future는 여러 메서드를 가지고 있습니다. https://docs.oracle.com/javase/7/docs/api/ API 참고!


    어? 그러면 리턴 값이 없을때는? 이라고 생각할 수 있습니다. 이럴땐 그냥 작업처리의 성공 여부 또는 예외발생의 여부를 알기 위해 사용합니다.


    이 두 작업 처리에 대한 코드는 아래와 같습니다.




    작업이 완료된 순으로 통보

    아래 인터페이스의 poll과 take를 이용하여 작업 처리가 완료된 것만 통보받아 사용할 수 있습니다




    아래와 같이 사용할 수 있습니다.





    콜백방식으로 통보

    블로킹 방식처럼 기다리지 않고 다른일을 하다가 나 끝났어~ 라고 콜백 메서드가 실행됩니다.

    CompletionHandler라는 메서드의 completed와 failed 메서드를 사용할 수 있습니다.


    아래의 코드처럼 completionHandler를 구현시킨 뒤 작업에서 사용하면 됩니다.

    이때 CompletionHandler의<Interger,Void>로 return값을 원하는대로 조정해서 출력시키면 됩니다.





    profile
    위지원

    데이터 엔지니어로 근무 중에 있으며 데이터와 관련된 일을 모두 좋아합니다!. 특히 ETL 부분에 관심이 가장 크며 데이터를 빛이나게 가공하는 일을 좋아한답니다 ✨

    '2018년 > Java' 카테고리의 다른 글

    Exception Class  (0) 2018.03.15