원지의 개발
article thumbnail
728x90

 

Thread

  • 프로그램에서 가장 중요한 개념 중의 하나
  • 실제 생성해서 사용하는 경우는 안드로이드나 자바 애플리케이션을 만들 때 이고,
    Web Programming 에서는 직접 생성해서 사용하는 경우가 드뭄

작업 단위 (Process / Thread)

  • Process (공장): 실행 중인 프로그램, 자원(메모리, cpu 등) + thread
    프로세서를 할당받아서 실행되는 것
    한 번 실행되면 자신의 작업이 종료될 때 까지 제어권을 다른 프로세스에게 넘기지 않고 계속 수행
    절대로 멈추지 않음
  • Thread (일꾼): Process를 작게 나누어서 작업을 수행하는 단위
    Thread는 단독으로 실행할 수 없고 Process 안에서 실행되야 함
    자신의 작업 도중 쉬는 시간이 생기거나 일정한 시간이 지나면 다른 스레드에게 제어권을 양도할 수 있음
    동시에 처리하는 것은 아님 (그건 Parallel), 다른 Thread가 동시에 수행하는 것은 가능

Thread Programming 할 때 반드시 알아야 될 사항

  • 하나의 스레드가 사용 중인 자원은 다른 스레드가 수정하면 안됨
    Mutual Exclusion(상호 배제), Synchronus(동기화)
  • 생상자 와 소비자 문제
    소비자는 생상자가 물건을 생성해 주어야만 작업을 수행
  • Dead Lock (교착 상태)
    결코 발생할 수 없는 사건을 무한정 기다리는 것
    여러개 같이 작업할 때 무한 대기 상태가 되는 것
  • try ~ catch
    Thread에서 발생한 예외 처리 하기

Thread 생성

## Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable 인터페이스를 구현하는 방법이 일반적 - run()을 오버라이딩 하여 그 안에 작업 내용을 적으면 됨

1. Thread 클래스로부터 상속받는 클래스를 만들고 public void run 이라는 메서드에 스레드로 수행할 내용을 작성한 후 인스턴스를 만들고 start메서드를 호출 - run 메서드의 내용을 수행 (run메서드 호출하면 안됨, 스레드가 아닌 프로세스처럼 동작되므로)

2. Runnable 인터페이스로부터 상속받는 클래스를 만들고 public void run 이라는 메서드에 스레드로 수행할 내용을 작성한 후 Thread 클래스의 인스턴스를 만들 때 생성자에 생성한 클래스의 인스턴스를 대입하고 Thread 클래스의 인스턴스가 start 메서드를 호출하면 됨

3. Callable 인터페이스를 구현한 클래스를 이용해서도 생성 가능

Thread 실행

  • 생성 후 start() 호출해야만 실행
  • 호출되었다고 바로 실행되는 것이 아니라 일단 실행 대기 상태
  • 실행 순서는 OS의 스케줄러가 작성한 스케줄에 의해 결정 (임의로), 어떤 순서에 의해 동시성으로 실행할 것인가?
  • 한번 종료된 쓰레드는 다시 실행할 수 없음, 두 번 호출하면 IllegalThreadStateException 발생
  • 하나의 쓰레드에 대해 start()가 한 번만 호출 될 수 있으므로 쓰레드 작업을 한 번 더 수행해야 한다면 새로운 쓰레드를 생성한 후 start() 호출해야 함
run() 메서드를 작성했는데 왜 start()를 호출 하지?
- run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메서드 호출
- start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택(call stack)을 생성한 후 run() 호출
▶ 모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 함
1. main 메서드에서 쓰레드의 start() 호출
2. start()는 새로운 쓰레드 생성, 작업하는데 사용될 호출 스택 생성
3. 호출 스택에 run() 호출되어 쓰레드가 독립된 공간에서 작업 수행
4. 호출 스택이 2개 이므로 스케줄러가 정한 순서에 의해 번갈아 가면서 실행

실습

더보기

ThreadCreate - 한번에 보기

package chapter13;

public class ThreadCreate {
	public static void main(String[] args) {
		ThreadCreate_1 t1 = new ThreadCreate_1();
		
		Runnable r = new ThreadCreate_2();
		Thread t2 = new Thread(r);
		//Thread t2 = new Thread(new ThreadCreate_2()); 한줄로
		
		t1.start();
		t2.start();
	}
} //end main

class ThreadCreate_1 extends Thread {
	@Override
	public void run() {
		for(int i = 0; i < 5; i++) {
			System.out.println(getName());
		}
	}
} //end ThreadCreate_1

class ThreadCreate_2 implements Runnable {

	@Override
	public void run() {
		for(int i = 0; i < 5; i++) {
			System.out.println(Thread.currentThread().getName());
			//currentThread() - 현재 실행중인 쓰레드의 참조 반환
			//getName() - 쓰레드의 이름 반환
            //자동적으로 이름은 Thread-n(숫자) 설정
		}
	}
} //end ThreadCreate_2
-----------------------------------------------------------------------
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1

ThreadCreate - 1. Thread 클래스로부터 상속받는 클래스 생성

package _api.thread;

//Thread 클래스로부터 상속받는 클래스를 생성
class ThreadEx extends Thread {
	//public void run 이라는 오버라이딩
	@Override
	//위의 어노테이션은 상위 클래스 나 인터페이스에서 제공하는 메서드가 아닌 경우
	//에러를 발생시켜 주는 어노테이션임
	public void run() {
		//스레드로 수행할 내용
		//1초마다 Thread 클래스라는 문장을 10번 실행
		for(int i = 0; i < 10; i++) {
			try {
				Thread.sleep(1000); //try, catch 예외 안에 작성
				System.out.println("Thread 클래스");
			}catch(Exception e) {
				System.out.println(e.getLocalizedMessage());
			}
		}
	}
	
}

public class ThreadCreate {

