본인은 온라인 게임에 대해 그다지 큰 흥미를 느끼지 않는다.
그나마 축구를 좋아해서 피파 온라인을 몇 번 플레이 하긴 했다.
과거부터 현재까지 내가 가장 인상깊게 본 온라인 게임들을 고르자면 아무래도 스타크래프, 피파, 롤이라고 할 수 있겠다.
new Thread(new User(computer, "Starcraft"), "User1").start();
new Thread(new User(computer, "FIFA"), "User2").start();
new Thread(new User(computer, "LOL"), "User3").start();
오늘은 이 게임들을 플레이하는 가상의 User 들을 사용해 쓰레드의 개념을 연습해보자.
필요한 클래스는 세 개다. 유저, 기업(게임을 제공하는), 그리고 유저와 기업이 공유하는 컴퓨터.
안타깝게도 내가 만든 코드에서 게임은 한 번 플레이하면 사라지는 속성을 가지고 있다. 즉 한 번 게임을 플레이하고 나서는 다시 기업에게 게임을 받아야 플레이할 수 있다는 점.
본격적으로 클래스를 만들어보자.
class User implements Runnable{}
class Enterprise implements Runnable {}
class Computer {}
유저는 게임을 플레이하고, 기업은 게임을 인스톨하는 작업을 수행하기 때문에 쓰레드를 구현하였다. 쓰레드의 구현 방법에는 Thread 클래스를 상속받거나, 혹은 Runnable interface를 구현하는 두 가지가 있다. 이번에는 Runnable interface를 구현하겠다.
먼저 유저와 기업이 접촉할 수 있는 컴퓨터 클래스를 만들어보자.
class Computer {
String[] games = {"Starcraft", "FIFA", "LOL"};
final int MAX_GAME = 7;
private ArrayList<String> volumes = new ArrayList<>();
}
게임 목록들을 담을 수 있는 String 배열을 생성하고 스타크래프, 피파, 롤을 원소로 넣어주었다. 상수로써 최대 게임의 개수는 7개이고 제네릭의 타입을 String으로 지정한 ArrayList volumes도 같이 생성해주었다.
public synchronized void install(String game1) {
while (volumes.size() >= MAX_GAME) {
String name = Thread.currentThread().getName();
System.out.println(name + " is watiting");
try {
wait();
Thread.sleep(500);
} catch (InterruptedException e) {}
}
volumes.add(game1);
notify();
System.out.println("Volumes" + volumes.toString());
}
1.
install이라는 메서드를 synchronized, 즉 동기화하여 메서드 전체를 임계영역으로 설정한다. 이 때 volumes의 사이즈가 최대 게임 개수보다 많거나 같을 경우, 현재 이 임계 영역을 실행하고 있는 쓰레드의 이름을 반환함과 동시에 그 기업이 is waiting이라는 상태를 프린트하도록 설정하였다.
최대 게임 수보다 volumes의 크기가 많거나 같다는 것은 해당 쓰레드가 lock을 가지고 있음에도 불구하고 현재 작업을 실행할 상황이 안되므로, 이 쓰레드를 대기 시킨다.
만약 volumes의 크기가 게임의 최대 개수보다 적다면, volumes에 매개변수 game1을 넣어준다.
이후 volumes의 내부 원소들의 내용을 프린트한다.
public void play(String game2) {
synchronized (this) {
String name = Thread.currentThread().getName();
while (volumes.size() == 0) {
System.out.println(name + " is waiting");
try {
wait();
} catch (InterruptedException e) {}
}
2.
이번에는 객체의 참조변수를 이용하여 동기화한 play메서드로 이동해보자.
먼저 실행하는 쓰레드의 이름을 반환하도록 설정하였다. 그 후, volumes의 크기가 0일 때에 첫번째 while반복문을 실행이 된다. 크기가 0이라 함은 play할 수 있는 게임이 없다는 것을 의미하므로 쓰레드가 작업을 진행할 수 없는 상황을 의미한다. 따라서 쓰레드를 대기시킨다. 0 이 아니라면, 조건식이 false가 되므로 첫 while문은 실행되지 않고 두 번째 while문으로 넘어간다.
while (true) {
for (int i = 0; i < volumes.size(); i++) {
if (game2.equals(volumes.get(i))) {
volumes.remove(i);
notify();
return;
}
}
try {
System.out.println(name + " is waiting");
wait();
Thread.sleep(500);
} catch (InterruptedException e) {}
}
3.
두 번째 while문은 조건식의 전제가 true이다. 따라서 volumes의 사이즈 만큼 i를 증가시킨다. 이 때 volumes의 i 번째 게임이 remove의 매개변수 game2와 같다면 이를 제거해준다. 이후 notify()를 통해 wait()하고 있던 쓰레드에게 작업을 재개하라고 알려주는데, 여기서 notify()되어 wait()의 상태를 벗어난 쓰레드가 다름 아닌 1의 install 메서드의 내용을 수행하다가 volumes의 사이즈가 MAX_GAME보다 커서 대기상태가 된 쓰레드를 지칭한다. 즉 후에 install 메서드를 수행하는 기업 쓰레드가 대기하고 있다면 여기서의 notify()를 통해 다시 lock을 얻고 해당 임계영역에서 작업을 수행할 수 있게 되어 volumes에 game1을 추가할 수 있게 된다.
만약 game2와 이름이 일치하는 게임이 volumes에 없다면 gama2를 플레이하는 쓰레드의 이름에 is waiting을 출력하고, 이 쓰레드는 대기 상태로 변환된다.여기서 wait()에게 작업을 재개하라고 알려주는 notify()는 어디서 찾을 수 있을까?
public synchronized void install(String game1) {
while (volumes.size() >= MAX_GAME) {
String name = Thread.currentThread().getName();
System.out.println(name + " is watiting");
try {
wait();
Thread.sleep(500);
} catch (InterruptedException e) {}
}
volumes.add(game1);
notify();
System.out.println("Volumes" + volumes.toString());
}
4.
바로 1에서 마주했던 install 메서드에서 찾을 수 있다. volumes에 게임을 더해주고, 이 더해준 게임이 만약 game2와 같다면, 3에서 작업이 중지되어 대기하던 쓰레드는 이 게임을 지우는(플레이하는) 작업을 재개할 수 있게 된다.
public int gameNum() {return games.length; }
마지막으로 Enterprise 메서드에서 무작위로 game을 설치하기 위해 필요한 games배열의 길이를 반환하는 gameNum 메서드를 작성해준다.
곧바로 install 메서드를 실행할 Enterprise 클래스로 넘어가자.
class Enterprise implements Runnable {
private Computer computer;
Enterprise(Computer computer) { this.computer = computer;}
}
접근제어자가 private인 computer를 선언해주고 생성자로 매개변수를 초기화해준다.
public void run() {
while(true) {
int idx = (int)(Math.random() * computer.gameNum());
computer.install(computer.games[idx]);
try {
Thread.sleep(100) ;
} catch(InterruptedException e) {}
}
}
Runnable을 구현하는 클래스이어야 하기에 run() 메서드의 구현부를 작성해야한다. while의 기본 조건을 true로 설정해 무한반복되게 한다. idx라는 변수를 새로 선언하고, computer 객체에 있는 gameNum()을 호출하여 0 과 game배열의 길이 사이에 존재하는 무작위의 정수로 초기화 해준다.
이후 install 메서드를 호출하여 game의 배열에서, 무작위로 뽑힌 정수의 인덱스가 가리키는 값을 game2 매개변수 값으로 넣어준다.
이번에는 play메서드를 실행할 user 클래스에 대해 작성해보자.
class User implements Runnable {
private Computer computer;
private String game;
User(Computer computer, String game) {
this.computer = computer;
this.game = game;
}
}
마찬가지로 접근제어자가 private인 인스턴스 변수, computer 와 game을 선언해준다. 그리고 곧바로 생성자를 이용해 초기화를 설정해준다.
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
String name = Thread.currentThread().getName();
computer.play(game);
System.out.println(name + " play the " + game);
}
}
run() 메서드의 구현부를 작성하고. while의 기본 조건을 true로 설정해 무한반복 한다. 이 때 해당 영역을 진행하는 쓰레드의 이름을 반환하도록 String name 변수를 선언 및 초기화 해준다. 여기서 보이는 컴퓨터 객체의 play메서드는 우리가 computer 클래스에서 정의한 play 메서드로 만약 해당 메서드 내에서 wait()이 걸리지 않았다면, 해당 쓰레드의 이름 play the game 을 출력하도록 한다.
import java.util.*;
public class Main {
public static void main(String[] args) throws Exception {
Computer computer = new Computer();
new Thread(new Enterprise(computer), "provide").start();
new Thread(new User(computer, "Starcraft"), "User1").start();
new Thread(new User(computer, "FIFA"), "User2").start();
new Thread(new User(computer, "LOL"), "User3").start();
Thread.sleep(2000);
System.exit(0);
}
}
마지막 메인 클래스다. 앞서 써놓은 쓰레드들을 드디어 실행해볼 시간이다. 각 쓰레드들을 실행시킨 후, 2초동안 메인 쓰레드를 잠재운뒤 시스템을 종료하도록 설정하였다.
실행하기 앞서 다소 길 수 있지만, 작성한 코드의 전체 그림을 살펴보자.
import java.util.*;
class User implements Runnable {
private Computer computer;
private String game;
User(Computer computer, String game) {
this.computer = computer;
this.game = game;
}
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
String name = Thread.currentThread().getName();
computer.play(game);
System.out.println(name + " play the " + game);
}
}
}
class Enterprise implements Runnable {
private Computer computer;
Enterprise(Computer computer) { this.computer = computer;}
public void run() {
while(true) {
int idx = (int)(Math.random() * computer.gameNum());
computer.install(computer.games[idx]);
try {
Thread.sleep(100) ;
} catch(InterruptedException e) {}
}
}
}
class Computer {
String[] games = {"Starcraft", "FIFA", "LOL"};
final int MAX_GAME = 7;
private ArrayList<String> volumes = new ArrayList<>();
public synchronized void install(String game1) {
while (volumes.size() >= MAX_GAME) {
String name = Thread.currentThread().getName();
System.out.println(name + " is watiting");
try {
wait();
Thread.sleep(500);
} catch (InterruptedException e) {}
}
volumes.add(game1);
notify();
System.out.println("Volumes" + volumes.toString());
}
public void play(String game2) {
synchronized (this) {
String name = Thread.currentThread().getName();
while (volumes.size() == 0) {
System.out.println(name + " is waiting");
try {
wait();
} catch (InterruptedException e) {}
}
while (true) {
for (int i = 0; i < volumes.size(); i++) {
if (game2.equals(volumes.get(i))) {
volumes.remove(i);
notify();
return;
}
}
try {
System.out.println(name + " is waiting");
wait();
Thread.sleep(500);
} catch (InterruptedException e) {}
}
}
}
public int gameNum() {return games.length; }
}
public class Main {
public static void main(String[] args) throws Exception {
Computer computer = new Computer();
new Thread(new Enterprise(computer), "provide").start();
new Thread(new User(computer, "Starcraft"), "User1").start();
new Thread(new User(computer, "FIFA"), "User2").start();
new Thread(new User(computer, "LOL"), "User3").start();
Thread.sleep(2000);
System.exit(0);
}
}
실행 결과
Volumes[LOL]
User1 is waiting
User2 is waiting
User3 play the LOL
User1 is waiting
User3 is waiting
Volumes[FIFA]
User2 play the FIFA
User1 is waiting
User2 is waiting
Volumes[FIFA]
User3 is waiting
Volumes[FIFA, Starcraft]
보다 원할한 게임추가를 위해 User3 쓰레드를 삭제하고 2명의 User만 가지고 재실행을 했다
Volumes[LOL]
User1 is waiting
User2 is waiting
Volumes[LOL, LOL]
User1 is waiting
Volumes[LOL, LOL, Starcraft]
User2 is waiting
Volumes[LOL, LOL, Starcraft, FIFA]
User1 play the Starcraft
성공적으로 출력되었다.
이렇게 쓰레드를 이용한 코드를 직접 작성하고 해결하는 과정을 통해 쓰레드에 대해 좀 더 깊이있게 알 수 있었다. 처음에는 어디서부터 설계해야할지, 무엇을 정해야할지 감이 안 왔지만 차근차근 구글링하고 찾아가면서 쓰레드의 재미를 느낄 수 있었다. 굉장히 재밌는 개념이지만 그 사용을 위해서는 고려해야할 점이 많다는 멀티쓰레드의 단점도 명확하게 느낄 수 있었다. 뿐만 아니라 notify()가 정확히 어떤 대기하고 있는 쓰레드를 가리키는지 애매하여 코드를 작성하는 데 있어 혼동될 소지가 존재하는 것 같다. lock&condition으로 이를 보완할 수 있다고 잠깐 듣긴 했는데 이 부분은 추가적인 공부가 좀 더 필요할 것 같다.
'Java > Java 문법 연습' 카테고리의 다른 글
람다식과 스트림 연습 (0) | 2023.06.15 |
---|---|
열거형 연습 (0) | 2023.05.30 |
제네릭스 연습 2 (2) | 2023.05.30 |
제네릭스 연습 1 (0) | 2023.05.29 |
컬렉션 프레임워크 연습 (1) | 2023.05.26 |