본문으로 바로가기

Multi Thread(멀티스레드)

category JAVA 2019. 12. 13. 18:11

멀티스레드 프로그래밍


스레드와 프로세스

  • Application(애플리케이션) : 컴퓨터에 설치하여 사용하는 이클립스, 워드 프로세서, 브라우저 등의 코드 덩어리
  • Process(프로세스) : 설치된 애플리케이션을 실행하게 되면 운영체제(os)로 부터 메모리의 일정 영역을 할당 받고 CPU와 HDD를 이용해서 동작하는데 이것을 프로세스라고 부른다.
  • Multi Thread(멀티 스레드) : 일반적으로 운영체제들은 동시에 여러 프로세스를 실행시킬 수 있는데 이것을 멀티 프로세스 라고 한다.
    예 ) 음악을 들으며 인터넷을 하는 것
  • Thread(스레드) : 프로세스 동작의 최소 단위이다.
     - 모든 프로세스는 하나 이상으로 스레드로 구성된다.
     - 둘 이상의 스레드로 구성된 프로세스를 멀티스레드 프로세스라고 부른다.
     - 예) 메신저에서 채팅을 치면서 파일을 전송하는 것.
     - 여러개의 스레드가 동시에 동작할 수 없다. 단지, 여러개의 스레드가 아주 빠른 속도로 번갈아가며 동작하기 때문에 우리의 눈에는 동시에 동작하는 것 처럼 보일 뿐이다.

멀티 프로세스

 

 

 

멀티스레드 프로그래밍의 장/단점

(1) CPU 사용률 향상

  • 멀티스레드를 사용하면 동시에 여러개의 스레드를 실행시키기 위해 CPU가 아주 바쁘게 움직이다.
  • 즉, CPU의 활용률을 높일 수 있다.

 

(2) 작업의 분리로 응답성 향상

  • 멀티스레드 프로그램은 작업을 스레드 단위로 분리해서 병렬 수행할 수 있게한다.
  • 사용자의 입력을 기다리거나 파일 전송 등 시간이 걸리고 무거운 작업을 메인 스레드에서 분리하여 수행함으로써 애플리케이션의 응답성을 향상할 수 있다.

 

(3) 자원의 공유를 통한 효율성 증대

  • 하나의 프로세스 안에 있는 스레드들은 그 프로세스의 자원을 공유한다.
  • 예) 많은 학생의 시험 점수를 처리하기 위해 배열에 관리한다면 점수 입력 스레드와 점수 출력 스레드가 동일한 배열을 사용할 수 있다.
  • 따라서 효율적인 자원활용이 가능하다.

 

(4) 컨텍스트 스위칭 비용 발생

멀티스레드 프로그래밍은 작업전환(컨텍스트 스위칭)과정에서 싱글스레드에서는 필요없는 비용이 발생한다.

 

하나의 싱글 코어 CPU환경에서 A와 B작업을 수행한다고 생각해보자

두 작업을 순차적(Sequential)으로 수행하면 작업시간은 A+B가 된다.

두 작업을 순차적(Sequential)으로 수행

 

두 작업을 스레드로 만들어 수행하면 A와 B 작업을 교대하는 과정에서 컨텍스트 스위칭 비용이 발생한다.

이렇게 진행되는 형태를 병행(Concurrent)프로그래밍이라고 한다.

 

                              사진 수정 필요

결과적으로 작업시간 외에 추가 비용이 발생한 것을 알 수 있다.

 

 

(5) 스레드 제어의 어려움

싱글 스레드 프로그래밍에서는 제어에 대해 고민할 필요가 없었지만

멀티스레드 프로그래밍에서는 각 스레드의 상태를 파악하며 제어하기가 쉽지 않다.

 

 

 

스레드 생성과 수행


스레드 생성

  1. Runnable 인터페이스를 구현하는방법
  2. Thread 클래스를 상속 받는 방법

(1) Runnable 인터페이스를 구현

Runnable 인터페이스틑 전형적인 함수형 인터페이스로 익명클래스, 람다식을 이용해서 쉽게 작성할 수 있다.

1. Runnable 인터페이스를 구현한  클래스를  정의한다.  

2. run() 메소드를 오버라이드 한다.
3. Thread 클래스의 생성자로  Runable 인터페이스를 구현한 클래스로 생성한 객체를 전달해서 Thread 객체를 생성한다.
4. 생성한 Thread 객체의 start() 메소드를 호출하면 새로운 스레드가 시작된다.

 

[예시1]

public class MainClass02 {
	
