[혼공자] 12-1. 멀티 스레드

Benjamin·2023년 5월 31일
0

혼공자

목록 보기
27/27

12. 스레드

12-1. 멀티 스레드

  • 프로세스 = 애플리케이션을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당받아 애플리케이션의 코드를 실행 = 실행중인 하나의 애플리케이션
    -> 하나의 애플리케이션은 멀티 프로세스를 만들기도 한다.
    ex) 메모장 애플리케이션을 2개 실행했다면, 2개의 메모장 프로세스가 생성된 것

  • 스레드 = 프로세스 내부에서 코드의 실행 흐름

스레드

  • 멀티 태스킹 = 두 가지 이상의 작업을 동시에 처리

운영체제는 멀티 태스킹을 할 수 있도록 CPU 및 메모리 자원을 프로세스마다 적절히 할당해주고, 병렬로 실행시킨다.

멀티 태스킹은 꼭 멀티 프로세스를 뜻하는건 아니다.
한 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 애플리케이션도 있다.
ex) 미디어 플레이어 : 동영상 재생 + 음악 재생 동시처리
ex) 메신저 : 채팅기능 + 파일 전송 기능

하나의 프로세스가 두 가지 이상의 작업을 처리하는 비결? '멀티 스레드'

  • 스레드 = 한 가지 작업을 실행하기 위해 순차적으로 실행할 코드를 실처럼 이어놓았다
    -> 하나의 스레드 = 하나의 코드 실행 흐름
    -> 한 프로세스 내 스레드 2개 = 2개의 코드 실행 흐름
  • 멀티 프로세스 = 운영체제에서 할당받은 자신의 메모리를 가지고 실행
    -> 각 프로세스는 독립적
    -> 하나의 프로세스에서 오류 발생해도 다른 프로세스에 영향 미치지 않음

  • 멀티 스레드 = 하나의 프로세스 내부에 생성
    -> 하나의 스레드가 예외 발생시키면 프로세스 자체가 종료될 수 있어, 다른 스레드에 영향미침
    -> 예외 처리에 만전을 기해야!

멀티 스레드가 사용되는 곳

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

메인 스레드

자바의 모든 애플리케이션은 메인 스레드가 main()메소드를 실행하면서 시작

main() 메소드의 첫 코드부터 아래로 순차적으로 실행하며, main()메소드의 마지막 코드를 실행하거나 return문을 만나면 실행 종료

  • 메인스레드는 필요에 따라 작업 스레드를 만들어 병렬로 코드 실행할 수 있음 = 멀티 스레드를 생성해 멀티 태스킹을 수행
  • 싱글 스레드 애플리케이션 = 메인 스레드가 종료하면 프로세스도 종료됨
  • 멀티 스레드 애플리케이션 = 실행중인 스레드가 하나라도 있으면 프로세스는 종료되지않음
    -> 메인 스레드가 작업 스레드보다 먼저 종료되어도 마찬가지

작업 스레드 생성과 실행

멀티 스레드고 실행하는 애플리케이션 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고, 각 작업별로 스레드를 생성해야한다.

어떤 자바 애플리케이션이건 메인 스레드는 반드시 존재
-> 따라서 메인 작업 이외에 추가적인 병렬 작업의 수만큼 스레드 생성

작업 스레드도 객체로 생성되지 때문에 클래스가 필요

방법 1. java.lang.Thread 클래스를 직접 객체화해서 생성
방법 2. Thread 클래스를 상속해서 하위 클래스를 만들어 생성

Thread 클래스로부터 직접 생성

java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성하기 위해서는 Runnable을 매개값으로 갖는 생성자 호출

Thread thread = new Thread(Runnable target);

  • Runnable = 작업 스레드가 실행할 수 있는 코드를 갖고있는 객체이며, 인터페이스 타입이기때문에 구현 객체 만들어 대입해야함
    -> run()메소드가 정의되어있음 -> 구현 클래스에서 run()을 재정의해서 작업 스레드가 실행할 코드를 작성
class Task implements Runnable {
	public void run() {
    	//스레드가 실행할 코드;
	}
}

Runnable = 작업 내용을 갖고있는 객체이지, 실제 스레드 x
-> Runnable 구현 객체 생성 후, 이것을 매개값으로 해서 Thread 생성자 호출해야 작업 스레드 생성됨

Runnable task = new Task();

