코드 그라데이션

[Java] 프로그램, 프로세스, Thread 본문

Java, SpringBoot 추가 공부

[Java] 프로그램, 프로세스, Thread

완벽한 장면 2023. 5. 5. 14:55

프로그램, 프로세스, 쓰레드

프로그램(Program)

: 컴퓨터에서 실행 가능한 명령어들의 집합.

- 보통 하드디스크나 USB 등의 저장 장치에 파일 형태로 저장되며,

  이를 실행하면 컴퓨터의 메모리에 로드되어 실행된다.

 

프로세스(Process)

: 실행 중인 프로그램. 실행 중인 프로그램의 인스턴스(instance)라고 할 수 있음.

- 컴퓨터 메모리에서 실행 중인 프로그램의 코드와 데이터를 저장하는 메모리 영역을 할당받으며,

  실행 중인 프로그램의 상태를 유지하고 프로그램의 실행 흐름을 제어한다.

 

쓰레드(Thread)

:  프로세스 내에서 실행되는 실행 단위를 의미.

- 한 프로세스는 여러 개의 쓰레드를 가질 수 있으며, 각각의 쓰레드는 독립적으로 실행될 수 있다.

- 하나의 프로세스 내에서 각 쓰레드는 공유된 자원(메모리, 파일 등)을 사용하면서 작업을 처리함.

- 쓰레드는 프로세스 내에서 코드 실행 능력을 갖는 가장 작은 단위

- 하나의 프로세스에 있는 쓰레드는 다른 쓰레드와 같은 자원을 공유하므로,

  프로세스 내부에서 효율적인 작업 분배와 자원 활용이 가능해진다.

- 프로세스를 진행하는 worker 개념으로 봐도 무방. 

- 쓰레드를 정의한다는 건 일을 정의하는 것과 같다. 다만 실행과는 별개

 

1.

public class Controller {

  public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());
  }
}

- 이렇게 하면 현재 실행중인 쓰레드 이름을 가져올 수 있음

<실행 결과>

main

=> 기본적으로 프로그램에서는 main 쓰레드가 실행 중임을 알 수 있다.

currentThread()는 스태틱 메서드 getName()은 인스턴스 

 

 

2.

쓰레드를 일부러 만들고 싶으면, 일반 클래스를 하나 만들어서  쓰레드를 상속받으면,

이것이 쓰레드 역할을 수행할 수 있다.

=> 다형성

class MyNewThread extends Thread {
  
}
class MyNewThread extends Thread {

  @Override
  public void run() {
  }
}

이렇게 하면 자식클래스에서 재정의한 쓰레드를 실행 시킬 수 있게 된다.(여기까지 하면 정의 완료)

=> run() 메서드에서는

    쓰레드가 앞으로 할 일을 정의하는 느낌.

 

3.

class ThreadTest extends Thread{
	public void run() {
		for(int i = 1;i<=10;i++) {
			System.out.println("자바 쓰레드 너무 쉬워요 : "+ i);
		}
	}
}

public class EXThread1 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		ThreadTest tt = new ThreadTest(); // 생성
		tt.start(); // 실행 -> run()
		for(int i = 0;i<10;i++) {
			System.out.println("쓰레드 어려워요~~~~~");
		}
	}

}

- 쓰레드는 start() 를 했을 때  run()이 호출되어 비로소 실행된다.

 

 

4. 동시성으로 인해 문제 발생 가능

한 번에 하나씩만 실행하면 그런 문제가 없다

=> 해결 : synchronized

 

- "공유 자원이 있을 때, 번호표를 들고 있는 사람만 가져다 쓰자"

<모니터> = 소유권(열쇠)

= 이게 있어야 쓰레드에 접근이 가능한 뭐 그런 개념.

- 모든 객체가 모니터를 가지고 있다.

