ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스터디 12주차] 멀티쓰레드 프로그래밍
    프로그래밍 언어/JAVA 2021. 7. 14. 13:48
    더보기

    목표: 자바의 멀티쓰레드 프로그래밍에 대해 학습

     

     

     

    0. 들어가기 전

    오늘의 큰 주제인 멀티쓰레드는 무엇일까?

    멀티쓰레드를 알아보기 전에 멀티태스킹(multitasking)이라는 개념을 알아두는 것이 좋다.

    멀티태스킹두 가지 이상의 작업을 동시에 처리하는 것으로 예를 들어 우리가 웹 브라우저를 통해 서칭을 하면서 뮤직 플레이어를 노래를 들을 수도 있고 PC카카오톡에서 알림도 받을 수 있다.

    위의 예시는 정확히 말하면 멀티태스킹 중에 멀티프로세스라는 표현이 더 맞을 수 있다.

    그러면 다른 예시를 알아보자.

    요즘은 잘 보기 힘들지만 브라우저를 통해 블로그에 들어가면 글을 보는 것과 함께 BGM, 음악이 함께 재생되는 것이 있었다.

    혹은 카카오톡같은 메신저 애플리케이션은 메시지를 주고받을 뿐만 아니라 동영상, 사진 등과 같은 파일도 보낼 수 있다.

    이렇게 하나의 프로세스(웹 브라우저, 카카오톡 등 실행 중인 하나의 애플리케이션)가 두 가지 이상의 작업을 하는 것은 어떻게 가능할까?

    여기서 멀티쓰레드라는 개념이 도입된다.

     

    첫 번재 예시에서 보여준 멀티 프로세스는 애플리케이션 단위의 멀티 태스킹이라면 두 번째 예시의 멀티 쓰레드는 애플리케이션 내부에서의 멀티 태스킹이라고 볼 수 있다.

     

    1. Thread 클래스와 Runnable 인터페이스

    위에서 알아본 멀티 쓰레드를 가진 프로그램을 만들기 위해서는 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야한다.

    자바에서는 작업 스레드도 객체로 생성되기 때문에 클래스가 필요하다.

    java.lang.Thread 클래스를 직접 객체화해서 생성하는 방법과 Thread를 상속해서 하위 클래스를 만들어 생성하는 방법이 있다.

     

    먼저, java.lang.Thread 클래스로부터 직접 생성하는 것을 알아보자.

    java.lang.Thread 클래스로부터 작업 스레드 객체를 생성하려면 Runnable을 매개값으로 갖는 생성자를 호출해야한다.

    Thread thread = new Thread(Runnable target);

    여기서 Runnable은 인터페이스 타입이기 때문에 구현 객체를 만들어 대입해야 한다.

    Runnable에는 run( )메소드가 있는데 구현 클래스에서 이 run( )메소드를 재정의해서 우리가 만들고 싶은 코드를 작성하면 된다.

    class 클래스명 implements Runnable{
        public void run() {
            // 실행할 코드
        }
    }

    구현 클래스를 만들었다면 Runnable 구현 객체를 생성한 후, 이것을 매개값으로 해서 Thread 생성자를 호출하면 작업 스레드가 생성된다.

    Runnable 클래스명 = new 클래스명();
    Thread thread = new Thread(클래스명);
    
    // 구현 클래스를 따로 만들지 않고 매개값을 주는 코드
    Thread thread = new Thread( new Runnable() {
        public void run() {
            // 실행할 코드
        }
    }
    
    
    // Runnable은 함수형 인터페이스이기 때문에 람다식 사용가능
    Thread thread = new Thread(() -> {
        //실행할 코드
    });

    이렇게 생성한 작업 스레드는 start( )메소드를 만나야 실행된다.

    thread.start();

     

     

     

    다음으로 Thread를 상속해서 하위 클래스를 만들어 생성하는 방법을 알아보자.

    작업 스레드가 실행할 코드를 Runnable로 만들지 않고 thread의 하위 클래스로 작업 스레드를 정의하여 작업 내용을 포함시킬 수 있다.

    Thread 클래스를 상속 후 run( )메소드를 오버라이딩해서 스레드가 실행할 코드를 작성 하면 된다.

    public class WorkerThread extends Thread {
        @Override
        public void run() {
            // 실행할 코드
        }
    }
    
    Thread thread = new WorkerThread();
    
    
    // 익명 객체로 잗업 스레드 객체 생성
    Thread thread = new Thread() {
        public void run() {
            // 실행할 코드
        }
    }

    이렇게 생성된 작업 스레드 객체에서 start()메소드를 호툴하면 우리가 정의한 실행 코드가 실행된다.

    thread.start();

     

    어떻게 Thread클래스를 상속받았는데 Runnable 인터페이스에 있는 run( )메소드를 overriding할 수 있는 것일까?

    이 의문에 대한 답은 자바 API documents에서 java.lang.Thread를 보면 알 수 있다.

    https://docs.oracle.com/javase/8/docs/api/index.html

     

    Java Platform SE 8

     

    docs.oracle.com

    출처: https://docs.oracle.com/javase/8/docs/api/index.html

    Thread클래스는 Runnable 인터페이스를 구현하는 클래스이기도 하다.

    그래서 Thread클래스를 상속받아 만든 클래스에서 run( )메소드를 오버라이딩할 수 있는 것이다.

     

     

     

    2. 쓰레드의 상태

     

     

     

    3. 쓰레드의 우선순위

    멀티스레드는 동시성(Concurrency)또는 병렬성(Parallelism)으로 실행된다.

    동시성은 멀티 태스킹을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행되는 것을 말한다.

    병렬성은 멀티 태스킹을 위해 여러 개의 코어에서 개별 스레드를 동시에 실행하는 것을 말한다.

    만약, 스레드의 개수가 코어의 수보다 많은 경우에는 여러 개의 스레드를 어떠한 순서에 의해 동시성으로 실행할지 결정해야한다.

    여기서 필요한 것이 스레드 스케쥴링이다.

     

    쓰레드 스케쥴링에는 우선순위 방식과 순환 할당 방식이 있다.

    1) 우선순위 방식

    우선순위 방식은 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 하는 스케쥴링이다.

    코드를 통해 스레드 객체마다 우선순위 번호를 부여하여 제어할 수 있다.

    우선순위는 1부터 10까지 부여되는데 1이 가장 낮은 우선순위이고 숫자가 커질수록 높은 우선순위가 된다.

    스레드에게 우선순위를 주는 방법은 아래의 코드와 같다.

    thread.setPriority(우선순위);

    우선순위의 매개값으로 1~10까지의 정수를 직접 줄 수도 있고 Thread클래스의 상수를 사용하여 줄 수도 있다.

    출처: https://docs.oracle.com/javase/8/docs/api/index.html

    MAX_PRIORITY는 10, NORM_PRIORITY는 5, MIN_PRIORTY는 1을 가진 상수이다.

    우선순위를 부여하지 않으면 모든 스레드는 기본값으로 5의 우선순위를 할당받는다.

     

    2) 순환 할당 방식

    순환 할당 방식은 시간 할당량을 정해서 스레드마다 정해진 시간만큼 실행되는 방식이다.

    순환 할당 방식은 우선 순위 방식과 달리 JVM에 의해 정해지기 때문에 개발자가 코드로 제어할 수 없다.

     

     

     

    4. Main 쓰레드

    모든 자바 애플리케이션은 메인 쓰레드(main thread)가 main() 메소드를 실행하면서 시작된다.

    main( ) 메소드는 우리가 클래스를 만들고 클래스 안에서 실행하고자하는 코드를 중괄호{ }안에 넣는 것으로

    실행하게 되면 첫 번째 줄의 코드부터 아래로 순차적으로 실행된다.

     

    이러한 메인 스레드는 필요에 따라 작업 스레드들을 만들어서 병렬로 코드를 실행할 수 있다.

    싱글 스레드에서의 main 쓰레드와 멀티 스레드에서의 main 쓰레드와의 차이점은 무엇일까?

    싱글 스레드 애플리케이션은 메인 쓰레드가 종료되면 프로세스도 종료된다.

    하지만 멀티 쓰레드 애플리케이션에서는 실행 중인 스레드가 하나라도 있다면 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행 중인 상태라면 프로세스는 종료되지 않는다.

     

     

     

    5. 동기화

    멀티 스레드 프로그램같은 경우에는 스레드들이 하나의 객체를 공유해서 작업해야하는 경우가 생긴다.

    코드를 보며 어떠한 경우를 애기하는지 알아보자.

    // 공유 객체를 위한 클래스 코드
    package Thread;
    
    public class Calculator {
    	private int memory;
    	
    	public int getMemory() {
    		return memory;
    	}
    	
    	public void setMemory(int memory) {
    		this.memory = memory;
    		try {
    			Thread.sleep(2000);
    			
    		}catch(InterruptedException e) {}
    		System.out.println(Thread.currentThread().getName()+':' + this.memory);
    	}
    
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    
    	}
    
    }
    // 스레드1 코드
    package Thread;
    
    public class User1 extends Thread {
    	private Calculator calculator;
    	
    	public void setCalculator(Calculator calculator) {
    		this.setName("CalculatorUser1");
    		this.calculator = calculator;
    	}
    	
    	public void run() {
    		calculator.setMemory(100);
    	}
    }
    // 스레드2 코드
    package Thread;
    
    public class User2 extends Thread {
    	private Calculator calculator;
    	
    	public void setCalculator(Calculator calculator) {
    		this.setName("CalculatorUser2");
    		this.calculator = calculator;
    	}
    	
    	public void run() {
    		calculator.setMemory(50);
    	}
    
    }
    // 메인 스레드가 실행되는 코드
    package Thread;
    
    public class MainThreadExample {
    
    	public static void main(String[] args) {
    		Calculator calculator = new Calculator();
    		User1 user1 = new User1();
    		user1.setCalculator(calculator);
    		user1.start();
    		
    		User2 user2 = new User2();
    		user2.setCalculator(calculator);
    		user2.start();
    
    	}
    
    }

     

    위 코드를 보면 User1 스레드와 User2 스레드가 calculator라는 객체를 공유하고 있다.

    User1 스레드는 memory값을 100으로 설정했고 잠시 쉬고와서 memory값을 불러왔을 때 100이 나오는 것을 원할 것이다.

    User2 스레드는 memory값을 50으로 설정했고 잠시 쉬고와서 memory값을 불러왔을 때 50이 나오는 것을 원할 것이다.

    하지만, 결과는 각각의 스레드가 원하는대로 나오지 않는다.

    User1 스레드가 설정한 100이라는 값은 공유 객체의 memory에 저장했기에 User2 스레드가 이후에 설정한 50으로 변경되어 원치 않은 결과를 얻었다.

     

     

    이러한 현상을 방지하려면 스레드가 사용 중인 객체를 다른 스레드가 사용할 수 없도록 스레드 작업이 끝날 때까지 객체에 잠금을 걸어야할 것이다.

    멀티 스레드 프로그램에서 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역(critical section)이라고 한다.

    자바는 임계 영역지정을 위해 동기화 메소드와 동기화 블록을 제공한다.

    스레드가 객체 내부의 동기화 메소드 혹은 동기화 블록에 들어가면 즉시 객체에 잠금을 걸어 다른 스레드가 임계 영역 코드에 들어오지 못하도록 해준다.

     

    그러면 동기화 메소드는 어떻게 만들까?

    동기화 메소드를 만드는 방법은 메소드를 선언할 때 synchronized 키워드를 붙여주면 된다.

    public synchronized void method() {
        // 임계 영역
    }

    메소드 전체가 아닌 메소드의 내용 중 일부만 임계 영역으로 만들고 싶다면 동기화 블록으로 만들면 된다.

    public void method() {
        // 여러 스레드가 실행 가능한 영역
        
        synchronized(공유객체) {
            // 임계 영역
        }
        
        // 여러 스레드가 실행 가능한 영역
    }
    이러한 동기화 메소드, 동기화 블록이 하나의 객체에 여러 개가 있을 경우에는
    어떠한 스레드가 여러 개의 동기화 메소드, 블록 중 하나를 접근하여 실행하고 있을 때,
    다른 스레드들은 접근 중인 동기화 메소드, 블록은 물론이고 다른 동기화 메소드, 블록도 접근할 수 없다.

     

     


    <참고자료>

    이것이 자바다 vol.2 (신용권 지음)

    https://docs.oracle.com/javase/8/docs/api/index.html

    '프로그래밍 언어 > JAVA' 카테고리의 다른 글

    [스터디 14주차] 제네릭  (0) 2021.07.24
    [스터디 13주차] I/O  (0) 2021.07.20
    [이클립스] javadoc 만들기  (0) 2021.07.11
    [스터디 11주차] Enum  (0) 2021.07.10
    [스터디 10주차] 애노테이션 (annotation)  (0) 2021.07.05

    댓글

Designed by Tistory.