Thread thread = new Thread(task);

위 코드는 Thread 생성자 호출할 때, Runnable 익명 객체를 매개값으로해서 코드를 절약할 수 있다.

Thread thread = new Thread(new Runnable() {
	public void run() {
    	//스레드가 실행할 코드;
	}
});

작업스레드는 생성즉시 실행되는 것이 아니라, start()메소드를 호출해야 비로소 실행된다.
thread.start();

start()메소드가 호출되면, 작업 스레드는 매개값으로 받은 Runnable의 run()메소드를 실행하면서 작업을 처리한다.

0.5초 주기로 비프beep음을 발생시키면서 동시에 출력을 하는 작업이 있다고 가정해보자.
비프음 발생과 출력은 서로 다른 작업이므로 메인 스레드가 동시에 두 가지 작업을 처리할 수 없다.

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

출력은 메인 스레드가 담당하고, 비프음을 들려주는 것은 작업 스레드가 담당하도록 코드를 작성해보겠다.

<비프음을 들려주는 작업 정의>

import java.awt.*;

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 BeepPrintExample {
    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("beep");
            try {
                thread.sleep(500);
            } catch(Exception e) {

            }
        }
    }
}
  • Toolkit?
    java.awt패키지안에 존재한다.
    aws = Abstract Window Toolkit = Window프로그래밍을 하기 위한 GUI의 도구

의문점❓

위 코드를 돌리니 정확히는 5번째 소리가 시작될때쯤 "beep"이 적힌 5줄이 동시에 떴다.
이게 어떻게 된 일일까?
혹시 너무 짧은 시간을 간격으로 반복해서 그런걸까싶어서, sleep에 5000값을 넣어봤다. (5초)
그 결과 첫번째 'beep'는 첫 소리가 난 후 1초정도 늦게 출력됐는데, 두번째부터 끝까지는 소리와 동시에 'beep'가 출력됐다.
위 코드는 병렬처리를 구현한건데, 정말 말그대로 동시에 실행되는건지 스레드에서 사용되는 병렬처리 동작과 개념이 뭔지 알아봐야겠다.

위 코드를 Runnable 익명 구현 객체를 이용하는 방식으로 수정해보자.

import java.awt.*;

public class BeepPrintExample3 {
    public static void main(String[] args) {
        Thread thread = new Thread(new 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) {

                    }
                }
            }
        });
        thread.start();

        for(int i=0; i<5; i++) {
            System.out.println("beep");
            try {
                Thread.sleep(500);
            } catch (Exception e) {

            }
        }
    }
}

Thread 하위 클래스로부터 생성

작업 스레드가 실행할 작업을 Runnable로 만들지 않고, Thread의 하위 클래스로 작업 스레드를 정의하면서 작업내용을 포함시킬 수 있다.

Thread클래스를 상속한 후 run()메소드를 재정의(오버라이딩)해서 스레드가 실행할 코드를 작성하면된다.

public class WorkerThread extends Thread {
	@Override
    public void run() {
    	//스레드가 실행할 코드;
	}
}
Thread thread = new WorkerThread();

Thread 익명 객체로 작업 스레드 객체를 생성해서 코드를 절약할 수 있다.

Thread thread = new Thread() {
	public void run() {
    	//스레드가 실행할 코드;
	}
}

이렇게 생성된 작업 스레드 객체에서 start()메소드를 호출하면 작업 스레드는 자신의 run()메소드를 실행한다.

thread.start();

이제 이전 예제를 수정해서 Runnable을 생성하지 않고, Thread의 하위 클래스로 작업 스레드(BeepThread 클래스)를 정의해보자.

<비프음을 들려주는 스레드>

import java.awt.*;

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 BeepPrintExample4 {
    public static void main(String[] args) { //메인스레드
        Thread thread = new BeepThread();
        thread.start(); //Beep 스레드

        for(int i=0; i<5; i++) {
            System.out.println("beep");
            try {
                Thread.sleep(500);
            } catch (Exception e) {

            }
        }
    }
}

위 코드를 Thread익명 자식 객체를 이용해 작업 스레드를 만들어보자.

import java.awt.*;

public class BeepPrintExample5 {
    public static void main(String[] args) {
        Thread thread = new 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) {
                    }
                }
            }
        };
        thread.start();

        for(int i=0; i<5; i++) {
            System.out.println("beep");
            try{
                Thread.sleep(500);
            } catch (Exception e) {
            }
        }
    }
}

