You will be fine

<Java> Thread 와 비동기

by BFine
반응형

가. Thread 만들기

 a. 비동기

  -  Java에서는 Thread를 만들어서 비동기 처리가 가능하다. 만들어서 테스트 해보자

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

        System.out.println(Thread.currentThread()+" ThreadTest 실행");
        
        Thread thread = new Thread(new MyThread());
        thread.start();

        System.out.println(Thread.currentThread()+" ThreadTest 끝");
        
    }
    
    static class MyThread implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread()+" MyThread 실행 ");
        }
    }
}

Thread[이름, 우선순위, 그룹명]

  -  결과를 보면 알 수 있듯이 main에 있던 작업이 먼저 종료가 되고 Thread가 실행되었다.  즉 비동기는 작업의 순서를 기다리지 않고 실행하는 것을 의미한다.

 

 b. 람다 표현식, 익명함수

  -  위의 만든 예제보다 더 간결하게 Thread를 생성할 수 있다. 만들어보자

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

        System.out.println(Thread.currentThread()+" ThreadTest 실행");

        new Thread(()->{
            System.out.println(Thread.currentThread()+" 람다표현식 Thread 실행");
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread()+" 익명내부클래스 Thread 실행");
            }
        }).start();

        System.out.println(Thread.currentThread()+" ThreadTest 끝");
    }
}

 

 c. 번외 

  -   위의 예제에서 간단하게 Thread를 생성해보았다. 번외로 람다 표현식과 익명내부클래스의 외부변수에 대해 알아보았다.

  -   기본형 변수를 선언하고 사용해보면 별다른 컴파일 오류는 보이지 않지만 변경하려고 할 경우에는 컴파일 오류가 발생한다.

  -  java: local variables referenced from a lambda expression must be final or effectively final

   => 여기서 effectively final의 의미는 final 선언은 하지 않았지만 final 처럼 추가로 변경하지 않는 사용하는 변수를 의미한다.  

  -  이러한 컴파일 오류가 나는 이유는 외부지역변수를 복사해서 가져오기 때문이다.  

   => 복사했으니 내부용도만 사용하면 되지않을까? 라는 생각이 들수도 있지만 위의 코드를 보면 의미가 마치 외부변수에 대한 변경으로 보여질 수 있다.

        이러한 상황을 피하기 위해 컴파일 오류가 발생한다. 즉 외부지역변수는 불변값에 대해서만 사용이 가능하다.

  -  엇 참조형(Reference) 지역변수는 Reference 값을 복사할테니 이걸 사용하면 되겠네? 라는 생각이 들수도 있다. 테스트해보자. 

   -  동일하게 컴파일 오류가 발생한다. 어 왜 참조하고 있는 데이터를 변경하는 오류가 발생하지? 라는 생각이 들수있는다.

   -  참조형 변수의 연산은 결과값을 힙 영역에 새로운 메모리로 할당하여 저장한다. 즉 주소값이 변하게 된다. 확인해보자

       => Long 클래스의 value는 final로 선언 되었기 때문에 내부값을 변경하는 것은 불가능하다.

public static void main(String[] args) {

    Long num = 2L;
    System.out.println(VM.current().addressOf(num));
    num += 1;
    System.out.println(VM.current().addressOf(num));
    
}

  -  결과를 보면 주소값이 다른 것을 알 수 있다. 즉 new Long() 한 것과 동일하다. 컴파일 오류는 참조값 자체를 변경하려 했기 때문에 오류가 발생한것이다.

      => 람다 표현식의 경우 실행시 해당 Thread의 스택을 복사하는 람다 캡쳐링을 통해 값을 복사한다. 

  -  물론 아래처럼 참조하고 있는 데이터에 값을 변경하는 것은 문제가 없다. 

 

나. Synchronized & CountDownLatch

 a. Main Thread을 맨 마지막에 끝내려면?

  -  위의 예제에서 새로운 Thread를 생성하여 각각의 작업들을 비동기 처리를 해보았다. 그리고 각각의 작업이 순서와 상관없이 처리가 된 것을 볼 수 있었다.  

  -  그렇지만 다른 비동기 처리가 끝나고 Main Thread 은 마지막에 끝내고 싶은 경우가 있을 수 있다. 이부분을 알아보자

 

 b. synchronized 와 wait & notify & notifyAll

  -  synchronized 키워드는 객체를 이용하거나 메서드 단위로 사용한다. 그리고 문을 잠그는 것 처럼 lock을 가진 Thread가 처리하는 동안

     lock이 없는 다른 Thread 의 접근을 방지하는 역할을 한다.

  -  .wait .notify .notifyAll 은 Object가 가지고 있는 메서드로 lock을 제어하는데 사용한다. 이를 이용해 Main Thread를 마지막에 종료하도록 만들어보자. 

