CSE/Java 검색 결과

89개 발견
  1. 미리보기
    2015.12.14 - Palpit

    [Java] JavaFX - Intro

  2. 미리보기
    2015.12.13 - Palpit

    [Java] 멀티 스레드 - 스레드풀(ThreadPool)

  3. 미리보기
    2015.12.12 - Palpit

    [Java] 멀티 스레드 - 데몬 스레드 & 스레드 그룹

  4. 미리보기
    2015.12.12 - Palpit

    [Java] 멀티 스레드 - 상태 제어

  5. 미리보기
    2015.12.12 - Palpit

    [Java] 멀티 스레드 - 상태 & 상태 제어

  6. 미리보기
    2015.12.12 - Palpit

    [Java] 멀티 스레드 - 우선순위, 동기화 메소드

  7. 미리보기
    2015.12.12 - Palpit

    [Java] 멀티 스레드 - 작업 스레드

  8. 미리보기
    2015.12.12 - Palpit

    [Java] 멀티 스레드

[Java] JavaFX - Intro

2015.12.14 12:34 - Palpit
조회수 확인

JavaFX는 여러 절로 구성되어 있습니다.





Intro

JavaFX 레이아웃(Layout)

JavaFX 컨테이너(Container)

JavaFX 이벤트 처리 & 속성 감시, 바인딩

JavaFX 컨트롤(Control)

JavaFX 메뉴바와 툴바 & 다이얼로그

JavaFX 스레드 동시성




Intro

 JavaFX는 크로스 플랫폼(Cross-Platform)에서 실행하는 리치 클라이언트 어플리케이션(Rich Client Application)을 개발하기 위한 그래픽과 미디어 패키지를 말합니다. JavaFX는 Java 7부터 JDK에 포함되어 있기 때문에 별도의 SDK 설치 없이도 사용 할 수 있습니다. 


 JavaFX는 Swing보다 더 가벼워졌고 더 강력해졌기 때문에 Swing을 대체하는 새로운 라이브러리로 자리매김을 하고 있습니다. JavaFX는 데스크톱뿐만 아니라 임베디드 장비에서 실행하는 UI 어플리케이션을 개발할 수 있도록 가볍고 풍부한 UI를 제공하고 있습니다.


 JavaFX는 어플리케이션에서 UI 생성, 이벤트 처리, 멀티미디어 재생, 웹 뷰 등과 같은 기능은 JavaFX API로 개발하고 그 이외의 기능은 자바 표준 API를 활용해서 개발할 수 있습니다. 멀티 스레딩과 같은 서버와의 통신 기능은 자바 표준 API를 사용하면서 JavaFX에서 제공하는 풍부한 UI 기능을 활용할 수 있다는 점에서 매우 큰 장점이 될 수 있습니다.


 JavaFX는 화면 레이아웃과 스타일, 어플리케이션 로직을 분리할 수 있기 때문에 디자이너와 개발자들이 협력해서 JavaFX 어플리케이션을 개발할 수 있는 구조를 가지고 있습니다. 자바 코드와 분리해서 스타일 시트(CSS)로 외관을 작성할 수 있기 때문에 분리가 가능합니다. 다음은 JavaFX 어플리케이션을 구성하는 파일 단위 요소를 보여줍니다.



 






JavaFX 어플리케이션 개발 시작

 JavaFX 어플리케이션을 개발하려면 제일 먼저 메인 클래스를 작성해야 합니다.


 메인 클래스

  JavaFX 어플리케이션을 시작시키는 메인 클래스는 추상 클래스인 javafx.application.Application을 상속받고, start() 메소드를 재정의(Overriding)해야 합니다. 그리고 main() 메소드에서 Application의 launch() 메소드를 호출해야 합니다. launch() 메소드는 메인 클래스의 객체를 생성하고, 메인 윈도우를 생성한 다음 start() 메소드를 호출하는 역할을 합니다.





 위에서 언급했듯이 별도의 SDK 설치 없이 개발이 가능하나, 초기 JAR 파일을 Build Path에 잡아줘야 자동 import가 됩니다.


 개발 환경을 구성하기 위해서 아래 순서를 따라주세요.


  1. JavaFX 해당 프로젝트의 오른쪽 클릭해서 Properties 클릭

  2. 아래와 같은 화면이 나오면 왼쪽 메뉴에서 Java Build Path 클릭하고, 오른쪽 화면에서 Libraries 탭 클릭

  3. 아래 화면에서 Add External JARs 클릭









 4. 경로는 java/jdk/jre/lib/ext 순입니다. 경로내에서 "jfxrt.jar" 찾아서 열기

  * 제 기준으로 C:\Program Files\Java\jdk1.8.0_65\jre\lib\ext 경로입니다.








 



 5. 다음 OK 클릭해주시면 됩니다.

 6. 아래 예제를 통해 상속받는 Application이나 Stage를 로드하는데 문제가 없는지 확인한 후 실행해봅니다.




 * AppMain.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package javaFX;
 
import javafx.application.Application;
import javafx.stage.Stage;
 
public class AppMain extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.show();    // 윈도우 보여주기
    }
    
    public static void main(String[] args) {
        launch(args);            // AppMain 객체 생성 및 메인 윈도우 생성
    }
}
 
cs





 간단한 윈도우가 하나 생성되는 것을 볼 수 있습니다. JavaFX에서는 윈도우를 무대(javafx.stage.Stage)로 표현합니다. launch() 메소드에서 생성된 메인 윈도루를 start()의 primaryStage 파라미터로 제공하는데, start() 메소드의 맨 마지막에서 primaryStage.show() 메소드를 호출함으로써 메인 윈도우가 보여집니다.


 








 JavaFX 라이프사이클(LifeCycle)

  JavaFX 어플리케이션은 Application.launch() 로 시작하여 아래와 같은 순으로 진행됩니다.










  - 메인 클래스의 기본 생성자를 호출해서 객체를 생성

  - init() 메소드 호출(메인 클래스의 실행 파라미터를 얻어 어플리케이션이 이용할 수 있게 해줌)

  - int() 끝나고 start() 메소드 호출하여 메인 윈도우 실행

  - JavaFX App 종료



  JavaFX App의 종료 조건은 세 가지가 있습니다.

  - 마우스로 마지막 윈도우(Stage)의 닫기 버튼 클릭 시

  - 자바 코드로 마지막 윈도우의 close() 메소드 호출 시

  - 자바 코드로 Platform.exit() 또는 System.exit(0)을 호출 시



  JavaFX App은 종료되기 직전에 stop() 메소드를 호출하는데, stop() 메소드는 앱이 종료되기 전에 자원을 정리(파일 닫기, 네트워크 끊기)할 기회를 줍니다. init() 과 stop() 메소드는 옵션으로 필요한 경우에 재정의(Override)해서 사용하면 됩니다.


  주목할 점은 라이프사이클의 각 단계에서 호출되는 메소드는 서로 다른 스레드 상에서 실행된다는 점입니다. JVM이 생성한 main 스레드가 Application.launch() 를 실행하면 launch() 메소드는 다음과 같은 이름을 가진 두 스레드를 생성하고 시작시킵니다.


  - JavaFX-Launcher: init() 실행

  - JavaFX Application Thread: 메인 클래스 기본 생성자, start() 및 stop() 실행




  JavaFX App에서 윈도우(Stage)를 비롯한 UI 생성 및 수정 작업, 입력 이벤트 처리 등은 모두 JavaFX Application Thread에서 관장합니다. 그 이유는 JavaFX API는 스레드에 안전하지 않아서 멀티 스레드가 동시에 UI를 생성하거나 수정하게 되면 문제가 발생하기 때문입니다. 그래서 JavaFX Application Thread만 UI를 생성하거나 수정할 수 있도록 되어 있고, 다른 스레드가 UI에 접근하게 되면 예외가 발생합니다. init() 메소드에서 UI를 생성하는 코드를 작성하면 안되는데, 그 이유는 init() 메소드가 JavaFX-Launcher 스레드에서 실행되기 때문입니다. 


  다음 예제는 기본 생성자, init(), start(), stop() 메소드가 어떤 스레드 상에서 실행되는지 볼 수 있는 예제입니다.




 * AppMain.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 
package javaFX;
 
import javafx.application.Application;
import javafx.stage.Stage;
 
public class AppMain extends Application {
 
    public AppMain() {
        System.out.println(Thread.currentThread().getName() + ": AppMain() 호출");
    }
 
    @Override
    public void init() throws Exception {
        System.out.println(Thread.currentThread().getName() + ": init() 호출");
    }
 
    @Override
    public void start(Stage primaryStage) throws Exception {
        System.out.println(Thread.currentThread().getName() + ": start() 호출");
        primaryStage.show(); // 윈도우 보여주기
    }
 
    @Override
    public void stop() throws Exception {
        System.out.println(Thread.currentThread().getName() + ": stop() 호출");
    }
 
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": main() 호출");
        launch(args); // AppMain 객체 생성 및 메인 윈도우 생성
    }
}
 
cs

  

 









 메인 클래스 실행 파라미터 얻기

  init() 메소드의 역할은 메인 클래스의 실행 파라미터를 얻어 앱의 초기값으로 이용할 수 있도록 하는 것이라고 언급했습니다. 다음과 같이 메인 클래스를 실행할 때 -- 실행 옵션을 주었다고 가정해봅시다.



1
2
java AppMain --ip=192.168.214.22 --port=50020
 
cs




  Application.launch()는 main() 메소드의 파라미터를 그대로 넘겨 받는데, 이 파라미터는 init() 메소드에서 다음과 같은 두 가지 방법으로 얻을 수 있습니다.

1
2
3
4
Parameters params = getParameters();
 
