멀티스레드프로그래밍1 : 개념, 용어정리, 스래드 상태와 제어

멀티스레드프로그래밍1 : 개념, 용어정리, 스래드 상태와 제어

개념, 용어정리

용어 개념
애플리케이션 이클립스나 파워포인트, 브라우저와 같은 코드 덩어리
프로세스 설치된 애플리케이션을 실행하게되면 운영체제(OS)로부더 메모리의 일정영역을 할당받고 CPU와 HDD를 이용해서 동작하는 것
멀티프로세스 동시에 여러 프로세스를 실행
스레드 프로세스 동작의 최소 단위. 모든 프로세스는 하나 이상의 스레드로 구성
메인스레드 메인스레드가 main()메서드를 실행하면서 애플리케이션이 구동됨
멀티스레드 둘 이상의 스레드로 구성된 프로세스
멀티스레드 프로그램 작업을 스레드 단위로 분리해서 병렬 수행가능
  • 스레드는 모든 게임에 자주 사용된다
    • 총게임에서 총알을 발생하기 등등




멀티스레드 장단점

장점

  • CUP사용률 향상
  • 작업의 분리로 응답성 향상
  • 자원의 공유를 통한 효율성 증대




단점

  • 컨텍스트 스위칭 과정에 별도의 비용발생
  • 제어가 어려움 : 어떤 스레드를 먼저 실행할지 제어하기 힘들다
  • 동기화(synchronization), 교착상태(deadlock)와 같은 문제 발생

https://goodgid.github.io/What-is-Multi-Thread/




스레드 생성

2가지 생성방법

  1. Runnable 인터페이스를 구현하는 방법 : 오버라이딩 -> 생성자에 파라미터를 넘겨주면서 객체생성
  2. Thread클래스를 상속받는 방법 : implements 끝

다음 예시코드를 보자. 아래는 -를 5번반복하고 @는 10번 반복후 출력하는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {

@Override
public void run() {
for(int i =0 ; i<5; i++){
System.out.print("-");
}
}
});

Thread t2 = new Thread(() -> {
for(int i = 0; i<10; i++){
System.out.print("@");
}
});

//JVM스케줄러가 달라져서 출력값이 매번 다름
t1.start();
t2.start();
System.out.println("메인끝");
}
//출력값
메인끝
@@--@@@@@---@@@




예시코드의 의문점 3가지

위의 코드에 3가지 의문점이 있다.

  1. run()이 없는데 실행되었다 : start()가 호출되면 JVM이 운영체제의 스레드 스케줄러에 의해 가능할때 run()을 호출한다.
  2. 출력값이 매번다르다 : JVM스케쥴러가 달라져서.
  3. 메인끝은 맨 마지막에 작성했는데 제일 처음 출력되었다 : main()스레드가 t1의 start()스레드를 생성

하게되면 별도의 스택공간을 구성한다. 이 공간은 메인스레드공간과 전혀 무관한 t1스레드의 공간으로 별도의 흐름이 생겨난다. 이후 두 스레드는 병렬스레드로 번갈아가면서 작업이 되는데 start()스레드 전에 main()스레드가 종료되었기때문에 제일 먼저 출력되었다.




만약 위의 코드에서 start()가 아닌 run()을 하면 어떻게 될까?

1
2
3
4
5
6
t1.run();
t2.run();
System.out.println("메인끝");

//출력값
-----@@@@@@@@@@메인끝
  • 이때는 메인스레드에서 실행되는 것이고 t1과t2가 별도의 스택을 구성하지않기때문에 싱글스레드이므로 코드 순서대로 실행된다.

https://xeros.dev/63




어떤 스레드가 먼저 실행될까?

스레드들은 별도이 스택에서 따로따로 동작하기 때문에 제어가 어렵다.




1 우선순위를 통한 제어

  • 스레드가 runnable상태에서 선택될 때는 우선순위가 적용됨.

  • MIN_PRIORITY = 1, MAX_PRIORITY = 10이다. 노말은 5.

  • 아래예시는 F에 우선순위 10을 주고 T에는 제일 낮은 우선순위를 준 뒤 출력하는 값이다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static class MessengerThread extends Thread{
public MessengerThread(String name){
super(name);
}
public void run(){
for(int i=0; i<10; i++){
System.out.print(this.getName());
}
}
}

public static void main(String[] args) {
Thread ft = new MessengerThread("F");
Thread tt = new MessengerThread("T");
ft.setPriority(Thread.MIN_PRIORITY); //MIN_PRIORITY은 1이다.
tt.setPriority(Thread.MAX_PRIORITY); //MAX_PRIORITY는 10이다
ft.start();
tt.start();
}
//출력값
FFFFFFFTTTTTTTTTTFFF

우선순위때문에 F가 모두 출력된뒤 T가 출력되어야하지만 출력값은 그게 아니다.