	public static void main(String[] args) {
		//Thread 클래스로부터 상속받은 클래스를 이용해서 스레드를 생성하고 실행
		ThreadEx th = new ThreadEx();
		//start를 호출하면 run 메서드의 내용을 수행
		th.start();
		
	}
}
  • run메서드 호출하면 Thread가 아닌 Process처럼 동작되므로 start() 호출

ThreadCreate - 2. Runnable 인터페이스로부터 상속받는 클래스

                          3. Runnable 인터페이스 - Anonymous Class를 이용

                          4. Runnable 인터페이스 - 람다를 이용해서 작성

package _api.thread;

//Runnable 인터페이스를 구현한 클래스를 생성
class RunnableImpl implements Runnable {
	@Override
	public void run() {
		for(int i = 0; i < 10; i++) {
			try {
				Thread.sleep(1000); //try, catch 예외 안에 작성
				System.out.println("Runnable 인터페이스");
			}catch(Exception e) {
				System.out.println(e.getLocalizedMessage());
			}
		}
	}
	
}

public class ThreadCreate {

	public static void main(String[] args) {
		
		//Runnable 인터페이스를 implements 클래스를 이용해서 스레드를 생성하고 실행
		Thread th2 = new Thread(new RunnableImpl());
		th2.start();
		
		//Runnable 인터페이스를 Anonymous Class를 이용해서 사용
		Thread th3 = new Thread(new Runnable() {
			@Override
			public void run() {
				for(int i = 0; i < 10; i++) {
					try {
						Thread.sleep(1000); //try, catch 예외 안에 작성
						System.out.println("Anonymous 활용");
					}catch(Exception e) {
						System.out.println(e.getLocalizedMessage());
					}
				}
			}
		});
		th3.start();

		//Runnable 인터페이스는 public void run 메서드 1개만 소유
		//람다를 이용해서 작성하는 것도 가능
		Thread th4 = new Thread(() -> {
			for(int i = 0; i < 10; i++) {
				try {
					Thread.sleep(1000); //try, catch 예외 안에 작성
					System.out.println("Lambda 활용");
				}catch(Exception e) {
					System.out.println(e.getLocalizedMessage());
				}
			}
		});
		th4.start();
		
	}
}

Thread 종류

1. Main Thread

  • main 메서드의 코드를 수행하는 쓰레드
  • 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램 종료
  • non-deamon thread

2. Daemon Thread

  • 다른 스레드가 수행 중이 아니면 자동으로 종료되는 스레드
  • 스레드의 역할을 도와주는 보조적인 목적으로 사용
  • start 하기 전에 setDaemon 메서드를 호출하면 되는데 true 값을 설정해주면 됨
  • ex) MMORPG에서 사냥터에 사람이 없으면 몬스터 생성 중지하는 것
    ex) 가비지 컬렉터, 워드프로세서의 자동저장, 화면자동갱신 등
boolean isDaemon() //데몬스레드인지 확인
void setDaemon(boolean on) //스레드 → 데몬 스레드 or 사용자 스레드오 변경, on값을 true로 지정하면 데몬 스레드가 됨
  • 스레드 생성한 다음 실행하기 전에 setDaemon(true) 호출하면 됨
  • 데몬 스레드가 생성한 스레드는 자동적으로 데몬 스레드가 됨
더보기

DaemonThread_1

package _api.thread;

public class DaemonThread {

	public static void main(String[] args) {
		//1부터 10까지 1초씩 딜레이하면서 출력해주는 스레드
		Thread th = new Thread(() -> {
			for(int i = 1; i <= 10; i++) {
				try {
					Thread.sleep(1000);
					System.out.println(i);
				}catch(Exception e) {
					System.out.println(e.getLocalizedMessage());
				}
			}
		});
		//데몬 스레드로 설정 - 다른 작업이 없으면 자동을 종료
		th.setDaemon(true); //setDaemon(true) 1,2,메인 종료 끝
		th.start(); //위 줄 없으면 무조건 본인 작업 끝마침

		try {
			Thread.sleep(3000);
			System.out.println("메인 종료");
		}catch(Exception e) {
			System.out.println(e.getLocalizedMessage());
		}
		
	}
}

주의점은 스레드를 실행하는 start() 메소드가 호출되고 나서 setDaemon(true)를 호출하면 IllegalThreadStateException이 발생하기 때문에 start() 메소드 호출 전에 setDaemon(true)를 호출해야 함

출처: https://widevery.tistory.com/32 [Everything:티스토리]

 

DaemonThread_2 - 1초마다 카운트 하면서 5초에 자동 저장

package chapter13;

public class DaemonThread implements Runnable{
	static boolean autoSave = false;
	
	public static void main(String[] args) {
		Thread t = new Thread(new DaemonThread());
		t.setDaemon(true); //데몬 스레드 지정
		t.start();
		
		//main 스레드 - 1부터 10까지 카운트하는 루프를 실행
		for(int i = 1; i <= 10; i++) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) { }
				System.out.println(i);
				if(i == 5) autoSave = true;
		}
	} //end main

	//daemon 스레드
	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(3 * 1000); //3초마다 변수 autoSave 값을 인하여 autoSave() 메서드 호출
			} catch (InterruptedException e) { }
				//autoSave값이 true이면 autoSave 호출
				if(autoSave) autoSave();
		}
	} //end run
	
	public void autoSave() {
		System.out.println("작업파일이 자동저장 되었습니다.");
	}
}
--------------------------------------------------------------------------------------------
1
2
3
4
5
작업파일이 자동저장 되었습니다.
6
7
8
작업파일이 자동저장 되었습니다.
9
10
  • Thread의 매개값으로 Runnable 구현한 인스턴스를 넣고, 데몬 스레드 설정 후 시작(3초부터 시작됨) - main 스레드 시작(1초부터 시작됨)
  • main 스레드에서는 1부터 10까지 카운트 다운
  • 1초마다 i를 출력하고, i가 5가 되면 autoSave를 true로 변환
  • daemon 스레드에서는 무한루프로 3초마다 autoSave값을 확인,
    5초가 될 때 main에서 true로 변환하므로 autoSave() 메서드를 호출하여 "작업파일 자동저장 문구" 출력
    ▶ daemon thread는 5초 이후부터 autoSave = true가 되므로 그때부터 문구 출력