List<String> list = params.getRaw();  // 1
Map<StringString> map = params.getNamed();   // 2 
cs



  Parameters의 getRaw() 메소드는 ip와 port 을 요소로 갖는 List 컬렉션을 리턴하고, getNamed() 메소드는 ip를 키로 개체값을 저장하고, port를 키로해서 개체값을 저장하는 Map 컬렉션을 리턴합니다.







 무대(Stage)와 장면(Scene)
  JavaFX는 윈도우를 무대(Stage)로 표현합니다. 무대는 한 번에 하나의 장면을 가질 수 있는데, JavaFX는 장면을 javafx.scene.Scene 클래스로 표현합니다. 메인 윈도우는 start() 메소드의 primaryStage 파라미터로 전달되지만, 장면(Scene)은 직접 생성해야 합니다. Scene을 생성하려면 UI의 루트 컨테이너인 javafx.scene.Parent가 필요합니다.


1
2
Scene scene = new Scene(Parent root);
 
cs



  Parent는 추상 클래스이기 때문에 하위 클래스로 객체를 생성해서 제공해야 합니다. 주로 javafx.scene.layout 패키지의 컨테이너들이 사용됩니다. 실제로 UI 컨트롤들이 추가되는 곳은 Parent가 되고, Parent 폭과 높이가 장면의 폭과 높이가 됩니다.

  장면을 생성한 후에는 윈도우에 올려야 하는데, Stage의 setScene() 메소드를 사용합니다. setScene() 메소드는 파라미터로 받은 Scene을 윈도우 내용으로 설정합니다.


1
primaryStage.setScene(scene);
cs


  
  다음 예제는 Parent 하위 클래스인 javafx.scene.layout.VBox을 이용해서 Scene을 생성하고 메인 윈도우(primaryStage)의 장면으로 설정했습니다. VBox에는 Label과 Button 컨트롤을 배치했습니다.



 * AppMain.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package javaFX;
 
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
 
public class AppMain extends Application {
 
    @Override
    public void start(Stage primaryStage) throws Exception {
        VBox root = new VBox(); // parent 하위 객체인 vBox 샐성
        root.setPrefWidth(300);
        root.setPrefHeight(200);
        root.setAlignment(Pos.CENTER); // 컨트롤을 중앙 정렬
        root.setSpacing(20); // 컨트롤 수직 간격
 
        Label label = new Label(); // Label 컨트롤 생성
        label.setText("Hi, F(x)? ");
        label.setFont(new Font(50));
 
        Button button = new Button();
        button.setText("Confirm");
        button.setOnAction(event -> Platform.exit()); // 클릭 이벤트 처리
 
        // 라벨과 버튼을 VBox의 자식으로 추가
        root.getChildren().add(label);
        root.getChildren().add(button);
 
        // VBox를 루트 컨테이너로 해서 Scene 생성
        Scene scene = new Scene(root);
 
        primaryStage.setTitle("AppMain");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
 
    public static void main(String[] args) {
        launch(args);
    }
}
 
cs







  VBox는 수직 방향으로 자식 컨트롤을 배치하는 컨테이너로 먼저 Label을 배치하고 그 아래에 Button을 배치했습니다. Button 클릭시 이벤트 발생은 ActionEvent를 람다식으로 처리한 것입니다.








 * 이 포스트은 서적 '이것이 자바다' 를 참고하여 작성한 포스트입니다.


다른 카테고리의 글 목록

CSE/Java 카테고리의 포스트를 톺아봅니다
조회수 확인

멀티 스레드는 여러 절로 구성되어 있습니다.





Intro

작업스레드

스레드 우선순위 & 동기화 메소드와 동기화 블록








스레드 풀

 병렬 작업 처리가 많아지면 스레드 개수가 증가되고 그에 따른 스레드 생성과 스케줄링으로 인해 CPU가 바빠져 메모리 사용량이 늘어납니다. 따라서 어플리케이션의 성능이 저하됩니다.


 갑작스런 병렬작업의 극대화로 인한 스레드 증폭을 막으려면 스레드 풀(Thread Pool)을 사용해야 합니다. 


 스레드 풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐(Queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리합니다.


 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리합니다. 


 자바에서는 스레드 풀을 생성하고 사용할 수 있도록 java.util.concurrent 패키지에서 ExecutorService 인터페이스와 Excutors 클래스를 제공합니다. Executors의 다양한 정적 메소드를 이용해서 ExecutorService 구현 객체를 만들 수 있는데, 이것이 바로 스레드 풀입니다.





 





 스레드 풀 생성 및 종료

  스레드 풀 생성

   ExecutorService 구현 객체는 Executors 클래스의 다음 두 가지 메소드 중 하나르 이용해서 간편하게 생성할 수 있습니다.







   초기 스레드 수는 ExecutorService 객체가 생성될 때 기본적으로 생성되는 스레드 수를 말하고, 코어 스레드 수는 스레드 수가 증가된 후 사용되지 않는 스레드를 스레드 풀에서 제거할 때 최소한 유지해야 할 스레드 수를 말합니다. 최대 스레드 수는 스레드 풀에서 관리하는 최대 스레드의 개수입니다.


   newCachedThreadPool() 메소드로 생성된 스레드 풀의 특징은 초기 스레드 수와 코어 스레드 수는 0개이고, 스레드 개수보다 작업 개수가 많으면 새 스레드를 생성시켜 작업을 처리합니다. 이론적으로는 int 값이 가질 수 있는 최대값만큼 스레드가 추가되지만, 운영체제의 성능과 상황에 따라 달라집니다. 1개 이상의 스레드가 추가되었을 경우 60초 동안 추가된 스레드가 아무 작업을 하지 않으면 추가된 스레드를 종료하고 풀에서 제거합니다. 다음은 newCachedThreadPool() 메소드를 호출해서 ExecutorService 객체를 얻는 코드입니다.



1
2
ExecutorService executorService = Executors.newCachedThreadPool();
 
cs





   newFixedThreadPool(int nThreads) 메소드로 생성된 스레드 풀의 초기 스레드 개수는 0개이고, 코어 스레드 수는 nThreads입니다. 스레드 개수보다 작업 개수가 많으면 새 스레드를 생성시키고 작업을 처리합니다. 최대 스레드 개수는 파라미터로 준 nThreads 입니다. 이 스레드 풀은 스레드가 작업을 처리하지 않고 놀고 있더라도 스레드 개수가 줄지 않습니다. 다음은 CPU 코어의 수만큼 최대 스레드를 사용하는 스레드 풀을 생성합니다.



1
2
3
ExecutorService executorService = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors()
);
cs








  스레드 풀 종료

   스레드 풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아있습니다. 그래서 main() 메소드가 끝나도 어플리케이션 프로세스는 종료되지 않습니다.


   어플리케이션 프로세스를 종료시키려면 스레드 풀을 종료시켜 스레드들이 종료 상태가 되도록 처리해야 합니다. ExecutorService는 종료와 관련해서 다음 세 개의 메소드를 제공하고 있습니다.









   남아있는 작업을 마무리하고 스레드 풀을 종료할 때에는 shutdown()을 일반적으로 호출하고, 남아있는 작업과는 상관없이 강제 종료할 때에는 shutdownNow()를 호출합니다.



1
2
3
executorService.shutdown();
 
executorService.shutdownNow();
cs






 작업 생성과 처리 요청

  작업 생성

   하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현합니다. Runnable과 Callable의 차이점은 작업 처리 완료 후 리턴값이 있느냐 없느냐입니다. 









   Runnable의 run() 메소드는 리턴값이 없고, Callable의 call() 메소드는 리턴값이 있습니다. call()의 리턴 타입은 implements Callable<T> 에서 지정한 타입입니다. 스레드 풀의 스레드는 작업 큐에서 Runnable 또는 Callable 객체를 가져와 run() 과 call() 메소드를 실행합니다.





  작업 처리 요청

   작업 처리 요청이란 ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 말합니다. ExecutorService는 작업 처리 요청을 위해 두 가지 메소드를 제공합니다.






   execute()와 submit() 메소드의 차이점은 두 가지 입니다. 

   - execute()는 작업 처리 결과를 받지 못하지만, submit()은 작업 처리 결과를 Future 타입으로 리턴합니다.

   - execute()는 작업 처리 중 예외가 발생하면 스레드가 종료되고 해당 스레드는 스레드 풀에서 제거되고, 스레드 풀은 다른 작업 처리를 위해 새로운 스레드를 생성합니다.

   - submit()은 작업 처리 중 예외가 발생하면 스레드는 종료되지 않고 다음 작업을 위해 재사용 됩니다.




   다음 예제는 Runnable 작업을 정의할 때 Integer.parseInt("숫자")을 넣어서 NumberFormatException이 발생하도록 유도했습니다. 10개의 작업을 execute() 와 submit() 메소드로 각각 처리 요청했을 경우 스레드 풀의 상태를 살펴보도록 합시다. 먼저 execute() 메소드로 작업 처리 요청을 했을 경우입니다.






 * ExecuteExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package mT;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
 
public class ExecuteExam {
    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
 
        for (int i = 0; i < 10; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
 
                    int poolSize = threadPoolExecutor.getPoolSize();
                    String threadName = Thread.currentThread().getName();
                    System.out.println("[총 스레드 개수: " + poolSize + "] 작업 스레드 이름: " + threadName);
 
                    int value = Integer.parseInt("숫자");
                }
            };
 
            executorService.execute(runnable);
            // executorService.submit(runnable);
 
            Thread.sleep(10);
        }
        executorService.shutdown();
 
    }
}
 
