chapter12
멀티 스레드(Multi Thread)
멀티 스레드 개념
-
운영체제에서는 실행 중인 하나의 어플리케이션을 프로세스(process)라고 부른다. 사용자가 어플리케이션을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당받아 에플리케이션의 코드를 실행하는 데 이것이 프로세스(process)이다.
-
멀티 태스킹(Multi Tasking)은 두 가지 이상의 작업을 동시에 처리하는 것을 말하는데, 운영체제는 멀티 태스킹을 할 수 있도록 CPU 및 메모리 자원을 프로세스마다 적절히 할당해주고, 병렬로 실행시킨다. 예를 들어, 웹 사이트를 구경하면서 동시에 미디어 플레이어로 음악을 듣는 것이 멀티 태스킹이다.
-
멀티 태스킹은 꼭 멀티 프로세스를 뜻하지 않는다. 하나의 어플리케이션에서 병렬로 두가지 이상의 일을 동시에 하는 것도 멀티 태스킹이라 할 수 있다. 예를 들어, 미디어 플레이어는 동영상 재생과 음악 재생이라는 두가지 작업을 동시에 처리하고, 메신저는 채팅 기능을 수행하면서 동시에 파일 전송을 처리할 수 있다. 어떻게 하나의 어플리케이션에서 두가지 이상의 작업을 동시에 처리할 수 있을까? 그 해답은 바로 멀티 스레드에 있다.
스레드(thread)?
: 스레드(thread)란 사전적 의미로 한 가닥의 실이라는 뜻인데, 한 가지 작업을 실행하기 위해 순차적으로 실행할 코드를 실처럼 이어놓았다고 해서 유래된 이름이다. 하나의 스레드는 하나의 코드 실행 흐름을 뜻하므로, 만약 한 프로세스 내에 스레드가 2개라면 2개의 코드 실행 흐름이 생긴다는 것이고 동시에 2개의 작업을 처리한다는 것을 의미한다.
- 멀티 프로세스가 어플리케이션 단위의 멀티 캐스팅이라면 멀티 스레드는 애플리케이션 내부에서의 멀티 태스킹이라고 할 수 있다.
- 멀티 스레드: 하나의 프로세스에 메인 스레드외에 또 스레드가 존재하여 병렬적으로 작업이 처리되는 것을 말함.
=> 주의할 점: 멀티 프로세스들은 운영체제에서 할당받은 자신의 메모리를 가지고 실행하기 때문에 서로 독립적이다. 하지만 멀티 스레드는 한 프로세스 내에서 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에게 영향을 미치게 된다. 따라서 멀티 스레드에서는 예외처리에 대해 보다 만전을 기해야 한다.
(예를 들어, 멀티 프로세스인 워드와 엑셀을 동시에 사용하던 도중, 워드에 오류가 생겨 먹통이 되더라도 엑셀은 여전히 사용 가능하다. 그러나 멀티 스레드로 동작하는 메신저의 경우 파일을 전송하는 스레드에서 예외가 발생되면 메신저 프로세스 자체가 종료되기 때문에 채팅 스레드도 같이 종료된다.)
=> 멀티 스레드는 다양한 곳에 사용된다. 사용되는 곳의 예:
- 대용량 데이터의 처리 시간을 줄이기 위해 데이터를 분할해서 병렬로 처리하는 곳에서 사용된다.
- UI를 가지고 있는 애플리케이션에서 네트워크 통신을 하기 위해 사용된다.
- 다수 클라이언트의 요청을 처리하는 서버를 개발할 때에도 사용된다.
메인 스레드
- 모든 자바 에플리케이션은 메인 스레드(main thread)가 main() 메소드를 실행하면서 시작된다.
=> 메인 스레드는 main() 메소드의 첫 코드부터 아래로 순차적으로 실행하고, main() 메소드의 마지막 코드를 실행하거나 return문을 만나면 실행이 종료된다.
- 메인 스레드는 필요에 따라 작업 스레드를 만들어서 병렬로 코드를 실행할 수 있다. 즉 멀티 스레드를 생성해서 멀티 태스킹을 수행한다.
- 싱글 스레드 애플리케이션에서는 메인 스레드가 종료되면 프로세스도 종료된다. 하지만 멀티 스레드 애플리케이션에서는 실행 중인 스레드가 하나라도 있으면, 프로세스는 종료되지 않는다. 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않는다.
작업 스레드 생성과 실행
Thread 클래스로부터 직접 생성
메인스레드만 사용한 경우
//메인 스레드만 사용한 경우 => 작업의 병렬 처리가 이루어지지 않는다.
public class BeepPrintExample {
public static void main(String[] args) {
for(int i=0;i<5;i++){
//비프음 발생.
System.out.println("땅");
try {
Thread.sleep(500);
} catch (InterruptedException e) { // Thread의 메소드 사용은 InterruptedException 발생.
e.printStackTrace();
}
}
for(int i=0; i<5;i++){
System.out.println("띵");
try {
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
비프음을 들려주는 작업 정의
// 비프음을 들려주는 작업 정의
public class BeepTask implements Runnable {
@Override
public void run() {
for(int i=0; i<5; i++){
System.out.println("띵");
try {
Thread.sleep(500); // 스레드 실행 내용.
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
BeepTask 클래스 사용 예
public class BeepExample {
public static void main(String[] args) {
BeepTask 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 (InterruptedException e) { // Thread의 메소드 사용은 InterruptedException 발생.
e.printStackTrace();
}
}
}
}
익명 객체 사용
public class BeepExample2 {
public static void main(String[] args) {
//익명 객체 사용
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0; i<5; i++){
System.out.println("띵");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
for(int i = 0; i<5; i++){
System.out.println("땅");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
람다식 사용
public class BeepExample3 {
public static void main(String[] args) {
//람다식 사용
Thread thread = new Thread(()->{
for(int i =0; i<5; i++) {
System.out.println("땅");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
for(int i=0; i<5; i++){
System.out.println("띵");
try{
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
Thread 하위 클래스로부터 생성
- 작업 스레드가 실행할 작업을 Runnable로 만들지 않고, Thread의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을 포함시킬 수도 있다.
( extends Thread )
public class WorkderThread extends Thread {
@Override
public void run() {
//스레드가 실행할 코드
}
}
Thread thread = new WorkderThread();
// Thread의 run() 메소드를 재정의한다.
익명 자식 객체로도 생성가능 하다.
Thread thread2 = new Thread(){
@Override
public void run() {
//스레드가 실행할 코드
}
};
=> 이렇게 생성된 작업 스레드 객체에서 start() 메소드를 호출하면 작업 스레드는 자신의 run() 메소드를 실행하게 된다.
thread.start();
Thread의 자식 클래스 만들어서 사용하기
public class WorkderThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
메인에서
public class WorkerExample {
public static void main(String[] args) {
Thread thread = new WorkderThread();
thread.start();
for(int i=0; i<5; i++){
System.out.println("땅");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
익명 자식 객체 사용
public class WorkerExample2 {
public static void main(String[] args) {
// 익명 자식 객체 사용
Thread thread = new Thread(){
@Override
public void run() {
for(int i =0; i<5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
for(int i=0; i<5; i++){
System.out.println("땅");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
스레드의 이름 => 스레드는 자신만의 이름이 있는데, 스레드의 이름이 큰 역할을 하는 건 아니지만 디버깅할 때 어떤 스레드가 어떤 작업을 하는지 조사할 목적으로 가끔 사용된다. 메인 스레드는 “main”이라는 이름을 가지고 있고, 우리가 직접 생성한 스레드는 자동적은 “Thread-n”이라는 이름으로 설정된다. n은 스레드의 번호를 말한다.
=> Thread-n 대신 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메소드로 변경하면 된다.thread.setName("스레드 이름");
만약 스레드의 이름을 리턴받고 싶다면 Thread 클래스의 getName() 메소드를 호출하자.
thread.getName();
- setName()과 getName() 메소드는 Thread의 인스턴스 메소드이므로 스레드 객체의 참조가 필요한데, 만약 스레드 객체의 참조를 가지고 있지 않다면, Thread 클래스의 currentThread()로 코드를 실행하는 현재 스레드의 참조를 얻을 수 있다.
Thread thread = Thread.currentThread();
클래스 Thread의 자식 클래스 ThreadA
public class ThreadA extends Thread{
ThreadA(){
setName("ThreadA");
}
@Override
public void run() {
for(int i =0; i<2; i++)
System.out.println(getName() + "가 출력한 내용");
}
}
클래스 Thread의 자식 클래스 ThreadB
public class ThreadB extends Thread {
@Override
public void run() {
for(int i=0; i<2; i++)
System.out.println(getName() + "가 출력한 내용");
}
}
메인에서 실행
public class ThreadExample {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
System.out.println("프로그램 시작 스레드 이름: "+mainThread.getName());
Thread threadA = new ThreadA();
System.out.println("작업 스레드1 이름: " + threadA.getName());
threadA.start();
Thread threadB = new ThreadB();
System.out.println("작업 스레드2 이름: " + threadB.getName());
threadB.start();
}
}
//실행내용
//프로그램 시작 스레드 이름: main
//작업 스레드1 이름: ThreadA
//작업 스레드2 이름: Thread-1 => setName()메소드로 지정하지않으면 getName()의 리턴값이 Thread-n 형식임을 알 수 있다.
//ThreadA가 출력한 내용
//ThreadA가 출력한 내용
//Thread-1가 출력한 내용
//Thread-1가 출력한 내용
스레드 우선순위
-
동시성(Concurrency): 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 성질 => cpu의 속도가 워나 빨라 병렬적으로 이루어지는 것처럼 보이지만 한개의 코어(cpu)일 경우 시분할 정책 등을 이용하여 스레드를 번갈아가며 처리한다.
-
병렬성(Parallelism): 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질 => 한 컴퓨터에 복수개의 cpu가 있는 경우를 멀티 코어라고 하는데, 이 경우 하나의 cpu당 하나의 스레드를 맡을 수 있어 여러개의 스레드를 병렬적으로 처리할 경우 한 개의 코어보다 속도가 훨씬 빠르다. 하지만 오버헤드 때문에 딱 비례하게 빠르진 않는다고 들었다.
-
스레드의 개수가 코어의 수보다 많을 경우(즉 동시성이 강할 경우), 스레드를 어떤 순서에 의해 동시성으로 실행할 것인가를 결정해야 하는데, 이를 스레드 스케쥴링(Thread Scheduling)이라 한다. 스레드 스케쥴링에 의해 스레드들은 아주 짧은 시간에 번갈아가면서(시분할 정책) 그들의 run() 메소드를 조금씩 실행한다.
=> 자바의 스레드 스케쥴링은 우선순위(Priority) 방식과 순환 할당(Round-Robin)방식을 사용한다.
=> 우선순위(Priority) 방식은 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 하는 스케쥴링을 말한다. Priority 방식은 스레드 객체에 우선순위 번호를 부여할 수 있기 때문에 개발자가 제어할 수 있다.
=> 순환 할당(Round-Robin) 방식은 시간 할당량(Time Slice)을 정해서 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른 스레드를 실행하는 방식을 말한다.(모든 스레드를 동등하게) Round-Robin 방식은 자바 가상 기계(JVM)에 의해서 정해지기 때문에 코드로 제어할 수 없다.
Priority 방식
thread.setPriority(우선순위);
- 우선순위 방식에서 우선순위는 1에서부터 10까지 부여되는데 , 1이 가장 우선순위가 낮고, 10이 가장 높다.(명심)
- 우선순위의 매개값으로 1~10까지의 값을 직접 주어도 되지만, 다음과 같이 코드의 가독성(이해도)를 높이기 위해 Thread 클래스의 상수를 사용할 수도 있다.
thread.setPriority(Thread.MAX_PRIORITY); // 우선순위의 값 10을 가리킨다.
thread.setPriority(Thread.NORM_PRIORITY); // 우선순위의 값 5를 가리킨다.
thread.setPriority(Thread.MIN_PRIORITY); // 우선순위의 값 1을 가리킨다.
예제
public class CalcThread extends Thread {
CalcThread(String name){
setName(name); // 이름 변경
}
@Override
public void run() {
for(int i=0; i<200000000;i++){
}
System.out.println(getName()+"가 작업을 완료하였습니다.");
}
public static void main(String[] args) {
Thread[] thread = new CalcThread[10];
for (int i=0; i<10; i++){
thread[i] = new CalcThread(String.valueOf(i));
if(i!=9)// index가 9가 아닌 경우
thread[i].setPriority(MIN_PRIORITY);
else// index가 9인 경우
thread[i].setPriority(MAX_PRIORITY);
thread[i].start();
}
}
}
//실행내용 :
//9가 작업을 완료하였습니다.
//0가 작업을 완료하였습니다.
//8가 작업을 완료하였습니다.
//6가 작업을 완료하였습니다.
//5가 작업을 완료하였습니다.
//4가 작업을 완료하였습니다.
//3가 작업을 완료하였습니다.
//7가 작업을 완료하였습니다.
//2가 작업을 완료하였습니다.
//1가 작업을 완료하였습니다.
// => Priority가 가장 높은 index가 9인 CalcThread 객체가 제일 먼저 수행됨을 알 수 있다.
동기화 메소드와 동기화 블록
-
멀티 스레드 프로그램에서는 스레드들이 객체를 공유해서 작업해야 하는 경우가 있다. 이 경우, 스레드 A를 사용하던 객체가 스레드 B에 의해 상태가 변경될 수 있기 때문에 스레드 A가 의도했던 값과 다른 값이 산출될 수 있다. 즉, 호환성 문제(Coherency Problem)가 발생하는 것이다.(Coherency: 일관성, 일치성)
-
이는 마치 여러사람이 계산기 쓰는 상황과 같은데 사람 A가 계산기 가지고 계산하고 값을 저장한뒤 자리를 비웠을때 사람 B가 같은 계산기를 가지고 다른 계산을 하고 메모리에 저장한 값을 다른 값으로 저장하는 상황과 같다. 즉 A가 돌아왔을때는 원하는 값이 산출되지 않으므로 일관성, 호환성에 문제가 생기는 것이다.
-
예를 들어, 스레드1과 스레드2가 동시에 작업을 처리하고 있다고 가정하자. 스레드 1이 memory에 값을 100을 저장하고 sleep() 메소드에 의해 2초간 잠재워지고, 스레드 2가 memory의 값에 50을 저장하고 sleep()메소드에 의해 2초간 잠재워진다고 가정해보자. 이 경우 스레드1과 스레드2의 경우 모두 메모리의 값이 50이 산출될 것이다. 즉, 스레드1이 원하는 메모리 값이 산출되지않은 것이다.
동기화가 안된 Calculator
public class Calculator {
private int memory;
public int getMemory(){
return memory;
}
void setMemory(int memory){
this.memory = memory;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("스레드 이름: "+ Thread.currentThread().getName()+ " ,memory 값: " + this.memory);
}
}
메인 함수
public class MainThreadExample {
public static void main(String[] args) {
Calculator2 calculator = new Calculator2();
Thread user1 = new User1(calculator);
user1.start();
Thread user2 = new User2(calculator);
user2.start();
}
}
// 실행 내용:
//스레드 이름: User1 ,memory 값: 50
//스레드 이름: User2 ,memory 값: 50
해결책: 동기화(synchronized) 메소드, 동기화(synchronized) 블록.
- 멀티 스레드가 공유하는 코드인데, 둘 이상의 스레드가 동시에 접근해서는 안되는 코드 영역을 임계 영역(Critical Section)이라고 한다. 자바는 Critical Section을 지정하기 위해 동기화(synchronized) 메소드와 동기화 블록을 제공한다.
- 스레드가 객체 내부의 동기화 메소드 또는 블록에 들어가면 즉시 객체에 잠금을 걸어 다른 스레드가 임계 영역(Critical Section) 코드를 실행하지 못하도록 한다.
- synchronized 키워드는 인스턴스와 정적(static) 메소드 어디든 붙일 수 있다.
동기화 메소드
public synchronized void method(){
//임계 영역 -> 단 하나의 스레드만 실행
}
=> 동기화 메소드는 메소드 전체 내용이 임계영역 이므로 스레드가 동기화 메소드를 실행하는 즉시 객체에는 잠금이 일어나고, 스레드가 동기화 메소드를 실행 종료하면 잠금이 풀린다.
메소드의 일부 내용만 임계영역으로 만들고 싶은 경우: 동기화 블록 사용
public void method(){
//여러 스레드가 실행가능 영역
synchronized(공유 객체){ // 공유 객체가 클래스 자신이면 this를 넣으면 된다.
//임계 영역 -> 단 하나의 스레드만 실행
}
//여러 스레드가 실행가능 영역
}
=> 동기화 블록 밖의 외부코드들은 여러 스레드가 동시에 실행할 수 있지만, 동기화 블록 내부의 코드는 임계영역이므로 한번에 하나의 스레드만 실행할 수 있고 다른 스레드는 실행할 수 없다
- 만약 동기화 메소드와 블록이 여러개 있는 경우, 하나의 스레드가 이들 중 하나를 선택할때 다른 스레드들은 그 하나도 물론이고 다른 동기화 메소드나 블록을 실행할 수 없다. 이 경우에도 다른 메소들은 오직 동기화가 안된 일반 메소드만 실행 가능하다.
동기화 메소드로 동기화한 Calculator
public class Calculator {
private int memory;
public int getMemory(){
return memory;
}
// 동기화 메소드 : 동기화...잠금이 일어나는 것은 좋지만 너무 느린데..?
synchronized void setMemory(int memory){
this.memory = memory;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("스레드 이름: "+ Thread.currentThread().getName()+ " ,memory 값: " + this.memory);
}
}
=> User1 스레드는 Calculator 객체의 동기화 메소드인 setMemory()를 실행하는 순간 Calculator 객체를 잠근다. 따라서 메인 스레드가 아무리 User2 스레드를 실행시킨다한들, Calculator 객체의 setMemory() 메소드가 잠겨있기때문에 이 메소드를 실행시키지 못한다. 이후 User1 스레드가 setMemory() 메소드를 다 실행시키고 난 후에(int memory= 100을 출력) 비로소 동기화 메소드인 setMemory()가 잠금이 풀리게 되고 이후 User2가 접근하여 setMemory() 메소드를 실행시키고 또 잠근이 일어나게 된다. 이후 User2 스레드는 memory = 50을 출력하게 되어 두 개의 스레드 모두 자신이 원하는 memory 값을 얻게 되었다.
동기화 블록
public class Calculator2 {
private int memory;
public int getMemory(){
return memory;
}
void setMemory(int memory){
synchronized (this) {
try {
Thread.sleep(500); //Coherency가 깨질 수 있으므로 앞에도 sleep() 메소드를 호출한다.
} catch (InterruptedException e) {
e.printStackTrace();
}
this.memory = memory;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("스레드 이름: " + Thread.currentThread().getName() + " ,memory 값: " + this.memory);
}
}