Multi Thread

  • 2개 이상의 Thread가 수행중인 상황
  • 대부분의 경우 (99.9% 이상)

I/O 블락킹

  • 입출력 처리를 위해 기다리는 것
  • 두 쓰레드가 서로 다른 자원을 사용하는 경우 멀티 쓰레드 사용
I/O: 외부기기와의 입출력을 필요로 하는 경우, 시간이 오래 걸리는 작업
       (파일 입출력, 네트워크 입출력, 화면 입출력)
연산: 시간이 짧게 걸리는 작업

I/O가 먼저 실행되면 기다리는데 너무 오래 걸리니까 연산 먼저 실행해야 함
시간이 오래 걸리는 작업은 Thread 나 비동기(순서대로 하지 않는 것) 필수

Thread의 동시성, 병렬성

  • 동시성(Concurrency): 하나의 코어에서 멀티 스레드가 번갈아가며 실행
  • 병렬성(Parallelism): 멀티 코어에서 개별 스레드를 동시에 실행하는 성질

  • 스케줄링: 스레드의 개수가 코어 수보다 많을 경우, 어떤 순서에 의해 동시성으로 실행할 것인가
  • 우선순위(Priority) 방식: 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링
  • 순환 할당(Round-Robin) 방식: 시간 할당량을 정해서 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른 스레드를 실행하는 방식, 자바 가상기계에 의해서 정해지기 때문에 코드로 제어할 수 없음

Thread Pirority(우선 순위) - 추가 / 먼저 / 자주

  • 작업 중요도에 따라 우선순위를 다르게 하여 더 많은 작업 시간을 갖게 할 수 있음
  • 여러 개의 스레드가 수행될 때 어떤 스레드가 먼저 되고 어떤 스레드가 나중에 수행될지는 알 수 없음
  • 우선 순위를 설정하면 확률적으로 먼저 또는 자주 실행되도록 할 수 있음
setPriority(int priority) 메서드 이용
매개변수가 정수이지만 아무 정수나 사용하는 것은 안되고 제약이 있음
일반 정수를 사용해도 에러는 발생하지 않지만 되도록이면 Thread.MAX_PRIORITY, NORMAL, MIN의 상수를 사용하는 것을 권장
void setPriority(int newPriority) //우선순위를 지정한 값으로 변경
int getPriority() //우선순위를 반환

public static final int MAX_PRIORITY = 10 //최대 우선순위
public static final int MIN_PRIORITY = 1 //최소 우선순위
public static final int NORM_PRIORITY = 5 //기본 우선순위
더보기

실습

package chapter13;

public class ThreadPriority {
	public static void main(String[] args) {
		ThreadPriority_1 th1 = new ThreadPriority_1();
		ThreadPriority_2 th2 = new ThreadPriority_2();
		
		th2.setPriority(7);
		
		System.out.println("Priority of th1(-) : " + th1.getPriority());
		System.out.println("Priority of th2(|) : " + th2.getPriority());
		
		th1.start();
		th2.start();
	}
} //end main

class ThreadPriority_1 extends Thread {
	@Override
	public void run() {
		for(int i = 0; i < 300; i++) {
			System.out.print("-");
			for(int x = 0; x < 100000000; x++); //시간 지연용 for문
		}
	}
} //end ThreadPriority_1

class ThreadPriority_2 extends Thread {

	@Override
	public void run() {
		for(int i = 0; i < 300; i++) {
			System.out.print("|");
			for(int x = 0; x < 100000000; x++);
		}
	}
} //end ThreadPriority_2
----------------------------------------------------------------------------------
Priority of th1(-) : 5
Priority of th2(|) : 7
-|-|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||--------------------
----------------------------------------------------------------------------------
-||||||||||||||||----|||||||-|||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||--------||||||||||||||||||||||--
------------------------||||||||||------------------------------------------------
----------------------------------------------------------------------------------
--------------------------

Thread Group

  • 여러개의 Thread 관리
  • 관련된 스레드들은 하나의 그룹으로 묶어서 사용하기 위한 개념
  • 스레드 그룹을 지정하지 않고 생성하면 자동적으로 main 스레드 그룹에 속함
  • 자신을 생성한 스레드(부모 스레드)의 그룹과 우선순위를 상속 받음
  • ThreaGroup 이라는 클래스를 제공하지만 몇 몇 메서드가 제대로 동작하지 않는 문제 때문에
    대부분의 경우 배열이나 List를 이용해서 구현하는 것을 권장
  • 스레드 그룹에 다른 스레드 그룹을 포함시킬 수 있음

자바 어플리케이션이 실행되면,
JVM은 main과 system이라는 스레드 그룹을 만들고
JVM 운영에 필요한 스레드들을 생성해서 이 스레드 그룹에 포함시킴
main 메서드 수행하는 main이라는 이름의 스레드 - main스레드 그룹
가비지컬렉션을 수행하는 Finalizer 스레드 - system 스레드 그룹
ThreadGroup getThreadGroup() //스레드 자신이 속한 스레드 그룹 반환
void uncaughtException(Thread t, Throwable e)
//처리되지 않은 예외에 의해 스레드 그룹의 스레드가 실행종료 되었을 때,
//JVM에 의해 이 메서드가 자동적으로 호출