class A{
	synchronized void plus(int i){
		for(int j = 0;j<5;j++) {
			System.out.println(j*i);
			try {
				Thread.sleep(800);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

class B extends Thread{
	A a; //has 관계 
	int i;
	B(A a, int i){
		this.a = a;
		this.i = i;
	}
	
	public void run() {
		a.plus(i);
	}
}

public class EXThread2 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		A a = new A();
		B b1 = new B(a,3);
		B b2 = new B(a,7);
		b1.start();
		b2.start();
	}

}

A 클래스가 객체가 되면 모니터를 들고 있고 1번 쓰레드가 진입을 한다고 하면,

일단 진입하기 전에 모니터가 있는지를 확인한다.

A에게 모니터가 있다는 말은 다른 객체가 이걸 안 쓰고 있다는 말.

그러니까 A에게 모니터를 달라고 한다.

그럼 1번 쓰레드가 모니터를 받아서 작업에 들어가는 것.

실행 흐름

=> 쓰레드끼리 서로 싸울 일이 생기지 않음.

 

그런데, 동시에 접근한다면?

=> 일관성 깨질 수 있다. (예상된 결과와 달리 결과가 꼬일 수 있다.)

==> 반드시 교통 정리 필요

 

그렇다면, 반드시 모니터를 쓰는게 좋은 것인가?

=> 꼭 그렇지만은 않다.

1. 복잡도

2. 속도(오버헤드 발생 가능성)

 

이제 위 코드를 해석해보면

A는 하나 만들고, B는 두개 만듦.

A는 공유 자원이었고,

위로 올라가보면 A는 synchronized가 붙은 plus 메서드가 있다. 

여기서 하는 일은 어떤 인자를 받아서 명령대로 프린트해주는 것. 

B를 보면 A를 가지고 있고, 어떤 인자(i)를 하나 받은 다음

a에게 plus(i)를 넘겨서 실행하는 게 전부임.

 

실행 결과

0
7
14
21
28
0
3
6
9
12

=> 동시성 문제 해결

 

 

5.

임계 영역 = 공유 자원

 

이런 경우도 생각해볼 수 있음

class A{
  synchronized void plus(int i){
    for(int j = 0;j<5;j++) {
      System.out.println(j*i);
      try {
        Thread.sleep(800);
      } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    }
  }
}

class B extends Thread{
  A a; //has 관계
  int i;
  B(A a, int i){
    this.a = a;
    this.i = i;
  }

  public void run() {
    a.plus(i);
  }
}

public class EXThread2 {

  public static void main(String[] args) {

//    A a = new A();
    B b1 = new B(new A(),3);
    B b2 = new B(new A(),7);
    b1.start();
    b2.start();
  }

}

실행 결과

0
0
3
7
6
14
21
9
28
12

=> 위 결과와 다른 결과가 출력(섞임)

그런데 분명 synchronized는 붙어있다!

왜?

 

일단,

모니터는 객체마다 1개 존재한다.

클래스 자체로 존재하는 게 아니라 객체단위

그럼 여기서는 b1은 첫 번째 A에 대한 lock을 획득한 거고

b2는 두 번째 A에 대한 lock을 획득한 것이다.

 

둘이 다른 것이므로 싸울 일이 없다.

=> 따라서 얼마든지 둘이 동시에 실행할 수 있는 것이다.

 

 

6.

Thread도 결국엔 클래스다.

생성자 만들어서 하고싶은 것 해도 된다.

class DThread extends Thread{
	DThread(String str){
		setName(str);
	}
	
	public void run() {
		for(int i = 1;i<=10;i++) {
			System.out.println(getName() + i);
		}
		
		System.out.println("완료 !!!!"+getName());
	}
}

public class ExDoubleThread1 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		DThread t1 = new DThread("자바");
		DThread t2 = new DThread("스프링");
		t1.start();
		t2.start();
		System.out.println("바바바이!!!");
	}

}

여기서 getName과 setName은?

- 먼저 DThread에서 만든 메서드인지 확인. => 아니다.

=> 그러면 무조건 부모에게 있는 것.

 

Thread 클래스 내부 메서드

    public final synchronized void setName(String name) {
        checkAccess();
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
        if (threadStatus != 0) {
            setNativeName(name);
        }
    }
public final String getName() {
        return name;
    }

또는 @Lombok 이용해서 log를 찍으면 쓰레드의 이름이 출력됨.

 

 

7.

@Lombok 이용해서 로그를 찍을 때 @Slf4j 어노테이션을 활용

(설명 by chat GPT)

@Slf4j 

자동으로 로깅 코드를 생성해주는 역할을 합니다.

 

@Slf4j 어노테이션을 클래스에 적용하면,

Lombok은 자동으로 해당 클래스의 인스턴스 변수로 log 라는 이름의 org.slf4j.Logger 객체를 생성해줍니다.

이 객체를 사용하여 클래스에서 로그 메시지를 기록할 수 있습니다.

 

예를 들어, 다음과 같은 코드에서는 @Slf4j 어노테이션을 사용하여 log 변수를 생성하고,

