[이것이자바다] chapter 12. 멀티 스레드(Multi Thread)

chapter12

멀티 스레드(Multi Thread)

멀티 스레드 개념

스레드(thread)?

: 스레드(thread)란 사전적 의미로 한 가닥의 실이라는 뜻인데, 한 가지 작업을 실행하기 위해 순차적으로 실행할 코드를 실처럼 이어놓았다고 해서 유래된 이름이다. 하나의 스레드는 하나의 코드 실행 흐름을 뜻하므로, 만약 한 프로세스 내에 스레드가 2개라면 2개의 코드 실행 흐름이 생긴다는 것이고 동시에 2개의 작업을 처리한다는 것을 의미한다.

=> 주의할 점: 멀티 프로세스들은 운영체제에서 할당받은 자신의 메모리를 가지고 실행하기 때문에 서로 독립적이다. 하지만 멀티 스레드는 한 프로세스 내에서 하나의 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있어 다른 스레드에게 영향을 미치게 된다. 따라서 멀티 스레드에서는 예외처리에 대해 보다 만전을 기해야 한다.
(예를 들어, 멀티 프로세스인 워드와 엑셀을 동시에 사용하던 도중, 워드에 오류가 생겨 먹통이 되더라도 엑셀은 여전히 사용 가능하다. 그러나 멀티 스레드로 동작하는 메신저의 경우 파일을 전송하는 스레드에서 예외가 발생되면 메신저 프로세스 자체가 종료되기 때문에 채팅 스레드도 같이 종료된다.)

=> 멀티 스레드는 다양한 곳에 사용된다. 사용되는 곳의 예:

  1. 대용량 데이터의 처리 시간을 줄이기 위해 데이터를 분할해서 병렬로 처리하는 곳에서 사용된다.
  2. UI를 가지고 있는 애플리케이션에서 네트워크 통신을 하기 위해 사용된다.
  3. 다수 클라이언트의 요청을 처리하는 서버를 개발할 때에도 사용된다.

메인 스레드

=> 메인 스레드는 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 하위 클래스로부터 생성

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가 출력한 내용

스레드 우선순위

Priority 방식

thread.setPriority(우선순위);
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 객체가 제일 먼저 수행됨을 알 수 있다.

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

동기화가 안된 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) 블록.

동기화 메소드

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

}