Thread 종료

  • run 메서드 종료
  • main 스레드가 종료되어도 다른 스레드가 존재하면 끝난 것은 아님
  • 스레드의 interrupt 메서드를 호출하고 스레드의 run 메서드 안에서 InterruptedException이 발생하면 run 메서드를 종료하도록 만들어서 강제 종료하도록 할 수 있음
  • Deamon Thread를 Daemon이 아닌 다른 스레드가 존재하지 않으면 자동으로 종료

Thread 상태

  • static이 붙은 메서드는 자기 자신을 제어함
  • resume, suspend, stop 메서드는 deprecated

Thread 실행 제어 메서드

  • void join(long millis, int nanos) : 지정된 시간동안 스레드를 수행하고 다른 스레드에게 제어권을 넘기는 메서드
  • void suspend() : 스데르르 일시정지시키는 메서드로 resume 메서드로 다시 시작
  • void resume() : suspend 된 스레드를 다시 시작
  • void yield() : 다른 스레드에게 제어권을 넘겨주는 메서드

1. sleep (long msec, [long nano])

[long nano]는 있을수도 있고 없을수도 있음

  • msec 밀리초(1/1000) 동안 현재 스레드를 중지
  • nano를 입력하면 msec 밀리초 + nano 나노초 만큼 대기
  • 시간이 다 되거나 interrupt()가 호출되거나 InterrupedException이 발생되면 잠에서 깨거나 실행대기 상태가 됨
  • 이 메서드를 사용할 때는 InterrupedException 을 처리해주어야 함
    항상 try ~ catch문으로 예외처리 필수
  • 실습을 할 때 이 메서드를 사용하는 이유는 일정 시간 이상 대기를 해야만 다른 스레드에게 제어권이 이동되기 때문
  • 실제 시간의 의미는 거의 없음
//매번 예외처리 하는 것은 번거롭기 때문에 메서드 만들어서 사용
void delay(long msec) {
	try {
    	Thread.sleep(msec);
   	} catch(InterruptedException e) {} //Exception의 자손, 필수 예외 처리
    //예외와 try~catch를 이용해서 잠자는 상태에서 벗어나게 하기 위해 사용하므로 아무것도 쓰지 않아도 됨
}
  • 특정 스레드를 지정해서 멈추게 하는 것은 불가능 (static, 자기 자신만)
package chapter13;

public class SleepOfThread {
	public static void main(String[] args) {
		SleepOfThread_1 th1 = new SleepOfThread_1();
		SleepOfThread_2 th2 = new SleepOfThread_2();
		
		th1.start();
		th2.start();
		
		try {
        	//main 스레드에서 실행되고 있기 때문에 th1이 sleep되는 게 아님
			//th1.sleep(2000); 오류는 없지만 오해할 수 있음
            Thread.sleep(2000);
		} catch (InterruptedException e) {}
		
		System.out.print("<<main 종료>>");
	}// end main
}

class SleepOfThread_1 extends Thread {
	@Override
	public void run() {
		for(int i = 0; i < 300; i++) {
			System.out.print("-");
		}
		System.out.print("<<th1 종료>>");
	}
} //end SleepOfThread_1

class SleepOfThread_2 extends Thread {
	@Override
	public void run() {
		for(int i = 0; i < 300; i++) {
			System.out.print("|");
		}
		System.out.print("<<th2 종료>>");
	}
} //end SleepOfThread_2
---------------------------------------------------------------------------------------
------------||||||||||||||||||||||-----------------------------------------------------
------------------------------------------------------------------------------------|||
|||||||------|||||||||||||||||||||--------------------------||-------------------------
----------------------------------------------||||||--------------|||||||||||||||------
----------------------------||||||<<th1 종료>>|||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||<<th2 종료>><<main 종료>>

2. interrup

  • 대기 상태(sleep, join, wait) 스레드를 실행대기 상태로 만듦
public static void main(String[] args) {
	ThreadEx th1 = new ThreadEx();
    th1.start();
    	...
    th1.interrupt(); //interrupted 상태를 false → true 변경
    	...
    System.out.println("isInterrupted() : " + th1.isInterrupted()); //현재 상태인 true 반환
	//static boolean interrupted() : 현재 상태를 반환 후, false로 변경, 누가 interrupted를 호출했는지 알 수 있음
}
package chapter13;

import javax.swing.JOptionPane;

public class InterruptOfThread {
	public static void main(String[] args) throws Exception{
		InterruptOfThread_1 th1 = new InterruptOfThread_1();
		th1.start();
		
		String input = JOptionPane.showInputDialog("아무값이나 입력하세요.");
		System.out.println("입력하신 값은 " + input + "입니다.");
		th1.interrupt();
		System.out.println("isInterrupted() : " + th1.isInterrupted()); //true
	}
}

class InterruptOfThread_1 extends Thread {
	@Override
	public void run() {
		int i = 10;
		
		while(i != 0 && !isInterrupted()) {
			System.out.println(i--);
			for(long x = 0; x < 2500000000L; x++); //시간 지연
		}
		System.out.println("카운트가 종료되었습니다.");
	}
}
-----------------------------------------------------------------------------------
10
9
8
7
6
5
4
3
2
1
카운트가 종료되었습니다.
입력하신 값은 132입니다.
isInterrupted() : true

3. suspend, resume, stop

  • deprecated
  • volatile - 자주 바뀌니까 복사본 쓰지 말고, 원본 가져와서 써라

4. join, yield

  • join() - 지정된 시간동안 특정 스레드가 작업하는 것을 기다림, 예외처리 필수
  • yiedl() - static, 자신에게 주어진 실행 시간을 다음 차례의 스레드에게 양보

작업 시간 확인

package chapter13;

public class JoinOfThread {
	static long startTime = 0;
	
