[Thread] 쓰레드

포키·2023년 3월 13일
0

국비과정

목록 보기
44/73

기존 중요 부분 : 자료구조, 알고리즘 - 이제 검색하면 다 나옴
현재 : 운영체제, 소프트웨어공학 <- 중요

쓰레드 <- 이해하기 직접 쓰진 않을 것


Thread (쓰레드)

프로세스란? (Process)

컴퓨터에서 프로세스란 '현재 돌아가고 있는 프로그램'을 말한다.

참고 - 프로세스와 쓰레드의 개념과 구현

  • 초기 컴퓨터는 한 번에 하나의 프로세스만 진행 가능.
    이 한계를 극복하기 위해 나온 것이 '시분할 방식'. 이를 통해 우리는 멀티태스킹을 느낄 수 있다.

참고 - CPU가 멀티태스킹을 하는 방법: 프로세스 VS 스레드

  • o/s에서 프로세스에 자원을 할당한다. (o/s는 cpu, ram 등의 자원을 관리함.)
  • 스케줄링 알고리즘 : 자원을 배분하는 알고리즘, 선점형/비선점형 등 존재. os마다 다르고 우리가 제어할 수 없다.
  • 병렬진행 : 하드웨어적 요소 -> 한 번에 여러 개의 프로세스를 진행 (ex) 듀얼코어)
  • 병행진행 : 소프트웨어적 요소 -> 프로세스를 빠르게 번갈아가며 진행
    배수율과 클럭이 속도를 관장하지 (c.f. 오버클럭) 코어 개수는 속도보다는 성능에 관계됨

쓰레드란? (Thread)

프로세스 내부에 존재하는 '(병행 진행되는) 작업'을 의미한다.

  • 진행중인 여러 프로그램(=프로세스)의 동시진행이 가능 - OS가 자원 할당
    프로그램 내에서 여러 작업(=쓰레드)이 동시진행이 가능 - 가상머신이 자원 할당

  • 프로세스와 마찬가지로 쓰레드에 자원 배분을 우리가 결정할 수 없으나,
    우리는 쓰레드를 이해하고 시나리오를 예측하고 결과를 제어할 수 있어야 한다.

💦 쓰레드 실행 순서 예측의 어려움
thread-0, thread-1, thread-2... 와 같이 복수의 쓰레드가 존재할 때,

  • 다음에 실행될 thread가 무엇일지 알 수 없다. (연속으로 중복되게 뽑힐 수 있음)
  • 실행된 thread가 얼마나 지속될지 알 수 없다. (한 번만에 바뀌기도, 오래 지속되기도?)

thread를 만드는 방법1 - Thread 안에 run() 직접 정의

  • thread를 상속받는 클래스를 만들고, 내부에 run() 메서드 (실제로 수행할 내용)을 작성한다. 그 후 thread를 상속받은 클래스의 객체를 만들고, 그 객체의 start()를 호출한다.
//Thread 클래스를 상속
class ExtendThread extends Thread {
	public void run() {
		// run()을 오버라이딩하여 재정의
		System.out.println("Thread 클래스를 상속 : " + Thread.currentThread().getName());
	}
}

public class ExtendThreadTest {
	public static void main(String[] args) {	// main 또한 thread (main thread)
		ExtendThread et = new ExtendThread();
		// start()를 이용하여 스레드를 시작시킨다.
		// 이후 ExtendThread의 run()이 실행되고, run()이 조욯되면 바로 ExtendThread가 소멸된다.
		et.start();
		// et.start() <- start()의 소유자 et, start()를 실행시키는 주체 thread
		// start()를 통해 새로운 쓰레드를 시작시킨다. (이때부터 병행진행)
		System.out.println("end of main : " + Thread.currentThread().getName());
	}
}
  • 결과 :

따로 이름을 붙이지 않으면 thread-0, thread-1, ... 순으로 쓰레드의 이름이 붙는다.

  • 쓰레드 진행의 동시성 참고 코드
