프로세스와 스레드
* 실행중인 어플리케이션(프로그램)을 프로세스
* 실행되는 소스 코드의 흐름을 스레드
프로세스(Process)
실행 중인 application 으로 application을 실행하면 운영체제로부터 실행에 필요한 만큼의 메모리를 할당 받아 프로세스가 됨
프로세스 = 데이터 + 스레드 + 컴퓨터 자원(CPU, RAM, 보조기억장치 등 연산을 위해 필요한 장치)
스레드(Thread)
실행되는 소스 코드의 흐름
스레드 = data + application 자원 → source code 실행 (code 실행흐름)
- 메인 스레드(main thread)
java 에서 가장 먼저 실행되는 메서드는 main 메서드이며 main thread 가 main 메서드를 실행시켜준다. main thread는 main 메서드의 코드를 처음부터 끝까지 차례로 실행시키며 코드의 끝을 만나거나 return 문을 만나면 실행을 종료한다.
싱글 스레드라면 main thread 만 가지는 single thread process 이다.
반면, 또 다른 thread를 생성하여 실행시킨다면 multi trhead
- 멀티 스레드(multi thread)
하나의 프로세스는 여러 개의 스레드를 가질 수 있으며 n개의 스레드를 가지는 프로세스를 multi thread process 라고 부른다. 말 그대로 multi(여러 개)의 thread 가 동시에 작업을 수행하는 것을 의미하며 이를 multi threading 이라고 한다.
예를 들어 노래를 들으면서 카톡을 한다거나, 검색을 하는 등 동시 작업이 가능하다.
→ 이렇게 여러 작업을 동시에 하기 위해서는 작업을 동시에 실행해 줄 스레드가 필요한 것
- 스레드의 생성과 실행
작업 스레드가 수행할 코드를 작성하고 작업 스레드를 생성하여 실행시키는 것을 의미
run() 이라는 메서드 내에 스레드가 처리할 작업을 작성하도록 규정되어 있다.
run() 메서드는 Runnable 인터페이스와 Thread 클래스에 정의되어 있다. 따라서, 작업 스레드를 생성하고 실행하는 방법은 두 가지이다
- Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행
- Thread 클래스를 상속받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행
1. Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행
package java_0508.Thread;
public class ThreadExample1 {
// main 스레드
public static void main(String[] args) {
// Runnable 인터페이스를 구현한 객체 생성
Runnable task1 = new ThreadTask1();
// Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화하여 스레드 생성
Thread thread1 = new Thread(task1);
// 위의 두 줄을 아래와 같이 한 줄로 축약할 수도 있습니다.
// Thread thread1 = new Thread(new ThreadTask1());
// 작업 스레드를 생성하고 병렬 처리를 수행하려면 start() 메서드를 호출
// 직렬 처리 수행방법이기 때문에 앞에서 호출한 thread.run()을 수행하고 나서
// 아래에 나오는 조건문을 실행하게 됨
// thread1.run();
// 무작위로 나오는 것을 볼 수 있음 -> 동시에 수행됨으로 할 때 마다 다르게 나옴
thread1.start();
for(int i=0; i < 10; i++) {
for (int j = 0; j <= i; j++) {
System.out.print("-");
}
System.out.println("");
}
}
}
// 작업 스레드
// Runnable 인터페이스 구현 run() 메서드 오버라이딩
class ThreadTask1 implements Runnable {
// run() 메서드 바디에 스레드가 수행할 작업 내용 작성
@Override
public void run() {
for(int i=0; i < 10; i++){
for(int j=0; j<=i; j++) {
System.out.print("*");
}
System.out.println("");
}
}
}
출력 결과(thread.start()) // 병렬 방법✨ 우선 순위 처리로 인해 할 때 마다 다르게 나옴 ✨
출력결과(thread1.run()) // 직렬방법
2. Thread 클래스를 상속받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
package java_0508.Thread;
public class ThreadExample2 {
public static void main(String[] args) {
// 상속받은 클래스 인스턴스화 하여 스레드 생성
ThreadTask2 thread2 = new ThreadTask2();
thread2.start();
for(int i=0; i < 10; i++) {
for (int j = 0; j <= i; j++) {
System.out.print("-");
}
System.out.println("");
}
}
}
class ThreadTask2 extends Thread{
@Override
public void run() {
for(int i=1;i<=10;i++){
for(int j=9;j>0;j--){
if(i<j){
System.out.print(" ");
}else{
System.out.print("*");
}
}
System.out.println("");
}
}
}
출력 결과 // ✨ 우선 순위 처리로 인해 할 때 마다 다르게 나옴 ✨
익명 객체를 사용하여 스레드 생성하고 실행하기
새로운 class를 생성하지 않고 익명 객체를 이용하여 스레드를 생성하고 실행시킬 수도 있다.
→ 익명객체는 일회성으로만 사용할 수 있는 객체로 필요한 만큼 생성해서 사용이 가능하다.
1. 익명 Runnable 구현 객체를 활용하여 스레드 생성
package java_0508.Thread;
public class ThreadExample3 {
public static void main(String[] args) {
// 익명 Runnable 구현 객체를 활용하여 스레드 생성
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print("#");
}
}
});
thread1.start();
for(int i = 0; i<50; i++){
System.out.print("-");
}
}
}
2. Thread 클래스를 상속받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
package java_0508.Thread;
public class ThreadExample4 {
public static void main(String[] args) {
Thread thread4 = new Thread(){
public void run(){
for(int i = 0; i<100; i++){
System.out.print("협이");
}
}
};
thread4.start();
for(int i = 0; i<100; i++){
System.out.print("오하아사");
}
}
}
스레드의 이름 조회하기
thread.getName(); 메서드
* default thread name 이 Thread-0이기 때문에 set 하고 나면 정한 이름으로 바뀌게 된다.
스레드 이름 설정하기
thread.setName("설정할 이름"); 메서드
* default thread name 이 Thread-0이기 때문에 set 하고 나면 정한 이름으로 바뀌게 된다.
스레드 인스턴스의 현재 주소값 얻기
Thread.currentThread().getName()
스레드 동기화
아래 예시는 똑같은 계좌에서 금액을 랜덤으로 빼서 쓰는 것으로 여러 스레드가 공유 자원에 접근하는 경우를 보여준다.
package java_0508.Thread;
public class ThreadExample8 {
public static void main(String[] args) {
Runnable threadTask8 = new ThreadTask8();
Thread thread8_1 = new Thread(threadTask8);
Thread thread8_2 = new Thread(threadTask8);
thread8_1.setName("콜라");
thread8_2.setName("누룽지");
thread8_1.start();
thread8_2.start();
}
}
class Account {
// 계좌 잔액 balance
private int balance = 1000;
public int getBalance() {
return balance;
}
// 인출 성공 시 true, 실패 시 false 반환
public boolean withdraw(int money) {
// 인출 가능 여부 : 잔액이 인출하고자 하는 금액보다 같거나 많아야 합니다.
if (balance >= money) {
// if문의 실행부에 진입하자마자 해당 스레드를 일시 정지 시키고,
// 다른 스레드에게 제어권을 강제로 넘김
try { Thread.sleep(1000); } catch (Exception error) {}
// 남은 잔액 - 인출금
balance -= money;
return true;
}
return false;
}
}
class ThreadTask8 implements Runnable {
Account account = new Account();
@Override
public void run() {
while (account.getBalance() > 0){
// 100 ~ 300원의 인출금을 랜덤으로 정하기
int money = (int)(Math.random() * 3 + 1) * 100;
// withdraw를 실행시키는 동시에 인출 성공 여부를 할당
boolean denied = !account.withdraw(money);
// 인출 결과 확인
// 만약, withraw가 false를 리턴하였다면, 즉 인출에 실패했다면,
// -> DENIED를 출력
System.out.println(String.format("인출 %d₩ By %s. 잔액 : %d %s",
money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> DENIED" : "")
);
}
}
}
출력 결과에서 인출금과 잔액이 제대로 출력되지 못하고 있다.
결과 첫줄에서 콜라에 의해 300원이 인출되었는데 잔액은 700원이 아니라 600원이라고 출력되고 있다.
→ 두 스레드 간에 객체가 공유되기 때문에 발생하는 오류
withdraw() 에서는 잔액이 인출하고자 하는 금액보다 많은 경우에만 가능하도록 하였으나 음수 잔액이 발생한다.
→ 두 스레드가 하나의 Account 객체를 공유하는 상황에서 한 스레드가 if 문의 조건식을 true로 평가하여 if 문의 실행부로 코드의 흐름이 이동하는 시점에 다른 스레드가 끼어들어 balance를 했기 때문
이와 더불어 - > DENIED도 출력이 되지 않았다.
→ 이런 문제가 발생하지 않도록 하는 것이 스레드 동기화.
동기화를 적용하면 위의 예제에서 발생하는 문제 해결이 가능
동기화를 이해하기 위해서는 임계영역과 락을 알아야 한다.
임계영역과 락
여러 스레드가 공유하는 자원(변수, 객체 등)에 대해 동시에 접근하면, 자원의 일관성을 유지하기 어렵고 오류가 발생할 수 있다. 이러한 상황을 방지하기 위해 임계영역(critical section) 이라는 개념이 사용됨
임계영역(critical section) 이란, 여러 스레드가 동시에 접근할 수 있는 코드 영역
이 코드 영역에서는 자원에 대한 접근이 발생하므로, 하나의 스레드가 접근하는 동안에는 다른 스레드가 접근하지 못하도록 보호가 필요
이를 위해 락(lock) 이라는 개념이 사용 되는데
락은 스레드의 실행 순서를 제어하여 하나의 스레드가 자원을 사용하는 동안 다른 스레드가 접근하지 못하도록 한다. 락은 자바에서 "synchronized" 키워드를 통해 구현
synchronized 키워드를 사용하여 락을 설정하면, 해당 코드 영역에서는 하나의 스레드만 실행
다른 스레드가 이 코드 영역에 접근하려 하면, 락이 걸려있는 동안 대기
락이 해제되면 대기하던 스레드 중 하나가 락을 획득하고 해당 코드 영역을 실행
이렇게 함으로써 여러 스레드가 동시에 접근하는 것을 막고, 자원의 일관성을 유지
⇒ 동시 접근하는 메서드 : withraw()
withdraw()가 호출되면, withdraw()를 실행하는 스레드는 withdraw()가 포함된 객체의 락을 얻으며,
해당 스레드가 락을 반납하기 이전에 다른 스레드는 해당 메서드 코드 실행 불가
1. 메서드 전체를 임계 영역으로 지정하는 방법
public synchronized boolean withdraw(int money) {
if (balance >= money) {
try { Thread.sleep(1000); } catch (Exception error) {}
balance -= money;
return true;
}
return false;
}
2. 특정한 영역을 임계 영역으로 지정
public boolean withdraw(int money) {
synchronized (this) {
if (balance >= money) {
try { Thread.sleep(1000); } catch (Exception error) {}
balance -= money;
return true;
}
return false;
}
}
스레드의 상태와 실행 제어
스레드를 실행시키기 위해서는 start() 메서드를 호출해야 한다고 했다.
하지만, start()는 스레드를 실행시키는 메서드는 아니다.
start()는 스레드의 상태를 실행 대기 상태로 만들어주는 메서드이며, 어떤 스레드가 start()에 의해 실행 대기 상태가 되면 운영체제가 스레드를 실행시켜 준다.
스레드의 상태
- NEW: 스레드가 생성되었지만, 아직 start() 메서드가 호출되지 않은 상태
- RUNNABLE: 실행 대기 상태. 스레드 스케줄링에 의해 선택되어 실행될 수 있는 상태
- BLOCKED: 다른 스레드가 락(lock)을 가지고 있어서 해당 스레드가 락이 풀릴 때까지 대기하는 상태
- WAITING: 스레드가 특정 조건을 만족할 때까지 무한정 기다리는 상태
- TIMED_WAITING: 스레드가 특정 시간동안 기다리는 상태
- TERMINATED: 스레드의 실행이 종료된 상태
스레드 상태를 바꾸는 메서드
스레드의 상태를 바꾸는 메서드들은 ‘Thread’ 클래스에 포함되어 있다. ‘Thread’ 클래스에서 제공하는 스레드 상태 전이 메서드
- start(): 스레드를 실행 대기 상태로 만듭니다.
- join(): 다른 스레드가 종료될 때까지 대기합니다.
- sleep(): 스레드를 지정한 시간만큼 일시 정지 상태로 만듭니다.
- wait(): 다른 스레드가 통지(notify)할 때까지 스레드를 일시 정지 상태로 만듭니다.
- notify(): 일시 정지 상태인 스레드 중 하나를 깨웁니다.
- notifyAll(): 일시 정지 상태인 스레드를 모두 깨웁니다.
스레드의 상태와 실행 제어 메서드 요약
지난 주는 문제를 풀면서 보낸 시간이 많았다. 물론... 기억은 못한다. 쉬는 날 동안 할머니댁에도 다녀오고 이렇게 저렇게 보내다보니 시간이 훅 지나가버렸다.ㅜㅜ
지난 주에 했던 람다식과 같은 것들은 정리는 필요할 것 같으나, 문제를 많이 풀어보고 익숙하게 만드는 것이 더욱 중요할 듯 하다.
오늘은 스레드와 JVM에 대해 배웠는데 사실 JVM은 다 못봐서 다시 봐야한다. 스레드 부분을 이해하고 계속해서 궁금한 것을 찾아가며 정리하다 보니 시간이 모자랐다. 다시 이번 주도 화이팅 해야겠다.
그래도 이렇게 적어보면서 공부를 하니 thread와 흐름에 대해 쉽게 이해가 됐다.
문제는 라이브 세션 때 조금 머리가 혼란스러웠다는 것이다ㅋㅋ 스레드의 개수가 몇 개냐고 묻는데서 클래스의 개수가 스레드의 개수인가? 라는 의문이 들었다.
결론은 아니다. 아래의 예시를 통해 살펴볼 수 있다.
public class Main {
public static void main(String[] args) {
int numThreads = 3;
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < numThreads; i++) {
threadList.add(new Thread(new MyRunnable()));
}
for (int i = 0; i < threadList.size(); i++) {
threadList.get(i).start();
}
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread " + Thread.currentThread().getName() + " is running");
}
}
위 코드에서는 numThreads 변수를 이용해 3개의 스레드를 생성하고, MyRunnable 클래스에서 스레드 실행 내용을 구현한다. Main 클래스에서는 numThreads 개수만큼 스레드를 생성하여 시작시킨다. 이 경우 스레드의 개수는 3개이지만, MyRunnable 클래스는 하나뿐이다.
챗지피티 없이 공부할 때는 어떻게 했나 싶을 정도로 정말.. 모든 궁금증을 해결해줘서 너무 너무넘무 좋다.
남은 공부도 홧팅하구 자야지 : )
'LANGUAGE > JAVA' 카테고리의 다른 글
[자료구조] 그래프(Graph)란? / 인접 행렬(Adjacency Matrix)과 인접 리스트(Adjacency List) (1) | 2023.05.17 |
---|---|
[자료구조] 트리 구조 (Tree) / 이진 트리 (Binary Tree) (0) | 2023.05.16 |
[JAVA] 컬렉션 프레임워크(Collection Framework) 기본 정리 / List, Set, Map (0) | 2023.05.03 |
[JAVA] 객체 지향 프로그래밍 / 추상화(Abstract) - 상속 , 오버라이딩, 사용 이유 (0) | 2023.04.27 |
[JAVA] 객체 지향 프로그래밍 / 다형성(polymorphism) - 오버로딩, 오버라이딩, 참조변수의 타입 변환, instanceof 연산자 (0) | 2023.04.27 |