	public static void main(String[] args) {
		JoinOfThread_1 th1 = new JoinOfThread_1();
		JoinOfThread_2 th2 = new JoinOfThread_2();
		
		th1.start();
		th2.start();
		startTime = System.currentTimeMillis(); //시작 시간
		
		try {
			th1.join(); //main스레드가 th1의 작업이 끝날 때까지 기다림
			th2.join(); //main스레드가 th2의 작업이 끝날 때까지 기다림
		} catch (InterruptedException e) {	}
		
		System.out.print("소요시간: " + (System.currentTimeMillis() - JoinOfThread.startTime)); //종료 시간
	}
}

class JoinOfThread_1 extends Thread {
	@Override
	public void run() {
		for(int i = 0; i < 300; i++) {
			System.out.print(new String("-"));
		}
	}
} //end JoinOfThread_1

class JoinOfThread_2 extends Thread {
	@Override
	public void run() {
		for(int i = 0; i < 300; i++) {
			System.out.print(new String("|"));
		}
	}
} //end JoinOfThread_2
---------------------------------------------------------------------------------------------
---------||||||||||||||||||||------------------||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||-----------------------------------
-------------------|||||||||||||||||||||||---------------------------------------------------
---------------------------|||--|||--||||||||------------------||||||||||||||||||--||||||----
--|||----------|||||||||||||||-----------------||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||-|||--||||||---------------||||||||||||||||||||||||||||||------------------------
------------------------------------------소요시간: 8

Synchronization

  • 진행중인 작업이 다른 스레드에게 간섭받지 않게 하려면 동기화 필요
  • 동기화하려면 간섭받지 않아야 하는 문장들은 '임계 영역(critical section)'으로 설정
  • 임계 영역은 최소화 해야함
  • 임계영역은 락을 얻은 단 하나의 스레드만 출입가능 (객체1개에 락 1개)

설정

1. 메서드 전체 - 메서드에 synchronized

public synchronized void withdraw(int money) {
	if(balance >= money) {
    	try {
        	Thread.sleep(1000);
        } catch(Exception e) { }
        balance -= money;
    }
}

2. 특정 영역 - 영역에 synchronized(객체의 참조변수)를 넣고 {} 묶기

public void withdraw(int money) {
	synchronized(this) {
        if(balance >= money) {
            try {
                Thread.sleep(1000);
            } catch(Exception e) { }
            balance -= money;
        }
    } //end critical section
}
더보기
package chapter13;

public class SynchronizedOfThread {
	public static void main(String[] args) {
		Runnable r = new SynchronizedOfThread_1();
		new Thread(r).start();
		new Thread(r).start();
	}
}

class Account {
	private int balance = 1000; //private으로 해야 동기화 의미 있음
	
	public int getBalance() {
		return balance;
	}
	
	public synchronized void withdraw(int money) {
		if(balance >= money) {
			try {
				Thread.sleep(1000); } catch (InterruptedException e) { }
			balance -= money;
		}
	}
} //end Account

class SynchronizedOfThread_1 implements Runnable {
	Account acc = new Account();
	
	@Override
	public void run() {
		while(acc.getBalance() > 0) {
			//100, 200, 300 중 한 값을 임의로 선택해서 출금(withdraw)
			int money = (int)(Math.random() * 3 + 1) * 100;
			acc.withdraw(money);
			System.out.println("balance: " + acc.getBalance());
		}
	}
}
------------------------------------------------------------------------
balance: 900
balance: 600
balance: 400
balance: 100
balance: 0
balance: 0

1. Mutual Exception (상호 배제)

  • 하나의 스레드가 사용 중인 공유 자원을 다른 스레드가 수정하면 안됨 (보는 건 상관X)
  • Listener 객체와 Callback 함수 사용: 어떤 상태가 끝나는지 확인해야 해서

→ Lock 과 Synchronized 로 해결

1. 공유 자원을 여러 개의 스레드가 동시에 사용했을 때 문제

  • synchronized(공유객체)() 사용
더보기

ShareData implements Runnable - 공유 자원으로 사용할 데이터 클래스

package _api.thread.multithread;

//자원을 가지고 연산을 하는 스레드에 사용할 클래스
public class ShareData implements Runnable{
	//연산 결과를 저장할 속성
	private int result;
	//연산에 사용할 인덱스
	private int idx;
	
	//result 의 getter 메서드
	public int getResult() {
		return result;
	}
	
	@Override
	public void run() {
		try{
			for(int i = 0; i < 10; i++) {
				// synchronized가 없으면 th1, th2가 있을 때 이상한 결과가 출력됨
                synchronized(this) {
					idx++;
					Thread.sleep(10); //없거나 너무 작으면 0,1같은 작은 숫자가 나옴
					result = result + idx;
				}
			}
			
		}catch(Exception e) {
			System.out.println(e.getLocalizedMessage());
		}
		
	}

}

MutexMain - 공유 자원을 사용하는 실행 클래스

package _api.thread.multithread;

public class MutexMain {

	public static void main(String[] args) {
		//Runnable 인터페이스로부터 상속받은 클래스
		ShareData shareData = new ShareData();
		//스레드 생성
		Thread th1 = new Thread(shareData);
		th1.start();
		//하나 했을때는 제대로 결과가 나왔는데 두개하니 잘못된 결과가 나옴
		//for문이 두 개 있는 것과 같음
        Thread th2 = new Thread(shareData);
        th2.start();
		
		try{
			//30초 대기 - 앞의 작업이 스데르도 동작하기 때문에 작업이 끝날때까지 대기하고 결과를 출력
			Thread.sleep(3000); 
			System.out.println(shareData.getResult());
		}catch(Exception e) {
			System.out.println(e.getLocalizedMessage());
		}
	}
}
  • (synchronized를 안하고) 실행을 하면 이상한 결과가 출력
  • ShareData 인스턴스를 1개만 만들어서 공유하는데 하나의 작업이 완료되기 전에 다른 스레드가 공유 자원을 수정하기 때문에 발생하는 문제
  • 해결방법
    한번에 실행되어야 하는 코드를 가진 영역을 찾고
    메서드에 synchronized를 붙이던가 코드 영역을 synchronized(공유객체)()로 묶어주면 됨
    이렇게 묶어주면 묶인 영역의 코드는 동시에 수정할 수 없음
    되도록이면 synchronized 메서드를 만드는 것보다는 블럭을 만드는 것을 권장
    메서드를 묶게 되면 영역이 커져서 공유도가 떨어지기 때문