class MyThread extends Thread {
	@Override
	public void run() {
		String name = Thread.currentThread().getName();
		for(int i = 0; i < 10000; i++) {
			System.out.println(name);
		}
	}
}
public class ThreadEx1 {
	public static void main(String[] args) {	// main 또한 thread (main thread)
		MyThread t = new MyThread();
		t.start();
		String name = Thread.currentThread().getName();
		for(int i = 0; i < 10000; i++) {
			System.out.println(name);
		}
	}
}
  • 결과 :
  • main과 thread-0이 번갈아 나옴.
  • 공평하게 나누어가지는 것을 의미하지 x, 누가 먼저 나올지 보장하지 않음.

thread를 만드는 방법2 - Runnable 인터페이스 구현, Thread에 패러미터로 전달

// Runnable 인터페이스를 구현
class RunnableThread implements Runnable {
	// run()을 오버라이딩하여 재정의
	public void run() {
		System.out.println("Runnable 인터페이스를 구현");
	}
}
public class RunnableThreadTest {
	public static void main(String[] args) {
		// r은 쓰레드 아님. 쓰레드가 할 일을 나타내주는 객체.
		RunnableThread r = new RunnableThread();
		
		// Thread 생성자에 RunnableThread 인스턴스를 파라미터로 전달
		Thread t = new Thread(r);
		t.start();
		System.out.println("end of main");
	}
}

Thread에 직접 작성 vs Thread에 Runnable 인터페이스 객체 전달

  • 어느 방법이 낫다고 말하기 어렵지만, 후자는 객체 상속 여지를 남겨둔다는 장점이 있다.

다중 쓰레드 사용의 단점

  • context swiching 비용 (쓰레드 전환을 위해 전환점을 읽고 쓰는 비용) 이 존재
  • 단일 쓰레드보다 언제나 느리다
  • 실제 성능이 아닌 사용자 체감 성능을 위한 기능
class ThreadEx2 extends Thread {
	public void run() {
		for(int i = 0; i < 300; i++) {
			System.out.println("|");
		}
		
		System.out.println(
				"소요시간2 : " + 
				(System.currentTimeMillis() - UsingThreadProcess.startTime)
		);
	}
}
public class UsingThreadProcess {
	static long startTime = 0;
	
	public static void main(String[] args) {
		ThreadEx2 th1 = new ThreadEx2();
		
		startTime = System.currentTimeMillis();
		th1.start();
		for(int i = 0; i < 300; i++) {
			System.out.print("-");
		}
		
		System.out.println(
				"소요시간1 : " + 
				(System.currentTimeMillis() - UsingThreadProcess.startTime)
		);
	}
}

Thread 상태 주기

  • 쓰레드 전환은 문장 단위도 아님!!!!! 문장 중간에서 쓰레드 전환이 일어날 수 있다!!!!!
  • Thread 생성 -> RUNNABLE -> 실행 -> RUNNABLE -> 실행...
  • Thread는 주어진 run() 메서드가 끝나면 TERMINATED로 전환 (종료)
  • TERMINATED된 Thread는 재활용 불가 (새로 만들어 사용해야 함)
  • 모든 Thread가 TERMINATED 되면 프로그램 종료
  • 모든 쓰레드는 callStack이 존재.

    참고 - callStack

스윙에는 EventDispacherThread가 존재. 예외처리를 할 때도 프로그램 돌아감.
스윙 배우기 전에 정말 main만 다룰 때는 예외처리로 넘어가면 프로그램 종료됨.
참고 - [자바] Event-Dispatching Thread
참고 - Java Event Dispatch Thread

Object에도 쓰레드 관련 메서드 존재 -> 자바는 쓰레드를 사용하는 언어다.

참고 - Lifecycle and States of a Thread in Java

프로그램 종료의 기준

  • main()이 끝날 때 (X)
    main을 포함한 모든 쓰레드가 끝날 때 (O)
public class NormalThreadTest {
	public static void main(String[] args) {
		// 스레드 생성
		Thread t = new Thread() {
			public void run() {
				try {
					// 5초간 멈춤
					Thread.sleep(5000);
					// 스레드 종료 메세지
					System.out.println("MyThread 종료");
				} catch (Exception e) {
					// 무시...
				}
			}
		};
		// 스레드 시작
		t.start();
		
		// main 메소드 종료 메시지
		System.out.println("main() 종료");
	}
}
  • 결과 :