	public static void main(String[] args) {
		System.out.println("main 스레드 시작");
		//3,4. Thread 클래스의 생성자로  Runable 인터페이스를 구현한 클래스로 생성한 객체를 전달해서 
		//Thread 객체를 생성하고, start()메서드를 호출하여 새로운 스레드가 시작된다.
		new Thread(new DanceThread()).start();
		draw();
		System.out.println("main 스레드 끝");
	}
	
	//main 스레드의 흐름
	public static void draw() {
		System.out.println("5초 동안 그림 그리기");
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	//1. Runnable 인터페이스를 구현한  클래스를  정의한다.
	static class DanceThread implements Runnable{
		
		//2. run()메소드를 오버라이드한다.
		@Override
		public void run() {
			System.out.println("새로운 스레드 시작");
			int count=0;
			while (true) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				count++;
				System.out.println(count+" 번째 춤을 취요!");
				if(count==10)break;
			}
			System.out.println("새로운 스레드 끝");
		}
		
	}//class DanceThread implements Runnable
}

 

  • 익명의 local inner class 를 이용하기
public class MainClass04 {
	
	public static void main(String[] args) {
		System.out.println("main 스레드 시작");
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				System.out.println("새로운 스레드 시작");
				int count=0;
				while (true) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					count++;
					System.out.println(count+" 번째 춤을 취요!");
					if(count==10)break;
				}
				System.out.println("새로운 스레드 끝");
			}
		}).start();
		draw();
		System.out.println("main 스레드 끝");
	}
	
	//main 스레드의 흐름
	public static void draw() {
		System.out.println("5초 동안 그림 그리기");
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

 

  • 람다식으로 Runnabel 인터페이스 구현하기
package Tread;

public class MainClass05 {
	
	public static void main(String[] args) {
		
		System.out.println("main 스레드 시작");
		
		new Thread(()->{
				System.out.println("새로운 스레드 시작");
				int count=0;
				while (true) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					count++;
					System.out.println(count+" 번째 춤을 취요!");
					if(count==10)break;
				}
				System.out.println("새로운 스레드 끝");
		}).start();
		draw();
		System.out.println("main 스레드 끝");
	}
	
	//main 스레드의 흐름
	public static void draw() {
		System.out.println("5초 동안 그림 그리기");
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

 

 

(2) Thread 클래스를 상속

  • Thread 클래스는 Runnable 인터페이스를 구현하고 있다.
    따라서 별도로 Runnable 객체를 파라미터로 넣을 필요 없이 Thread 클래스만 가지고 스레드를 만들 수 있다.
    1. Thread 클래스를 상속받은 클래스를 정의한다.
    2. run()메소드를 오버라이드 한다.
static class CounterThread extends Thread{...}

 

[예시]

public class MainClass01 {
	
	public static void main(String[] args) {
		System.out.println("main 스레드 시작");
		//3. 만든 클래스를 이용해서 객체를 생성하고 start() 메소드를 호출하면 
		//생성된 객체의 run() 메소드에서 새로운 작업 단위가 시작된다.
		new CounterThread().start();
		draw();
		System.out.println("main 스레드 끝");
	}
	
	//메인 스레드의 흐름
	public static void draw() {
		System.out.println("5초 동안 그림 그리기");
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	//1. Thread 클래스를 상속받은 클래스를 정의한다.
	static class CounterThread extends Thread{
		//2. run() 메소드를 오버라이드 한다.
		@Override
		public void run() {
			System.out.println("새로운 스레드가 시작됨");
			int count=0;
			while(true) {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}//try
				count++;
				System.out.println(count);
				if(count==10)break;
				
			}//while
		}
	}
}

 

  • 익명의 local inner class 를 이용하기
ublic class MainClass03 {
	public static void main(String[] args) {
		System.out.println("main 메소드가 시작 되었습니다.");
		
		//익명의 local inner class 를 이용하기
		//static class DanceThread implements Runnable 클래스를 따로 선언하지 않고 
		//new Thread() 객체를 생성하면서 
		new Thread() {
			public void run() {
				System.out.println("새로운 스레드가 시작됨");
				int count=0;
				while (true) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					count++;
					System.out.println(count);
					if(count==10)break;
				}
				System.out.println("새로운 스레드가 끝");
			}//run()
		}.start();
		draw();
		System.out.println("main 메소드가 종료 되었습니다.");
	}
	
	public static void draw() {
		System.out.println("5초 동안 그림 그리기");
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}//public static void draw()
}

 

 

 

스레드의 실행

  • 스레드를 실행할때는 override한 run() 메서드를 호출하는 것이 아니라 Thread 클래스에 선언된 start()메서드를 호출해야한다.
    (run()메서드를 호출하는 것은 mainMethod의 흐름을 변경하는 것이다.)
  • 한번 사용한 스레드는 다시 재사용할 수 없다. 항상 new 키워드를 통해 새로운 스레드 객체를 생성해주어야함.
    한번 사용된 스레드는 버려진다.

[예시]

mainTread에서는 5초동안 그림을 그리면서, 동시에 카운드 세기

package test.main;

public class MainClass01 {
	public static void main(String[] args) {
		System.out.println("main 메소드가 시작 되었습니다.");
		// 3. 만든 클래스를 이용해서 객체를 생성하고, start() 메소드를 호출하면, 생성된 객체의 run() 메소드에서 새로운 작업 단위가 시작된다.
		new CounterThread().start();  //run()메서드는 main thread를 보내는 것 이기 때문에 반드시 start()메서드를 사용해야한다.
		draw();
		System.out.println("main 메소드가 종료 되었습니다.");
	}
	
	public static void draw() {
		System.out.println("5초 동안 그림 그리기");
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	/*
	 * - 새로운 작업단위(Thread) 만들기
	 *   동시에 어떤 작업을 처리하기 위해 이thread를 사용한다.
	 * 1. Thread 클래스를 상속받은 클래스를 정의한다.
	 * 2. run() 메소드를 오버라이드 한다.
	 * 3. 만든 클래스를 이용해서 객체를 생성하고 start() 메소드를 호출하면 
	 * 	  생성된 객체의 run() 메소드에서 새로운 작업 단위가 시작된다.
	 */
	static class CounterThread extends Thread{   // 1. extends Thread : 스레드를 상속받는다.
		//2. run()메소드를 오버라이드 한다.
		@Override 
		public void run() {
			System.out.println("새로운 스레드가 시작됨");
			int count=0;
			while(true) {
				try {
					Thread.sleep(1000);
				}catch (InterruptedException e) {
					e.printStackTrace();
				}
				count++;
				System.out.println(count);
				if(count==10)break;
			} //while
			System.out.println("새로운 스레드가 끝남");
		}//run()
	}
}

 

[예시]

 

 

 

 

(1)run() vs start()

  • run() : 스레드에서 수행할 작업을 정의하는 메서드
  • start() : 스레드의 run()메서드가 호출될 수 있도록 준비하는 과정.
  • 실제 run()메서드를 호출하는 것은 JVM이다.
    JVM이 운영체제의 스레드 스케줄러에 의해 가능할 때 스레드의 run()메서드를 호출한다.
    따라서 우리 코드 상에서는 run()는 호출하지 않는 것이다.
    (예 : 직접 main()를 호출하지 않는 것)

 

(2)멀티스레드와 메모리

두 메서드가  호출되면서 동작하는 과정을 메모리 관점에서 단계적으로 살펴보자

 

[단계1] JVM의 메모리 구조는 stack과 heap영역으로 구성되어있다. 이 stack 공간은 Thread 별로 생성된다. 최초 애플리케이션이 구동되면 동작하는 메인 스레드도 하나의 스택을 자지한다. 스택에서는 프로세스의 힙/공유 자원을 자유롭게 이용할 수 있다. 잏 스택에는 메인 메서드가 호출하는 메서드들이 쌓였다 없어지기를 반복한다.

 

[단계2] 그러다가 메인 스레드가 t1 스레드의 start()메서드를 호출하면 t1 스레드를 위한 별도의 stack 공간을구성한다.
이 공간은 메인 메인스레드와는 전혀 무관한 t1 스레드 만의 공간이다. 따라서 메인 스레드가 t1스레드의 stack에 있는 자원을 사용할 수는 없다. 
하지만 힙/공유 자원은 두개의 스레드가 모두 접근 가능하다.

 

[3단계] 이제 새로운 스택에서 t1 스레드의 run()메서드가 실행되면서 기존의 메인 스레드와 전혀 별도로 존재하는 흐름이 생겨나게 되었다. 이후 두 스레드는 병렬(번갈아 가며)로 작업을 수행하게된다.
앞의 코드에서 메인메서드가 'main 메소드가 종료되었습니다.' 라고 출력 하고 종료되었음에도 아직 다른 스레드가 할일이 남아 있다면, 애플리케이션은 종료되지 않는다. 모든 스레드가 종료될 때 비로소 애플리케이션이 종료된다.

 

 

(그림 첨부)

 

 

 

스레드의 상태와 제어


 

 

 

 

 

 

 

멀티스레드의 문제점과 해결


 

 

 

 

 

 

 

 

스레드 풀(Thread Pool) 활용