ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java Thread(with Multi Thread)
    Java/Basics 2020. 6. 21. 20:29



    ■Thread란?


    프로세스 내에서 실행되는 독립적인 흐름 단위.




    ■Thread 구현 방법 2가지


    1. Thread class를 상속받는다.


    -> 다른 클래스를 상속받을 수 없다는 특징이 있다.

    public class ExtendedThread extends Thread {
    @Override
    public void run() {
    System.out.println("Thread를 상속받은 스레드 생성");
    }
    }

    2. Runnable interface를 구현한 후 Thread class 생성자 매개변수에 넣는다.


    -> 구현하려는 클래스가 이미 다른 클래스를 상속하고 있을 때 사용한다.

    public class ImplementsThread implements Runnable {
    @Override
    public void run() {
    System.out.println("Runnable을 구현한 스레드 생성");
    }
    }




    ■Thread 호출 과정


    ① 메인 메서드에서 start() 호출


    ② 호출 스택 생성


    ③ 호출 스택에서 run() 호출하여 작업 수행

    public class Main {
    public static void main(String[] args) {
    Thread extendThread = new ExtendThread();
    Thread ImplementsThread = new Thread(new ImplementsThread());

    extendThread.start();
    ImplementsThread.start();
    }
    }




    ■Thread 우선 순위


    ☞ Thread가 수행하는 작업의 중요도에 따라 우선 순위를 다르게 설정하여 


    특정 Thread가 더 많은 작업 시간을 갖도록 할 수 있다.


    ☞ Thread class에 우선 순위를 지정하기 위한 상수가 존재한다.


    -> static final int MAX_PRIORITY : 우선순위 10 - 가장 높은 우선 순위


    -> static final int MIN_PRIORITY  :  우선순위 1 - 가장 낮은 우선 순위


    -> static final int NORM_PRIORITY : 우선순위 5 - 보통의 우선 순위


    (main()스레드의 우선 순위 값은 초기값이 5)


    ☞ 아래에 ExtendThread를 통해 실험해봤다. 


    예상으로는 for 각 수행 순서마다 Thread 2가 Thread 1보다 먼저 실행되어야 했다.

    public class ExtendThread extends Thread {
    ExtendThread(String name) {
    super(name);
    }

    @Override
    public void run() {
    try{
    for(int i = 0 ; i < 5 ; i++)
    {
    Thread.sleep(1000);
    System.out.println(this.getName() + " " + i + "번째 수행");
    }
    }catch (InterruptedException e){
    e.printStackTrace();
    }
    }
    }
    public class Main {
    public static void main(String[] args) {
    Thread thread1 = new ExtendThread("Thread 1");
    Thread thread2 = new ExtendThread("Thread 2");
    thread1.setPriority(Thread.MIN_PRIORITY);
    thread2.setPriority(Thread.MAX_PRIORITY);
    thread1.start();
    thread2.start();
    }
    }

    하지만 결과는 달랐다.



    아래 출처에 따르면, JVM이 멀티 코어를 사용한다면 스레드들이 서로 다른 코어에


    배치될 수 있다. 그렇게 되면 각 스레드들은 각각 다른 프로세스에 배치되고(추측)


    스레드들은 서로 영향을 주고 받을 수 없기 때문에 스레드들간의 우선순위 설정은 


    무의미해질 수 있다. 출처


    그래서 또 실험을 해봤다. 모든 코어에 순서를 지정한 Thread1과 Thread2를 생성했다.

    public class Main {
    public static void main(String[] args) {

    for (int i = 0; i < Runtime.getRuntime().availableProcessors(); i++){
    Thread thread1 = new ExtendThread("Thread 1 " + i);
    thread1.setPriority(Thread.MAX_PRIORITY);
    thread1.start();

    Thread thread2 = new ExtendThread("Thread 2 " + i);
    thread2.setPriority(Thread.MIN_PRIORITY);
    thread2.start();
    }

    }
    }

    그런데 결과가 또 이상하다... (위의 결과 로그처럼)순서가 매번이 동일하지가 않다. 


    자바에서 싱글 코어로 실행시킬 수 있는 환경을 찾아서 실행시켜봐야 겠다.


    아니면 혹시 priority는 아래에 나오는 synchronized 동기화 처리가 된 영역을 


    멀티 스레드들이 접근 시도할 때 유효한 것일까??


    지금 당장은 할게 너무 많으니 나중으로 잠깐 미뤄두자!




    ■스레드의 상태 6가지


    ① NEW : 스레드가 생성되었지만 스레드가 아직 실행할 준비가 되지 않았음


    ② RUNNABLE : 스레드가 실행되고 있거나 실행 준비되어 스케쥴링을 기다리는 상태


    ③ WAITING : 다른 스레드가 notify(), notifyAll()을 불러주기를 기다리고 있는 상태(동기화)


    ④ TIMED_WAITING : 스레드가 sleep(n) 호출로 인해 n 밀리초동안 


    대기(WAITING)하고 있는 상태


    ⑤ BLOCK : I/O 작업을 요청한 후 I/O 처리가 완료되기를 기다리는 상태


    ⑥ TERMINATED : 스레드가 종료한 상태




    ■Multi Thread란?


    ☞ 프로세스 내에 2개 이상의 독자적인 흐름 단위(Thread)가 실행되는 환경


    ☞ 멀티 프로세스(2개 이상의 프로세스가 실행)와는 데이터를 서로 공유하는지의 여부에서


    차이가 있다. 멀티 스레드는 Metaspace와 Heap 내의 데이터를 서로 공유할 수 있지만


    멀티 프로세스는 데이터를 서로 공유할 수 없다. 


    컨텍스트 스위칭(현재 실행되는 프로세스를 교체)은 데이터도 함께 교체되어야 


    하기 때문에 비용이 크다. 그래서 이 단점을 보완하기 위해 멀티 스레드가 탄생했다. 


    출처




    ■Multi Thread 주의점


    Multi Thread는 Metaspace와 Heap을 공유하기 때문에 컨텍스트 스위칭에 필요한


    비용이 Process의 컨텍스트 스위칭의 그 비용보다 적다. 


    하지만 데이터를 공유한다는 특징 때문에 공유하는 데이터에 대한 동기화가 중요하다.


    학부 운영체제 수업때 교수님이 항상 예로 드시던 것이 바로 화장실 변기칸이었다.


    화장실 변기칸을 예로 들어서 아래 동기화 방법을 설명해 볼 것이다.




    ■Thread 동기화 방법


    ① 메소드 내부에서 동기화가 필요한 부분에 synchronized lock을 사용


    화장실에 거울과 변기 1개가 있다고 하자.


    useKey 메소드를 실행하여 거울을 볼 수도 있고 변기를 사용할 수도 있다.


    거울을 보는 행위는 특정 사용자가 독점할 필요는 없으므로 동기화 대상에서 제외한다.


    변기를 사용하는 부분에만 동기화를 적용해보자.

    public void useKey(String name) {
    lookIntoAMirror(name);
    synchronized (this) {
    open(name);
    defecate(name);
    close(name);
    }
    }

    synchronized 키워드를 사용하여 변기를 사용하는 부분에만 동기화를 적용했고


    동기화로 인한 스레드 독점을 해제하는 Lock으로 this(해당 객체의 인스턴스)를 지정했다.


    (Lock은 다른 스레드의 접근을 차단, 허용하는 자물쇠 역할을 한다.)


    화장실(this)에 대해 동기화가 걸렸으므로 화장실 사용이 끝나면(useKey 메소드가 끝나면)


    동기화가 풀리고 다른 사용자가 화장실에 들어올 수 있다.


    이때 주의할 점이 있다.


    Lock으로 this를 걸면, 해당 this(해당 인스턴스)가 Lock으로 걸려있는 


    모든 synchronized에 동기화가 걸린다. this(해당 인스턴스)를 사용하는 


    synchronized 메소드는 전부 진입이 불가능하게 되는 것이다. 


    (이는 성능 저하로도 이어질 수 있다.)


    각 스레드들이 각각 다른 인스턴스들의 synchronized 메소드를 사용하는 경우는 


    그나마(?) 괜찮지만 그런 경우가 아니라면 조심해서 사용해야 할 것이다.


    ② 메소드에 synchronized 예약어를 사용


    화장실에 변기 1개만 있다고 하자.


    변기만 있으므로 메소드 모든 영역에 synchronized 키워드를 붙이면 된다

    public synchronized void useKey(String name) {
    synchronized (this) {
    open(name);
    defecate(name);
    close(name);
    }
    }

    위처럼 메소드 전체에 this로 Lock을 걸어 동기화하는 경우엔 아래처럼 사용할 수도 있다.

    public synchronized void useKey(String name){
    open(name);
    defecate(name);
    close(name);
    }

    출처


    여기에서도 주의할 점이 있다.


    synchronized 키워드를 static 메소드에 위 코드처럼 붙이면 성능 이슈가 생길 수 있다.


    static 메소드에 synchronized를 붙이게 되면 

    public void useKey(String name) {
    synchronized (Class.class) {
    open(name);
    defecate(name);
    close(name);
    }
    }

    위 코드처럼 Lock으로 클래스 타입을 걸어버리게 된다. 


    해당 클래스의 인스턴스 전체에 대해 synchronized가 걸린 영역은 모두 동기화가 적용된다.


    Singleton 클래스에는 적절할 지 모르겠으나 일반적인 경우라면 조심해서 사용해야


    혹시 모를 성능 저하를 막을 수 있을것이다.




    https://includestdio.tistory.com/7


    https://raccoonjy.tistory.com/15


    https://reakwon.tistory.com/85

    댓글

Designed by Tistory.