DaemonThread

  • 쓰레드에는 두 종류가 있다: 일반 vs 데몬
  • 데몬 쓰레드는 '일반 쓰레드가 모두 종료된 순간' 자신도 따라 종료됨.
public class DaemonThreadTest {
	public static void main(String[] args) {
		//스레드 생성
		Thread t = new Thread() {
			public void run() {
				try {
					// 5초간 멈춤
					Thread.sleep(5000);
					// 스레드 종료 메시지
					System.out.println("MyThread 종료");
				} catch(Exception e) {
					// 무시...
				}
			}
		};
		// 데몬 스레드로 설정... (반드시 start() 호출 전에 사용해야 함!!!)
		t.setDaemon(true);
		// 스레드 시작
		t.start();
		
		// main 메소드 종료 메시지
		System.out.println("main() 종료");
	}
}
  • 쓰레드를 활용하여 하는 작업 중 많은 부분이 백그라운드 작업(일반 유저 눈에 보이지 않는 작업)
    일반 스레드로 포그라운드 작업을 진행한다면, 주요 작업은 모두 종료되었는데 프로그램이 끝나지 않는 경우가 발생함.
  • 예를 들어 가비지 콜렉터
    일반 쓰레드였다면 힙 상에 메모리가 남아있는 한 프로그램은 끝나지 않을 것
    데몬 쓰레드이기 때문에 메인이 종료되는 순간 같이 종료됨

쓰레드 우선순위(Priority)

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(InterruptedException e) {}
		}
	}
}
public class RunningTest {
	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);
		t2.setPriority(Thread.NORM_PRIORITY);
		t3.setPriority(Thread.MAX_PRIORITY);
		
		t1.start();
		t2.start();
		t3.start();
	}
}
  • 우선순위가 높다고 해서 무조건 먼저 선택되는 것은 아니지만,
    우선순위가 높은 쓰레드의 run()이 먼저 끝날 가능성이 높다
  • 우선순위는 절대적으로 보장되는 것은 아님. 그러나 쓰레드들이 길어질수록 결과가 우선순위에 수렴함.
    (실제 상황에서 대부분의 쓰레드는 무한루프처럼 움직임. 목적을 달성하기 전까지 끝나지 않는 식으로, 쓰레드는 재활용이 불가능하기 때문)

main thread의 우선순위는 NORM_PRIORITY(5) 이다.

  • 우선순위는 가급적 바꾸지 않는 것이 좋다.
    우선순위의 변경은 기존 스케줄링 알고리즘을 비트는 요소가 되어, 도리어 어떤 결과가 나올지 보장되지 않게 할 가능성이 매우매우 높다.
  • 기본 우선순위는 스레드가 생성된 스레드의 우선순위를 따라감.
    (ex) main thread에서 만든다면 우선순위는 NORM_PRIORITY(5))

쓰레드 관련 메서드

  • start()
  • run()
  • join()
  • wait()
  • notify(), notifyAll()
  • interrupt()

join()

  • join() : 호출하면 호출한 쓰레드가 실행불가 상태로 전환.
    쓰레드간 실행 순서를 조정해야 할 때 사용. (ex) 전처리-후처리 순서 조정)