public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {

        Object lock = new Object();

        synchronized (lock){ // (1)

            new Thread(()->{
               synchronized (lock){ // (2)
                    System.out.println(" 1번 실행");
                    lock.notify(); // (3)
                }
            }).start();

            new Thread(()->{ // (2)
                synchronized (lock){
                    System.out.println(" 2번 실행");
                }
            }).start();

            System.out.println(" 기다리는중...");
            lock.wait(); // (4)
            System.out.println(" ThreadTest 끝"); // (5)
        }
    }
}

  (1) . 동기화 블록으로 Object 객체(lock)를 이용하여 lock이 없는 다른 Thread로 부터의 접근을 보호하며 내부에서도 이를 이용해 제어가 가능하다.

  (2).  현재 Main Thread가 lock을 가지고 있기 때문에 lock을 가지고 있지 않은 동기화 블록은 실행할 수가 없다.

  (4).  lock이 가진 .wait 메서드를 호출하여 호출한 Thread의 lock을 회수하고 이 Thread는 잠자는 대기 상태로 들어간다.

  (2).  lock이 회수가 되었으니 두 Thread는 경쟁을 통해 하나는 lock을 획득하고 하나는 꺠어있는 대기 상태로 들어간다.

  (3).  여기서 lock을 .notify가 있는 Thread가 먼저 획득했다고 한다면 이 Thread는 .notify를 실행하여 자고있는 Main Thread를 깨우게 된다.

      => 만약에 두번째 Thread가 먼저 lock을 획득하는 경우에는 Main Thread가 제일 마지막에 처리가 된다.

  (2) vs (5). 잠에서 깬 Main Thread와 대기 중에 있던 Thread가 또 경쟁하여 lock을 획득하여 실행된다.

  -  여기서 lock 획득 순서에 따라 Main이 먼저 종료 될수도 있다는 것을 알게 되었다. 원하는 결과가 아니니까 하나의 synchronized 을 제거해보자

public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {

        Object lock = new Object();

        synchronized (lock){ 

            new Thread(()->{
                    System.out.println(" 1번 실행");
            }).start();

            new Thread(()->{ 
                synchronized (lock){
                    System.out.println(" 2번 실행");
                    lock.notify();
                }
            }).start();

            System.out.println(" 기다리는중...");
            lock.wait(); 
            System.out.println(" ThreadTest 끝"); 
        }
    }
}

   -  첫번째 Thread는 동기화 블록이 없기 때문에 lock 과 관계없이 실행되어진다. 하지만 두번째 Thread는 lock이 Main Thread에 있기 때문에 대기한다.

   -  Main Thread는 .wait으로 lock의 반납하고 두번째 Thread가 lock을 획득하여 실행후에 notify로 다시 Main Thread를 깨우기 때문에

       Main Thread는 마지막에 종료가 되어 원하는 결과를 얻을 수 있다. 

   -  그러나 Main Thread가 .wait을 호출하기 전까지는 두번째 Thread는 가만히 놀기만 하고 있어서 비효율적으로 보인다. 다른 방법을 찾아보자

 

 c. CountDownLatch

  -  CountDownLatch 이용하여 위의 문제를 해결할 수 있다. 지정한 숫자만큼 countDown이 발생해야 대기중인 .await를 호출했던 Thread를 실행시킨다. 


public class ThreadTest {

    public static void main(String[] args) throws InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(2);

        new Thread(()->{
            System.out.println(" 1번 실행");
            countDownLatch.countDown();
        }).start();

        new Thread(()->{
            System.out.println(" 2번 실행");
            countDownLatch.countDown();
        }).start();

        System.out.println(" 기다리는중...");
        countDownLatch.await();
        System.out.println(" ThreadTest 끝");
    }
}

  -  원하는데로 Main Thread는 가장 마지막에 실행되며 각각의 Thread는 대기 없이 실행되는 것을 알 수 있다. 

  -  일반적으로는 CountDownLatch는 최대 Thread 확인하는 용도로 반대로 사용하는 것 같다.

     => https://stackoverflow.com/questions/184147/countdownlatch-vs-semaphore

 

CountDownLatch vs. Semaphore

Is there any advantage of using java.util.concurrent.CountdownLatch instead of java.util.concurrent.Semaphore? As far as I can tell the following fragments are almost equivalent: 1. Semaphore

stackoverflow.com

반응형

블로그의 정보

57개월 BackEnd

BFine

활동하기