스레드의 개념을 간단히 정리 하자면..
스레드는 메모리를 공유한다 ( 프로세스는 각자 할당 )
따라서 메모리공간(힙)을 이용해 데이터를 주고 받고 작업을 처리할 수 있다.
메모리를 공유함으로써 동기화 문제가 생길수 있다.
( synchronized 붙인 동기화 메소드를 이용하기도 한다 )
하지만 동기화메소드로 인해서 데드락과같은 문제가 발생할 수 있어 주의가 필요하다.
프로세스 생성에는 많은 자원이 소모 되므로 스레드를 이용한다.
자바에서 멀티스레드는 기본적으로 라운드로빈방식을 이용한다
-> 슬라이싱된 작업들중에 우선순위가 같으면 가장 빨리 끝나는 것부터 진행
cpu는 시분할 프로그래밍을 이용 -> Context Switching 을 따른다
-> 어디까지 진행했는지 기억함(저장). 다음작업때 이어서 작업
메인스레드는 작업큐를 이용해 라인들을 실행한다
( 메소드는 큐에 넣고 변수들은 스택에 넣은뒤 실행 )
동기화(syncronous)와 비동기화(Asyncronous)
동기화 - 하나의 스레드만 있을때( main ) main스레드는 작업에 순서를 가진다
( 주의점 ! - 데이터 관점에서 동기화는 데이터가 일치하다는 뜻 )
비동기 - 멀티스레드에는 작업에 순서가 없다. -> 동시에 진행
(Context Switching을 이용해서 작업이 진행됨
-> 매우 빠르게 처리하기 때문에 동시에 처리 되는 것처럼 보인다 )
하지만, 멀티스레드를 이용해도 속도가 더 느려질때가 있다. 그럼에도 사용하는 이유는
UX( 사용자경험 ) 를 개선하기 위해 -> 동시에 진행되는것처럼 보이면 사용자가 답답하지 않다.
CPU는 자신보다 느린 일처리( 통신 )를 대기해야 하기때문에
( 동기화 때문에 의미없이 대기하면 자원낭비 ) -> 대기할 시간에 다른 작업을 처리
간단한 예를 들어보면 프로그램을 설치할때 순서대로 next를 눌러서 설치하는 과정을 동기화라고 표현할수 있고, 여러 파일을 한번에 다운받는것을 비동기라고 표현할수 있다.
먼저 간단하게 서버와 클라이언트가 1:1로 채팅을 계속해서 칠수 있게 만들어 보자.
// 클라이언트
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class App {
Socket socket;
BufferedReader br, keyboard;
PrintWriter pw;
public App() {
try {
socket = new Socket("192.168.200.175", 10000);
pw = new PrintWriter(socket.getOutputStream(),true);
br = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
keyboard = new BufferedReader(new InputStreamReader(System.in));
Thread t1 = new Thread(new Runnable() { // 익명함수
@Override
public void run() {
// 계속해서 메세지를 수신해서 콘솔에 출력
while (true) {
try {
String clientInput = br.readLine();
System.out.println(clientInput);
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
t1.start();
Thread t2 = new Thread(()->{ // 람다식
@Override
public void run() {
// 계속해서 입력받은 메세지 전송
while (true) {
String keyboardInput;
try {
keyboardInput = keyboard.readLine();
pw.println(keyboardInput);
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
t2.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new App();
}
}
// 서버
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
// Point to Point, 점대점방식, 1:1
public class App {
ServerSocket serverSocket;
Socket socket;
BufferedReader br, keyboard;
PrintWriter pw;
public App() {
try {
serverSocket = new ServerSocket(10000);
socket = serverSocket.accept();
pw = new PrintWriter(socket.getOutputStream(), true);
br = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
keyboard = new BufferedReader(new InputStreamReader(System.in));
// 계속해서 메세지를 수신해서 콘솔에 출력
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
String clientInput = br.readLine();
System.out.println(clientInput);
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
t1.start();
// 계속해서 입력받은 메세지 전송
Thread t2 = new Thread(()->{
@Override
public void run() {
while (true) {
String keyboardInput;
try {
keyboardInput = keyboard.readLine();
pw.println(keyboardInput);
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
t2.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new App();
}
}
서버와 클라이언트가 번갈아가지 않고 마음대로 1:1로 채팅을 할수 있다.
서버와 클라이언트 모두 2개의 데몬스레드를 가진다(수신,송신)
서버가 클라이언트들의 채팅을 중계만 한다면
// 서버 작성, 클라이언트는 위 코드 재사용
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Vector;
public class App1 {
ServerSocket serverSocket;
Vector<SocketThread> vc; // 세션저장소로 이용.. 벡터는 동기화 기능 있음
public App1() {
try {
serverSocket = new ServerSocket(10000); // 최초에 한번 실행
vc = new Vector<>();
// 메인스레드는 요청에 따른 소켓을 생성하기만 한다
while (true) {
Socket socket = serverSocket.accept();
SocketThread st = new SocketThread(socket);
// 벡터에 넣기 위해 밖에서 객체 생성
Thread t1 = new Thread(st);
t1.start();
vc.add(st);
}
} catch (IOException e) {
e.printStackTrace();
}
}
class SocketThread implements Runnable {
Socket socket; // 클라이언트 수만큼 필요
BufferedReader br;
PrintWriter pw;
public SocketThread(Socket socket) { // 생성자
try {
this.socket = socket;
// 소켓을 생성할때마다 두개를 만든다
pw = new PrintWriter(socket.getOutputStream(), true);
br = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
while (true) {
for (SocketThread socketThread : vc) {
if (socketThread != this) {
// 모두에게 보내는데 입력한 사람에게는 보내면 안된다.
socketThread.pw.println(br.readLine());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new App1();
}
}
간단하게 작성해서 예외처리도 불완전하고 기능이 별로 없긴 하지만 구현이 되었다
while을 이용 -> 계속해서 입력기다림/ Polling(폴링) 방식 ( 자원 낭비 )
서버가 벡터의 클라이언트들에게 입력된 내용을 Push 방식으로 준다.
반대로 클라이언트가 서버에 요청하는것을 Pull 방식이라고 한다.
소켓을 생성할때는 시간이 소요된다. 만약 많은 사용자가 서버에 접속하고 나간다면 소켓생성시간 때문에 서버가 느려지게된다.
-> 서버시작시에 소켓풀을 만들어서 사용자의 요청을 처리후 연결을 끊고 다음 사용자의 요청을 처리하는 과정을 반복하면 전체적인 속도가 개선된다. ( http 는 stateless 이용 )
이러한 풀을 이용하는 방식을 Pooling(풀링) 이라고 한다.
( 한정된 자원 재활용, 오버헤드 줄임 )