실습 - 웹 어플리케이션
git/eomcs-java-basic/src/main com.eomcs.net.ex11.step13
원래의 계산기 서버는 자바 코드로 이뤄진 클라이언트가 직접 콘솔로 연결 요청해야 하는 어플리케이션이었다. 이 서버를 http 프로토콜 요청에 대해 적절히 응답할 수 있는 웹 서버 프로그램으로 바꿀 것이다. 이렇게 프로그램을 바꾸면 이 서버에 대한 클라이언트는 웹 브라우저가 된다.
기존 프로그램과 마찬가지로 클라이언트와 연결하는 역할만 수행하는 CalculatorServer와 클라이언트의 요청에 대한 응답을 하는 RequestProcessor 클래스를 그대로 사용한다.
package com.eomcs.net.ex11.step13;
import java.net.ServerSocket;
public class CalculatorServer {
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(80)) {
System.out.println("서버 실행 중...");
while (true) {
RequestProcessor thread = new RequestProcessor(serverSocket.accept());
thread.start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
단, RequestProcessor에서 클라이언트의 요청에 대해 http 프로토콜로 응답하는 메서드를 만들 것이다.
또한 클라이언트의 http 요청 메시지 중 필요한 부분만을 읽어 사용할 것이다. 예를 들어, 클라이언트 측에서 브라우저의 도메인 검색 창에 다음과 같이 적어 서버에 접속할 것이다. 서버 측에서 변수값으로 사용할 파라미터들은 가장 마지막에 "연산자"?a="피연산자"&b="피연산자" 이렇게 표현된다.
http://localhost/plus?a=100&b=200
이렇게 검색창에 작성하면 웹 브라우저는 이것을 사용하여 다음과 같이 http 프로토콜 요청 메시지를 만들 것이다.
GET /plus?a=100&b=200 HTTP/1.1
Host: localhost
.
.
.
(CRLF)
서버에서 이에 맞는 응답을 해주기 위해서 필요한 것은 첫째줄에 있는 정보뿐이므로 첫줄만 읽고 나머지 밑의 줄은 모두 읽지 않고 버린다.
try (Socket socket = this.socket;
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintStream out = new PrintStream(socket.getOutputStream());) {
String requestLine = in.readLine();
while (true) {
if (in.readLine().length() == 0) {
break;
}
}
그리고 변수값을 갖고 결과를 계산하는 compute 메서드에서는 이 requestLine에서 원하는 값, 즉 연산자와 두 개의 피연산자만 추출해야한다. String에서 제공하는 split 메서드를 통해 손쉽게 추출이 가능하다. 단, ?을 구분자로 split할 때에 "?"을 파라미터로 넣어주면 정규표현식의 메타문자로 인식하여 예외를 띄우기 때문에 역슬래시 두개를 넣어 이스케이프 처리해주어야한다.
* 정규표현식이란?
대부분의 언어에서 제공되고 있는, 문자열의 추출을 돕는 함수에 대한 규칙, 문법이다.
private String compute(String request) {
try {
// 웹브라우저가 보낸 request line에서 데이터를 추출한다.
// 예) "GET /plus?a=100&b=200 HTTP/1.1"
String[] values = request.split(" ")[1].split("\\?"); // ["/plus", "a=100&b=200"]
String op = getOperator(values[0]); // "/plus", "/multiple" 등
String[] parameters = values[1].split("&"); // "a=100&b=200" ==> ["a=100", "b=200"]
int a = 0;
int b = 0;
for (String parameter : parameters) {
String[] kv = parameter.split("=");
if (kv[0].equals("a")) { // "a=100"
a = Integer.parseInt(kv[1]);
} else if (kv[0].equals("b")) { // "b=200"
b = Integer.parseInt(kv[1]);
}
}
.
.
.
private String getOperator(String name) {
switch (name) {
case "/plus": return "+";
case "/minus": return "-";
case "/multiple": return "*";
case "/devide": return "/";
default:
return "?";
}
}
이렇게 compute 메서드에서 계산하여 결과를 리턴하면 그것을 httpResponse 메서드에서 http 프로토콜 응답 메시지로 가공하여 클라이언트 측으로 출력한다.
private void sendHttpResponse(PrintStream out, String message) throws Exception {
out.println("HTTP/1.1 200 OK");
out.println("Content-Type: text/plain;charset=UTF-8");
out.println();
out.print(message);
out.flush();
}
}
따라서 다음과 같은 RequestProcessor 클래스가 정의된다.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
public class RequestProcessor extends Thread {
Socket socket;
public RequestProcessor(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (Socket socket = this.socket;
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintStream out = new PrintStream(socket.getOutputStream());) {
String requestLine = in.readLine();
while (true) {
if (in.readLine().length() == 0) {
break;
}
}
sendHttpResponse(out, compute(requestLine));
} catch (Exception e) {
System.out.printf("클라이언트 요청 처리 중 오류 발생! - %s\n", e.getMessage());
}
}
private String compute(String request) {
try {
String[] values = request.split(" ")[1].split("\\?"); // ["/plus", "a=100&b=200"]
String op = getOperator(values[0]); // "/plus", "/multiple" 등
String[] parameters = values[1].split("&"); // "a=100&b=200" ==> ["a=100", "b=200"]
int a = 0;
int b = 0;
for (String parameter : parameters) {
String[] kv = parameter.split("=");
if (kv[0].equals("a")) { // "a=100"
a = Integer.parseInt(kv[1]);
} else if (kv[0].equals("b")) { // "b=200"
b = Integer.parseInt(kv[1]);
}
}
int result = 0;
switch (op) {
case "+": result = a + b; break;
case "-": result = a - b; break;
case "*": result = a * b; break;
case "/": result = a / b; break;
default:
return "해당 연산자를 지원하지 않습니다.";
}
return String.format("결과는 %d %s %d = %d 입니다.", a, op, b, result);
} catch (Exception e) {
return String.format("계산 중 오류 발생! - %s", e.getMessage());
}
}
private String getOperator(String name) {
switch (name) {
case "/plus": return "+";
case "/minus": return "-";
case "/multiple": return "*";
case "/devide": return "/";
default:
return "?";
}
}
private void sendHttpResponse(PrintStream out, String message) throws Exception {
out.println("HTTP/1.1 200 OK");
out.println("Content-Type: text/plain;charset=UTF-8");
out.println();
out.print(message);
out.flush();
}
}
실습 - 채팅 프로그램
git/eomcs-java-basic/src/main com.eomcs.net.ex12
이 프로그램은 강사님이 먼저 코드를 짜시고, 간단히 실행만 해본 채팅 프로그램이다. UI에 대한 다양한 자바 API가 사용되었지만, 지금은 서버와 클라이언트의 소통에 집중하여 살펴본다.
먼저 채팅 프로그램 서버는 다음과 같이 작성되었다.
- 클라이언트가 몇 개든 받아서 모두에게 같은 내용을 출력하여 통일된 내용을 받을 수 있도록 각 클라이언트들의 출력도구를 담는 ArrayList 객체를 필드로 만든다.
- 포트 번호를 담는 port 라는 필드를 만든다.
- 포트 번호를 파라미터로 주는 생성자를 만든다.
- service 메서드
- 서버 소켓을 생성하여 서버를 시작시킨다.
- 클라이언트가 요청할 때마다 accept하여 리턴받은 소켓을 ChatAgent(Runnable 구현체) 객체에게 넘기고 이것에 대하여 스레드를 생성하고 실행시킨다.
- 아직은 반복문을 빠져나와 서버를 종료하는 코드를 구현하지 않았다.
- ChatAgent-run
- Runnable 구현체로써 클라이언트마다 한 스레드로 분리되어 run이 실행된다.
- run이 실행되자마자 클라이언트에 대해 만든 출력 도구를 outputStreams 리스트에 담는다.
- 이 객체는 클라이언트가 출력한 문자열을 한줄씩 읽는다.
- 읽은 문자열이 quit이면 반복문을 빠져나와 서버와의 연결을 끊는다.
- quit이 아니면 문자열을 읽을 때마다 스레드를 생성하여 읽은 문자열을 모든 클라이언트에게 출력하는 messageSender(Runnable 구현체)의 run 메서드를 실행시킨다.
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
public class ChatServer {
ArrayList<PrintStream> outputStreams = new ArrayList<>();
int port;
public ChatServer(int port) {
this.port = port;
}
public void service() {
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("채팅 서버 시작!");
while (true) {
new Thread(new ChatAgent(serverSocket.accept())).start();
System.out.println("채팅 클라이언트가 연결되었음!");
}
} catch (Exception e) {
e.printStackTrace();
}
}
synchronized private void send(String message) {
for (PrintStream out : outputStreams) {
try {
out.println(message);
} catch (Exception e) {
outputStreams.remove(out);
}
}
}
class ChatAgent implements Runnable {
Socket socket;
public ChatAgent(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (Socket socket = this.socket;
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintStream out = new PrintStream(socket.getOutputStream())) {
outputStreams.add(out);
while (true) {
String message = in.readLine();
if (message.equals("quit"))
break;
new Thread(new MessageSender(message)).start();
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("채팅 클라이언트가 종료되었음!");
}
}
class MessageSender implements Runnable {
String message;
public MessageSender(String message) {
this.message = message;
}
@Override
public void run() {
send(message);
}
}
public static void main(String[] args) {
ChatServer chatServer = new ChatServer(8888);
chatServer.service();
}
}
이 채팅 서버가 보내는 응답 메시지를 받아 채팅 창에 띄워 사용자에게 보여주는 채팅 클라이언트가 있다. 이 클라이언트는 채팅 창을 만들기 위해 자바의 AWT 패키지를 사용했다. 그러나 우리는 서버와 클라이언트의 소통을 위한 코드에 집중해서 살펴보겠다.
- main
- ChatClient 객체를 생성하여 어플리케이션을 시작하고 채팅창을 띄운다.
- connectChatServer
- 서버와 연결되기 위해 소켓을 생성하고, 서버와 소통하기 위한 입출력 도구를 생성한다.
- 연결이 되자마자 새로운 스레드를 만들어 MessageReceiver의 run 메서드를 실행하여 서버에서 보내는 메시지를 수신한다.
- MessageReceiver-run
- 서버에서 보낸 메시지를 한줄씩 읽어 창에 띄운다.
import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Frame;
import java.awt.Panel;
import java.awt.TextArea;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
public class ChatClient extends Frame implements ActionListener {
private static final long serialVersionUID = 1L;
TextField addressTF = new TextField(20);
TextField portTF = new TextField(4);
Button connectBtn = new Button("연결");
TextArea chattingPane = new TextArea();
TextField messageTF = new TextField();
Button sendBtn = new Button("보내기");
Socket socket;
BufferedReader in;
PrintStream out;
public ChatClient(String title) {
super(title);
this.setSize(600, 480);
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
try {
in.close();
} catch (Exception ex) {
}
try {
out.close();
} catch (Exception ex) {
}
try {
socket.close();
} catch (Exception ex) {
}
System.exit(0);
}
});
Panel topPane = new Panel();
topPane.add(addressTF);
topPane.add(portTF);
topPane.add(connectBtn);
this.add(topPane, BorderLayout.NORTH);
this.add(chattingPane, BorderLayout.CENTER);
Panel bottomPane = new Panel();
bottomPane.setLayout(new BorderLayout());
bottomPane.add(messageTF, BorderLayout.CENTER);
bottomPane.add(sendBtn, BorderLayout.EAST);
this.add(bottomPane, BorderLayout.SOUTH);
connectBtn.addActionListener(this);
sendBtn.addActionListener(this);
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == sendBtn) {
out.println(messageTF.getText());
messageTF.setText("");
} else if (e.getSource() == connectBtn) {
connectChatServer();
}
}
private void connectChatServer() {
try {
socket = new Socket(addressTF.getText(), Integer.parseInt(portTF.getText()));
out = new PrintStream(socket.getOutputStream());
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
chattingPane.append("서버와 연결됨!\n");
new Thread(new MessageReceiver()).start();
} catch (Exception e) {
chattingPane.append("서버 연결 오류!\n");
}
}
class MessageReceiver implements Runnable {
@Override
public void run() {
try {
while (true) {
String message = in.readLine();
chattingPane.append(message + "\n");
}
} catch (Exception e) {
chattingPane.append("메시지 수신 중 오류 발생!\n");
}
}
}
public static void main(String[] args) {
ChatClient app = new ChatClient("비트 채팅!");
app.setVisible(true);
}
}
스레드
git/eomcs-java-basic/src/main com.eomcs.concurrent.ex01
git/eomcs-java-basic/src/main com.eomcs.concurrent.ex02
git/eomcs-java-basic/src/main com.eomcs.concurrent.ex03
git/eomcs-java-basic/src/main com.eomcs.concurrent.ex04.Exam0110~0130.java
멀티태스킹(multitasking)
다중작업(이하 멀티태스킹)이라고 하며 다수의 작업이 CPU와 같은 공용자원을 나누어 사용하는 것을 말한다. 한정된 자원인 CPU를 사용하여 CPU 스케줄링 알고리즘에 따라 다수의 작업에 분할 하는 과정으로, CPU 속도가 매우 빨라 동시에 여러 작업을 하는 것같지만, 그렇게 보이는 것일뿐, 일을 번갈아가면서 하는 것이다.
두가지 종류의 멀티 태스킹이 있다.
- 병행 프로그램(Concurrency) : 하나의 CPU가 여러개의 작업을 돌아가면서 명령어를 차례로 실행하는 것
- 병렬 프로그램(parallelism) : 여러개의 CPU가 작업을 하나씩 맡아 동시에 실행하는 것
*CPU 스케줄링
다중 프로그래밍을 가능케 하는 운영체제의 동작 기법이다. 운영체제는 프로세스들에게 CPU 등의 자원 배정을 적절히 함으로써 시스템의 성능을 개선할 수 있다.
CPU 스케줄링 방법에는 두 가지가 있다.
- Round-Robin : 각 프로세스에 균등한 시간을 부여한다.(Windows)
- Priority + Aging : 우선순위가 높은 프로세스에 CPU 사용 권한을 더 많이 배분한다. 단, 우선순위가 낮아서 실행이 밀릴 떄마다 연기될 때마다 우선순위 레벨을 높여(Aging) 더 빨리 실행하게 한다. (Unix, Linux, macOS)
멀티 태스킹 (muiti-tasking)의 구현 방법
- 멀티 프로세싱(multi-processing)
- 프로세스를 여러 개 복제하여 실행하는 방법
- fork() 메서드만 호출하면 되므로 구현(코딩)이 쉽다.
- 메모리를 그대로 복제하므로 메모리 낭비가 심하다.
- 자식 프로세스는 부모 프로세스에 종속되지 않는다. 부모 프로세스가 종료되어어도 자식은 영향을 받지 않는다.
- 멀티 스레딩(multi-threading)
- 프로세스의 작업 중 멀티 태스킹이 필요한 작업만 분리하여 여러 스레드로 나누어 실행하는 방법
- 스레드들은 한 프로세스의 스택을 나눠 가진 상태로 하나의 힙을 공유하므로 멀티 프로세싱보다 메모리를 절약한다.
- 스레드는 프로세스에 종속되므로 프로세스가 종료되면 스레드도 종료된다.
- 메모리가 절약되기 때문에 현대적인 프로그래밍은 멀티 스레딩 방식을 많이 사용한다.
스레드(Thread)란?
- 프로세스를 구성하는 하나의 실행 흐름을 말한다.
- JVM을 실행할 때 main 메서드를 실행하는 것은 자동으로 생성되는 main 스레드이다.
- 스레드 생성이란 새 실행 흐름을 시작함을 의미한다.
- CPU는 스레드를 프로세스와 동일한 자격을 부여하여 스케줄링에 참여시키며 단독적인 프로세스처럼 실행 시간을 부여한다.
자바는 멀티 태스킹 중 Parallelism를 제외하고 Concurrency만을 멀티 스레딩을 통해 구현하고 있으며, 이를 담당하는 자바 API는 Thread클래스이다.
실행되는 스레드 조회
자바의 Thread 클래스를 이용해서 실행되고 있는 스레드와 스레드 그룹들을 조회할 수 있다.
- (Thread)currentThread 스태틱 메서드 : 이 메서드가 호출되는 그 순간에 실행 중인 스레드가 무엇인지 알 수 있다.
public class Exam0110 {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println("실행 흐름명 = " + t.getName());
}
}
// 실행 결과!
// 실행 흐름명 = main
- (Thread)getThreadGroup 인스턴스 메서드 : 해당 스레드의 소속 그룹이 무엇인지 알 수 있다.
*스레드 그룹 - 여러개의 스레드를 묶는 그룹으로, 여러개의 파일을 담는 디렉토리와 같은 것이다. 그 자체는 스레드가 아니다.
public class Exam0120 {
public static void main(String[] args) {
Thread main = Thread.currentThread();
ThreadGroup group = main.getThreadGroup();
System.out.println("그룹명 = " + group.getName());
}
}
// 실행 결과!
// 그룹명 = main
- (ThreadGroup)enumerate 인스턴스 메서드 : 첫번째로 Thread 배열을 주면, 배열에 해당 스레드 그룹에 소속된 스레드들을 담는다. 또한 두번째 파라미터로 true를 주면, 하위 그룹에 있는 모든 스레드들을 담고, false를 주면 현재 그룹에 소속된 스레드들만 담는다.
public class Exam0130 {
public static void main(String[] args) {
Thread main = Thread.currentThread();
ThreadGroup mainGroup = main.getThreadGroup();
Thread[] arr = new Thread[100];
int count = mainGroup.enumerate(arr, false);
System.out.println("main 그룹에 소속된 스레드들:");
for (int i = 0; i < count; i++)
System.out.println(" => " + arr[i].getName());
}
}
// 실행 결과!
// main 그룹에 소속된 스레드들:
=> main
- (ThreadGroup)enumerate 인스턴스 메서드 : 첫번째로 ThreadGroup 배열을 주면 배열에 해당 스레드 그룹에 소속된 스레드 그룹들만을 담는다. 또한 두번째 파라미터로 true를 주면, 하위 그룹에 있는 모든 스레드 그룹들을 담고, false를 주면 현재 그룹에 소속된 스레드 그룹들만 담는다.
public class Exam0140 {
public static void main(String[] args) {
Thread main = Thread.currentThread();
ThreadGroup mainGroup = main.getThreadGroup();
ThreadGroup[] groups = new ThreadGroup[100];
int count = mainGroup.enumerate(groups, false);
System.out.println("main 그룹에 소속된 하위 그룹들:");
for (int i = 0; i < count; i++)
System.out.println(" => " + groups[i].getName());
}
}
// 실행 결과!
// main 그룹에 소속된 하위 그룹들:
- (ThreadGroup)getParent : 해당 그룹의 소속 그룹이 무엇인지 알 수 있다.
public class Exam0150 {
public static void main(String[] args) {
Thread main = Thread.currentThread();
ThreadGroup mainGroup = main.getThreadGroup();
ThreadGroup parentGroup = mainGroup.getParent();
System.out.printf("main 스레드 그룹의 부모: %s\n", parentGroup.getName());
ThreadGroup grandparentGroup = parentGroup.getParent();
if (grandparentGroup != null) {
System.out.printf("%s 스레드 그룹의 부모: %s\n",
parentGroup.getName(),
grandparentGroup.getName());
}
}
}
// 실행 결과!
// system
JVM 기본 스레드 계층도
JVM은 기본적으로 자바 프로그램을 실행할 때, main 메서드 이외에도 여러개의 스레드를 실행한다.
JVM이 프로그램을 실행할 때, 가장 상위에 있는 스레드 그룹은 system이다. 이 스레드 그룹 하위에 있는 모든 스레드들과 스레드 그룹들을 출력하는 예제는 다음과 같다.
어떤 스레드 그룹에 있는 모든 스레드들과 스레드 그룹들을 하위 그룹까지 모두 출력하는 코드를 printThreads라는 메서드로 정의했으며 이는 재귀 호출을 통해 구현된다.
public class Exam0180 {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
ThreadGroup mainGroup = mainThread.getThreadGroup();
ThreadGroup systemGroup = mainGroup.getParent();
printThreads(systemGroup, "");
}
static void printThreads(ThreadGroup tg, String indent) {
System.out.println(indent + tg.getName() + "(TG)");
Thread[] threads = new Thread[10];
int size = tg.enumerate(threads, false);
for (int i = 0; i < size; i++) {
System.out.println(indent + " ==> " + threads[i].getName() + "(T)");
}
ThreadGroup[] groups = new ThreadGroup[10];
size = tg.enumerate(groups, false);
for (int i = 0; i < size; i++) {
printThreads(groups[i], indent + " ");
}
}
}
다음 예제에 대한 결과는 JVM이 프로그램을 실행할 때 가장 상위에 있는 스레드 그룹인 system에 있는 모든 하위 그룹의 스레드와 스레드 그룹을 출력한 것이다.
JVM의 스레드 계층도: (openjdk 11 기준)
system(TG)
==> Reference Handler(T)
==> Finalizer(T)
==> Signal Dispatcher(T)
==> Attach Listener(T) (Windows에만 있다)
==> main(TG)
==> main(T)
==> InnocuousThreadGroup(TG)
==> Common-Cleaner(T)
이중에 대표적인 것은 다음과 같은 역할을 한다.
- Reference Handler : 객체들의 참조 개수를 관리하는 스레드
- Finalizer : 가비지 컬렉터
- Signal Dispatcher : 외부 이벤트를 관리하는 스레드
- main : 메인 메서드를 실행하는 스레드
스레드 생성 방법
자바가 스레드를 생성하는 방법은 다음과 같다.
- Thread 상속하는 방법 : Thread를 상속받고독립적으로 수행할 작업을 run 메서드로 오버라이딩한다. 외부에서 이 객체를 생성하여 start메서드를 호출한다.
- Runnable 인터페이스 구현하는 방법 : Runnable 인터페이스를 구현하고 독립적으로 수행할 작업을 run 메서드로 구현한다. 그리고 Runnable 구현체를 파라미터로 주어 Thread 객체를 생성하고, start 메서드를 호출한다.
일단 Thread 클래스를 직접 상속하는 방법부터 살펴보자.
- main 스레드와 분리하여 새로운 스레드를 실행고자하는 메서드를 가진 클래스는 Thread 클래스를 상속받는다.
- Thread를 상속받은 클래스에서 다른 스레드로 분리 실행하고자 하는 메서드를 run 메서드로 오버라이딩한다.
- 현재의 스레드에서 분리하고자 하는 시점에 Thread 하위 객체를 생성하여 start 메서드를 호출한다.
- start 메서드를 호출하는 순간에 스레드가 분리되어 해당 객체의 run 메서드를 실행한다.
- start 메서드는 run의 리턴 여부와 상관없이 스레드가 분리되면 그즉시 리턴한다.
public class Exam0110 {
public static void main(String[] args) {
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("===> " + i);
}
}
}
MyThread t = new MyThread();
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println(">>>> " + i);
}
}
}
Thread 하위 객체를 한번만 생성하여 사용한다면 익명 클래스로 정의할 수도 있다.
public class Exam0120 {
public static void main(String[] args) {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("===> " + i);
}
}
}.start();
for (int i = 0; i < 1000; i++) {
System.out.println(">>>> " + i);
}
}
}
이제 Runnable 인터페이스를 구현하는 방법을 살펴보자. 참고로 Runnable 구현은 동시에 다른 클래스를 상속받을 수 있기 때문에 실무에서 자주 사용된다.
- main 스레드와 분리하여 새로운 스레드를 실행고자하는 메서드를 가진 클래스가 Runnable 인터페이스를 구현하도록 한다.
- Runnable 구현체에서 다른 스레드로 분리 실행하고자 하는 메서드를 run 메서드로 오버라이딩한다.
- 현재의 스레드에서 분리하고자 하는 시점에 Runnable 구현체를 파라미터로 주어 Thread 객체를 생성한후, start 메서드를 호출한다.
- start 메서드를 호출하는 순간에 스레드가 분리되어 Runnable 구현체의 run 메서드를 실행한다.
- start 메서드는 run의 리턴 여부와 상관없이 스레드가 분리되면 그즉시 리턴한다.
public class Exam0210 {
public static void main(String[] args) {
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("===> " + i);
}
}
}
Thread t = new Thread(new MyRunnable());
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println(">>>> " + i);
}
}
}
Thread를 상속받았을 때와 달리 이것은 Runnable은 functional Interface이므로 익명 클래스로 구현할 수도 있고, 람다로 구현할 수도 있다.
public class Exam0220 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("===> " + i);
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("---> " + i);
}
}).start();
for (int i = 0; i < 1000; i++) {
System.out.println(">>>> " + i);
}
}
}
JVM의 스레드 실행 배분
CPU 사용을 스레드에게 배분할 때, 스레드를 생성한 순서대로 배분하지는 않고, OS의 CPU 스케줄링 정책에 따라 스레드가 실행된다. 즉 JVM에서 스레드를 실행하는 것이 아니라 OS가 실행한다. 따라서 똑같은 자바의 스레드 코드가 OS에 따라 실행 순서가 달라질 수 있다.
개발자가 직접 각 스레드별로 우선순위를 지정할 수도 있지만, 이에 대한 결과도 OS에 따라 크게 달라질 수 있기 때문에 가능한한 각 스레드들의 실행 비중을 조정하기 위해 우선순위를 변경하지않는 것이 좋다.
ex) Windows OS의 경우 우선 순위(priority) 값이 실행 순서나 실행 회수에 큰 영향을 끼치지 않는다.
JVM의 종료 시점
JVM은 모든 스레드가 종료되어야만 비로소 종료된다. 따라서 main 메서드에서 여러 스레드를 생성한 경우, 메인 메서드가 끝나더라도 다른 스레드가 모두 종료되지 않으면 JVM은 계속 실행된다.
다음 프로그램은 새로 생성한 스레드에서 입력을 받을 때까지 블로킹 상태가 되므로 사용자가 입력하지 않으면 메인 스레드는 이미 완료했지만 해당 스레드는 완료되지 못하는 상황을 재현했다. 메인 스레드가 끝나도 여전히 JVM은 종료되지 않고 해당 스레드가 끝날 때까지 기다린다.
import java.util.Scanner;
public class Exam0310 {
static class MyThread extends Thread {
@Override
public void run() {
Scanner keyboard = new Scanner(System.in);
System.out.print("입력하시오> ");
String input = keyboard.nextLine();
System.out.println("입력한 문자열 => " + input);
keyboard.close();
}
}
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
System.out.println("프로그램 종료?");
}
}
스레드의 생명 주기
스레드는 Running 상태 혹은 Not-Runnable 상태에 있을 수 있다.
- Running : CPU를 받아 실행 중이거나 CPU를 받을 수 있는 상태이다.
- Not Runnable : CPU를 받지 못하는 상태이다. 스레드가 run() 메서드를 완료하고 나면 영구적으로 Not Runnable 상태가 된다.
run() 메서드가 완료되어 Not Runnable 상태가 된 스레드를 '죽은 스레드'라고 하며 죽은 스레드는 다시 살릴 수 없으므로 start 메서드를 두번 이상 실행하면 IllegalThreadStateException 예외가 발생한다.
public class Exam0111 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("===> " + i);
}
});
t.start();
t.start();
System.out.println("main() 종료!");
}
}
어떤 스레드가 다른 스레드가 실행되고 완료될 때까지 잠시 멈추는 방식으로 스레드의 생명주기를 늘리고 싶다면 join 메서드를 사용할 수 있다.
어떤 스레드에서 또다른 스레드가 분리되어 실행되면 분리된 스레드는 원래의 스레드와 독립적으로 실행되지만, 분리된 메서드에 대하여 joun 메서드를 호출하면 join 메서드가 호출된 시점에서 기존의 스레드는 분리된 스레드가 실행을 완료하고 종료할 때까지 기다린다. 마치 어던 메서드를 실행하는 도중에 다른 메서드를 호출하면 그 메서드를 모두 실행 완료하고 난 후 원래 호출된 자리로 돌아가는 것과 유사하다.
public class Exam0120 {
public static void main(String[] args) throws Exception {
System.out.println("스레드 실행 전");
Thread t = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("===> " + i);
}
});
t.start();
t.join();
System.out.println("스레드 종료 후");
}
}
스레드의 생명주기를 조정할 수 있는 또 다른 방법은 Thread의 sleep 스태틱 메서드를 호출하는 것이다.
Thread의 sleep 메서드를 호출하면, 그 순간에 실행되던 스레드가 파라미터로 받은 밀리초동안 Not Runnable 상태가 도니다. 그리고 지정된 시간이 지나면 timeout이 되어 다시 running 상태가 된다.
public class Exam0130 {
public static void main(String[] args) throws Exception {
System.out.println("스레드 실행 전");
new Thread() {
@Override
public void run() {
System.out.println("Hello!");
}
}.start();
Thread.sleep(3000);
System.out.println("스레드 실행 후");
}
}
'국비 교육' 카테고리의 다른 글
2020.10.12 일자 수업 : Observer 디자인 패턴 (0) | 2020.10.12 |
---|---|
2020.10.6 일자 수업 : 네트워크 실습, 계산기 서버 (0) | 2020.10.07 |
2020.10.5일자 수업 : connectionless, HTTP, URL, base64 (0) | 2020.10.06 |