[Java] 멀티스레드(multi thread)

Jeini·2023년 3월 15일
0

☕️  Java

목록 보기
55/72
post-thumbnail

📌 멀티 스레드와 멀티 프로세스

✔️ 멀티 스레드
: 하나의 프로세스 내에서 둘 이상의 스레드가 동시에 작업을 수행

  • 각 스레드가 자신이 속한 프로세스의 메모리를 공유

✔️ 멀티 프로세스
: 여러 개의 CPU를 사용하여 여러 프로세스를 동시에 수행

  • 각 프로세스가 독립적인 메모리를 가지고 별도로 실행

➡️ 둘 모두 여러 흐름을 동시에 수행하다는 공통점을 가지고 있다.


📎 멀티 스레드의 장단점

⭐️ 장점

  • 시스템 자원을 보다 효율적으로 사용할 수 있다.
  • 사용자에 대한 응답성(responseness)이 향상된다.
  • 작업이 분리되어 코드가 간결해 진다.
  • 여러 모로 좋음 🙂

단점

  • 동기화(synchronization )에주의해야 한다.
  • 교착상태(dead-lock)가 발생하지 않도록 주의해야 한다.
  • 각 스레드가 효율적으로 고르게 실행될 수 있게 해야 한다.

✅ 병렬 VS 병행

📌 병행(Concurrent)

  • 멀티스레드 프로그래밍을 의미

📌 병렬(Parallel)

  • 멀티코어 프로그래밍을 의미

➡️ 우리가 살펴 볼 것은 병행 프로그래밍

  • 동시성 프로그래밍
  • 멀티스레드 프로그래밍

💡 멀티스레드 실행 방식

✅ 메인 스레드(Main Thread)

  • 모든 자바 애플리케이션은 메인 스레드(main thread)가 main() 메소드를 실행하면서 시작된다.

  • Java Thread 순서가 보장되지 않는다.

❗️ Thread 동기화 문제


  • 여러 스레드가 공통 영역을 동시 접근하여 수정하는 상황일 경우,
    충돌과 일관성 문제가 발생할 수 있다.
  • 공유 자원은 변수, 객체, 파일, DB 등이 될 수 있다.

👉 이걸 Race Condition(경쟁 상태) 라고 부른다. 🏁

✏️ 예시로 보는 문제 상황

class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for(int i=0; i<1000; i++) counter.increment();
        });

        Thread t2 = new Thread(() -> {
            for(int i=0; i<1000; i++) counter.increment();
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("최종 count = " + counter.getCount());
    }
}

💥 결과:

  • count 는 2000이 나와야 할 것 같지만, 실제로는 1900~1999 사이 랜덤한 값이 나온다.

🧩 왜 이런 일이 생기냐면?

  • count++ 는 사실 한 줄로 보이지만 내부적으로 이렇게 3단계이다 👇

1️⃣ count 값을 메모리에서 읽음
2️⃣ 값에 +1
3️⃣ 다시 메모리에 저장

🚨 근데 두 스레드가 동시에 1~3단계를 수행하면
서로의 변경 내용이 덮여버리는 문제가 생긴다 → 결과값 꼬임


💡 해결 방법: 동기화(Synchronization)

✅ 여러 스레드가 공유 자원(객체, 변수 등) 에 동시에 접근할 때
데이터가 꼬이지 않도록 “한 번에 하나의 스레드만” 접근하도록 잠그는(lock) 기능.

  • 자바는 이런 경쟁 상태를 막기 위해 임계 구역(Critical Section) 을 보호하는 기능을 제공한다.
  • 쉽게 말하면, “이 코드 실행 중에는 다른 스레드는 잠깐 기다려!” 하는 개념이다 🧱
  • synchronized 는 “한 스레드가 자물쇠를 걸고 작업하는 동안, 다른 스레드는 문 앞에서 대기한다.” 🚪🔒

🧠 1️⃣ 사용 형태 2가지

형태설명예시
메서드 동기화메서드 전체에 lock을 걸어 한 번에 하나의 스레드만 실행 가능public synchronized void run()
블록 동기화특정 코드 블록만 잠금 (효율적)synchronized(this) { ... }

🧩 2️⃣ 메서드에 synchronized 걸기

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

⚙️ 3️⃣ synchronized 블록 사용하기

  • 전체 메서드에 잠그는 건 비효율적일 때,
    공유 자원을 접근하는 부분만 잠금 걸면 된다 👇
class Counter {
    private int count = 0;

    public void increment() {
        synchronized(this) {  // 현재 객체에 lock 걸기
            count++;
        }
    }
}
  • this 는 현재 객체,
    즉 같은 Counter 객체를 공유하는 스레드끼리만 lock 경쟁을 함.

🧱 4️⃣ 클래스 단위 잠금 (static)

  • static 메서드는 인스턴스가 아니라 클래스 자체에 잠금을 걸어야 한다.
public static synchronized void printStatic() {
    ...
}

또는

synchronized(MyClass.class) {
    ...
}
  • ✅ 모든 인스턴스가 공유하는 lock이라,
    클래스 전체적으로 한 번에 하나의 스레드만 접근 가능!

🔑 synchronized의 동작 방식

자바는 내부적으로 모니터 락(Monitor Lock) 이라는 개념을 쓴다.

  • 어떤 객체에 synchronized 가 걸리면
    → JVM이 그 객체에 대한 모니터 락(Monitor Lock) 을 획득해야 함

  • 락을 가진 스레드만 코드 실행 가능

  • 끝나면 락을 반납 (lock release)