public class ThreadJoinTestB {
	public static void main(String[] args) {
		// 스레드 생성
		Thread t = new Thread() {
			public void run() {
				try {
					// 2초간 멈춤
					Thread.sleep(2000);
					// 스레드 종료 메시지
					System.out.println("MyThread 종료");
					// 3초간 멈춤
					Thread.sleep(3000);
				} catch (Exception e) {}
			}
		};
		// 스레드 시작
		t.start();
		try {
			// join 메서드 실행...
			// t 스레드가 종료될 때까지 main 스레드가 기다림. (실행불가 상태로 전환)
			
			t.join();
			// t가 영향을 받는 것이 아니라, t를 실행한 쓰레드가 영향을 받는 경우!!!
			// t를 호출한 쓰레드가 실행불가 상태로 전환됨.
			// 이처럼 메서드 소유 객체가 아니라 메서드 호출 객체가 영향을 받는 케이스가 앞으로 종종 나올 것.
		} catch(InterruptedException e) {
			// InterruptedException : 
			e.printStackTrace();
		}
		// main 종료 메시지
		System.out.println("main() 종료");
	}
}
class ThreadEx13_1 extends Thread {
	public void run() {
		for(int i = 0; i < 300; i++) {
			System.out.print("-");
		}
	} // run()
}
class ThreadEx13_2 extends Thread {
	public void run() {
		for(int i = 0; i < 300; i++) {
			System.out.print("|");
		}
	} // run()
}
public class ThreadEx13 {
	static long startTime = 0;
	public static void main(String[] args) {
		ThreadEx13_1 th1 = new ThreadEx13_1();
		ThreadEx13_2 th2 = new ThreadEx13_2();
		
		th1.start();
		th2.start();
		startTime = System.currentTimeMillis();
		try {
			th1.join();
		} catch(InterruptedException e) {}
		try {
			th2.join();
		} catch (InterruptedException e) {}
		
		System.out.print("소요시간 : " + (System.currentTimeMillis() - ThreadEx13.startTime));
	} // main()
}

동기화

  • 하나의 객체를 두 개의 객체가 공유할 때(공유자원), 두 객체가 보유한 공유자원의 정보값은 서로 같아야 한다.
  • 그러나 두 객체가 동시에 공유자원의 정보(상태)를 변경할 경우, 서로 보유한 공유자원의 정보값이 달라질 수 있다. = 동기화 오류? 동기화 깨짐?

    동기화를 보장하는 (thread-safe한) 객체들
    Vector, HashSet, Hashtable, StringBuffer...

상호배제 - synchronized

  • 상호배제(Mutex, Mutual Exclusion) : 문을 달아 동기화가 깨지는 것을 막는 방법
  • synchronized : lock을 가진 객체에게만 내부 접근을 허용하는 키워드.
  1. method : lock의 주체는 this(메서드 보유객체)
  2. block : lock의 주체를 본인이 결정
  3. static method : lock의 주체는 클래스 객체(메서드 보유한 클래스 객체)
  • 자바의 모든 객체는 lock을 가지고 태어난다. lock은 한 번에 하나의 객체만 빌려갈 수 있다.
  • lock을 가지고 있지 않아 막힌 쓰레드는 BLOCKED 상태가 된다 (Not Runnable, 실행불가 상태)

    대드락DeadLock
    모든 쓰레드가 종료 혹은 실행불가 상태로 정적인 상태로 프로그램이 더이상 진행되지 않는 것. 쓰레드 상태 전이 없음.
    (sleep처럼 기다리면 풀리는 실행불가x)
    라이브락LiveLock
    대드락과 달리 동적인 상태로 프로그램이 더이상 진행되지 않는 것. 쓰레드 상태 전이 존재.
    (쓰레드가 동작하지만 프로그램의 목적에 다가가지 못함.)
    기아 상태
    기다려도 영원히 자원을 받지 못하는 상태.

synchronized 단점

public class SynchVsNotSynch {
	private static final long CALL_COUNT = 100000000L;
	public static void main(String[] args) {
		trial(CALL_COUNT, new NotSynch());
		trial(CALL_COUNT, new Synch());
	}
	private static void trial(long count, Object obj) {
		String msg = obj.toString();
		System.out.println(msg + " : BEGIN");
		long startTime = System.currentTimeMillis();
		for(int i = 0; i < count; i++) {
			obj.toString();
		}
		System.out.println(
				"Elapsed time = " + 
				(System.currentTimeMillis() - startTime) + "ms"
		);
		System.out.println(msg + " : END");
	}
}
class Synch {
	private final String name = "Synch";
	@Override
	public synchronized String toString() {
		return "[" + name + "]";
	}
}
class NotSynch extends Synch {
	private final String name = "NotSynch";
	@Override
	public String toString() {
		return "[" + name + "]";
	}
}

  • 연속적인 작업량이 많고 그것이 빠르게 처리되어야 하는 경우 threadsafe가 되기 어렵다.
  • 스윙 컴포넌트는 기본 threadsafe 아님.
  • 병목현상 발생!