cs





   스레드 풀의 스레드 최대 개수는 변함이 없지만, 실행 스레드의 이름을 보면 모두 다른 스레드가 작업을 처리하고 있습니다. 이것은 작업 처리 도중 예외가 발생하여 해당 스레드는 제거되고 새 스레드를 생성했기 때문입니다.













   이번에는 submit() 메소드로 작업 처리 요청을 한 경우입니다. 25번 라인을 주석 처리하고 26번 라인을 주석 제거해서 실행시키세요. 실행 결과를 보면 예외가 발생하더라도 스레드는 종료되지 않고 계속 재사용되어 다른 작업을 처리하고 있는 것을 볼 수 있습니다.














 블로킹 방식의 작업 완료 통보

  ExecutorService의 submit() 메소드는 파라미터로 준 Runnable 또는 Callable 작업을 스레드 풀의 작업 큐에 저장하고 즉시 Future 객체를 리턴합니다.




 










  Future 객체는 작업 결과가 아니라 작업이 완료될 때까지 기다렸다가 최종 결과를 얻는데 사용합니다. 그래서 Future는 지연 완료(pending Completion) 객체라고 합니다. Future의 get() 메소드를 호출하면 스레드가 작업을 완료할 때까지 블로킹되었다가 작업을 완료하면 처리 결과를 리턴합니다. 이것이 블로킹을 사용하는 작업 완료 통보 방식입니다. 










  리턴 타입인 V는 submit(Runnable task, V result) 의 두 번째 파라미터인 V 타입이거나 submit(Callable<V> task)의 Callable 타입 파라미터 V 타입입니다. 다음은 세 가지 submit() 메소드 별로 Future의 get() 메소드가 리턴하는 값이 무엇인지 보여줍니다.









  Future를 이용한 블로킹 방식의 작업 완료 통보에서 주의할 점은 작업을 처리하는 스레드가 작업을 완료하기 전까지는 get() 메소드가 블로킹되므로 다른 코드를 실행할 수 없습니다. 다른 코드를 하는 스레드가 get() 메소드를 호출하면 작업을 완료하기 전까지 다른 코드를 처리할 수 없게 됩니다. 그렇기 때문에 get() 메소드를 호출하는 스레드는 새로운 스레드이거나 스레드 풀의 또 다른 스레드가 되어야 합니다.










  Future 객체는 작업 결과를 얻기 위한 get() 메소드 이외에도 다음과 같은 메소드를 제공합니다.













  리턴값이 없는 작업 완료 통보

   리턴값이 없는 작업일 경우는 Runnable 객체로 생성하면 됩니다. 다음은 Runnable 객체를 생성하는 방법을 보여줍니다.



1
2
3
4
5
6
7
Runnable task = new Runnable() {
    @Override
    public void run() {
        // Code
    }
}
 
cs






   결과값이 없는 작업 처리 요청은 submit(Runnable task) 메소드를 이용하면 됩니다. 결과값이 없음에도 불구하고 다음과 같이 Future 객체를 리턴하는데, 이것은 스레드가 작업 처리를 정상적으로 완료했는지, 아니면 작업 처리 도중에 예외가 발생했는지 확인하기 위해서 입니다.



1
Future future = executorService.submit(task);
cs




   작업 처리가 정상적으로 완료되었다면 Future의 get() 메소드는 null을 리턴하지만 스레드가 작업 처리 도중 인터럽트되면 InterruptedException을 발생시키고, 작업 처리 도중 예외가 발생하면 ExecutionException을 발생시킵니다. 그래서 다음과 같은 예외 처리 코드가 필요합니다.




1
2
3
4
5
6
7
try {
    future.get();
catch (InterruptedException e) {
 
catch (ExecutionException e) {
 
}
cs





   다음 예제는 리턴값이 없고 단순히 1부터 10까지의 합을 출력하는 작업을 Runnable 객체로 생성하고 스레드 풀의 스레드가 처리하도록 요청한 것입니다.




 * NoResultExam.java



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package mT;
 
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class NoResultExam {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors()
        );
        
        System.out.println("[작업 처리 요청]");
        
        Runnable run = new Runnable() {
            @Override
            public void run() {
                int sum = 0;
                
                for (int i = 1; i <=10; i++) {
                    sum += i;
                }
                
                System.out.println("[처리 결과] : " + sum);
            }
        };
        
        Future future = executorService.submit(run);
        
        try {
            future.get();
            System.out.println("[작업 처리 완료]");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        executorService.shutdown();
    }
}
 
cs












  리턴값이 있는 작업 완료 통보

   스레드 풀의 스레드가 작업을 완료한 후에 어플리케이션이 처리 결과를 얻어야 된다면 작업 객체를 Callable로 생성하면 됩니다. 다음은 Callable 객체를 생성하는 코드인데, 주의할 점은 제네릭 타입 파라미터 T는 call() 메소드가 리턴하는 타입이 되도록 합니다.



1
2
3
4
5
6
7
8
Callable<T> task = new Callable<T>() {
    @Override
    public T call() throws Exception {
        // Code
        return T;
    }
};
 
cs




   Callable 작업의 처리 요청은 Runnable 작업과 마친가지로 ExecutorService의 submit() 메소드를 호출하면 됩니다. submit() 메소드는 작업 큐에 Callable 객체를 저장하고 즉시 Future<T>를 리턴합니다. 이때 T는 call() 메소드가 리턴하는 타입입니다.



1
Future<T> future = executorService.submit(task);
cs




   스레드 풀의 스레드가 Callable 객체의 call() 메소드를 모두 실행하고 T 타입의 값을 리턴하면, Future<T>의 get() 메소드는 블로킹이 해제되고 T 타입의 값을 리턴하게 됩니다.



1
2
3
4
5
6
7
try {
    T result = future.get();
catch (InterruptedException e) {
 
catch (ExecutionException e) {
 
 
cs




   다음 예제는 1부터 10까지의 합을 리턴하는 작업을 Callable 객체로 생성하고, 스레드풀의 스레드가 처리하도록 요청한 것입니다.



 * ResultByCallableExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 
package mT;
 
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class ResultByCallableExam {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors()
        );
        
        System.out.println("[작업 처리 요청]");
        Callable<Integer> task = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                
                for (int i = 1; i <= 10; i++) {
                    sum += i;
                }
                
                return sum;
            }
        };
        
        Future<Integer> future = executorService.submit(task);
        
        try {
            int sum = future.get();
            System.out.println("[처리 결과] : " + sum);
            System.out.println("[작업 처리 완료]");
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        executorService.shutdown();
    }
}
 
cs






  작업 처리 결과를 외부 객체에 저장

   상황에 따라서 스레드가 작업한 결과를 외부 객체에 저장해야 할 경우도 있습니다. 예를 들어 스레드가 작업 처리를 완료하고 외부 Result 객체에 작업 결과를 저장하면, 어플리케이션이 Result 객체를 사용해서 어떤 작업을 진행할 수 있을 것입니다. 대개 Result 객체는 공유 객체가 되어, 두 개 이상의 스레드 작업을 취합할 목적으로 이용됩니다.













   이런 작업을 위해서는 ExecutorService의 submit(Runnable task, V result) 메소드를 사용할 수 있는데, V가 바로 Result 타입이 됩니다. 메소드를 호출하면 즉시 Future<V>가 리턴되는데, Future의 get() 메소드를 호출하면 스레드가 작업을 완료할 때까지 블로킹되었다가 작업을 완료하면 V 타입 객체를 리턴합니다. 리턴된 객체는 submit() 의 두 번째 파라미터로 준 객체와 동일한데, 차이점은 스레드 처리 결과가 내부에 저장되어 있다는 것입니다.





1
2
3
4
5
Result result = ...;
Runnable task = new Task(result);
Future<Result> future = executorService.submit(task, result);
result = future.get();
 
cs






   작업 객체는 Runnable 구현 클래스로 생성하는데 주의할 점은 스레드에서 결과를 저장하기 위해 외부 Result 객체를 사용해야 하므로 생성자를 통해 Result 객체를 주입받도록 해야합니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
class Task implements Runnable {
    Result result;
    Task (Result result) {
        this.result = result;
    }
 
    @Override
    public void run() {
        // Code
 
    }
}
 
cs




   다음 예제는 1부터 10까지의 합을 계산하는 두 개의 작업을 스레드풀에 처리 요청하고, 각각의 스레드가 작업 처리를 완료한 후 산출된 값을 외부 Result 객체에 누적하도록 했습니다.







 * ResultByRunnableExam.java



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
 
package mT;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class ResultByRunnableExam {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors()
        );
        
        System.out.println("작업 처리 요청 !");
        
        class Task implements Runnable {
            Result result;
                
            Task(Result result) {
                this.result = result;
            }
            
            
            @Override
            public void run() {
                int sum = 0;
                
                for (int i = 1; i <= 10; i++) {
                    sum += i;
                }
                
                result.addValue(sum);
            }
        };
        
        Result result = new Result();
        Runnable task1 = new Task(result);
        Runnable task2 = new Task(result);
        Future<Result> future1 = executorService.submit(task1, result);
        Future<Result> future2 = executorService.submit(task2, result);
        
        try {
            result = future1.get();
            result = future2.get();
            System.out.println("처리 결과: " + result.accumValue);
            System.out.println("처리 완료");
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        executorService.shutdown();
    }
}
 
class Result {
    int accumValue;
    synchronized void addValue(int value) {
        accumValue += value;
    }
}
cs












  작업 완료 순으로 통보

   작업 요청 순서대로 작업 처리가 완료되는 것은 아닙니다. 작업의 양과 스케줄링에 따라서 먼저 요청한 작업이 나중에 완료되는 경우도 발생합니다. 


   스레드 풀에서 작업 처리가 완료된 것만 통보받는 방법이 잇는데, CompletionService를 이용하는 것 입니다. CompletionService는 처리 완료된 작업을 가져오는 poll()과 take() 메소드를 제공합니다. 



 






   CompletionService 구현 클래스는 ExecutorCompletionService<V> 입니다. 객체를 생성할 때 생성자 파라미터로 ExecutorService를 제공하면 됩니다.




1
2
3
4
5
6
7
8
ExecutorService executorService = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors()
);
 