🚨 주의할 점

문제설명해결 방법
Deadlock(교착 상태)두 스레드가 서로의 락을 기다려서 무한 대기락 순서 동일하게 유지
성능 저하락 걸린 영역이 많으면 병렬성 떨어짐필요한 부분에만 잠금
객체 기준 잠금객체가 다르면 synchronized 무의미같은 객체 공유 시에만 의미 있음

📌 데몬 스레드(deamon thread)

✔️ 다른 일반 스레드의 작업을 돕는 보조적인 역할을 하는 스레드
✔️ 일반 스레드가 종료되면 데몬 스레드는 강제적으로 자동 종료됨

데몬 스레드는 일반 스레드가 모두 종료되면 더는 할 일이 없으므로, 데몬 스레드 역시 자동으로 종료된다.

이러한 데몬 스레드는 백그라운드 작업에서 활용된다.
포그라운드에서 작업이 끝날 때 백그라운드도 같이 종료돼야 하는데 이것을 데몬스레드로 처리한다.

또한 일정 시간마다 자동으로 수행되는 저장 및 화면 갱신, 가비지 컬렉터 등에도 이용되고 있다.

📎 가비지 컬렉터(gabage collector)
: 프로그래머가 동적으로 할당한 메모리 중 더 이상 사용하지 않는 영역을 자동으로 찾아내어 해제해 주는 데몬 스레드


✏️ 데몬 스레드 실행 방법

데몬 스레드의 생성 방법과 실행 방법은 모두 일반 스레드와 같다.

  • void setDaemon(boolean ..): 스레드를 데몬 스레드 또는 일반 스레드로 변경
  • boolean isDaemon(): 스레드가 데몬 스레드인지 확인. 맞으면 true 반환

❗️setDaemon(boolean ..) 은 반드시 start() 메서드를 호출하기 전에 실행되어야 한다. 그렇지 않으면 IllegalThreadStateException 이 발생한다.

✏️ Normal thread

public class NormalThreadTest {
	public static void main(String[] args) {
    
		Thread t = new Thread() {
			public void run() {
				try {
					System.out.println("MyThread 시작");
					Thread.sleep(5000); // 
					System.out.println("MyThread 종료");
				} catch (Exception e) {
				}
			}
		};
		t.start();
		System.out.println("main() 종료");
	}
}
main() 종료
MyThread 시작
MyThread 종료

모든 스레드가 종료되어야 프로그램이 종료된다는 것을 확인 할 수 있다.

✏️ deamon thread

public class DeamonThreadTest {
	public static void main(String[] args) {
		
		Thread t = new Thread() {
			public void run() {
				try {
					System.out.println("MyThread 시작");
					Thread.sleep(5000);
					System.out.println("MyThread 종료");
					
				} catch (Exception e) {}
			}
		};
		// 반드시 start() 호출 전에 사용해야 한다!
        // 데몬스레드
		t.setDaemon(true);
		t.start();
		
		System.out.println("main() 종료");
	}
}
main() 종료
MyThread 시작

일반 스레드와 비교했을 때, 주 스레드가 종료되자 같이 종료되어버렸다.


💡 스레드 스케줄링(thread scheduling)

✔️ 멀티스레드의 순서를 정하는 것

📎 우선순위(priority)방식

: 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링 하는 것
: setPriority() 메소드를 사용하여 우선순위를 설정
: 우선순위는 1에서 10까지 부여할 수 있고 1이 가장 낮고 10이 가장 높다.

✏️ 우선순위 방식_setPriority 활용

// Thread 상속
class SomeThread extends Thread {
	public SomeThread(String name) {
		super(name);
	}
	
	@Override
	public void run() {
		String name = this.getName();
		for(int i = 0; i < 10; i++) {
			System.out.println(name + " is working");
			try {
				Thread.sleep(500);
			} catch (Exception e) {}
		}
	}
}

public class RunningTest {
    // main thread는 우선순위가 NORM_PRIORITY(5) 이다.
	public static void main(String[] args) {
		SomeThread t1 = new SomeThread("A");
		SomeThread t2 = new SomeThread("B");
		SomeThread t3 = new SomeThread("C");
        
        t1.setPriority(Thread.MIN_PRIORITY); // = 1
		t2.setPriority(Thread.NORM_PRIORITY); // = 5
		t3.setPriority(Thread.MAX_PRIORITY); // = 10
		
		t1.start();
		t2.start();
		t3.start();

	}
}
A is working
B is working
C is working
B is working
A is working
C is working
A is working
B is working
C is working
B is working
C is working
A is working
C is working
A is working
B is working
A is working
B is working
C is working
A is working
B is working
C is working
B is working
C is working
A is working
A is working
B is working
C is working
A is working
C is working
B is working

우선순위가 높은 스레드를 무조건 실행하게 하는 것이 아니고 확률이 약간 더 올라간다.

100%가 아니기 때문에 우선순위에 의존해서 선택하지 않는다.
C가 먼저 동작하는 경우는 없지만 아예 없는 것도 아니다.

즉, 우선 순위를 제일 높게 설정한다고 항상 먼저 실행됨을 보장 할 수는 없다.

📎 순환 할당(Round-Robin)방식

시간 할당량(Time Slice)을 정해서 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른 스레드를 실행하는 방식

profile
Fill in my own colorful colors🎨

0개의 댓글