ShareData 클래스를 수정해서 다시 실행

  • 최근에는 ReentrantLock이라는 클래스 이용을 권장
인스턴스를 생성 (구현받는 클래스에)
lock 메서드로 공유 영역을 만듦
unlock 메서드로 공유 영역 해제
더보기

ShareData2 implements Runnable - ReentrantLock 이라는 클래스를 이용

package _api.thread.multithread;

import java.util.concurrent.locks.ReentrantLock;

//자원을 가지고 연산을 하는 스레드에 사용할 클래스
public class ShareData2 implements Runnable{
	//연산 결과를 저장할 속성
	private int result;
	//연산에 사용할 인덱스
	private int idx;
	
	//공유 코드 영역을 설정하기 위한 객체
	static final ReentrantLock Lock = new ReentrantLock();
	
	//result 의 getter 메서드
	public int getResult() {
		return result;
	}
	
	@Override
	public void run() {
		try{
			for(int i = 0; i < 10; i++) {
            		//자물쇠를 채워서 unlock 을 만날때까지는 이 영역의 자원을 수정할 수 없음
					Lock.lock();
					idx++;
					Thread.sleep(10); //없거나 너무 작으면 0,1같은 작은 숫자가 나옴
					result = result + idx;
					Lock.unlock();
			}
			
		}catch(Exception e) {
			System.out.println(e.getLocalizedMessage());
		}
		
	}

}

2. 생산자와 소비자 문제

→ wait 와 notification 으로 해결

  • 생산자와 서비자는 동시에 수핼할 수 있는데 소비자는 생산자가 자원을 생성해준 경우에만 동작을 해야 함
    소비자가 자원이 생성되지 않았는데 작업을 수행하면 예외가 발생
  • 앞에서 하던게 멈췄는데 lock을 가지고 있으면 아무것도 못하니까 기다리라고 하고, 다음 꺼에 넘기는 것
  • 동기화의 효율을 높이기 위해 사용
  • notify(아무거나 하나만 깨우기), notifyAll(전부다 깨우기)
  • Thread에 사용하는데 Object 클래스에 있음 - synchronized 해서 Thread에 있다고 알려줘야 함

2-1. 창고 입,출고

더보기

공유 자원의 역할을 수행할 클래스 - Product

package _api.thread;

import java.util.ArrayList;
import java.util.List;

//공유자원의 역할을 수행할 클래스 - 진열대 역할
public class Product {

	//문자를 저장할 수 있는 List - 공유 자원
	List<Character> list;

	//생성자
	public Product() {
		list = new ArrayList<>();
	}

	//생산자 메서드
	public synchronized void put(Character ch) {
		list.add(ch);
		System.out.println("창고에 제품" + ch + " 입고 되었습니다.");
		try {
			Thread.sleep(1000);
			System.out.println("재고 수량 : " + list.size());
		} catch (Exception e) {
			System.out.println(e.getLocalizedMessage());
		}
		//자원을 생산했다고 알림
		notify();
	}

	//소비자 메서드
	public synchronized void get() {
		try {
			if(list.size() == 0) {
				wait();
			}
		} catch (Exception e) {
			System.out.println(e.getLocalizedMessage());
		}

		//첫번째 데이터를 꺼내서 ch에 대입
		Character ch = list.remove(0);
		System.out.println("창고에서 제품" + ch + " 출고 하였습니다.");
		try {
			Thread.sleep(1000);
			System.out.println("재고 수량 : " + list.size());
		} catch (Exception e) {
			System.out.println(e.getLocalizedMessage());
		}
	}

}

생산자 클래스 Producer - put

package _api.thread;

public class Producer extends Thread {

	//공유 자원 속성
	private Product myList;

	//생성자
	public Producer(Product myList) {
		this.myList = myList;
	}

	@Override
	public void run() {
		for(char ch = 'A'; ch <= 'Z'; ch++) {
			myList.put(ch);
		}
	}
}

소비자 클래스 Customer - get

package _api.thread;

public class Consumer extends Thread {
	public Product myList;

	public Consumer(Product myList) {
		this.myList = myList;
	}

	@Override
	public void run() {
		for(int i = 0; i < 26; i++) {
			myList.get();
		}
	}
}

Main 클래스

package _api.thread;

public class ConsumerMain {
	public static void main(String[] args) {

		Product prd = new Product();

		new Producer(prd).start();
		new Consumer(prd).start();
	}

}
  • 실행을 하다보면 예외 발생
    소비자 스레드인 Customer 스레드가 리스트에 아무런 데이터가 없는데 꺼내려고 해서 발생하는 문제
    소비자 스레드의 메서드는 데이터가 없을 때는 대기하고 (wait)
    생산자 스레드는 데이터를 만들어 낸 경우 데이터가 만들어졌다고 알려줘야 함 (notify)
    기다리기 위해서 호출하는 메서드는 wait
    알려주는 메서드는 notify, notifyAll 메서드
    이 메서드들은 Object 클래스의 메서드이고, synchronized 메서드 안에서만 동작

Product 클래스를 수정하고 실행

2-2. 테이블 위의 요리

더보기
  • 요리사는 한 테이블 위에 임의로 요리를 놓고, 소비자들은 요리를 먹음
  • wait(), notify() 사용
  • 음식이 없을 때, wait()으로 손님이 lock을 풀고 기다리게 하자
  • 요리사가 음식을 추가하면 norify()로 손님에게 알리자 (손님이 lock을 획득하게끔)

공유 자원의 역할을 수행할 클래스 - Table (add, remove)

