스레드(Thread)
스레드는 프로세스 내에서 실제 작업을 수행하는 것으로 모든 프로세스는 최소한 하나의 스레드를 가지고 있다.
스레드의 종류로는 단일 스레드, 멀티 스레드, 데몬 스레드로 나뉜다.
스레드는 공유 메모리를 사용하지만 동일한 메모리를 공유함에도 불구하고 다른 스레드의 작업에 영향을 주지 않는 스레드에 예외가 있는 경우 독립적으로 작동한다. (스레드풀?)
스레드를 사용하는 이유는 멀티 태스킹에 기여할 수 있는 스레드를 사용한다.
멀티 스레드
멀티 스레드는 하나의 자원에 여러 개의 스레드를 붙인 것을 의미한다.
멀티 스레드는 단일 스레드보다 시간이 더 걸릴 수 있는데 그 이유는 중간에 컨택스트 스위칭이 발생하기 때문이다.
멀티 스레드를 주로 사용하는 이유는 하나의 새로운 프로세스를 생성하는 것보다 하나의 새로운 스레드를 생성하는 것이 더 적은 비용이 든다.
멀티 스레드의 장단점
장점
- 시스템 자원을 보다 효율적으로 사용할 수 있다.
- 사용자에 대한 응답성이 향상된다.
- 작업이 분리되어 코드가 간결해 진다.
단점
- 동기화에 주의해야 한다.
- 교착 상태가 발생하지 않도록 주의해야 한다.
- 각 스레드가 효율적으로 고르게 실행될 수 있게 해야 한다.
스레드의 구현
Thread 클래스를 상속하여 구현
public class Ex_1 {
public static void main(String[] args) {
ThreadEx th1 = new ThreadEx();
th1.start(); // start() 했다고 바로 작업을 시작하지 않는다.
}
}
class ThreadEx extends Thread { // Thread 클래스를 상속받아서 스레드를 구현
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(getName());
}
}
}
Thread를 구현하는 첫 번째 방법은 Thread 클래스를 상속받아서 구현하는 것이다.
상속을 받으면 run() 메서드를 오버라이딩하여 사용하게 되는데 main에서는 start로 호출하여 스레드를 실행하게 된다.
Runnable 인터페이스를 구현
public class Ex_1 {
public static void main(String[] args) {
Thread th2 = new Thread(new ThreadEx1()); // 생성자 Thread(Runnable target)
th2.start(); // start() 했다고 바로 작업을 시작하는 것은 아님
}
}
class ThreadEx1 implements Runnable { // Runnable 인터페이스를 구현해서 스레드를 구현
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}
Thread를 구현하는 두 번째 방법은 Runnable 인터페이스를 구현하는 방법이다.
상속받는 것과 마찬가지로 run() 메서드를 오버라이딩하여 사용하게 되는데 main에서 스레드를 만들 때는 Thread() 생성자에 객체를 넘겨줘야 한다.
스레드의 실행
스레드를 생성한 후에 start() 메서드를 호출해야 스레드가 작업을 실행한다.
여러 가지 작업 중 실행하는 순서는 OS 스케줄러가 결정하게 된다. 스케줄러가 어떤 작업을 먼저 수행할지는 모르기 때문에 코드를 실행하면 여러 스레드의 작업이 뒤섞이게 된다.
start() 메서드를 호출하면 호출 스택을 하나 더 생성하여 run() 메서드를 넣어주게 된다.
main 스레드
main 스레드는 main 메서드의 코드를 수행하는 스레드이다.
자바 프로그램이 시작되면 하나의 스레드가 즉시 실행되는데 해당 스레드가 바로 main 스레드이다.
스레드는 기본적으로 사용자 스레드와 데몬 스레드 총 두가지 종류가 있는데 프로그램이 종료되는 것은 사용자 스레드가 하나도 없을 때 종료된다.
데몬 스레드
일반 스레드의 작업을 돕는 보조적인 역할을 수행한다.
일반 스레드가 모두 종료되면 자동적으로 종료된다. (무한 루프여도 자동 종료됨)
가비지 컬렉터, 자동 저장, 화면 자동갱신 등에 사용된다.
무한루프와 조건문을 이용해서 실행 후 대기하다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.
setDaemon()은 반드시 start()를 호출하기 전에 실행되어야 한다. 왜냐하면 start 후에는 스레드를 바꿀 수 없고 예외가 발생하기 때문이다.
스레드의 우선순위
thread1.setPriority(5);
thread2.setPriority(7);
작업의 중요도에 따라 스레드의 우선순위를 다르게 하여 특정 스레드가 더 많은 작업시간을 갖게 할 수 있다.
위의 코드와 같이 setPriority() 메서드를 사용하여 우선순위를 지정해줄 수 있는데 기본 값은 5로 되어있다.
우선순위는 일종의 사용자가 희망하는 우선순위로 스케줄러가 참고만 하지 꼭 원하는 우선순위로 OS 스케줄러가 처리해주진 않는다.
다만, 우선순위가 높을 수록 작업이 먼저 끝날 확률이 커진다.
스레드의 상태
스레드는 여러 가지 상태를 가지고 있다.
NEW: 스레드가 생성되고 아직 start가 호출되지 않은 상태를 의미한다.
RUNNABLE: 실행 중 또는 실행 가능한 상태를 의미하며 생성된 스레드가 start() 메서드를 호출하면 해당 상태로 전환된다.
BLOCKED: 동기화블럭에 의해서 일시정지된 상태를 의미하며 앞선 스레드의 작업이 완료될 때까지 대기하는 상태이다.
WAITING: 스레드의 작업이 종료되지 않았지만 실행가능하지 않은 일시정지 상태를 의미한다.
TIME_WAITING: 일시정지시간이 지정된 경우를 의미하며 대기하는 시간이 길어져서 기아 현상이 발생하는 경우를 방지하기 위한 상태이다.
TERMINATED: 스레드의 작업이 종료된 상태를 의미한다.
스레드의 실행제어
스레드의 실행을 제어할 수 있는 메서드가 제공된다.
sleep
Thread.sleep(2000);
sleep() 메서드는 지정된 시간동안 스레드를 일시정지시키는 메서드이다. 지정한 시간이 지나고 나면 자동적으로 다시 실행대기상태가 된다.
join
thread.join(1000);
join() 메서드는 지정된 시간동안 스레드가 실행되도록 한다. 지정된 시간이 지나거나 작업이 종료되면 join을 호출한 스레드로 다시 돌아와 실행을 계속한다.
interrupt
thread.interrupt();
interrupt() 메서드는 sleep이나 join에 의해 일시정지 상태인 스레드를 깨워서 실행대기 상태로 만든다.
yield
Thread.yield();
yield() 메서드는 스레드 실행 중에 자신에게 주어진 실행 시간을 다른 스레드에게 양보하고 자신은 실행대기 상태가 된다.
참고로 sleep과 yield는 static 메서드이기 때문에 현재 스레드 자신한테만 동작한다. 그리고 sleep은 예외처리를 해줘야 하는데 예외가 발생하면 깨어나기 때문이다.
스레드의 동기화
멀티 스레드 프로세스에서는 스레드가 같은 메모리를 공유하기 때문 다른 스레드의 작업에 영향을 미칠 수 있다. 따라서 진행중인 작업이 다른 스레드에게 간섭받지 않게 하려면 동기화가 필요하다.
동기화하려면 간섭받지 않아야 하는 문장들을 임계 영역으로 설정해야 한다.
임계영역은 락(lock)을 얻은 단 하나의 스레드만 출입이 가능하다. (객체 1개에 락 1개)
효율적인 스레드 작업을 위해 하나의 스레드에 한 개의 임계영역으로 설정하는 것이 좋다.
하지만 반대로 임계 영역이 너무 많으면 비효율적이다. 왜냐하면 객체에 락을 설정하는 것과 락을 푸는 비용이 추가적으로 발생하기 때문이다.
synchronized로 임계 영역을 설정하는 방법 2가지(스레드의 동기화)
1. 메서드 전체를 임계 영역으로 지정
public synchronized void method() {...}
2. 특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조 변수) {...}
동기화는 효율이 떨어진다는 단점이 존재하는데, 이러한 문제를 해결하기 위해 wait()와 notify() 메서드를 사용하여 해결할 수 있다.
해당 메서드들은 Object 클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.
wait(): 객체의 lock을 풀고 스레드를 해당 객체의 waiting pool에 넣는다.
notify(): waiting pool에서 대기중인 스레드 중의 하나를 깨운다.
notifyAll(): waiting pool에서 대기중인 모든 스레드를 깨운다.
예제: 소켓, 스트림, 스레드를 사용하여 채팅 프로그램 만들기
지난 번에 공부한 소켓과 스트림에서 이번에 배운 스레드를 더해 채팅 프로그램을 진행해봤다.
간단하게 그림을 그려보았는데 한 번 살펴보면 채팅에 참여하는 유저는 Client1과 Client2가 있고 서버를 통해서 데이터를 주고 받게 된다.
Client1이 가지고 있는 Test1 폴더에 이미지 파일을 Client2에게 보낸다고 했을 때 입출력 스트림을 통해 바이트 기반으로 데이터를 보내게 된다.
이미지 외에 채팅을 한다면 각자 화면에 보여야 되기 때문에 문자 기반의 스트림을 진행하게 된다.
채팅 서버
public class ChattingServer {
// 클라이언트 소켓의 정보를 HashMap에 담아서 관리하는데 static을 통해서 관리한다.
static Map<String, Socket> clientSockets = new HashMap<>();
public static void main(String[] args) {
final int SERVER_PORT = 9000;
try {
ServerSocket socket = new ServerSocket(SERVER_PORT); // 소켓 통신을 연다.
while (true) {
Socket clientSocket = socket.accept(); // 클라이언트 접속을 대기한다.
// 처음 유저가 입력하는 아이디를 받는다.
InputStream is = clientSocket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String id = br.readLine();
System.out.printf("%s님이 입장하셨습니다.\n", id);
clientSockets.put(id, clientSocket); // 클라이언트 소켓 정보를 담는다.
// 클라이언트 메시지 처리 스레드 생성
Thread messageThread = new MessageHandlingThread(clientSocket);
messageThread.start();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
해당 코드에서 중요한 부분은 클라이언트의 소켓 정보를 HashMap으로 담는데 static으로 설정하였다. 각 스레드마다 독립적인 호출 스택을 가지고 있기 때문에 동일한 메모리를 사용하기 위해서는 메모리를 공유하기 위해서 static으로 설정해줘야 한다.
공유 메모리를 통해 서버에서 HashMap에 저장된 클라이언트의 소켓 정보를 가져와 데이터를 전달하게 되고 각 클라이언트 화면에 채팅이 나오게 된다.
public class MessageHandlingThread extends Thread{
Socket clientSocket;
public MessageHandlingThread(Socket clientSocket) {
this.clientSocket = clientSocket;
}
@Override
public void run() {
try {
//클라이언트에서 전달받은 데이터
InputStream is = clientSocket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedInputStream bis = new BufferedInputStream(is);
BufferedReader bir = new BufferedReader(isr);
while (true) {
String message = bir.readLine();
if (message.contains(".png")) { // 파일을 전송하는 경우
for (String key : ChattingServer.clientSockets.keySet()) {
Socket client = ChattingServer.clientSockets.get(key);
OutputStream os = client.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os);
BufferedWriter bw = new BufferedWriter(osw);
bw.write(message + "\n");
bw.flush();
BufferedOutputStream bos = new BufferedOutputStream(os);
int data = 0;
while ((data = bis.read()) != -1) {
bos.write(data);
}
bos.flush();
}
} else { // 문자 채팅을 하는 경우
for (String key : ChattingServer.clientSockets.keySet()) {
Socket client = ChattingServer.clientSockets.get(key);
OutputStream os = client.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os);
BufferedWriter bw = new BufferedWriter(osw);
bw.write(message + "\n");
bw.flush();
}
}
}
} catch (IOException e) {
System.out.println("클라이언트와 접속이 끊겼습니다.");
throw new RuntimeException(e);
}
}
}
위의 코드는 클라이언트에서 받아오는 메시지를 처리하는 스레드이다.
클라이언트에서 전달받은 메세지에서 ".png" 확장자가 있는 메시지면 내용을 바이트로 변환하여 다른 클라이언트에 전달해준다.
채팅 클라이언트
클라이언트와 서버간의 대략적인 구조를 그림으로 그려보았다.
서버에서 main과 MessageHandleThread는 같은 공유 메모리를 사용하게 되는데 해당 공유 메모리가 앞서 설명했던 static으로 설정한 HashMap을 의미한다.
클라이언트와 서버 간의 소켓이 연결되면 서버에서 해당 클라이언트의 소켓 정보를 가지고 있게 되므로 다른 스레드에서도 참조해서 사용할 수 있게 된다. 이를 통해 스레드에서 스트림을 처리하며 클라이언트에게 전달할 수 있다.
public class Client {
public static void main(String[] args) {
final String IP_ADDR = "서버IP";
final int PORT = 9000;
Scanner sc = new Scanner(System.in);
try {
Socket clientSocket = new Socket(IP_ADDR, PORT); // 서버와 소켓 연결
// 서버에 데이터 전달
OutputStream os = clientSocket.getOutputStream();
OutputStreamWriter output = new OutputStreamWriter(os);
BufferedWriter bw = new BufferedWriter(output);
System.out.print("ID를 입력해주세요. : ");
String msg = sc.next();
bw.write(msg + "\n");
bw.flush();
// 스레드 실행
MessageSendThread send = new MessageSendThread(clientSocket);
send.start();
MessageReceiveThread receive = new MessageReceiveThread(clientSocket);
receive.start();
// 반복문 설정
while (true) {
...
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
클라이언트 코드를 살펴보면 먼저 IP와 포트 번호를 지정하여 서버와 연결하게 된다.
그 다음으로는 서버에 현재 클라이언트 아이디를 입력하여 보내게 되고 MessageSend와 Receive 스레드를 실행된다.
MessageSendThread
MessageReceiveThread
'자바' 카테고리의 다른 글
[Java] - GC(Garbage Collection) (1) | 2024.08.29 |
---|---|
[Java] - JVM(Java Virtual Machine) (0) | 2023.12.21 |
[Java] - 입출력 스트림 (0) | 2023.11.29 |
[Java] - JDK? JVM? JRE? 이게 다 무슨 소리지? (0) | 2023.11.26 |
[Java] - Optional (0) | 2023.11.19 |