스레드의 개념을 간단히 정리 하자면..
스레드는 메모리를 공유한다 ( 프로세스는 각자 할당 )
따라서 메모리공간(힙)을 이용해 데이터를 주고 받고 작업을 처리할 수 있다.
메모리를 공유함으로써 동기화 문제가 생길수 있다.
( 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(풀링)
이라고 한다.
( 한정된 자원 재활용, 오버헤드 줄임 )