반응형

출처 : 이것이 자바다 

멀티 스레드

12.1. 멀티 스레드 개념

12.1.1 프로세스와 스레드

운영체제에서는 실행 중인 하나의 애플리케이션을 프로세스(process) 라고 부른다. 사용자가 애플리케이션을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당받아 애플리케이션의 코드를 실행하는데 이것이 프로세스이다.

 

멀티 태스킹 (multi tasking)은 두 가지 이상의 작업을 동시에 처리하는 것을 말하는데, 운영체제는 멀티 태스킹을 할 수 있도록 CPU 및 메모리 자원을 프로세스마다 적절히 할당해주고, 병렬로 실행시킨다.

 

12.1.2 메인 스레드

모든 자바 프로그램은 메인 스레드가 (main thread) 메소드 실행하며 시작된다.

메인 스레드는 main() 메소드의 첫 코드부터 아래로 순차적으로 실행하고,main()메소드의 마지막 코드 실행하거나 return 문을 만나면 실행이 종료된다.

 

메인 스레드는 작업 스레드들을 만들어 병렬로 코드들 실행할수 있다. 즉 멀티 스레드 생성해 멀티 태스킹 수행한다.

 

프로세스의 종료

싱글 스레드: 메인 스레드가 종료하면 프로세스도 종료

멀티 스레드: 실행 중인 스레드가 하나라도 있다면, 프로세스 미종료

 

12.2 작업 스레드 생성과 실행

 java.lang.Thread 클래스를 직접 객체화해서 생성해도 되지만, Thread를 상속해서 하위 클래스를 만들어 생성할 수도 있다.

 

12.2.1 Thread 클래스로부터 직접 생성

Runnable은 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체라고 해서 붙여진 이름이다.

Runnable은 인터페이스 타입이기 때문에 구현 객체를 만들어 대입해야 한다.

Runnable에는 run() 메소드 하나가 정의되어 있는데, 구현 클래스는 run()을 재정의해서 작업 스레드가 실행할 코드를 작성해야 한다. 

class Task implements Runnable {
  public void run() {
  	스레드가 실행할 코드;
  }
}

Runnable 인터페이스는 run() 메소드 하나만 정의되어 있기 때문에 함수적 인터페이스이다.

Thread thread = new Thread( () -> {
	스레드가 실행할 코드;
});
thread.start();

read.strt();

import java.awt.*;
 
public class BeepPrintExample1 {    //메인 스레드만 이용한 경우
 
    public static void main(String[] args) {
        Toolkit toolkit = Toolkit.getDefaultToolkit();         //Toolkit 객체 얻기
        for (int i = 0; i<5; i++) {
            toolkit.beep();                                    //비프음 발생
            try { Thread.sleep(500); } catch (Exception e) {} //0.5초간 일시정지
        }
        
        for (int i = 0; i <5; i++) {
            System.out.println("띵");
            try { Thread.sleep(500); } catch (Exception e) { //0.5초간 일시정지
            }
        }
    }
 
}

비프음 발생시키면서 동시에 프린팅을 하려면 두 작업 중 하나를 메인 스레드가 아닌 다른 스레드에서 실행시켜야 한다.

프린팅은 메인 스레드가 담당하고 비프음을 들려주는 것은 작업 스레드가 담당하도록 수정해보자.우선 작업을 정의하는 Runnable 구현 클래스를 다음과 같이 작성한다.

 

public class BeepTask implements Runnable{
    @Override
    public void run() {
        Toolkit toolkit = Toolkit.getDefaultToolkit();         
        for (int i = 0; i<5; i++) {
            toolkit.beep();                                    
            try { Thread.sleep(500); } catch (Exception e) {} 
        }
    }
}
public class BeepPrintExample2 {
 
    public static void main(String[] args) {
        Runnable beepTask = new BeepTask();
        Thread thread = new Thread(beepTask);
        thread.start();
        
        for (int i = 0; i <5; i++) {
            System.out.println("띵");
            try { Thread.sleep(500); } catch (Exception e) {} //0.5초간 일시정지
        }
    }
}

12.2.2 Thread 하위 클래스로부터 생성

다음은 작업 스레드 클래스를 정의하는 방법인데, Thread 클래스를 상속한 후 run 메소드를 재정의(overriding) 해서 스레드가 실행할 코드를 작성하면 된다.

Thread thread = new Thread() {
  public void run() {
  	스레드가 실행할 코드;
  }
};
import java.awt.Toolkit;
 