이를 사용하여 로그 메시지를 출력합니다.

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MyClass {
    public void myMethod() {
        log.debug("Debugging message");
        log.info("Informational message");
        log.warn("Warning message");
        log.error("Error message");
    }
}

위 코드에서 @Slf4j 어노테이션을 사용하면, log 변수를 생성할 때 다음과 같은 코드가 자동으로 생성됩니다.

private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MyClass.class);

이렇게 자동으로 생성된 로그 객체를 사용하여 로그 메시지를 출력하면,

코드의 가독성을 높이고 로그 메시지의 출력 여부를 손쉽게 제어할 수 있습니다.

 

 

8.

Thread를 만드는 방법이 하나 더 있다

그게 바로 Runnable

 

- 자바에서 상속을 쓰면, 상속받은 애는 또 다른 애의 자식이 될 수 없다(다중상속 불가)

- 그런 한계를 극복하고 싶을 때, 인터페이스인 Runnable을 활용한다.

이 때 Implement로 바꿔버리면 얼마든지 다른 것도 더 상속을 받고 만들 수 있게 된다는 것

 

예시

import lombok.extern.slf4j.Slf4j;

@Slf4j
class MyThread extends Thread {
  public void run() {
    setName("Mythread 1");
    for(int i=0; i<10; i++) {
      log.info("abcd");
      log.info("hello");
    }
  }
}

public class Main {

  public static void main(String[] args) {
    MyThread thread = new MyThread();
    thread.start();
  }
}

이런 코드를

 

import lombok.extern.slf4j.Slf4j;

@Slf4j
class MyRunnable implements Runnable {
  public void run() {
    for(int i=0; i<10; i++) {
      log.info("abcd");
      log.info("hello");
    }
  }
}

public class Main {
  public static void main(String[] args) {
    MyRunnable runnable = new MyRunnable();
    Thread thread = new Thread(runnable);
    thread.start();
  }
}

이런 모양으로 바꾸어 활용하면 됨.

 

위는 Thread 자체를 만들었는데,

아래는 Runnable을 쓰레드의 인자로 넣어줘야 함.

-> 이제 이러면 얘가 쓰레드를 상속받는 게 아니라 Runnable을 구현하는 것이 되기 때문에

   결과는 똑같이 나오고 여러개를 더 구현할 수도 있고 다른 걸 더 상속받을 수 있고 해서 더 유연해지게 됨.

 

 

9.

join

public class ExDoubleThread2 {

	public static void main(String[] args) throws InterruptedException {
		// TODO Auto-generated method stub
		DThread t1 = new DThread("자바");
		DThread t2 = new DThread("스프링");
		System.out.println("쓰레드 동작 전 ============");
		t1.start();
		t2.start(); // 얘네들 먼저 시작을 걸어놓고 join()을 불러야.
		t1.join(); // 예외처리 필요
		t2.join();
		System.out.println("쓰레드 동작 후 ==============="); // 이게 가장 마지막에 나옴 무조건. 기다려준다고 했기 때문에

	}

}

- 지금 여기는 쓰레드가 3개가 있다.

t1, t2, main

두 개의 작업과 별개로 main도 자기 갈 길 그대로 간다.

 

t1.join(); 과 t2.join()은 main이 하는 것이다.

t1.join()을 하면, main은 t1을 기다린다.

t2.join()을 하면, 역시 main은 t2를 기다린다.

(물론 언제나 main이 기다린다는 뜻은 아니다. 지금 실행하고 있는 게 main이니까 그런 거다.)

그래서 그 두 개의 작업이 모두 끝난 다음에 System.out.println("쓰레드 동작 후 ===============");

이게 출력이 되는 것.

 

 

10.

그럼 이 join()은 synchronized 개념과 어떤 차이가 있는 것인가?

 

후자는 공유 자원이라는 개념이 있고, 이것 때문에 싸우지 않게 하는게 목표.

전자는 공유 자원 개념 자체가 없다.

그래서 t1과 t2는 서로 싸우지 않고 자기 갈 길 가기 때문에 출력 결과가 섞여서 나옴.

 

synchronized에서 기다리는 이유는 공유 자원이 없어서 기다렸던 것

(어쩔 수 없이, 나도 들어가고 싶은데 - 모니터를 기다림)

 

join은 그 쓰레드가 끝날때 까지 기다리는 것

('내가 원한다'는 개념과는 무관)

 

=> 두개가 목적이 좀 다른 거죠.

 

 

11.

"실행 흐름을 통합" 의 의미

 