CompletionService<V> completionService = new ExecutorCompletionService<V>(
    executorService
);
 
cs



   poll()과 take() 메소드를 이용해서 처리 완료된 작업의 Future를 얻으려면 CompletionService 의 submit() 메소드로 작업 처리 요청을 해야 합니다.



1
2
3
completionService.submit(Callable<V> task);
completionService.submit(Runnable task, V result);
 
cs





   다음은 take() 메소드를 호출하여 완료된 Callable 작업이 있을 때까지 블로킹 되었다가 완료된 작업의 Future를 얻고, get() 메소드로 결과값을 얻어내는 코드입니다. while문은 어플리케이션이 종료될 때까지 반복 실행해야 하므로 스레드 풀의 스레드에서 실행하는 것이 좋습니다.



1
2
3
4
5
6
7
8
9
10
11
executorService.submit(new Runnable() {
    public void run() {
        while (true) {
            try {
                Future<Integer> future = completionService.take();
                int value = future.get();
            } catch (Exception e) { break; }
        }
    }
});
 
cs





   take() 메소드가 리턴하는 완료된 작업은 submit() 으로 처리 요청한 작업의 순서가 아님을 명심해야 합니다. 작업의 내용에 따라서 먼저 요청한 작업이 나중에 완료될 수 있기 때문입니다. 더 이상 완료된 작업을 가져올 필요가 없다면 take() 블로킹에서 빠져나와 while 문을 종료해야 합니다. ExecutorService의 shutdownNow()를 호출하면 take() 에서 InterruptedException이 발생하고 catch 절에서 break 가 되어 while문을 종료하게 됩니다.


   다음 예제는 3개의 Callable 작업을 처리 요청하고 처리가 완료되는 순으로 작업의 결과값을 출력했습니다.





  * CompletionServiceExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
 
package mT;
 
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
 
public class CompletionServiceExam {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(
            Runtime.getRuntime().availableProcessors()
        );
        
        // CompletionService 생성
        CompletionService<Integer> completionService = 
                new ExecutorCompletionService<>(executorService);
        
        System.out.println("처리 요청");
        
        for (int i = 0; i < 3; i++) {
            completionService.submit(new Callable<Integer>() {
                public Integer call() throws Exception {
                    int sum = 0;
                    
                    for (int i = 1; i <= 10; i++) {
                        sum += i;
                    }
                    
                    return sum;
                }
            });
        }
        
        System.out.println("처리 완료된 작업 확인");
        executorService.submit(new Runnable() {
            public void run() {
                while (true) {
                    try {
                        Future<Integer> future = completionService.take();
                        int value = future.get();
                        System.out.println("처리 결과: " + value);
                    } catch (Exception e) {break;}
                }
            }
        });
        
        try { Thread.sleep(3000); } catch (InterruptedException e) {}
        executorService.shutdownNow();
    }
}
 
cs














 콜백 방식의 작업 완료 통보

  이번에는 콜백(callback) 방식을 이용해서 작업 완료 통보를 받는 방법에 대해서 알아봅시다.


  콜백이란 어플리케이션이 스레드에게 작업 처리를 요청한 후, 스레드가 작업을 완료하면 특정 메소드를 자동 실행하는 기법을 말합니다. 이때 자동 실행되는 메소드를 콜백 메소드(callback Method)라고 합니다. 다음은 블로킹 방식과 콜백 방식을 비교한 그림입니다.











  콜백 방식은 작업 처리를 요청한 후 결과를 기다릴 필요 없이 다른 기능을 수행할 수 있습니다. 그 이유는 작업 처리가 완료되면 자동적으로 콜백 메소드가 실행되어 결과를 알 수 있기 때문입니다.


  아래는 CompletionHandler를 이용해서 콜백 객체를 생성하는 코드입니다.



1
2
3
4
5
6
7
8
9
10
11
12
CompletionHandler<V, A> callback = new CompletionHandler<V, A>() {
    @Override
    public void completed(V result, A attachment) {
 
    }
 
    @Override
    public void failed(Throwable exc, A attachment) {
 
    }
}
 
cs






  CompletionHandler는 completed()와 failed() 메소드가 있습니다.

  - completed(): 작업을 정상 처리 완료했을 때 호출되는 콜백 메소드입니다.

  - failed(): 작업 처리 도중 예외가 발생했을 때 호출되는 콜백 메소드입니다.

  


  다음 예제를 통해 세부적으로 알아봅시다.




 * CallbackExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
 
package mT;
 
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class CallbackExam {
    private ExecutorService executorService;
    
    public CallbackExam() {
        executorService = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors()
        );
    }
    
    private CompletionHandler<Integer, Void> callback = 
            new CompletionHandler<Integer, Void>() {
 
                @Override
                public void completed(Integer result, Void attachment) {
                    System.out.println("completed() 실행: " + result);
                }
 
                @Override
                public void failed(Throwable exc, Void attachment) {
                    System.out.println("failed() 실행: " + exc.toString());
                }
                
    };
    
    public void doWork(final String x, final String y) {
        Runnable task = new Runnable() {
            public void run() {
                try {
                     int intX = Integer.parseInt(x);
                     int intY = Integer.parseInt(y);
                     
                     int result = intX + intY;
                     callback.completed(result, null);
                     
                } catch (NumberFormatException e) {
                    callback.failed(e, null);
                }
            }
        };
        executorService.submit(task);
    }
    
    public void finish() {
        executorService.shutdown();
    }
    
    public static void main(String[] args) {
        CallbackExam exam = new CallbackExam();
        exam.doWork("4""7");
        exam.doWork("4""자바");
        exam.finish();
    }
}
 
cs


















 * 이 포스트은 서적 '이것이 자바다' 를 참고하여 작성한 포스트입니다.


다른 카테고리의 글 목록

CSE/Java 카테고리의 포스트를 톺아봅니다
조회수 확인

멀티 스레드는 여러 절로 구성되어 있습니다.

 


Intro

작업스레드

스레드 우선순위 & 동기화 메소드와 동기화 블록





데몬 스레드

 데몬(daemon) 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 하는 스레드입니다. 주 스레드가 종료되면 데몬 스레드는 강제적으로 종료되는데, 그 이유는 주 스레드의 보조 역할을 수행하므로 주 스레드가 종료되면 데몬 스레드의 존재 의미가 없어지기 때문입니다.


 스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)을 호출해주면 됩니다. 아래 코드를 보면 메인 스레드가 주 스레드가 되고 AutoSaveThread가 데몬 스레드가 됩니다.




1
2
3
4
5
6
7
8
public static void main(String[] args) {
    AutoSaveThread thread = new AutoSaveThread();
    thread.setDaemon(true);
    thread.start();
 
    ...
}
 
cs





 주의할 점은 start() 메소드가 호출되고 나서 setDaemon(true)를 호출하면 IllegalThreadStateException이 발생하기 때문에 start() 메소드 호출 전에 setDaemon(true)를 호출해야 합니다.


 다음 예제는 1초 주기로 save() 메소드를 자동 호출하도록 AutoSaveThread()를 작성하고, 메인 스레드가 3초 후 종료되면 AutoSaveThread도 같이 종료되도록 데몬 스레드로 만들었습니다.



 * AutoSaveThread.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
package mT;
 
public class AutoSaveThread extends Thread {
    
    public void save() {
        System.out.println("작업 내용 저장함");
    }
    
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
            save();
        }
    }
    
 
}
 
cs





 * DaemonExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
package mT;
 
public class DaemonExam {
    public static void main(String[] args) {
        AutoSaveThread asThread = new AutoSaveThread();
        asThread.setDaemon(true);
        asThread.start();
        
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {}
        
        System.out.println("메인 스레드 종료");
    }
}
 
cs






 











스레드 그룹

 스레드 그룹(Thread Group)은 관련된 스레드를 묶어서 관리할 목적으로 이용합니다. JVM이 실행되면 system 스레드 그룹을 만들고, JVM 운영에 필요한 스레드들을 생성해서 system 스레드 그룹에 포함시킵니다. 그리고 system의 하위 스레드 그룹으로 main을 만들고 메인 스레드를 main 스레드 그룹에 포함시킵니다.


 스레드는 반드시 하나의 그룹에 포함되는데, 명시적으로 스레드 그룹에 포함시키지 않으면 기본적으로 자신을 생성한 스레드와 같은 그룹에 속하게 됩니다. 




 스레드 그룹 이름 얻기

  현재 스레드가 속한 스레드 그룹의 이름을 얻고 싶다면 다음과 같은 코드를 사용할 수 있습니다.


1
2
3
4
ThreadGroup threadGroup = Thread.currentThread.getThreadGroup();
 
String groupName = threadGroup.getName();
 
cs




  Thread의 정적 메소드인 getAllStackTraces()를 이용하면 프로세스 내에서 실행하는 모든 스레드에 대한 정보를 얻을 수 있습니다.



1
2
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
 
cs




  getAllStackTraces() 메소드는 Map 타입의 객체를 리턴하는데, 키는 스레드 객체이고 값은 스레드의 상태 기록들을 갖고 있는 StackTraceElement[] 배열입니다.


  다음 예제는 현재 실행하고 있는 스레드의 이름과 데몬 여부 그리고 속한 스레드 그룹 이름이 무엇인지 출력합니다.





 * ThreadInfoExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 
package mT;
 
import java.util.Map;
import java.util.Set;
 
public class ThreadInfoExam {
    public static void main(String[] args) {
        AutoSaveThread asThread = new AutoSaveThread();
        asThread.setName("AutoSaveThread");
        asThread.setDaemon(true);
        asThread.start();
 
        Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
        Set<Thread> threads = map.keySet();
 
        for (Thread thread : threads) {
            System.out.println("Name: " + thread.getName() + ((thread.isDaemon()) ? "(Daemon)" : "(Main)"));
            System.out.println("\t" + "소속그룹: " + thread.getThreadGroup().getName());
            System.out.println();
        }
    }
 
}
 