public class BeepThread extends Thread{
    @Override
    public void run() {
    Toolkit toolkit = Toolkit.getDefaultToolkit();
    for(int i=0; i<5; i++) {
        toolkit.beep();
        try {Thread.sleep(500);} catch (Exception e) {}
        }
    }
}
public class BeepPrintExample3 {
    public static void main(String[] args) {
        Thread thread = new BeepThread();
        thread.start();
        
        for(int i=0;i<5;i++) {
            System.out.println("띵");
            try { Thread.sleep(500); }
            catch (Exception e) {}
        }
    }
}

12.2.3 스레드의 이름

thread.setName("스레드 이름");

public class ThreadNameExample {
 
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread();        
        System.out.println("프로그램 시작 스레드 이름: " + mainThread.getName());
        
        ThreadA threadA = new ThreadA();   
        System.out.println("작업 스레드 이름:" + threadA.getName());
        threadA.start();
        
        ThreadB threadB = new ThreadB();
        System.out.println("작업 스레드 이름:" + threadB.getName());
        threadB.start();
    }
}
public class ThreadA extends Thread{
    public ThreadA() {
        setName("ThreadA");       
    }
    
    @Override
    public void run() {        
        for(int i=0; i<2; i++) {        
            System.out.println(getName() + "가 출력한 내용");       
        }
    }
}

thread.stNam

public class ThreadB extends Thread{
    @Override
    public void run() {   
        for(int i=0;i<2;i++) {
            System.out.println(getName() + "가 출력한 내용");
        }
    }
}

 

12.3 스레드 우선순위

멀티 스레드는 동시성(Concurrency) 또는 병렬성(Parallelism) 으로 실행되기 때문에 이 용어들에 대해 정확히 이해하는 것이 좋다.동시성은 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질을 말하고 , 병렬성은 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질을 말한다. 

 

스레드의 개수가 코어의 수 보다 많을 경우, 스레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정해야 하는데, 이것을 스레드 스케줄링이라고 한다. 스레드 스케줄링에 의해 스레드들은 아주 짧은 시간에 번갈아가면서 그들의 run() 메소드를 조금씩 실행한다.

 

자바의 스레드 스케줄링은 우선순위(Priority) 방식과 순환 할당(Round-Robin) 방식을 사용한다.우선순위 방식은 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링하는 것을 말한다. 순환 할당 방식은 시간 할당량(Time Slice)을 정해서 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른 스레드를 실행하는 방식을 말한다. 

 

우선순위 방식에서 우선순위 방식은 우선순위는 1에서부터 10까지 부여되는데 , 1이 가장 우선순위가 낮고 10이 가장 높다.defualt=5

 

만약 우선순위를 변경하고 싶다면 Thread 클래스가 제공하는 setPriority() 메소드를 이용하면 된다.

thread.setPriority(우선순위);

 

thread.setPriorty(Thread.MAX_PRIORITY);
thread.setPriorty(Thread.NORM_PRIORITY);
thread.setPriorty(Thread.MIN_PRIORITY);
public class CalcThread extends Thread{
    public CalcThread(String name) {
        setName(name);   
    }
    
    public void run() {   
        for(int i=0;i<2000000000; i++) {
        }            
        System.out.println(getName());
    }
}
public class PriorityExample {
 
    public static void main(String[] args) {
        for(int i=1;i<=10;i++) {
            Thread thread = new CalcThread("thread" + i);
            if(i==10) {
                thread.setPriority(Thread.MAX_PRIORITY);
            } else {
                thread.setPriority(Thread.MIN_PRIORITY);
            }
            thread.start();
        }
    }
 
}

12.4 동기화 메소드와 동기화 블록

12.4.1 공유 객체를 사용할 때의 주의할 점

싱글 스레드 프로그램에서는 한 개의 스레드가 객체를 독차지해서 사용하면 되지만, 멀티 스레드 프로그램에서는 스레드들이 객체를 공유해서 작업해야 하는 경우가 있다.

public class MainThreadExample {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        
        User1 user1 = new User1();
        user1.setCalculator(calculator);   
        user1.start();
        
        User2 user2 = new User2();
        user2.setCalculator(calculator);
        user2.start();
    }
}
public class Calculator {
    private int memory;
    
    public int getMemory() {
        return memory;
    }
    
    public void setMemory(int memory) {   
        this.memory = memory;    
        try {   
            Thread.sleep(2000);
        } catch (InterruptedException e) {}
            System.out.println(Thread.currentThread().getName() + ": " + this.memory);
    }
}
public class User1 extends Thread {
    private Calculator calculator;
    
    public void setCalculator(Calculator calculator) {
        this.setName("User1"); 
        this.calculator = calculator; 
    }
    