class KeyA {}
class KeyB {}
class Human2 {
	private String name;
	private int age;
	private int birthYear;
	private KeyA keyA = new KeyA();	// lockA
	private KeyB keyB = new KeyB();	// lockB
    
	// keyA의 lock 범위
	public void setName(String name) {	
		synchronized(keyA) {
			try {
				this.name = name;
				System.out.println("name changed");
				Thread.sleep(3000);
			} catch(Exception e) {}
		}
	}
	// keyB의 lock 범위
	public void setAge(int age) {
		synchronized(keyB) {
			try {
				this.age = age;
				System.out.println("age changed");
				Thread.sleep(3000);
			} catch(Exception e) {}
		}
	}
	public void setBirthYear(int birthYear) {
		try {
			this.birthYear = birthYear;
			System.out.println("age changed");
			Thread.sleep(3000);
		} catch(Exception e) {}
	}
    // keyB lock 여기까지
}
public class SynchronizedBlockCase {
	public static void main(String[] args) {
		final Human2 h = new Human2();
		Thread t1 = new Thread() {
			@Override
			public void run() {
				h.setName("춘식");
			}
		};
		t1.setName("first");
		
		Thread t2 = new Thread() {
			@Override
			public void run() {
				h.setAge(10);
			}
		};
		t2.setName("second");
		
        Thread t3 = new Thread() {
			@Override
			public void run() {
				h.setBirthYear(2013);
			}
		};
		t3.setName("third");
        
		t1.start();
		t2.start();
        t3.start();
		// t1과 t2는 관계없이 자유롭게 진행되지만,
        // t2와 t3는 서로 진행에 제약을 받는다.
	}
}
  • synchronized method 대비 synchronized block의 장점
  1. lock검사 대상을 직접 설정할 수 있다. (서로 다른 멤버변수에 대한 연산)
  2. 병목구간을 최소화할 수 있다. (method보다 더 작은 범위 지정 가능. 꼭 필요한 부분만 동기화 구간 지정.)
  • 하지만 그만큼 실수할 가능성은 올라간다. 늘 조심하고, 자신 없으면 속도는 포기하고 안전을 택하자.
  • getter, setter도 같이 synchronized로 묶어야 한다.
    (값을 가져올 때, 값을 바꿀 때, 같은 대상이라면 묶여야 함.)

wait()

  • wait() : 객체의 락을 가진 상태로 synchronized 범위 안에 들어간 쓰레드에게, 락을 풀고 실행불가 상태로 전환할 것을 명령.
    실행불가 쓰레드는 nofity() 혹은 notifyAll()을 통해 다시 실행(대기)상태로 바꾼다.

    notify()는 기아상태를 부를 수 있어 잘 사용하지 않는다.

wait() 예시 - '생산자-소비자 문제'

class Producer2 extends Thread {
	private MyBox2 box;
	public Producer2(MyBox2 box) {
		this.box = box;
	}
	@Override
	public void run() {
		for(int i = 0; i < 20; i++) {
			box.put(i);
			try {
				sleep(100);
			} catch(InterruptedException e) {}
		}
	}
}
class Consumer2 extends Thread {
	private MyBox2 box;
	public Consumer2(MyBox2 c) {
		box = c;
	}
	@Override
	public void run() {
		int value = 0;
		for(int i = 0; i < 10; i++) {
			box.get();
			try {
				sleep(100);
			} catch(InterruptedException e) {}
		}
	}
}
class MyBox2 {
	private int contents;
	private boolean isEmpty = true;
	public synchronized int get() {
		while(isEmpty) {
			try {
				wait();	// -> 실행불가열에 넣기
			} catch(InterruptedException e) {}
		}
		isEmpty = !isEmpty;
		notifyAll();
		System.out.println(
				Thread.currentThread().getName() + " : 소비 " + contents
		);
		return contents;
	}
	public synchronized void put(int value) {
		while(!isEmpty) {
			try {
				wait();
			} catch (InterruptedException e) {}
		}
		contents = value;
		System.out.println(
				Thread.currentThread().getName() + " : 생산 " + value
		);
		isEmpty = !isEmpty;
		notifyAll();
	}
}
public class ProducerConsumer2 {
	public static void main(String[] args) {
		MyBox2 c = new MyBox2();
		Producer2 p1 = new Producer2(c);
		Consumer2 c1 = new Consumer2(c);
		Consumer2 c2 = new Consumer2(c);
		p1.start();
		c1.start();
		c2.start();
	}
}