cs















 스레드 그룹 생성

  명시적으로 스레드 그룹을 만들고 싶다면 다음 생성자 중 하나를 이용해서 ThreadGroup 객체를 만들면 됩니다. threadGroup 이름만 주거나, 부모 ThreadGroup과 이름을 파라미터로 줄수 있습니다.



1
2
ThreadGroup tg = new ThreadGroup(String name);
ThreadGroup tg = new ThreadGroup(ThreadGroup parent, String name);
cs




  스레드 그룹 생성 시 부모(parent) 스레드 그룹을 지정하지 않으면 현재 스레드가 속한 그룹의 하위 그룹으로 생성됩니다.


  새로운 스레드 그룹을 생성한 후, 이 그룹에 스레드를 포함시키려면 Thread 객체를 생성할 때 생성자 파라미터로 스레드 그룹을 지정하면 됩니다. 스레드 그룹을 파라미터로 갖는 Thread 생성자는 다음 네 가지가 있습니다.



1
2
3
4
Thread t = new Thread(ThreadGroup tg, Runnable target);
Thread t = new Thread(ThreadGroup tg, Runnable target, String name);
Thread t = new Thread(ThreadGroup tg, Runnable target, String name, long stackSize);
Thread t = new Thread(ThreadGroup tg, String name);
cs




  Runnable 타입의 target은 Runnable 구현 객체를 말하며, String 타입의 name은 스레드의 이름입니다. 그리고 long 타입의 stackSize는 JVM이 이 스레드에 할당할 stack 크기입니다.








 스레드 그룹의 일괄 interrupt()

  스레드를 스레드 그룹에 포함시키면 스레드 그룹에서 제공하는 interrupt() 메소드를 이용하면 그룹 내에 포함된 모든 스레드들을 일괄 interrupt 할 수 있습니다.


  스레드 그룹의 interrupt() 메소드는 소속된 스레드의 interrupt() 메소드를 호출만 할 뿐 개별 스레드에서 발생하는 InterruptedException에 대한 예외 처리를 하지 않습니다. 따라서 안전한 종료를 위해서는 개별 스레드가 예외 처리를 해야 합니다.


  다음은 ThreadGroup이 가지고 있는 주요 메소드들입니다.



 






  다음 예제는 스레드 그룹을 생성하고, 정보를 출력해봅니다. 그리고 3초 후 스레드 그룹의 interrupt() 메소드를 호출해서 스레드 그룹에 포함된 모든 스레드들을 종료시킵니다.





 * WorkThread.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package mT;
 
public class WorkThread extends Thread {
    public WorkThread(ThreadGroup tg, String threadName) {
        super(tg, threadName);
    }
    
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                System.out.println(getName() + " interrupted"); 
                break;
            }
        }
        
        System.out.println(getName() + " 종료됨");
        
    }
}
 
cs






 * ThreadGroupExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 
package mT;
 
public class ThreadGroupExam {
    public static void main(String[] args) {
        ThreadGroup myGroup = new ThreadGroup("myGroup");
        WorkThread wThreadA = new WorkThread(myGroup, "wThreadA");
        WorkThread wThreadB = new WorkThread(myGroup, "wThreadB");
        
        wThreadA.start();
        wThreadB.start();
        
        System.out.println("[ main 스레드 그룹의 list() 메소드 출력 내용] ");
        ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
        mainGroup.list();
        System.out.println();
        
        try { Thread.sleep(3000); } catch (Exception e) {}
        
        System.out.println("[ myGroup 스레드 그룹의 interrupt() 메소드 호출]" );
        myGroup.interrupt();
    }
 
}
 
cs




 














 * 이 포스트은 서적 '이것이 자바다' 를 참고하여 작성한 포스트입니다.

다른 카테고리의 글 목록

CSE/Java 카테고리의 포스트를 톺아봅니다

[Java] 멀티 스레드 - 상태 제어

2015.12.12 18:55 - Palpit
조회수 확인

멀티 스레드는 여러 절로 구성되어 있습니다.

 


Intro

작업스레드

스레드 우선순위 & 동기화 메소드와 동기화 블록



 


상태 제어
 스레드 간 협업(wait(), notify(), notifyAll())
  경우에 따라서는 두 개의 스레드를 교대로 번갈아가며 실행해야 할 경우가 있습니다. 정확한 교대작업이 필요할 경우, 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만드는 것입니다. 이 방법의 핵심은 공유 객체에 있습니다.

  공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 구분해 놓습니다. 한 스레드가 작업을 완료하면 notify() 메소드로 풀어주고, 자신은 wait() 메소드를 호출하여 일시 정지 상태로 만듭니다.

 






  wait() 메소드의 시간 파라미터를 주게 되면 notify()를 호출하지 않아도 지정 시간이 지나면 스레드가 자동적으로 실행 대기 상태가 됩니다. 

  notify() 메소드와 동일한 역할을 하는 notifyAll() 메소드도 있는데, notify() 는 wait()에 의해 일시 정지된 스레드 중 한개를 실행 대기 상태로 만들고, notifyAll() 메소드는 wait()에 의해 일시 정지된 모든 스레드들을 실행 대기 상태로 만듭니다.

  위 메소드들은 동기화 메소드 또는 동기화 블록 내에서만 사용할 수 있습니다.

  다음 예제는 두 스레드의 작업을 WorkObject의 methodA()와 methodB()에 정의해 두고, 두 스레드 ThreadA와 ThreadB가 교대로 methodA()와 methodB()를 호출하게 했습니다.



 * WorkObject.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package mT;
 
public class WorkObject {
    public synchronized void methodA() {
        System.out.println("ThreadA의 methodA() 작업 실행");
        notify(); // 일시 정지 상태에 있는 ThreadB를 실행 대기 상태로 만듬
 
        try {
            wait(); // ThreadA를 일시 정지 상태로 만듬
        } catch (InterruptedException e) {
        }
    }
 
    public synchronized void methodB() {
        System.out.println("ThreadB의 methodB() 작업 실행");
        notify();
 
        try {
            wait();
        } catch (InterruptedException e) {
        }
    }
}
 
cs


 * ThreadA.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
package mT;
 
public class ThreadA extends Thread {
    
    private WorkObject workObject;
    
    public ThreadA(WorkObject workObject) {
        this.workObject = workObject;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            workObject.methodA();
        }
    }
 
}
 
cs


 * ThreadB.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
package mT;
 
public class ThreadB extends Thread {
    
    private WorkObject workObject;
    
    public ThreadB(WorkObject workObject) {
        this.workObject = workObject;
    }
    
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            workObject.methodB();
        }
    }
 
}
 
cs





 * WaitNotifyExam.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 
package mT;
 
public class WaitNotifyExam {
    public static void main(String[] args) {
        WorkObject workObject = new WorkObject();
 
        ThreadA threadA = new ThreadA(workObject);
        ThreadB threadB = new ThreadB(workObject);
 
        threadA.start();
        threadB.start();
    }
}
 
cs



 두 프로세스가 한치에 오차없이 번갈아 출력한다는 것을 확인할 수 있습니다.





  다음 예제는 데이터를 저장하는 스레드가 데이터를 저장하면, 데이터를 소비하는 스레드가 데이터를 읽고 처리하는 과정을 예제로 보겠습니다.

  생성자 스레드는 소비자 스레드가 읽기 전에 새로운 데이터를 두 번 생성하면 안 되고(setData() 메소드를 두 번 실행하면 안 됨), 소비자 스레드는 생성자 스레드가 데이터를 생성하기 전에 이전 데이터를 두 번 읽어서는 안 됩니다.




 * DataBox.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 
package mT;
 
public class DataBox {
    private String data;
 
    public synchronized String getData() {
        if (this.data == null) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
 
        String returnValue = data;
        System.out.println("ConsumerThread가 읽은 데이터: " + returnValue);
        data = null;
        ;
        notify();
 
        return returnValue;
 
    }
 
    public synchronized void setData(String data) {
        if (this.data != null) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
 
        this.data = data;
        System.out.println("ProducerThread가 생성한 데이터: " + data);
        notify();
    }
 
}
 
cs


 * ProducerThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
package mT;
 
public class ProducerThread extends Thread {
 
    private DataBox dataBox;
 
    public ProducerThread(DataBox dataBox) {
        this.dataBox = dataBox;
    }
 
    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            String data = "Data-" + i;
            dataBox.setData(data);
        }
    }
 
}
 
cs


 * ConsumerThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 
package mT;
 
public class ConsumerThread extends Thread {
 
    private DataBox dataBox;
 
    public ConsumerThread(DataBox dataBox) {
        this.dataBox = dataBox;
    }
 
    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            String data = dataBox.getData();
        }
    }
}
 
cs


 * WaitNotifyExam2.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 
package mT;
 
public class WaitNotifyExam2 {
    public static void main(String[] args) {
        DataBox dataBox = new DataBox();
 
        ProducerThread pThread = new ProducerThread(dataBox);
        ConsumerThread cThread = new ConsumerThread(dataBox);
 
        pThread.start();
        cThread.start();
    }
}
 
cs











 스레드의 안전한 종료(stop 플래그, interrupt())
  스레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료합니다. 하지만 경우에 따라 실행 중인 스레드를 즉시 종료시킬 필요가 있습니다.

  * 기존의 stop() 메소드가 있었지만 Deprecated 되었습니다. stop() 메소드를 통해서 자원의 safety가 지켜지지 않기 때문입니다.

  

  stop 플래그를 이용하는 방법
   스레드는 run() 메소드가 끝나면 자동적으로 종료되므로, run() 메소드가 정상적으로 종료되도록 유도하는 것이 최선의 방법입니다.

   다음 코드는 stop 플래그를 이용해서 run() 메소드의 종료를 유도합니다.