    public void run() {
        calculator.setMemory(100);
    }
}
 

e("스레드 이름"

public class User2 extends Thread{
    private Calculator calculator;
    
    public void setCalculator(Calculator calculator) {
        this.setName("User2");        
        this.calculator = calculator;    
    }
    
    public void run() {
        calculator.setMemory(50);   
    }
}

12.4.2 동기화 메소드 및 동기화 블록

멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역(critical section)이라고 한다.

자바는 임계 영역을 지정하기 위해 동기화(synchronized) 메소드와 동기화 블록을 제공한다.

public synchronized void method() {
	임계 영역; //단 하나의 스레드만 실행
}

); 으로 변경 가능thr

public void method() {
  //여러 스레드가 실행 가능 영역
  synchronized(공유객체) {
  	임계 영역 // 단 하나의 스레드만 실행
  }
  //여러 스레드가 실행 가능 영역
}

ead.setName("

public class Calculator {
 private int memory;

 public int getMemory() {
  return memory;
 }

 public synchronized void setMemory(int memory) { 
  this.memory = memory;
  try {
   Thread.sleep(2000);
  } catch(InterruptedException e) {} 
  System.out.println(Thread.currentThread().getName() + ": " +  this.memory);
 }
}
public void setMemory(int memory) {
	synchronized(this){
		this.memory = memory;
		try{
			Thread.sleep(2000);
		}catch(InterruptedException e){ }
        System.out.println(Thread.currentThread().getName() + ": " +  this.memory + "저장");
    }
}

12.5 스레드(Thread) 상태

스레드의 일반적인 상태

일시 정지 상태는 WAITING, TIMED_WAITING, BLOCKED 가 있습니다. 

public class StatePrintThread extends Thread { 

 private Thread targetThread;
 
 public StatePrintThread(Thread targetThread) { // 상태를 조사할 스레드
  this.targetThread = targetThread;
 }

 public void run() {
  while(true) {
   Thread.State state = targetThread.getState(); //스레드 상태 얻기
   System.out.println("타겟 스레드 상태: " + state);

   if(state == Thread.State.NEW) {//객체 새성 상태일 경우, 실행 대기 상태로 만듬
    targetThread.start(); 
   }
   
   if(state == Thread.State.TERMINATED) { //종료 상태일 경우 while문을 종료
    break; 
   }
   try {
    //0.5초간 일시 정지듬
    Thread.sleep(500);
   } catch(Exception e) {}
  }
 }
}

12.스레드 이름"); 11225.으로 변경

public class TargetThread extends Thread { 
 public void run() {
  for(long i=0; i<1000000000; i++) {}
  
  try {
   //1.5초간 일시 정지
   Thread.sleep(1500);
  } catch(Exception e) {}

  for(long i=0; i<1000000000; i++) {}
 }
}

public class ThreadStateExample {
 public static void main(String[] args) {
  StatePrintThread statePrintThread = new StatePrintThread(new TargetThread());
  statePrintThread.start();
 }
}

12.6 스레드 상태 제어

실행 중인 스레드의 상태를 변경하는 것을 스레드 상태 제어라고 한다.

 

12.6.1 주어진 시간동안 일시 정지 sleep()

try{
   Thread.sleep(1000);
}catch(InterruptedException e){
  //interrupt() 메서드가 호출되면 실행
}

12thread.setN1

import java.awt.Toolkit;

public class SleepExample {
 public static void main(String[] args) {
  Toolkit toolkit = Toolkit.getDefaultToolkit();  
  for(int i=0; i<10; i++) {
   toolkit.beep();
   try {
    Thread.sleep(1500); // 1.5초동안 메인 스레드를 일시 정지 상태로 만듭니다
   } catch(InterruptedException e) {   
   }  
  } 
 }
}

12.6.2 다른 스레드에게 실행 양보 yield() 

public void run() {
	while(true) {
    	if(work) {
            System.out.println("Thread1 작업 내용");
		}
	}
}

스레드가 시작되어 run() 메서드를 실행하면 while(true) {} 블록이 무한 루프를 돌게 됩니다.  만약 work의 값이 false 라면 그리고 work 값이 false 에서 true로 변경되는 시점이 불분명하다면,  while문은 어떤한 실행문도 실행하지 않고 무의미한 반복을 한다.

public void run() {
   while(true) {
        if(work) {
            System.out.println("Thread1 작업 내용");
        } else {
            Thread.yield();
        }
   }
}

12.6.3 다른 스레드의 종료를 기다림 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 JoinExample {
 public static void main(String[] args) {
  SumThread sumThread = new SumThread();
  sumThread.start();
  try {
   sumThread.join();
  } catch (InterruptedException e) {
  }
  
  System.out.println("1~100 합: " + sumThread.getSum());

 }

}

12.6.4 스레드 간 협업 wait(), notify(), notifyAll()

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) {
  }
 }
}

