Thread 클래스와 Runnable 인터페이스
- 메모리를 할당받아 실행 중인 프로그램을 프로세스라고 한다.
- 프로세스 내의 명령어 블록으로 시작점과 종료점을 가진다.
- 실행 중에 멈출 수 있으며 동시에 수행 가능하다.
- 어떠한 프로그램내에서 특히 프로세스 내에서 실행되는 흐름의 단위.
작업 Thread 생성과 실행
Thread 클래스로부터 직접 생성
class Task implements Runnable {
public void run() {
// run code
}
}
java.lang.Thread 클래스로부터 작업 쓰레드 객체를 직접 생성 하려면 Runnable을 매개값으로 갖는 생성자를 호출해야 한다. Runnable은 작업 쓰레드가 실행할 수 있는 코드를 가지고 있는 객체라고 해서 붙여진 이름이다. Runnable에는 run() 메소드 하나가 정의되어 있는데, 구현 클래스는 run()을 재정의해서 작업 쓰레드가 실행할 코드를 작성해야 한다.
// Runnable 구현 객체를 생성한 후, 이것을 매개값으로 Thread 생성자를 호출
Runnable task = new Task();
Thread thread = new Thread(task);
// Thread 생성자를 호출할 때 Runnable 익명 객체를 매개값으로 사용할 수 있다. 이 방법이 더 많이 사용된다.
Thread thread = new Thread( new Runnable() {
public void run() {
쓰레드가 실행할 코드;
}
});
// 람다식을 매개값으로 사용하는 방법
Thread thread = new Thread( () -> {
쓰레드가 실행할 코드;
});
Thread 하위 클래스로부터 생성
작업 Thread가 실행할 작업을 Runnable로 만들지 않고, Thread의 하위 클래스로 작업 Thread를 정의하면서 작업 내용을 포함시킬 수도 있따. Thread 클래스를 상속한 후 run 메소드를 overriding해서 쓰레드가 실행할 코드를 작성하면된다.
public class WorkerThread extends Thread {
@Override
public void run() {
//스레드가 실행할 코드
}
}
Thread thread = new Thread() {
public void run() {
//스레드가 실행할 코드
}
}
thread.start()
쓰레드의 상태
1. 쓰레드 객체를 생성하고, start() 메소드를 호출하면 쓰레드는 실행 대기 상태가 된다.
- 실행 대기 상태 : 아직 스케줄링이 되지 않아 실행을 기다리고 있는 상태
2. 실행 대기 상태에 있는 메소드 중 쓰레드 스케줄링으로 선택된 쓰레드가 CPU를 점유하고 run() 메소드를 실행할 때 실행(Running) 상태라고 한다.
3. 실행 상태의 쓰레드는 run() 메소드를 모두 실행하기 전에 쓰레드 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 실행 대기 상태에 있는 다른 쓰레드가 선택되어. 실행 상태가 된다. 이렇게 쓰레드는 실행 대기 상태와 실행 상태를 번갈아가며 자신의 run() 메소드를 조금씩 실행한다.
4. 실행 상태에서 run() 메소드가 종료되면, 더 이상 실행할 코드가 없기 때문에 쓰레드의 실행은 멈춘다. 이 상태를 종료 상태라고 한다.
쓰레드가 실행 상태에서 실행 대기 상태로 가지 않고 일시 정시 상태로 가기도 한다.
- 일시 정지 상태 : 쓰레드가 실행할 수 없는 상태다.
- 일시 정지 상태의 종류 : WATING, TIMED_WATING, BLOCKED
getState()
Thread 클래스의 getState() 메소드를 통해 쓰레드의 상태를 코드로 확인할 수 있다.
상태 | 열거 상수 | 실행 |
객체 생성 | NEW | 스레드 객체가 생성된 후, 아직 start() 메소드가 호출되지 않은 상태 |
실행 대기 | RUNNABLE | 실행 상태로 언제든지 갈 수 있는 상태 |
일시 정지 | WATING | 다른 스레드가 통지할 때까지 기다리는 상태 |
TIMED_WATING | 주어진 시간 동안 기다리는 상태 | |
BLOCKED | 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태 | |
종료 | TERMINATED | 실행을 마친 상태 |
쓰레드 상태 제어
호출 주체 | 메소드 | 설명 |
Thread | interrupt() | 일시 정지 상태의 쓰레드에 InterruptedException 예외를 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있돌고 한다. |
Object | notify() / notifyAll() | 동기화 블록 내에서 wait() 메소드에 의해 일시 정지 상태에 있는 쓰레드를 실행 대기 상태로 만든다. |
Thread | sleep(long millis) / sleep(long millis, int nanos) | 주어진 시간 동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지남녀 자동저긍로 실행 대기 상태가 된다. |
Thread | join() / join(long mills) / join(long millis, int nanos) | join() 메소드를 호출한 쓰레드는 일시 정지 상태가 된다. 실행 대기 상태로 가려면 join() 메소드를 멤버로 가지는 쓰레드가 종료되거나, 매개값으로 주어진 시간이 지나야한다. |
Object | wait() / wait(long mills) / wait(long millis, int nanos) | 동기화(synchronized) 블록 내에서 쓰레드를 일시 정지 상태로 만든다. 매개값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다. 시간이 주어지지 않으면 notify(), notifyAll() 메소드에 의해 실행 대기상태로 갈 수 있다. |
Thread | yield() | 실행 중에 우선순위가 동일한 다른 쓰레드에게 실행을 양보하고, 실행 대기 상태가 된다. |
Interrupt()를 이용한 Thread의 종료
public class PrintThread2 extends Thread {
public void run(){
try{
while (true){
System.out.println("실행 중");
Thread.sleep(1);
// 쓰레드가 실행 대기 또는 실행 상태에서는 interrupt가 발생하지 않으므로
// sleep을 이용해 일시 정지 시켜준다.
}
}catch (InterruptedException e){}
System.out.println("자원 정리");
System.out.println("실행 종료");
}
}
public class InterruptExam {
public static void main(String[] atgs){
Thread thread = new PrintThread2();
thread.start();
try{Thread.sleep(1000);}catch (InterruptedException e){}
thread.interrupt(); //스레드를 종료시키기 위해 InterruptedException을 발생시킴
}
}
Thread간 협업 - wait(), notify(), notifyAll()
두개의 쓰레드를 교대로 번갈아가며 실행해야 할 경우 자신의 작업이 끝나면 상대방 쓰레드를 일시 정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만들어야 한다.
공유 객체는 두 쓰레드가 작업할 내용을 각각 동기화 메소드로 구분해 놓는다. 한 쓰레드가 작업을 완료하면 notify()메소드를 호출해서 일시 정지 상태에 있는 다른 쓰레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait()메소드를 호출하여 일시 정지 상태로 만든다.
notifyAll()의 경우는 notify() 메소드와 동일한 역할을 하지만 notifyAll()은 wait으로 일시 정지 상태인 모든 쓰레드들을 실행 대기 상태로 만든다.
위 메소드들은 모두 동기화 메소드 또는 동기화 블록 내에서만 사용 가능하다.
public class WorkObject {
public synchronized void methodA(){
System.out.println("ThreadA의 methodA() 작업 실행");
notify(); //일시 정지 상태의 ThreadB를 실행 대기 상태로 만듦
try{
wait(); //ThreadA를 일시 정지 상태로 만듦
}catch (InterruptedException e){}
}
public synchronized void methodB(){
System.out.println("ThreadB의 methodB() 작업 실행");
notify(); //일시 정지 상태의 ThreadA를 실행 대기 상태로 만듦
try{
wait(); //ThreadB를 일시 정지 상태로 만듦
}catch (InterruptedException e){}
}
}
public class ThreadA extends Thread{
private WorkObject workObject;
public ThreadA(WorkObject workObject){
this.workObject = workObject; //공유 객체를 매개값으로 받아 필드에 저장
}
@Override
public void run(){
for(int i=0; i<10; i++){ //공유 객체의 methodA()를 10번 반복 호출
workObject.methodA();
}
}
}
public class ThreadB extends Thread{
private WorkObject workObject;
public ThreadB(WorkObject workObject){
this.workObject = workObject; //공유 객체를 매개값으로 받아 필드에 저장
}
@Override
public void run(){
for(int i=0; i<10; i++){ //공유 객체의 methodB()를 10번 반복 호출
workObject.methodB();
}
}
}
public class Main {
public static void main(String[] args){
WorkObject sharedObject = new WorkObject();
Thread threadA = new ThreadA(sharedObject);
Thread threadB = new ThreadB(sharedObject);
//ThreadA와 ThreadB를 실행
threadA.start();
threadB.start();
}
}
주어진 시간동안 일시 정지 - sleep()
호출한 쓰레드는 주어진 시간 동안 일시 정지 상태가 되고, 다시 실행 대기 상태로 돌아간다.
public class SleepExam {
public static void main(String[] args){
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i=0; i<0; i++){
toolkit.beep();
try{
Thread.sleep(3000);
}catch(InterruptedException e){}
}
}
}
다른 쓰레드의 종료를 기다림 - join()
다른 쓰레드가 종료될 때까지 기다렸다가 쓰레들르 실행할 경우 join() 메소드를 사용한다.
public class SumThread extends Thread {
private long sum;
public long getSum(){
return sum;
}
public void setSum(long sum){
this.sum = sum;
}
public void run(){
for(int i=1; i<=100; i++){
sum+=i;
}
}
}
public class Main {
public static void main(String[] args){
SumThread sumThread = new SumThread();
sumThread.start();
try{
sumThread.join(); //sumThread가 종료할 때까지 메인 스레드를 일시 정지
}catch (InterruptedException e){
}
System.out.println("1-100 합: "+sumThread.getSum());
}
}
다른 쓰레드에게 실행 양보 - yield()
불필요한 반복문 등이 실행될 경우, 무의미한 반복을 피하기 위해 yield()를 사용하여 동일한 또는 보다 높은 우선 순위를 가진 다른 쓰레드에게 실행을 양보하고 자신은 실행 대기 상태로 갈 수 있다.
public class ThreadA extends Thread{
public boolean stop = false; //종료 플래그
public boolean work = true; //작업 진행 여부 플래그
public void run(){
while(!stop){ //stop이 true가 되면 while 종료
if(work){
System.out.println("ThreadA 작업 내용");
}else{
Thread.yield(); //work가 false가 되면 다른 스레드에게 실행 양보
}
}
}
}
public class ThreadB extends Thread{
public boolean stop = false; //종료 플래그
public boolean work = true; //작업 진행 여부 플래그
public void run(){
while(!stop){ //stop이 true가 되면 while 종료
if(work){
System.out.println("ThreadB 작업 내용");
}else{
Thread.yield(); //work가 false가 되면 다른 스레드에게 실행 양보
}
}
}
}
public class YieldExam {
public static void main(String[] args){
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
threadA.start();
threadB.start();
try{Thread.sleep(3000);}catch (InterruptedException e){}
threadA.work = false; //ThreadB만 실행
try{Thread.sleep(3000);}catch (InterruptedException e){}
threadA.work = true; //ThreadA, ThreadB 모두 실행
try{Thread.sleep(3000);}catch (InterruptedException e){}
threadA.stop = true; // ThreadA, ThreadB 모두 종료
threadB.stop = true;
}
}
쓰레드의 우선순위
동시성과 병렬성
동시성(Concurrency) : 멀티 작업을 위해 하나의 코어에서 멀티 쓰레드가 번갈아 가며 실행하는 성질
병렬성(Parallelism) : 멀티 작업을 위해 멀티 코어에서 개별 쓰레드를 동시에 실행하는 성질
※ 싱클 코어에서의 멀티 쓰레드 작업은 병렬적으로 실행되는 것처럼 보이지만, 사실은 번갈아가며 실행하는 동시성 작업이다. 번갈아 실행하는 것이 워낙 빨라서 병렬성으로 보일 뿐이다.
Thread Scheduling
Thread의 수가 코어의 수보다 많을 경우
- 쓰레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정해야 하는데, 이것을 쓰레드 스케줄링이라고 한다.
- 쓰레드 스케줄링에 의해 쓰레드들은 아주 짧은 시간에 번갈아가면서 그들의 run() 메소드를 조금씩 실행한다.
JAVA의 Thread Scheduling
- 우선 순위(priority) 방식과 순환 할당(Round-Robin) 방식을 사용
- 우선 순위 방식(코드로 제어 가능) : 우선 순위가 높은 쓰레드가 실행 상태를 더 많이 가지도록 스케줄링
- 순환 할당 방식(코드로 제어할 수 없음) : 시간 할당량(Time Slice)를 정해서 하나의 쓰레드를 정해진 시간만큼 실행하는 방식
Thread 우선 순위
- 쓰레드들이 동시성을 가질 경우 우선적으로 실행할 수 있는 순위
- 우선 순위는 1(낮음)에서부터 10(높음)까지로 부여
- 모든 스레드들은 기본적으로 5의 우선 순위를 할당
우선 순위 변경 방법
thread.setPriority(우선순위);
thread.setPriority(Thread.MAX_PRIORITY);
thread.setPriority(Thread.NORM_PRIORITY);
thread.setPriority(Thread.MIN_PRIORITY);
우선 순위 효과
- 싱글 코어
- 우선 순위가 높은 쓰레드가 실행 기회를 더 많이 가지기 때문에 우선 순위가 낮은 스레드보다 계산 작업을 빨리 끝낸다.
- 멀티 코어
- 코어의 수보다 많은 쓰레드가 실행되어야 우선 순위의 영향을 받는다.
Main 쓰레드
메인 스레드 이름 : main
작업 스레드 이름 : Thread-n
작업 쓰레드의 이름 변경
thread.setName("Thread Name");
코드를 실행하는 쓰레드의 참조 얻기
Thread thread = Thread.currentThread();
동기화
싱글 쓰레드 프로그램에서는 한 개의 쓰레드가 객체를 독차지해서 사용하면 되지만, 멀티 쓰레드 프로그램에서는 쓰레드들이 객체를 공유해서 작업해야 하는 경우가 있다. 이 경우, 쓰레드 A를 사용하던 객체가 쓰레드 B에 의해 상태가 변경될 수 있기 때문에 쓰레드 A가 의도했던 것과는 다른 결과를 산출할 수도 있다.
동기화 메소드 및 동기화 블록
임계 영역(critical section) : 멀티 쓰레드 프로그램에서 단 하나의 쓰레드만 실행할 수 있는 코드 영역
JAVA에서의 임계 영역 : 동기화(synchronized) 메소드와 동기화 블록을 제공. 스레드가 객체 내부의 동기화 메소드 또는 블록에 들어가면 즉시 객체에 잠금을 걸어 다른 쓰레드가 임계 영역 코드를 실행하지 못 하도록 한다.
동기화 메소드
동기화 메소드는 메소드 전체 내용이 임계 영역이므로 쓰레드가 동기화 메소드를 실행하는 즉시 객체에는 잠금이 일어나고, 쓰레드가 동기화 메소드를 실행 종료하면 잠금이 풀린다.
public synchronized void method() {
임계 영역; //단 하나의 스레드만 실행
}
동기화 블록
등기화 블록의 외부 코드들은 여러 쓰레드가 동시에 실행할 수 있지만, 동기화 블록의 내부 코드는 임계 영역이므로 한번에 한 쓰레드만 실행할 수 있고 다른 쓰레드는 실행할 수 없다. 만약 동기화 메소드와 동기화 블록이 여러개 있을 경우, 쓰레드가 이들 중 하나를 실행할 때 다른 쓰레드는 해당 메소드는 물론이고 다른 동기화 메소드 및 블록도 실행할 수 없다. 하지만 일반 메소드는 실행이 가능하다.
public vod method () {
//여러 스레드가 실행 가능한 영역
...
synchronized(공유객체) {
임계 영역; //단 하나의 스레드만 실행
}
//여러 스레드가 실행 가능한 영역
...
}
데드락
Thread DeadLock
멀티 쓰레드 프로그래밍에서 동기화를 통해 락을 획득하여 동일한 자원을 여러 곳에서 사용하지 못 하도록 하였을 때,
서로 다른 쓰레드가 서로가 가지고 있는 락이 해제되기를 기다리는 상태가 생길 수 있으며 이러한 상태를 교착상태(deadlock)이라 한다.
교착 상태는 어떤 작업도 실행되지 못하고 서로 상대방의 작업이 끝나기만 바라는 무한정 대기 상태이다.
DeadLock 발생 조건
- 상호 배제 (Mutual Exclusion) : 한 자원에 대해 여러 쓰레드 동시 접근 불가
- 점유와 대기 (Hold and Wait) : 자원을 가지고 있는 상태에서 다른 스레드가 사용하고 있는 자원 반납을 기다리는 것
- 비선점 (Non Preemptive) : 다른 쓰레드의 자원을 실행 중간에 강제로 가져올 수 없음
- 환형대기(Circle Wait) : 각 쓰레드가 순환적으로 다음 쓰레드가 요구하는 자원을 가지고 있는 것
위의 4가지 조건을 모두 충족할 경우 데드락이 발생. 반대로 말해, 위 4가지 중 하나라도 충족하지 않을 경우 데드락을 해결할 수 있다는 뜻이기도 하다.
public class Main {
public static Object object1 = new Object();
public static Object object2 = new Object();
public static void main(String[] args) {
FirstThread thread1 = new FirstThread();
SecondThread thread2 = new SecondThread();
thread1.start();
thread2.start();
}
private static class FirstThread extends Thread{
@Override
public void run() {
synchronized (object1){
System.out.println("First Thread has object1's lock");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("First Thread want to have object2's lock. so wait");
synchronized (object2){
System.out.println("First Thread has object2's lock too");
}
}
}
}
private static class SecondThread extends Thread{
@Override
public void run() {
synchronized (object2){
System.out.println("Second Thread has object2's lock");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Second Thread want to have object1's lock, so wait");
synchronized (object1){
System.out.println("Second Thread has object1's lock too");
}
}
}
}
}
/*************************************************
출력 결과
First Thread has object1`s lock
Second Thread has object2`s lock
First Thread want to have object2`s lock. so wait
Second Thread want to have object1`s lock. so wait
**************************************************
- 상호 배제 : object1과 object2 객체에 대해서 동시에 쓰레드가 사용할 수 없도록 하였다.
- 점유와 대기 : FirstThread에서는 object1의 락을 가지고 있으면서 object3에 대한 락을 원하고, SecondThread는 object2에 대한 락을 가지고 있으면서 object1의 락을 획득하기를 원한다.
- 비선점 : 쓰레드의 우선순위의 기본값은 NORM_PRIORITY로 동일하게 설정되어 있습니다.
- 환형대기 : FirstThread는 SecondThread의 object2 객체의 락을 대기하고 SecondThread는 FirstThread의 object1 객체의 락을 대기하고 있다.
public class Main {
public static Object object1 = new Object();
public static Object object2 = new Object();
public static void main(String[] args) {
FirstThread thread1 = new FirstThread();
SecondThread thread2 = new SecondThread();
thread1.start();
thread2.start();
}
private static class FirstThread extends Thread{
@Override
public void run() {
synchronized (object1){
System.out.println("First Thread has object1's lock");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("First Thread want to have object2's lock. so wait");
synchronized (object2){
System.out.println("First Thread has object2's lock too");
}
}
}
}
private static class SecondThread extends Thread{
@Override
public void run() {
synchronized (object1){
System.out.println("Second Thread has object2's lock");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Second Thread want to have object1's lock, so wait");
synchronized (object2){
System.out.println("Second Thread has object1's lock too");
}
}
}
}
}
/*****************************
First Thread has object1`s lock
First Thread want to have object2`s lock. so wait
First Thread has object2`s lock too
Second Thread has object2`s lock
Second Thread want to have object1`s lock. so wait
Second Thread has object21`s lock too
Process finished with exit cod 0
*****************************/
위 코드는 환형대기 조건을 만족하지 않기 때문에 데드락이 발생하지 않는다.
ref {
https://ict-nroo.tistory.com/41#recentEntries
[JAVA] 자바의 멀티 스레드
멀티 스레드 '이것이 자바다 - 신용권' 12장 학습 소스코드 repo 1절. 멀티 스레드 개념 2절. 작업 스레드 생성과 실행 3절. 스레드 우선순위 4절. 동기화 메소드와 동기화 블록 5절. 스레드 상태 6절.
ict-nroo.tistory.com
https://math-coding.tistory.com/175
https://scshim.tistory.com/243
}
'Programming > Java' 카테고리의 다른 글
[JAVA] Annotation (0) | 2022.03.28 |
---|---|
[JAVA] ENUM (0) | 2022.03.21 |
[JAVA] 예외 처리 (0) | 2022.03.04 |
[JAVA]인터페이스 (0) | 2022.02.28 |
[JAVA] 패키지 (0) | 2022.02.21 |
댓글