1
2
3
4
5
6
7
8
9
10
11
public class XXXThread extends Thread {
    private boolean stop;    // stop flag
 
    public void run() {
        while (!stop) {
 
        }
        
    }
}
 
cs
  



   위 코드에서 stop 필드가 false 일 경우에는 while문의 조건식이 true가 되어 반복 실행되지만, stop 필드가 true 일 경우 while 문의 조건식이 false가 되어 while문을 빠져나옵니다. 

  
   다음 예제는 PrintThread1을 실행한 후 1초 후에 PrintThread1을 멈추도록 setStop() 메소드를 호출합니다.



 * StopFlagExam.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
package mT;
 
public class StopFlagExam {
    public static void main(String[] args) {
        PrintThread1 pThread = new PrintThread1();
        pThread.start();
 
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
 
        }
        
        pThread.setStop(true);    // 스레드 종료를 위한 stop 플래그 조작
    }
}
 
cs


 * PrintThread1.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
package mT;
 
public class PrintThread1 extends Thread {
    
    private boolean stop;        // stop flag
    
    public void setStop(boolean stop) {
        this.stop = stop;
    }
    
    public void run() {
        while(!stop) {
            System.out.println("실행 중");
        }
        
        System.out.println("자원 정리");
        System.out.println("종료");
    }
 
}
 
cs









  interrupt() 메소드를 이용하는 방법
   interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 합니다. 이 메소드를 이용하면 run() 메소드를 정상 종료 할 수 있습니다.

   예를 들어 다음 그림과 같이 ThreadA와 ThreadB를 생성해서 start() 메소드로 ThreadB를 실행시켰다고 가정해봅시다.












   ThreadA가 ThreadB의 interrupt() 메소드를 실행하게 되면 ThreadB가 sleep() 메소드로 일시 정지 상태가 될 때 ThreadB에서 InterruptB에서 InterruptedException이 발생하여 예외 처리(catch) 블록으로 이동합니다. 결국 ThreadB는 while문을 빠져나와 run() 메소드를 정상 종료하게 됩니다. 

   다음 예제는 PrintThread2를 실행한 후 1초 후에 PrintThread2를 멈추도록 interrupt() 메소드를 호출합니다.



 * InterruptExam.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package mT;
 
public class InterruptExam {
    public static void main(String[] args) {
        Thread thread = new PrintThread2();
        
        thread.start();
        
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        
        thread.interrupt();
    }
}
 
cs


 * PrintThread2.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
package mT;
 
public class PrintThread2 extends Thread {
 
    public void run() {
        try {
            while (true) {
                System.out.println("실행 중");
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
        }
 
        System.out.println("자원 정리");
        System.out.println("종료");
    }
 
}
 
cs








   주목할 점은 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt() 메소드가 실행되면 즉시 InterruptedException 예외가 발생하지 않고, 스레드가 미래에 일시 정지 상태가 되면 InterruptedException 예외가 발생한다는 것입니다. 따라서 스레드가 일시 정지 상태가 되지 않으면 interrupt() 메소드 호출은 아무 의미가 없습니다.

   일시 정지를 만들지 않고도 interrupt() 호출 여부를 알 수 있는 방법이 있습니다. interrupt() 메소드가 호출되었다면 스레드의 interrupted() 와 isInterrupted() 메소드는 true을 리턴합니다. interrupted()는 정적 메소드로 현재 스레드가 interrupted 되었는지 확인하고, isInterrupted()는 인스턴스 메소드로 현재 스레드가 interrupted 되었는지 확인할 때 사용합니다.


1
2
boolean status = Thread.interrupted();
boolean status2 = objThread.isInterrupted();
cs




   












 * 이 포스트은 서적 '이것이 자바다' 를 참고하여 작성한 포스트입니다.



다른 카테고리의 글 목록

CSE/Java 카테고리의 포스트를 톺아봅니다
조회수 확인

멀티 스레드는 여러 절로 구성되어 있습니다.

 


Intro

작업스레드

스레드 우선순위 & 동기화 메소드와 동기화 블록





스레드 상태

 스레드 객체를 생성하고, start() 메소드를 호출하면 곧바로 스레드가 실행되는 것처럼 보이지만 사실은 대기 상태가 됩니다. 


 실행 대기 상태란 아직 스케줄링 되지 않아서 실행을 기다리고 있는 상태를 말합니다.


 실행 대기 상태에 있는 스레드 중에서 스케줄링으로 선택된 스레드가 CPU를 점유하고 run() 메소드를 실행하는데, 이를 실행(Running) 상태라고 합니다. 실행 상태의 스레드는 run() 메소드를 모두 실행하기 전에 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있습니다.


 실행 상태에서 run() 메소드가 종료되면, 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈추게 됩니다. 이 상태를 종료 상태라고 합니다.



 















 이러한 스레드의 상태를 코드에서 확인할 수 있도록 하기 위해 Java 5 부터 Thread 클래스에 getState() 메소드가 추가되었습니다. getState() 메소드는 다음 표처럼 스레드 상태에 따라서 Thread.State 열거 상수를 리턴합니다.







 다음은 스레드의 상태를 출력하는 StatePrintThread 클래스입니다. 생성자 파라미터로 받은 타겟 스레드의 상태를 0.5 초 주기로 출력합니다.




 * StatePrintThread.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package mT;
 
public class StatePrintThread extends Thread {
    private Thread targetThread;
    
    public StatePrintThread(Thread targetThread) {
        this.targetThread = targetThread;
    }
    
    @Override
    public void run() {
        while(true) {
            Thread.State state = targetThread.getState();
            System.out.println("타겟 스레드 상태: " + state);
            
            if (state == Thread.State.NEW) {
                targetThread.start();
            }
            
            if (state == Thread.State.TERMINATED) {
                break;
            }
            
            try {
                Thread.sleep(500);
            } catch (Exception e) {}
        }
    }
}
 
cs





 다음은 타겟 스레드 클래스입니다. 20억 번 루핑을 돌게하여 RUNNABLE 상태를 유지하고 sleep() 메소드를 호출해서 1.5초간 TIMED_WAITING 상태를 유지합니다.  



 * TargetThread.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
package mT;
 
public class TargetThread extends Thread {
    @Override
    public void run() {
        for (long i = 0; i < 2000000000; i++) {
            
        }
        
        try {
            Thread.sleep(1500);
        } catch (Exception e) {}
        
        for (long i = 0; i < 2000000000; i++) {
            
        }
    }
}
 
cs






 다음은 StatePrintThread를 생성해서 파라미터로 전달받은 TargetThread의 상태를 출력하도록 작성된 실행 클래스입니다.





 * ThreadStateExam.java


1
2
3
4
5
6
7
8
9
10
11
 
package mT;
 
public class ThreadStateExam {
    public static void main(String[] args) {
        StatePrintThread thread = new StatePrintThread(new TargetThread());
        thread.start();
    }
 
}
 
cs


















스레드 상태 제어

 다음 그림은 상태 변화를 가져오는 메소드의 종류를 보여줍니다.











 위 표에서 wait(), notify(), notifyAll()은 Object 클래스의 메소드이고, 그 이외의 메소드는 모두 Thread 클래스의 메소드들입니다. 






 주어진 시간동안 일시 정지(sleep())

  실행 중인 스레드를 일정 시간 멈추게 하고 싶다면 Thread() 클래스의 정적 메소드인 sleep()을 사용하면 됩니다. 파라미터로 얼마 동안 일시 정지 상태로 있을 것인지 밀리세컨드(1/1000) 단위로 시간을 주시면 됩니다.






 다른 스레드에게 실행 양보(yield())

  yield() 메소드를 호출한 스레드는 실행 대기 상태로 돌아가고 동일한 우선순위 또는 높은 우선순위를 갖는 다른 스레드가 실행 기회를 가질수 있도록 해줍니다.


  다음 예제는 처음 실행 후 3초 동안 ThreadA와 ThreadB를 번갈아가며 실행됩니다. 3초 후에 메인 스레드가 ThreadA의 work 필드를 false로 변경함으로써 ThreadA는 yield() 메소드를 호출합니다. 따라서 이후 3초 동안은 ThreadB가 더 많은 실행 기회를 얻게 됩니다. 메인 스레드는 3초 뒤에 다시 ThreadA의 work 필드를 true로 변경하여 ThreadA와 B가 번갈아가며 실행하도록 합니다. 마지막으로 메인 스레드는 3초 뒤에 ThreadA와 ThreadB의 stop 필드를 true로 변경해서 두 스레드가 반복 작업을 중지하고 종료하도록 합니다.





 * YieldExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 
package mT;
 
public class YieldExam {
    public static void main(String[] args) {
        ThreadA threadA = new ThreadA();
        ThreadB threadB = new ThreadB();
        
        threadA.start();
        threadB.start();
        
        try { Thread.sleep(3000); } catch(InterruptedException e) {}
        threadA.work = false;
        
        try { Thread.sleep(3000); } catch(InterruptedException e) {}
        threadA.work = true;
        
        try { Thread.sleep(3000); } catch(InterruptedException e) {}
        threadA.stop = true;
        threadB.stop = true;
    }
 
}
 
cs






 * ThreadA.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
package mT;
 
public class ThreadA extends Thread {
    public boolean stop = false;
    public boolean work = true;
    
    @Override
    public void run() {
        while(!stop) {
            if (work) {
                System.out.println("ThreadA 작업 내용");
            } else {
                Thread.yield();
            }
        }
        System.out.println("ThreadA 종료");
    }
 
}
 
cs




 * ThreadB.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
package mT;
 
public class ThreadB extends Thread {
    public boolean stop = false;
    public boolean work = true;
    