11ame("1스

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++) {
   workObject.methodA();
  }//공유 객체의 methodA()를 10번 반복
 }
}

12.111111

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++) {
   workObject.methodB();
  }//공유 객체의 methodB()를 10번 반복
 }
}

thread.

public class WaitNotifyExample {
 public static void main(String[] args) {
  WorkObject sharedObject = new WorkObject(); // 겅우 객체 생성
  ThreadA threadA = new ThreadA(sharedObject);
  ThreadB threadB = new ThreadB(sharedObject);
  threadA.start();
  threadB.start(); 스래드 A,B 실행
 }
}

12.6.5 스레드의 안전한 종료 stop플래그, interrupt()

스레드는 자신의 run() 메서드가 모두 실행되면 자동적으로 종료된다. 

 

스레드의 안전한 종료 stop플래그, interrupt()

 스레드는 자신의 run() 메서드가 모두 실행되면 자동적으로 종료됩니다.

경우에 따라 실행 중인 스레드를 즉시 종료할 필요가 생길 수 있습니다. 

예를 들면 동영상을 끝까지 보지 않고 사용자가 멈춤을 요구할 경우가  있을 수 있습니다. 

Thread는 스레드를 즉시 종료시키기 위해 stop()메서드를 제공하지만 

이 메서드는 Deprecated되어있습니다. 

 =>아래 두가지 방법은 스레드를 즉시 종료시키기 위한 최선의 방법입니다.

 

stop 플래그를 이용하는 방법

public class XXXThread extends Thread {
     private boolean stop; // stop 플래그 필드
     public void run() {
        while(!stop){ stop가 true가 되면 run() 종료
           스레드가 반복 실행하는 코드;
        }
       //스레드가 사용한 자원 정리
   }
}

setN112.2a

public class StopFlagExample {
 public static void main(String[] args)  {
  PrintThread1 printThread = new PrintThread1();
  printThread.start();
  try {
   Thread.sleep(1000);
  } catch (InterruptedException e) {
  }

  printThread.setStop(true);
 }
}

d.setName("스레드 이름"); 으로 변경 가

public class PrintThread1 extends Thread {
 private boolean stop;

 public void setStop(boolean stop) {
   this.stop = stop;
 }

 public void run() { 
  while(!stop) {
   System.out.println("실행 중");
  } 

  System.out.println("자원 정리");
  System.out.println("실행 종료");
 }
}

interrupt() 메서드를 이용하는 방법

public class InterruptExample {

 public static void main(String[] args)  {

  Thread thread = new PrintThread2();
  thread.start();
  try {
   Thread.sleep(1000);
  } catch (InterruptedException e) {
  }

  thread.interrupt();
 }
}
public class PrintThread2 extends Thread {
 public void run() { 
  try {
   while(true) {
    System.out.println("실행 중");
    Thread.sleep(1);
   } 
  } catch(InterruptedException e) {  
  }  

  System.out.println("자원 정리");
  System.out.println("실행 종료");
 }
}

12.7 데몬 스레드

데몬(daemon) 주 스레드의 작업 돕는 보조적인 역할 수행하는 스레드이다.

현재 실행 중인 스레드가 데몬 스레드인지 아닌지 구별하는 방법은 isDaemon()메소드의 리턴값을 조사해보면 된다.

public class AutoSaveThread extends Thread {
 public void save() {
  System.out.println("작업 내용을 저장함.");
 }

 @Override
 public void run() {
  while(true) {
   try {
    Thread.sleep(1000);
   } catch (InterruptedException e) {
    break;
   }
   save(); // 1초 주기로 save()메서드 호출
  }
 }
}
public class DaemonExample {
 public static void main(String[] args) {
  AutoSaveThread autoSaveThread = new AutoSaveThread();
  autoSaveThread.setDaemon(true); // 데몬스레드로 만들기
  autoSaveThread.start();
  try {
   Thread.sleep(3000); // 메인 스레드가 종료되면 데몬 스레드도 같이 종료
  } catch (InterruptedException e) {
  }
  System.out.println("메인 스레드 종료");
 }
}

12.8 스레드 그룹

관련된 스레드를 묶어서 관리할 목적으로 이용된다.

 

12.8.1 스레드 그룹 이름 얻기

public class ThreadInfoExample {