interrupt()

  • If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int) methods of the Objectclass, or of the join(), join(long), join(long, int), sleep(long), or sleep(long, int),methods of this class, then its interrupt status will be cleared and itwill receive an InterruptedException.
  • 실행불가 상태에 빠진 쓰레드를 Runnable 상태로 전환하는 메서드
  • 실행불가 상태에 빠지지 않는 경우 영향을 끼치지 않음
  • 실행불가 상태에서 빠져나온 쓰레드는 InterruptedException을 발생시키고 예외처리문으로 넘어간다.
  • interrupt()를 실행한 쓰레드는 interrupted = true 값을 가지며, 실행불가 상태를 탈출한 쓰레드는 초기화되어 interrupted = false 값을 가짐
  • 부루마블의 무인도 탈출권 같은 느낌!!!
  • 예측하지 못한 결과가 발생할 가능성이 높다! notifyAll()이 더 안전한 방법.
    (예측할 수 없으므로 실행불가 상태인 쓰레드를 적당한 타이밍에 깨우기 위한 메서드로 쓰지 않는 게 좋다)
class Dummy {
	public synchronized void todo() {
		try {
			System.out.println("start...");
			wait();
		} catch (InterruptedException e) {
			System.out.println("interrupted!!!");
		}
	}
}
public class InterruptEx {
	public static void main(String[] args) {
		final Dummy d = new Dummy();
		Thread t1 = new Thread() {
			@Override
			public void run() {
				d.todo();
				System.out.println("I'm dead");
			}
		};
		t1.start();
		t1.interrupt();
	}
}
  • interrupt() : 해당 쓰레드 객체에게 인터럽트를 건다.
  • isInterrupted() : 해당 쓰레드 객체가 인터럽트를 받았는지 확인한다.
  • Thread.interrupted() : 현재 쓰레드(static)가 인터럽트 받았는지 확인하고 상태를 초기화한다.
public class InterruptEx2 {
	public static void main(String[] args) {
		Thread t1 = new Thread() {
			@Override
			public void run() {
				long count = 0;
				while(!isInterrupted()) {
					// 뭔가 한다.
					count++;
					/*
					try {
						Thread.sleep(2000);
					} catch(InterruptedException e) {
						System.out.println("초기화");
					}
					 */
				}
				System.out.println("interrupted -> count = " + count);
				System.out.println("isInterrupted : " + isInterrupted());
			}
		};
		t1.start();
		try {
			Thread.sleep(1000);
		} catch (Exception e) {}
		
		t1.interrupt();
		// interrupt 타이밍을 잡을 수 없는 경우
		// while문 안에 
		// 1. 긴 명령이 들어간 경우 (조건을 확인하기 전까지 중단되지 않는다.)
		// 2. wait, join, sleep로 인한 예외처리부가 존재하는 경우 (조건 확인 전에 interrupted 값이 초기화된다.)
		// -> try ~ catch ~ 문을 while문 바깥에 두기 -> 하지만 어쨌든 예외 발생하고 중단되는 타이밍을 잘 인지해야 한다는 점.
	}
}

MySQL

  • 설치 과정

    root : db 전체관리자 (최고관리자)

  • mysql cmd 로그인 방법

    mysql -uroot -p : 'mysql에 root라는 유저에 password를 통해 로그인하겠다'
profile
welcome

0개의 댓글