    @Override
    public void run() {
        while(!stop) {
            if (work) {
                System.out.println("ThreadB 작업 내용");
            } else {
                Thread.yield();
            }
        }
        System.out.println("ThreadB 종료");
    }
 
}
 
cs










 다른 스레드의 종료를 기다림(join())

  스레드는 다른 스레드와 독립적으로 실행하는 것이 기본이지만 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우가 발생할 수도 있습니다. 


  이런 경우에 join() 메소드를 제공합니다. join() 메소드를 호출하면 호출된 스레드가 종료될 때까지 일시 정지 상태로 대기하다가 실행이 됩니다.


  다음 예제는 SumThread가 종료될 때까지 메인 스레드가 기달렸다가 출력하는 예제입니다.





 * SumThread.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
package mT;
 
public class SumThread extends Thread {
    private long sum;
    
    public long getSum() {
        return sum;
    }
    
    public void setSum(long sum) {
        this.sum = sum;
    }
    
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
    }
}
 
cs



 * JoinExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
package mT;
 
public class JoinExam {
    public static void main(String[] args) {
        SumThread thread = new SumThread();
        thread.start();
        
        try {
            thread.join();     // 메인 스레드가 해당 스레드의 종료까지 대기
        } catch (InterruptedException e) {}
        
        System.out.println("1 ~ 100 까지 합: " + thread.getSum());
    }
}
 
cs





 




















 * 이 포스트은 서적 '이것이 자바다' 를 참고하여 작성한 포스트입니다.



다른 카테고리의 글 목록

CSE/Java 카테고리의 포스트를 톺아봅니다
조회수 확인

멀티 스레드는 여러 절로 구성되어 있습니다.


  

Intro

작업스레드

스레드 우선순위 & 동기화 메소드와 동기화 블록





 

스레드 우선순위

 멀티 스레드는 동시성(Concurrency) 또는 병렬성(Parallelism)으로 실행되기 때문에 이 용어들에 대해 정확히 이해하는 것이 좋습니다.


 동시성은 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질을 말합니다.


 병렬성은 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질을 말합니다.




 






 스레드의 개수가 코어의 수보다 많을 경우, 스레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정해야 하는데, 이것을 스레드 스케줄링이라고 합니다. 스케줄링에 의해 스레드들은 아주 짧은 시간에 번갈아가면서 run() 메소드를 실행시킵니다.

 


 자바의 스레드 스케줄링은 우선순위(Priority) 방식과 순환 할당(Round-Robin) 방식을 사용합니다. 


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


 순환 할당 방식은 시간 할당량(Time Slice)를 정해서 하나의 스레드를 정해진 시간만큼 실행시키고 다시 다른 스레드를 실행하는 방식을 말합니다.


 우선순위 방식은 스레드 객체에 우선순위 번호를 부여할 수 있기 때문에 개발자가 코드로 제어할 수 있지만, 순환 할당 방식은 자바 가상 기계(JVM)에 의해 정해지기 때문에 코드로 제어할 수 없습니다.




 우선순위 방식에서 우선순위는 1에서부터 10까지 부여되는데, 1이 가장 우선순위가 낮고, 10이 가장 높습니다. 우선순위를 부여하지 않으면 모든 스레드들은 기본적으로 5의 우선순위를 할당받습니다.


 우선순위 변경 메소드는 Thread 클래스가 제공하는 setPriority() 메소드를 이용하면 됩니다.



1
thread.setPriority(Priority);
cs




 우선순위의 파라미터는 1~10까지의 값을 직접 주어도 좋지만, 코드의 가독성을 높이기 위해 Thread 클래스의 상수를 사용할 수도 있습니다.



1
2
3
thread.setPriority(Thread.MAX_PRIORITY);
thread.setPriority(Thread.MIN_PRIORITY);
thread.setPriority(Thread.NORM_PRIORITY);
cs






 이 상수는 각각 10, 1, 5의 값을 가지고 있습니다. 흔히 쿼드 코어에서는 4개의 스레드가 병렬성으로 실행될 수 있기 때문에 4개 이하의 스레드를 실행할 경우에는 우선순위 방식이 크게 영향을 미치지 못합니다. 최소한 5개 이상의 스레드가 실행되어야 우선순위에 영향을 받습니다.


 다음은 스레드 10개를 생성하고 20억번 루핑을 누가 더 빨리 끝내는가를 테스트하는 예제입니다. Thread10만 가장 높은 우선순위를 주었고, 나머지는 가장 낮은 우선순위로 설정했습니다.





 * CalcThread.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
package mT;
 
public class CalcThread extends Thread {
    public CalcThread(String name) {
        setName(name);
    }
    
    @Override
    public void run() {
        for (long i = 0; i < 2000000000; i++) {
            
        }
        System.out.println(getName());
    }
}
 
cs



 * PriorityExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
package mT;
 
public class PriorityExam {
    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            Thread thread = new CalcThread("thread" + i);
 
            if (i != 10) {
                thread.setPriority(Thread.MIN_PRIORITY);
            } else {
                thread.setPriority(Thread.MAX_PRIORITY);
            }
 
            thread.start();
        }
    }
 
}
 
cs

















동기화 메소드와 동기화 블록

 공유 객체를 사용할 때의 주의할 점

  싱글 스레드 프로그램에서는 한 개의 스레드가 객체를 독차지해서 사용하면 되지만, 멀티 스레드 프로그램에서는 스레드들이 객체를 공유해서 작업해야 하는 경우가 있습니다. 흔히 다루는 Computer Science에서 다루는 문제점으로 세마포어, C-P 문제, 락 등 여러 해결점이 있습니다.



  예제를 통해서 공유 객체의 문제점을 직접 보도록 합시다.




 * MainThreadExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
package mT;
 
public class MainThreadExam {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
 
        User1 user1 = new User1();
        user1.setCalculator(calc);
        user1.start();
 
        User2 user2 = new User2();
        user2.setCalculator(calc);
        user2.start();
    }
 
}
 
cs





 * Calculator.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
package mT;
 
public class Calculator {
    private int memory;
 
    public int getMemory() {
        return memory;
    }
 
    public void setMemory(int memory) {
        this.memory = memory;
        try {
            Thread.sleep(2000); // 2초간 sleep
        } catch (Exception e) {
        }
 
        System.out.println(Thread.currentThread().getName() + ": " + this.memory);
    }
}
 
cs





 * User1.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
package mT;
 
public class User1 extends Thread {
    private Calculator calc;
 
    public void setCalculator(Calculator calc) {
        this.setName("calcUser1");
        this.calc = calc;
    }
 
    public void run() {
        calc.setMemory(100);
    }
}
 
cs





 * User2.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
package mT;
 
public class User2 extends Thread {
    private Calculator calc;
 
    public void setCalculator(Calculator calc) {
        this.setName("calcUser2");
        this.calc = calc;
    }
 
    public void run() {
        calc.setMemory(50);
    }
 
}
 
cs












 동기화 메소드 및 동기화 블록

  스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸어서 다른 스레드가 사용할 수 없도록 해야 합니다. 멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역(critical Section)이라고 합니다.


  자바는 임계 영역을 지정하기 위해 동기화(synchronized) 메소드와 동기화 블록을 제공합니다. 동기화 메소드를 만드는 방법은 다음과 같이 메소드 선언에 synchronized 키워드를 붙이면 됩니다.



1
2
3
4
public synchronized void method() {
    critical Section; // 
}
 
cs






  동기화 메소드는 메소드 전체 내용이 임계 영역이므로 스레드가 동기화 메소드를 실행하는 즉시 객체에는 잠금이 일어나고, 스레드가 동기화 메소드를 종료하면 잠금 해제 됩니다. 메소드 전체 내용이 아니라, 일부 내용만 임계 영역으로 만들고 싶다면 동기화 블록을 만들면 됩니다.




1
2
3
4
5
6
7
8
9
10
public void method() {
    ...
 
    synchronized(sharedObject) {
        critical Section;
    }
 
    ...
 
}
cs





  동기화 블록의 외부 코드들은 여러 스레드가 동시에 실행할 수 있지만, 동기화 블록의 내부 코드는 임계 영역이므로 한 번에 한 스레드만 실행할 수 있고 다른 스레드는 실행 할 수 없습니다. 만약 동기화 메소드와 블록이 여러 개 있을 경우, 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론이고 다른 동기화 메소드 및 블록도 실행할 수 없습니다. 하지만 일반 메소드는 실행이 언제든 가능합니다.














  다음 예제는 이전 예제에서 문제가 된 공유 객체인 Calculator를 수정한 것입니다.



 * Calculator.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package mT;
 
public class Calculator {
    private int memory;
 
    public int getMemory() {
        return memory;
    }
 
    public synchronized void setMemory(int memory) {
        this.memory = memory;
        try {
            Thread.sleep(2000); // 2초간 sleep
        } catch (Exception e) {
        }
 
        System.out.println(Thread.currentThread().getName() + ": " + this.memory);
    }
}
 
cs

















 * 이 포스트은 서적 '이것이 자바다' 를 참고하여 작성한 포스트입니다.

다른 카테고리의 글 목록

CSE/Java 카테고리의 포스트를 톺아봅니다

[Java] 멀티 스레드 - 작업 스레드

2015.12.12 13:12 - Palpit
조회수 확인

멀티 스레드는 여러 절로 구성되어 있습니다.

 


Intro

작업스레드

스레드 우선순위 & 동기화 메소드와 동기화 블록





 

작업 스레드 생성과 실행

 멀티 스레드로 실행하는 어플리케이션을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야 합니다.




 








 어떤 자바 어플리케이션이건 메인 스레드는 반드시 존재하기 때문에 메인 작업 이외에 추가적인 병렬 작업의 수만큼 스레드를 생성하면 됩니다. 


 작업 스레드는 객체로 생성되기 때문에 java.lang.Thread 클래스가 필요합니다. 직접 객체화해서 생성해도 되지만, Thread를 상속하여 하위 클래스로 만들어 사용할 수 있습니다.







 Thread 클래스로부터 직접 생성

  java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 다음과 같이 Runnable을 파라미터로 갖는 생성자를 호출해야 합니다.