 public static void main(String[] args) {
  AutoSaveThread autoSaveThread = new AutoSaveThread();
  autoSaveThread.setName("AutoSaveThread");
  autoSaveThread.setDaemon(true);
  autoSaveThread.start();
  Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
  Set<Thread> threads = map.keySet(); 
  
  for(Thread thread : threads) {
   System.out.println("Name: " + thread.getName() + ((thread.isDaemon())?"(데몬)": "(주)"));
   System.out.println("\t" + "소속그룹: " + thread.getThreadGroup().getName());
   System.out.println();
  }
 }
}

12.8.2 스레드 그룹 생성

Thread t = new Thread(ThreadGroup group, Runnable target);
Thread t = new Thread(ThreadGroup group, Runnable target, String name);
Thread t = new Thread(ThreadGroup group, Runnable target, String name, long stackSize);
Thread t = new Thread(ThreadGroup group, String name);

12.8.3 스레드 그룹의 일괄 interrupt()

스레드 그룹에서 제공하는 interrupt() 메서드를 이용하면 그룹 내에 포함된 모든 스레드들을 일괄 interrupt()할 수 있다.

public class WorkThread extends Thread {

 public WorkThread(ThreadGroup threadGroup, String threadName) {
  super(threadGroup, threadName);
 }

 @Override
 public void run() {
  while(true) {
   try {
    Thread.sleep(1000);
   } catch (InterruptedException e) {
    System.out.println(getName() + " interrupted");
    break;
   }
  }
  System.out.println(getName() + " 종료됨");
 }
}
public class ThreadGroupExample {

 public static void main(String[] args) {
  ThreadGroup myGroup = new ThreadGroup("myGroup");
  WorkThread workThreadA = new WorkThread(myGroup, "workThreadA");
  WorkThread workThreadB = new WorkThread(myGroup, "workThreadB");

  workThreadA.start();
  workThreadB.start();

  System.out.println("[ main 스레드 그룹의 list() 메소드 출력 내용 ]");
  ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();  
  mainGroup.list();
  System.out.println();

  try {
   Thread.sleep(3000);
  } catch (InterruptedException e) {
  }

  System.out.println("[ myGroup 스레드 그룹의 interrupt() 메소드 호출 ]");
  myGroup.interrupt();
 }
}

12.9 스레드풀

스레드 폭증으로 일어나는 현상

병렬 작업 처리가 많아지면 스레드 개수 증가

스레드 생성과 스케줄링으로 인해 CPU바빠짐

메모리 사용량이 늘어남

애플리케이션의 성능 급격히 저하

 

스레드 풀(Thread Pool)

작업 처리에 사용되는 스레드를 제한된 개수만큼 미리 생성

작업 큐(Queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리

작업 처리가 끝난 스레드는 작업 결과를 애플리케이션으로 전달

스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리

 

12.9.1 스레드풀 생성 및 종료

스레드풀 생성

ExecutorService 구현 객체는 Executors Class의  두 메소드 중 하나를 이용해서 간편하게 생성할 수 있다.

ExecutorService threadPool= new ThreadPoolExecutor (

     3, // 코어 스레드 수

     100, // 최대 스래드 수

     120L, // 놀고 있는 시간

     TimeUnit.SECONDS, // 놀고 있는 시간 단위

     new SynchronousQueue<Runnable>() // 작업 큐

)

 

12.9.2 작업 생성과 처리 요청

작업 생성 

작업 처리 요청

execute()

스레드 종료 후 해당 스레드 제거

스레드 풀은 다른 작업 처리를 위해 새로운 스레드 생성

submit()

스레드가 종료되지 않고 다음 작업 위해 재사용

 

12.9.3 블로킹 방식의 작업 완료 통보 받기

Future 객체

작업 결과가 아니라 지연 완료(pending completion) 객체

작업이 완료될까지 기다렸다가 최종 결과를 얻기 위해 get() 메소드 사용

UI 변경과 같은 스레드에 사용 불가 

 

12.9.4 콜 백 방식의 작업 완료 통보 받기

콜백이란 애플리케이션이 스레드에게 작업 처리를 요청한 후, 스레드가 작업을 완료하면 특정 메소드를 자동 실행하는 기법을 말한다.

 

반응형

' > 이것이 자바다' 카테고리의 다른 글

15. 컬렉션 프레임워크  (0) 2020.10.13
13. 제네릭 14. 람다식  (0) 2020.10.10
11. 기본 API 클래스  (0) 2020.10.02
10. 예외처리  (0) 2020.10.01
09. 중첩 클래스와 중첩 인터페이스  (0) 2020.10.01

+ Recent posts