왜그럴까?

스레드의 우선순위 역할은 실행할 확률이 높아지는 것뿐 우선순위만으로는 100%제어할 수 없다.




2 sleep()을 통한 상태제어

  • sleep()메서드는 동작하는 스레드를 주어진 시간동안 일시정지키셔 대기 풀에서 sleep하게 한다.
  • 대기시간이 끝나거나 interrupt()메서드가 호출되면 대기 풀에서 벗어나 다시 runnable상태로 이동한다.




3 interrupt()를 통한 상태제어

  • interrupt()를 사용하여 대기 중인 스레드에게 InterruptedException을 발생시켜 즉시 runnable상태로 이동한다.
  • sleep()이나 join()으로 인해 대기 풀에 대기 중인 스레드들은 지정된 시간이 지나거나 끼웠던 스레드가 종료되면 자동으로 runnable 상태로 이동한다.
  • BUT 대기 중인 스레드가 임의로 runnable상태로 바꿔야할 때가 있다
    • 예를 들어 음악 재생에서 일시정지했다가 다시 실행하는 경우가 해당.
  • 예시 : 카운트10초동안 구구단 문제를 풀고 답을 입력하면 카운드가 종료되면서 사용자가 기입한 답과 정답을 리턴하는 코드이다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
Thread cnt = new Thread(()->{
for(int i=10; i>0; i--){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
break;
}
System.out.println("카운트다운: "+i);
}
});
cnt.start();

Random rd = new Random();
int first = rd.nextInt(9)+1;
int second = rd.nextInt(9)+1;

String result = JOptionPane.showInputDialog(first+" * "+second+" ? ");
cnt.interrupt();
System.out.println("사용자가 기입한 답: "+result+", 정답: "+(first*second));
}//end of main()




4 join()를 통한 상태제어

  • sleep()과 비슷하게 스레드를 대기 풀로 이동시키는 메서드
  • 다른 스레드의 작업이 종료될때까지 join()을 호출하는 스레드는 대기풀에서 대기
  • 다른 작업을 나의 작업에 참여(join)시킨다.
  • 예시 : 구구단만들기
    메인스레드와 9개의 구구단 스레드, 숫자를 곱할 때마다 1초씩 sleep()을 호출
    메인스레드는 구구단 스레드들이 각 단을 완성하면 최종 결과를 출력
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static class GuguThread extends Thread{
private int dan;
String[] result = new String[9];

public GuguThread(int dan){
this.dan = dan;
}

public void run(){
for(int i=1; i<10; i++){
result[i-1] = dan + "*"+i+"="+(dan*i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("단완료");
}
}

public static void main(String[] args) {
List<GuguThread> gs = new ArrayList<>();
for(int i=2; i <10; i++){
GuguThread g = new GuguThread(i); //생성
gs.add(g);
g.start(); //스레드생성ㄴ
}

for(GuguThread g : gs){
try {
g.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.println("\n 구구단출력");
for(GuguThread g : gs){
System.out.println(Arrays.toString(g.result));
}
}




5 yield()를 통한 상태제어

  • sleep()이나 join()은 스레드 상태를 대기상태로 변경하는 반면 yield()는 대기상태로 변하지 않고 동일한 우선순위를 가진 다른 스레드에게 실행을 양보하고 즉시 runnable상태가 변경된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static class YieldThread extends Thread{
String s;

public YieldThread(String s){ //생성자
this.s = s;
}

public void run(){
for(int i=0; i<60; i++){
if(i%2==0){ //짝수이면
System.out.print(s);
}else{
Thread.yield();
}
}
}
}

public static void main(String[] args) {
new YieldThread("@").start();
new YieldThread("*").start();
}

//출력값
교대로 마음대로 출력된다




스레드의 종료

  • run()스레드가 끝나면 자동으로 종료
  • 외부조건에 의해 스레드 종료 가능 : stop()메서드가 있지만 안정성을 위해 flag값을 이용해서 내부값을 자연스럽게 종료시켜야한다.
  • 종료된 메서드는 생존주기가 소멸된다 -> 한 번 소멸된 스레드는 다시 start()를 통해서 runnable로 이동할 수 없다.
  • 예시 : stop()과 flag사용법 차이
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static class Rut extends Thread{
boolean flag = true;
public void run(){
System.out.println("자원획득");
while(flag){
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}System.out.println("자원사용");
}System.out.println("자원반납");
}
}

public static void main(String[] args) {
Rut t = new Rut();
t.start();
Scanner s = new Scanner(System.in);
if(s.nextLine().equals("s")){
t.stop();
}else{
t.flag = false;
}
//자원해제
s.close();
System.out.println("메인끝");
}
  • flag는 자원반납까지가능
  • stop은 자원반납못하고 즉시 종료됨