1
2
Thread thread = new Thread(Runnable r);
 
cs

  


  Runnable은 인터페이스 타입이기 때문에 구현 객체를 만들어 대입해야 합니다. Runnable에는 run() 메소드가 정의되어 있는데, 구현 클래스는 run()을 재정의하여 작업 스레드가 실행할 코드를 작성해야 합니다. 다음은 Runnable 구현 클래스를 작성하는 방법을 보여줍니다.




1
2
3
4
5
class Task implements Runnable {
    public void run() {
        // Code 
    }
}
cs





  여기서 주의할 점은 Runnable은 작업 내용만 가지고 있는 객체입니다. 실제 스레드가 아니기 때문에 Runnable 구현 객체를 파라미터로 하는 Thread 생성자로 호출하여야만 작업 스레드가 생성되는 것 입니다.




1
2
3
Runnable task = new Task();
 
Thread thread = new Thread(task);
cs




  아니면 익명 구현 객체를 파라미터로 사용하여 코드를 절약할 수 있습니다.



1
2
3
4
5
6
Thread thread = new Thread(new Runnable() {
    public void run() {
        // Code
    }
});
 
cs







  Runnable 인터페이스는 run() 메소드 하나만 정의되어 있기 때문에 함수적 인터페이스입니다. 즉 람다식을 파라미터로 사용할 수도 있습니다. 



1
2
3
4
Thread thread = new Thread( () -> {
    // Code
});
 
cs






  위 세 가지 방법으로 작업 스레드를 생성하고 나면 start() 메소드로 다음과 같이 호출해야만 실행이 됩니다.



1
thread.start();
cs






  실제로 예제를 살펴보겠습니다. 0.5초 주기로 beep 음을 발생하면서 동시에 프린팅 작업을 한다고 가정해 봅시다.


  두 작업은 서로 다른 작업이기에 스레드를 분리해서 작성해야 합니다. 먼저 첫 예제를 살펴봅시다.






 * BeepPrintExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package mT;
 
import java.awt.Toolkit;
 
public class BeepPrintExam {
    public static void main(String[] args) {
        Toolkit toolkit = Toolkit.getDefaultToolkit(); // Toolkit 객체 생성
        
        for (int i = 0; i < 5; i++) {
            toolkit.beep();
            try { Thread.sleep(500); } catch(Exception e) {}
            // 0.5 초 간격
        }
        
        for (int i = 0; i < 5; i++) {
            System.out.println("Print" + i);
            try { Thread.sleep(500); } catch(Exception e) {}
        }
    }
 
}
 
cs




  실행 결과 Beep 음이 발생하고 나서, Print가 찍히는 걸 확인 할 수 있습니다. 원하는 건 Beep음과 출력이 동시에 이루어져야 하는데 말입니다.


  beep 음을 발생시키면서 동시에 프린팅을 하려면 두 작업 중 하나를 메인 스레드가 아닌 다른 스레드에서 실행시켜야 합니다. 프린팅은 메인 스레드가 담당하고 비프음을 들려주는 것은 작업 스레드가 담당하도록 수정해봅시다.






 * BeepWork.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
package mT;
 
import java.awt.Toolkit;
 
public class BeepWork implements Runnable {
    @Override
    public void run() {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        
        for (int i = 0; i < 5; i++) {
            toolkit.beep();
            try { Thread.sleep(500); } catch (Exception e) {}
        }
    }
 
}
 
cs





 * BeepPrintExam2.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
package mT;
 
public class BeepPrintExam2 {
    public static void main(String[] args) {
        Runnable beepWork = new BeepWork();
        Thread thread = new Thread(beepWork);
        thread.start();
        
        for (int i = 0; i < 5; i++) {
            System.out.println("Print" + i);
            try { Thread.sleep(500); } catch(Exception e) {}
        }
    }
 
}
 
cs







  위 방법도 있지만 익명 객체, 람다식을 통한 작성법도 있습니다. 아래 글 상자를 통해 살펴보세요.



더보기









 Thread 하위 클래스로부터 생성

  작업 스레드가 실행할 작업을 Runnable로 만들지 않고, Thread의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을 포함시킬 수도 있습니다. 다음은 작업 스레드 클래스를 정의하는 방법인데, Thread 클래스를 상속한 후 run 메소드를 재정의(override)해서 실행할 코드를 작성하면 됩니다.



1
2
3
4
5
6
public void workingThread extends Thread {
    @Override 
    public void run() {
        // Code
    }
}
cs





 위 방법으로 위와 같은 예제를 보도록 하겠습니다.




 * BeepThread.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package mT;
 
import java.awt.Toolkit;
 
public class BeepThread extends Thread {
    @Override
    public void run() {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        
        for (int i = 0; i < 5; i++) {
            toolkit.beep();
            try { Thread.sleep(500); } catch (Exception e) {}
        }
    }
}
 
cs






 * BeepPrintExam3.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
package mT;
 
public class BeepPrintExam3 {
    public static void main(String[] args) {
        Thread thread = new BeepThread();
        thread.start();
        
        for (int i = 0; i < 5; i++) {
            System.out.println("Print" + i);
            try { Thread.sleep(500); } catch(Exception e) {}
        }
    }
 
}
 
cs





 같은 방법으로 Thread 익명 객체를 생성하여 사용할 수도 있습니다.








 스레드 이름

  스레드는 자신만의 이름을 갖고 있습니다. 스레드의 이름은 디버깅할 때 어떤 스레드가 어떤 작업을 하는지 조사할 목적으로 사용되기도 합니다.


  메인 스레드는 "main"이라는 이름을 가지고 있고, 직접 생성한 스레드는 자동적으로 "Thread-n" 형식으로 설정됩니다. 


  직접 스레드 이름을 설정하고 싶다면 아래와 같이 setName() 메소드로 변경하면 됩니다.


  스레드 이름을 알고싶은 경우 getName() 메소드를 호출하면 됩니다.



1
2
3
Thread.setName("ThreadName");
 
Thread.getName();
cs




  setName()과 getName()은 Thread의 인스턴트 메소드이므로 스레드 객체의 참조가 필요합니다. 만약 스레드 객체의 참조를 갖고 있지 않다면, Thread의 정적 메소드인 currentThread()로 코드를 실행하는 현재 스레드의 참조를 알 수 있습니다.



1
Thread thread = Thread.currentThread();
cs





  다음 예제는 메인 스레드의 참조를 얻어 스레드 이름을 콘솔에 출력하고, 새로 생성한 스레드의 이름을 setName() 메소드로 설정한 후, getName() 메소드로 읽는 예제입니다.





 * ThreadNameExam.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package mT;
 
public class ThreadNameExam {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread();
        System.out.println("시작 스레드 이름: " + mainThread.getName());
 
        Thread threadA = new ThreadA();
        System.out.println("작업 스레드 이름: " + threadA.getName());
        threadA.start();
 
        Thread threadB = new ThreadB();
        System.out.println("작업 스레드 이름: " + threadB.getName());
        threadB.start();
    }
 
}
 
cs




 * ThreadA.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
package mT;
 
public class ThreadA extends Thread {
    public ThreadA() {
        setName("ThreadA");
    }
 
    @Override
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println(getName() + "가 출력한 내용");
        }
    }
}
 
cs




 * ThreadB.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
package mT;
 
public class ThreadB extends Thread {
    public ThreadB() {
        setName("ThreadB");
    }
 
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(getName() + "가 출력한 내용");
        }
    }
}
 
cs




















 * 이 포스트은 서적 '이것이 자바다' 를 참고하여 작성한 포스트입니다.

다른 카테고리의 글 목록

CSE/Java 카테고리의 포스트를 톺아봅니다

[Java] 멀티 스레드

2015.12.12 12:07 - Palpit
조회수 확인

멀티 스레드는 여러 절로 구성되어 있습니다.





Intro

작업스레드

스레드 우선순위 & 동기화 메소드와 동기화 블록




 


멀티 스레드 개념

 운영체제에서 실행 중인 하나의 어플리케이션을 프로세스(process)라고 부릅니다. 실행에 필요한 메모리를 할당받아 어플리케이션의 코드를 실행하게 됩니다. 


 다음은 멀티 프로세스와 그에 따른 싱글/멀티 스레드를 그림으로 보겠습니다.


 







 멀티 프로세스는 운영체제에서 할당받은 자신의 메모리를 가지고 실행하기 때문에 서로 독립적입니다. 따라서 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 끼치지 않습니다.


 하지만 멀티 스레드하나의 프로세스 내부에서 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에게 영향을 미치게 됩니다.


 멀티 스레드는 아래와 같은 예시를 들 수 있습니다.

  - 대용량 데이터의 처리 시간을 줄이기 위해 데이터를 분할해서 병렬로 처리

  - UI를 가지고 있는 어플리케이션에서 네트워크 통신

  - 다수 클라이언트의 요청을 처리하는 서버








 메인 스레드

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


  메인 스레드는 필요에 따라 작업 스레드들을 만들어서 병렬로 코드를 실행할 수 있습니다. 즉 멀티 스레드를 생성해서 멀티 태스킹을 수행합니다.


  다음은 싱글 스레드 어플리케이션입니다.












  다음은 멀티 스레드 어플리케이션입니다.







  멀티 스레드 어플리케이션에서 실행 중인 스레드가 하나라도 있다면, 해당 프로세스는 종료되지 않습니다.




















 * 이 포스트은 서적 '이것이 자바다' 를 참고하여 작성한 포스트입니다.


다른 카테고리의 글 목록

CSE/Java 카테고리의 포스트를 톺아봅니다