위에서 봤던 것처럼,

main에서 1번 쓰레드를 join()하면

1번 쓰레드가 끝났을 때 기다렸다가 main 쓰레드가 다시 실행되게 만드는 것.

 

12. 쓰레드 동기화

- 하나의 프로세스에 여러 개의 쓰레드가 있을 수 있기 때문에 발생하는 문제를 해결하는 과정

 

synchronized

 

 

13.

쓰레드 사이의 통신

wait(), notify(), notifyAll()

 

일반적으로

그냥 위에서부터 아래로 실행되는 코드를 만든다고 하면,

만들고 소비하고, 만들고 소비하고 반복하면 되는데,

대부분의 경우, 언제 만들어지고 언제 소비되는지를 알 수가 없다.

서로에게 알려주는 게 wait 과 notify

 

wait, notify의 흐름

 

생산자가 먼저 들어오는 경우도 실행 흐름은 동일함.

 

 

14.

예시코드

class Factory{
  private int value;
  private boolean check = false; // 처음엔 false로 시작

  synchronized void send(int value) {
    while(check == true) { // false부터 시작하므로 처음에 얘는 안 돈다.
      // check == true는 보낸 상태인데 아직 수신이 안 된 상태
      try { // true여서 여기로 들어올 수 있고
        wait(); // 대기하게 됨.
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    //check이 false가 됨 => 수신완료를 의미
    this.value = value;
    System.out.println("만드는 사람 : 만든다"+this.value); // 받았으니까 만듦
    notify(); // 얘가 여기서 깨웠지만 아직 true로 바뀐 게 아니기 때문에(얘가 깨우는순간 wait는 깨진다)->
    check = true; // 아래로 와서 이 작업들을 다 하고 true로 바꾸니까,
  }
  synchronized int get() {
    while(check == false) { // 발신이 안 된 상태
      // 아직 안 보낸 거니까 얘도 들어와서 잠드는 거고
      try { // 원래 처음이 false였으니까 사는 사람은 여기에 들어와 있었을 것.
        wait(); // 얘가 기다리고 있었는데, / -> 여기서 돌고 있다. // 위에서 true로 바뀌면 얘도 나와서
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }

    // 발신이 완료된 이후
    System.out.println("사는 사람 : 산다"+this.value);
    notify(); // 자고있는 만드는 메서드(발신자)를 깨워주고
    check = false; // 얘는 일을 다 했으니까 다시 false로 바꿔서 들어가서 또 잔다.
    return this.value;
    // 요러한 작업들을 계속 반복...
  }
}

class P extends Thread{
  Factory f;
  P(Factory f){
    this.f = f;
  }

  public void run() {
    for(int i = 0;i<10;i++) { // 10번동안 send
      f.send(i); // 얘(P)는 send에 접근하고(총 10번)
    }
  }
}

class C extends Thread{
  Factory f;
  C(Factory f){
    this.f = f;
  }

  public void run() {
    int temp = 0;
    for(int i = 0;i<10;i++) { // 10번동안 get
      temp = f.get(); // 얘(C)는 get에 접근한다. 총 10번
    }
  }
}


public class ThreadEX1 {

  public static void main(String[] args) {

    Factory f = new Factory(); // 얘가 임계영역을 들고 있고
    P p = new P(f); // 이것과 (보내는 애)
    C c = new C(f); // 이것 두 개가 경쟁 (받는 애)

    p.start(); //start가 내부적으로 run()을 호출함.
    c.start();
  }

}

 

=> wait()과 notify()를 이용하면 실행 순서를 보장할 수 있다.

 

 

15.

Thread의 우선순위

class PriTest extends Thread{
	PriTest(String str){
		setName(str);
	}
	
	public void run() {
		for(int i = 0;i<5;i++) {
			System.out.println(i+getName()+"순위 : "+getPriority());
		}
	}
}


public class ThreadPriorityTest {

	public static void main(String[] args) {

		PriTest p1 = new PriTest("1번");
		PriTest p2 = new PriTest("2번");
		PriTest p3 = new PriTest("3번");
		
		p1.setPriority(Thread.MAX_PRIORITY); // 1등
		p2.setPriority(Thread.NORM_PRIORITY); // 2등
		p3.setPriority(Thread.MIN_PRIORITY);//3등
		
		p1.start();
		p2.start();
		p3.start();
	}

}

- 솔직히 우선순위가 철저하게 지켜진다고 보기는 어렵다.

728x90
Comments