//공유 자원 Table - 진열대 역할
class Table2 {
	String[] dishNames = {"donut", "donut", "burger" };
	final int MAX_FOOD = 6;
	private ArrayList<String> dishes = new ArrayList<>(); //테이블 위의 접시들

	//생산자 메서드 - add
	public synchronized void add(String dish) { //synchronized 추가
		while(dishes.size() >= MAX_FOOD) { //테이블에 요리가 꽉 차 있으면
			String name = Thread.currentThread().getName();
			System.out.println(name + " is waiting.");
			try {
				wait(); //COOK 스레드를 기다리게 함, lock 반납하고 대기실로
				Thread.sleep(500);
			} catch (InterruptedException e) { }
		}
		dishes.add(dish);
		System.out.println("Dishes: " + dishes.toString());
	}

	//소비자 메서드 - remove
	public void remove(String dishName) {
		synchronized(this) {
			String name = Thread.currentThread().getName();

			//1. 테이블에 음식이 없는 경우
			while(dishes.size() == 0) { //0.5초마다 음식이 추가되었는지 확인, 테이블에 음식이 없으면 소비자가 기다리게
				System.out.println(name + "is waiting.");
				try {
					wait(); //CUST 스레드를 기다리게 함, lock 반납하고 대기실로
					Thread.sleep(500);
				} catch (InterruptedException e) { }
			}
			//2. 테이블에 음식이 있는 경우
			while(true) { //테이블에 음식이 있으면
				for(int i = 0; i < dishes.size(); i++) {
					if(dishName.equals(dishes.get(i))) {
						dishes.remove(i);
						notify(); //COOK에게 통보, 요리 놓을 자리 있다고 깨우기
						return;
					}
				}

				//3. 테이블에 원하는 음식이 없는 경우
				try {
					System.out.println(name + " is waiting.");
					wait(); //CUST 스레드를 기다리게 함, lock 반납하고 대기실로
					Thread.sleep(500);
				} catch (Exception e) { }
			}

		} //synchronized
	} //end remove()

	public int dishNum() {
		return dishNames.length;
	}
} //end Table2

생산자 클래스 Cook - put

//생산자 Cook
class Cook2 implements Runnable {
	private Table2 table;

	public Cook2() {}
	public Cook2(Table2 table) {
		this.table = table;
	}

	@Override
	public void run() {
		while(true) {
			int idx = (int)(Math.random() * table.dishNum()); //임의의 요리를 하나 선태해서 table에 추가
			table.add(table.dishNames[idx]);
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {}
		}
	}
} //end Cook2

소비자 클래스 Customer - get

//소비자 Customer2
class Customer2 implements Runnable {
	private Table2 table;
	private String food;

	public Customer2() {}
	public Customer2(Table2 table, String food) {
		this.table = table;
		this.food = food;
	}

	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {}
			String name = Thread.currentThread().getName();

			table.remove(food);
			System.out.println(name + " ate a " + food);
		}
	}
} //end Customer2

Main 클래스

public class WainNorifyOfThread2 {
	public static void main(String[] args) throws Exception{
		Table2 table = new Table2();

		new Thread(new Cook2(table), "COOK").start();
		new Thread(new Customer2(table, "donut"), "CUST1").start();
		new Thread(new Customer2(table, "burger"), "CUST2").start();

		Thread.sleep(2000);
		System.exit(0);
	}
}
----------------------------------------------------------------------
Dishes: [burger]
Dishes: [burger, burger]
Dishes: [burger, burger, donut]
Dishes: [burger, burger, donut, donut]
Dishes: [burger, burger, donut, donut, donut]
Dishes: [burger, burger, donut, donut, donut, donut]
COOK is waiting.
CUST2 ate a burger
Dishes: [burger, donut, donut, donut, donut, donut]
CUST2 ate a burger
CUST1 ate a donut
Dishes: [donut, donut, donut, donut, burger]
Dishes: [donut, donut, donut, donut, burger, donut]
COOK is waiting.
CUST2 ate a burger
Dishes: [donut, donut, donut, donut, donut, donut]
CUST2 is waiting.
CUST1 ate a donut
CUST2 is waiting.
CUST1 ate a donut
Dishes: [donut, donut, donut, donut, donut]

synchronized만 사용한 동기화 (비효율적)

  • 예외는 발생하지 않음
  • 음식이 없을 때, 손님이 Table의 lock을 쥐고 안 놓음, 음식 나올때까지
  • 요리사가 lock을 얻지 못해서 Table에 음식을 추가할 수 없음
package chapter13;

import java.util.*;

//생산자 Cook
class Cook implements Runnable {
	private Table table;
	
	public Cook() {}
	public Cook(Table table) {
		this.table = table;
	}
	
	@Override
	public void run() {
		int idx = (int)(Math.random() * table.dishNum()); //임의의 요리를 하나 선태해서 table에 추가
		table.add(table.dishNames[idx]);
		while(true) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {}
		}
	}
} //end Cook

//소비자 Customer
class Customer implements Runnable {
	private Table table;
	private String food;
	
	public Customer() {}
	public Customer(Table table, String food) {
		this.table = table;
		this.food = food;
	}
	
	@Override
	public void run() {
		while(true) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {}
			String name = Thread.currentThread().getName();
			
			if(eatFood()) //음식을 소비
				System.out.println(name + " ate a " + food);
			else
				System.out.println(name + " failed to eat. :(");
		}
	}
	boolean eatFood() {
		return table.remove(food); //테이블에서 음식을 제거
	}
} //end Customer

//공유 자원 Table - 진열대 역할
class Table {
	String[] dishNames = {"donut", "donut", "burger" };
	final int MAX_FOOD = 6;
	private ArrayList<String> dishes = new ArrayList<>(); //테이블 위의 접시들