스레드 이름

스레드는 자신의 이름을 갖고있다.
디버깅할 때 어떤 스레드가 작업을 하는지 조사할때 사용할 수 있다.

  • 메인 스레드 이름 = main
  • 우리가 생성하는 스레드 이름 = Thread-n (n = 스레드 번호)
    -> 다른 이름으로 설정하고싶다면 Thread 클래스의 setName()메소드로 변경
    thread.setName("스레드 이름");

스레드 이름 알고싶을 때 에는 getName() 호출
thread.getName();

setName(), getName()은 Thread 클래스의 인스턴스 메소드이므로 스레드 객체의 참조가 필요하다.
만약 스레드 객체를 갖고있지 않으면, Thread 클래스의 정적 메소드인 currentThread()를 이용해 현재 스레드의 참조를 얻을 수 있다.

Thread thread = Thread.currentThread();

예제를 보자.

<메인 스레드 이름 출력 및 UserThread 생성 및 시작>

public class ThreadNameExample {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread(); //이 코드를 실행하는 스레드 객체 얻기
        System.out.println("프로그램 시작 스레드 이름 = " + mainThread.getName());

        Thread threadA = new ThreadA();
        System.out.println("작업 스레드 이름 = " + threadA.getName());
        threadA.start();

        ThreadB threadB = new ThreadB();
        System.out.println("작업 스레드 이름 = " + threadB.getName());
        threadB.start();
    }
}

<ThreadA 클래스>

public class ThreadA extends Thread{
    public ThreadA() {
        setName("ThreadA");
    }

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

<ThreadB 클래스>

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

결과

동기화 메소드

싱글 스레드 프로그램에서는 1개의 스레드가 객체를 독차지해서 사용하면되지만, 멀티 스레드 프로그램에서는 스레드들이 객체를 공유해서 작업해야하는 경우가 있다.
따라서 주의할 점을 알아보자.

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

스레드A가 사용하던 객체를 스레드 B가 상태를 변경할 수 있기 때문에 스레드 A가 의도했던 것과는다른 결과를 산출할 수도 있다.

<메인 스레드가 실행하는 코드>

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

<User1 스레드>

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

<User2 스레드>

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

결과

동기화 메소드

스레드가 사용중인 객체를 다른 스레드가 변경할 수 없게 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸어서 다른 스레드가 사용할 수 없도록 해야한다.

  • 임계 영역 = 멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역

자바는 임계영역을 지정하기 위해 '동기화 메소드'를 제공한다.

스레드가 객체 내부의 동기화메소드를 실행하면 즉시 객체에 잠금을 걸어 다른 스레드가 동기화 메소드를 실행하지 못하도록 한다.

동기화 메소드를 만들려면 다음과 같이 메소드 선언에 synchronized 키워드를 붙이면 된다.
이 키워드는 인스턴스와 정적 메소드 어디든 붙일 수 있다.

public synchronized void method() {
	//임계영역
}

동기화 메소드는 메소드 전체 내용이 임계 영역이므로 스레드가 동기화 메소드를 실행하는 즉시 객체에는 잠금이 일어나고, 스레드가 동기화 메소드를 실행 종료하면 잠금이 풀린다.

만약 동기화 메소드가 여러개 있을 경우, 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론이고 다른 동기화 메소드도 실행할 수 없다.
하지만 다른 스레드에서 일반 메소드는 실행 가능하다.

의문점 ❓

객체 자체에 잠금이 일어나는데, 일반 메소드는 실행가능하다는게 어떻게 말이되는거지?

앞에서 본 Calculator의 setMemory() 메소드를 동기화 메소드로 만들어서 User1 스레드가 setMemory()를 실행할동안 User2 스레드가 setMemory() 메소드를 실행할 수 없도록 해보자.

<동기화 메소드로 수정된 공유 객체>

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

User1 스레드는 Calculator 객체의 동기화 메소드인 setMemory()를 실행하는 순간 Calculator 객체를 잠금처리한다.
메인 스레드가 User2 스레드를 실행하지만, 동기화 메소드인 setMemory()를 실행하지는 못하고 User1이 setMemory()를 모두 실행할 동안 대기해야한다.

이렇게 User1 스레드는 방해받지않고 안전하게 Calculator객체를 사용할 수 있게된다.

0개의 댓글