	//생산자 메서드 - add
	public synchronized void add(String dish) { //synchronized 추가
		if(dishes.size() >= MAX_FOOD) { //ArrayList 찼을 때
			return;
		}
		dishes.add(dish);
		System.out.println("Dishes: " + dishes.toString());
	}

	//소비자 메서드 - remove
	public boolean remove(String dishName) {
		synchronized(this) {

			while(dishes.size() == 0) { //0.5초마다 음식이 추가되었는지 확인
				String name = Thread.currentThread().getName();
				System.out.println(name + "is waiting.");
				try {
					Thread.sleep(500);
				} catch (InterruptedException e) { }
			}

			for(int i = 0; i < dishes.size(); i++) {
				if(dishName.equals(dishes.get(i))) {
					dishes.remove(i);
					return true;
				}
			}

		} //synchronized
		return false;
	} //end remove()
	
	public int dishNum() {
		return dishNames.length;
	}
} //end Table

public class WainNorifyOfThread1 {
	public static void main(String[] args) throws Exception{
		Table table = new Table();
		
		new Thread(new Cook(table), "COOK").start();
		new Thread(new Customer(table, "donut"), "CUST1").start();
		new Thread(new Customer(table, "burger"), "CUST2").start();
		
		Thread.sleep(5000);
		System.exit(0);
	}
}
----------------------------------------------------------------------
Dishes: [burger]
CUST2 ate a burger
CUST1 failed to eat. :( ← donut이 없어서 먹지 못함
CUST2is waiting. ← 음식이 없어서 테이블에 lock을 건 채로 계속 기다림
CUST2is waiting.
CUST2is waiting.
CUST2is waiting.
CUST2is waiting.
CUST2is waiting.
CUST2is waiting.
CUST2is waiting.
CUST2is waiting.
CUST2is waiting.

3. Dead Lock

  • 결코 발생할 수 없는 사건을 무한정 기다리는 것
  • 프로그래머가 직접 찾아야해서 어려움
  • Synchronize 블럭이 여러 개 있는 경우에 멀티스레드를 사용하면 발생
  • Synch 안에 Synch 있는데 main에서 Multi Thread 돌리면 DeadLock 발생!!

Semaphore

  • 동시에 사용할 수 있는 스레드의 개수를 제한
  • Semaphore 클래스를 이용해서 설정
  • acquire() : 리소스를 확보하는 메서드
    release() : 리소스를 해제하는 메서드

실습

더보기

세마포를 이용하지 않는 경우 - 스레드가 동시에 전부 실행

SemaphoreThread implements Runnable

package _api.thread.multithread;

public class SemaphoreThread implements Runnable{

	String message;
	
	public SemaphoreThread(String message) {
		this.message = message;
	}
	
	@Override
	public void run() {
		try {
			Thread.sleep(10000);
			System.out.println(message);
		}catch(Exception e) {}
	}
	
}

SemaphoreMain

package _api.thread.multithread;

public class SemaphoreMain {

	public static void main(String[] args) {
		Thread th1 = new Thread(new SemaphoreThread("제니"));
		Thread th2 = new Thread(new SemaphoreThread("지수"));
		Thread th3 = new Thread(new SemaphoreThread("리사"));
		Thread th4 = new Thread(new SemaphoreThread("로제"));
		
		th1.start();
		th2.start();
		th3.start();
		th4.start();
		
		//4개의 스레드가 10초 동시에 메세지로 출력
	}
}
  • 4개의 스레드가 10초 동시에 메세지로 출력

세마포어를 이용하는 경우

SemaphoreThread implements Runnable2

package _api.thread.multithread;

import java.util.concurrent.Semaphore;

public class SemaphoreThread implements Runnable{

	String message;
	Semaphore semaphore;
	
	public SemaphoreThread(String message, Semaphore semaphore) {
		this.message = message;
		this.semaphore = semaphore;
	}
	
	@Override
	public void run() {
		try {
			//리소스 확보
			semaphore.acquire();
			Thread.sleep(10000);
			System.out.println(message);
		}catch(Exception e) {}
		//리소스 해제, try 안에 써도 됨
		semaphore.release();
	}
	
}

SemaphoreMain2

package _api.thread.multithread;

import java.util.concurrent.Semaphore;

public class SemaphoreMain {

	public static void main(String[] args) {
		//2개씩 실행할 수 있는 세마포어
		Semaphore semaphore = new Semaphore(2);
		
		Thread th1 = new Thread(new SemaphoreThread("제니", semaphore));
		Thread th2 = new Thread(new SemaphoreThread("지수", semaphore));
		Thread th3 = new Thread(new SemaphoreThread("리사", semaphore));
		Thread th4 = new Thread(new SemaphoreThread("로제", semaphore));
		
		th1.start();
		th2.start();
		th3.start();
		th4.start();
		
		//4개의 스레드가 10초 동시에 메세지로 출력
	}
}

Timer

  • Thread와 비슷

관습적으로 이름쓰는 규칙

일반적으로 개발자들은 클래스의 인스턴스를 1개만 생성할 때는 변수의 이름을 클래스 이름과 동일하게 만들고 첫글자만 소문자로 변경
String string = "apple";

어떤 클래스로부터 상속받는 클래스를 1개만 만들 때는 클래스 이름 뒤에 Ex를 붙임
Thread 클래스로부터 상속받는 클래스
class ThreadEx extends Thread {

}

인터페이스를 구현한 클래스를 1개만 만들 때는 클래스 이름 뒤에 impl을 붙임
Runnable 인터페이스를 구현한 클래스
class RunnableI(i)mpl implements Runnable{

}

extends 하거나 implements 했을 때 변수의 이름은 상위 클래스 이름이나 인터페이스 이름을 사용
ArrayList<String>  list = new ArrayList<>();
 ▶ List<String> list = new ArrayList<>();
ThreadEx th = new TreadEx();
▶ Thread th = new TreadEx();
728x90
profile

원지의 개발

@원지다

250x250