14.1 멀티 스레드 개념
하나의 스레드는 하나의 실행 흐름을 갖는다. 멀티 스레드는 동시에 여러 실행 흐름을 갖는다.
운영체제는 실행 중인 프로그램을 프로세스 process로 관리한다.
운영체제는 멀티 프로세스를 생성해서 처리 -> 멀티 태스킹
멀티 프로세스 = 멀티 태스킹 이지만 멀티 스레드는 아니다.
멀티 스레드는 하나의 프로세스 즉 하나의 프로그램에서 여러 작업을 처리하는 것을 말한다. 하나의 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 프로그램들도 있다. 예를 들어 메신저는 채팅 작업을 하면서 동시에 파일 전송 작업을 수행하기도 한다.
멀티 프로세스가 프로그램 단위의 멀티 태스킹이라면 멀티 스레드는 프로그램 내부에서의 멀티 태스킹이라고 볼 수 있다. 다음 그림은 멀티 프로세스와 멀티 스레드의 차이점을 보여준다.
멀티 프로세스들은 서로 독립적이므로 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치지 않는다.
-> 워드하다가 포토샵 멈출 수 있다.
하지만 멀티 스레드는 프로세스 내부에서 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스가 종료되므로 다른 스레드에 영향을 미친다.
-> 포토샵 안에서 뭐 잘못되면 프로그램 그냥 꺼진다.
따라서 스레드를 잘 작성해야 다른 스레드에 영향을 주지 않는다. 멀티 스레드를 시용할 경우에는 예외 처리에 만전을 기해야 한다.
멀티 스레 드는 데이터 를 분할해서 병렬로 처리하는 곳에서 사용하기도 하고, 안드로이 드 앱에서 네트워크 통신을 하기 위해 사용하기도 한다.
또한 다수의 클라이언트 요청을 처리하는 서버를 개발할 때에도 시용된다. 프로그램 개발에 있어서 멀티 스레드는 꼭 필요한 기능이기 때문에 반드시 이해하고 활용할 수 있도록 한다.
-> 웹 개발에서는 거의 쓰이지 않지만 서버 개발에는 무조건 필요하다.
안드로이드 개발에는 사용한다.
14.2 메인 스레드
JVM은 메인 스레드를 만든다. 자바 프로그램은 메인 스레드가 main( ) 메소드를 실행하면서 시작된다. 지금까지 만든 main 메서드는 JVM이 만든 메인 스레드가 위에서부터 아래방향으로 실행했다.
메인 스레드는 main( ) 메소드의 첫 코드부터 순차적으로 실행하고, main( ) 메소드의 마지막 코드를 실행하거나 return문을 만나면 실행을 종료한다.
-> 싱글 스레드만을 의미한다.
왼쪽 싱글 스레드에서는 메인 스레드가 종료되면 프로세스도 종료된다.
멀티 스레드에서는 실행 중인 스레드가 하나라도 있다면 프로세스는 종료되지 않는다. 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않는다.
-> 렉 걸려서 끝내지 못하는 상황은 하나의 스레드를 끝내지 못해 전체 스레드를 종료하지 못한다.
⭐ 실습
main 스레드가 위에서 아래로 실행되는데 이 실행 흐름이 스레드이다. 자바에서는 스레드도 객체로 관리한다.
Thread 정적 클래스를 사용해서 현재 스레드를 갖고온다.
Thread 객체에 null을 대입하고, 실행하면 NullPointerException이 발생한다.
예외가 java:10, line 10번째 줄에서 발생하는데, currThread.getName( )이 발생한 위치가 thread "main"이다.
14.3 작업 스레드 생성과 실행
자바 프로그램은 메인스레드가 반드시 존재하기 때문에 메인작업 이외에 추가적인 작업 수만큼 스레드를 생성하면 된다.
2번째 에서는 2개의 스레드가, 3번째에서는 3개의 스레드가 만들어져야한다.
자바는 작업 스레드도 객체로 관리하므로 클래스가 필요하다. Thread 클래스로 직접 객체를 생성해도 되지만, 하위 클래스를 만들어 생성할 수도 있다.
✅ Thread 클래스로 직접 생성
Thread도 new를 사용해서 객체를 생성하고 객체를 직접 생성하려면 다음과 같이 Runnable 구현 객체를 매개값으로 갖는 생성자를 호출하면 된다.
작업 스레드 객체가 생성되었다고 해서 바로 작업 스레드가 실행되지는 않는다. 작업 스레드를 실행 하려면 스레드 객체의 start( ) 메소드를 호출해야한다.
Runnable은 스레드가 작업을 실행할 때 사용하는 인터페이스이다. 따라서 매개값으로 Runnable을 구현한 객체를 제공해야한다.
Runnable 인터페이스에는 딱 한개의 메서드인 run( )이 정의되어 있다.
Runnable에는 run( ) 메소드가 정의되어 있는데, 구현 클래스는 run( )을 재정의해서 스레드가 실행할 코드를 가지고 있어야 한다. 다음은 Runnable 구현 클래스를 작성하는 방법이다.
Runnable 구현 클래스는 작업 내용을 정의한 것이므로, 스레드에게 전달해야 한다. Runnable 구현 객체를 생성한 후 Thread 생성자 매개값으로 Runnable 객체를 다음과 같이 전달하면 된다.
-> Runnable task말고 Task task가 와도 상관없다. Runnable이 온 이유는 이 인터페이스 타입의 값을 대입하는 것으로 Task에 Runnable에 run( ) 메소드만 구현돼서 객체 생성할 때 문제만 없으면 된다.
start 메서드를 사용하면 실제 동작하는 것은 Runnable을 상속받은 클래스의 run 메서드가 동작한다.
익명 객체로 매개 변수에 Runnable을 구현하고 new 연산자로 객체를 바로 생성할 수 있다. 익명 객체는 바로 생성하기 때문에 이름이 없다.
-> run 메서드에는 실제로 스레드에서 실행될 코드를 작성한다.
이름이 Runnable인 이유는 스레드가 실행할 수 있어 Runnable이라는 이름을 갖는다.
Thread의 핵심은 위 두 코드이고, 생성자 매개값은 Runnable을 명시적으로 구현해도 되고 익명 객체를 넣어도 된다.
start( )로 실행하면 실제로는 run 메서드가 실행된다.
코드 실행 흐름은
1번 task 객체를 넣고
2번 start 메서드를 호출하고
3번 run 메서드가 실행된다.
start( ) 메서드를 호출하고 3번 작업 스레드가 실행될 때, 왼쪽 3번인 메인 스레드가 동시에 실행된다.
다음은 메인 스레드가 동시에 두 가지 작업을 처리할 수 없음을 보여주는 예제이다. 원래 목적은 0.5초 주기로 비프beep음을 발생시키면서 동시에 프린팅까지 하는 작업이었지만, 메인 스레드는 비프음을 모두 발생한 다음에야 프린팅을 시작한다.
기본적으로 위 예제는 main 스레드가 위에서부터 아래로 실행된다. 기본적으로 스레드는 1개만 실행되기 때문에 소리가 나고 출력이된다.
만약 소리가나면서 출력이 되게 하려면 2개 중에 1개의 작업은 새로운 스레드를 만들게 수정해야한다.
⭐ 실습
static 클래스인 ToolKit을 사용한다.
beep 메서드를 실행하면 너무 빨라서 5번이 들리지 않는다. 따라서 Thread.sleep 메서드에 밀리세컨드로 시간을 넣는다.
try-catch문을 작성하고 그냥 예외로 들어가게 만든다.
for문을 복붙에서 "띵"을 출력하게 만든다.
-> 실행하면 소리가 5번 들리고 다음 "띵이 5번 출력된다.
익명 객체로 소리가 5번 나는 코드를 새로운 스레드로 만들었다.
exam02에 복붙하자.
Runnable 익명객체를 만들어서 run( ) 메서드를 구현한다.
이렇게 실행하면 안들린다??
-> start 메서드 실행해야한다.
처음에 Runnable 구현할 때 위와 같이 만들고
추가하면 override 자동 추가된다.
빨간색 메인 스레드가 실행될 때 thread.start( ) 메서드를 만나면 run( )메서드에 새로 만들어진 thread와 작업2의 "띵"을 출력하는 작업이 동시에 실행된다.
❗thread.start( ) 메서드가 Runnable에 있는 run 메서드를 찾아서 자동으로 실행하지 thread.run( )으로 실행하면 안된다.
✅ Thread 자식 클래스로 생성
작업 스레드 객체를 생성하는 또 다른 방법은 Thread의 자식 객체로 만드는 것 이다. Thread 클래스를 상속한 다음 run( ) 메소드를 재정의해서 스레드가 실행할 코드를 작성하고 객체를 생성하면 된다.
-> 위와 다른점은 Runnable 인터페이스에 있는 run( ) 메소드를 재정의했다.
같은 점은 새로운 스레드가 실행할 코들르 run( ) 메서드 안에 넣는다.
만든 new WorkerThread( )를 부모 타입 Thread 매개변수에 넣는다. 자손 타입으로 넣어도 상관 없다.
여기도 thread를 만들고 start( ) 메서드를 실행해야 스레드가 실행된다.
명시적인 자식 클래스를 정의 하지 않고, 다음과 같이 Thread 익명 자식 객체를 사용-할 수도 있다. 오히려 이 방법이 더 많이 사용된다.
바로 익명 객체를 만들어서 run( )을 넣어도 된다.
위 예제와 같은데 다른점은 Thread 클래스를 상속받은 자손 객체가 새로운 Thread를 만든다.
⭐ 실습
exam03에 복붙하고 print 코드를 Thread로 따로 작성한다.
Runnable을 구현한 클래스는 작업이므로 Task로 rename한다.
-> Runnable을 구현한 클래스는 thread가 아니기 때문에 PrintThread는 맞지 않는다.
녹색은 메인 스레드이고 주황색과 파란색은 다른 스레드이다.
주황색은 익명 인터페이스를 사용해서 해당 코드에서만 사용할 경우 좋고
출력하는 명시적 클래스를 만들어서 thread를 만들면 재사용하기 좋다.
print부분을 Thread의 자식 클래스를 상속받아서 만들어보자.
이때 구현 클래스 이름은 PrintThread로 Thread가 붙는데 이유는 Thread를 상속받기 때문에 Thread이다. 하지만 Runnable을 구현한 것은 Thread가 아니라 작업 내용을 갖고있는 run( ) 메서드 이다.
출력 부분을 복붙하고
매개변수를 조상인 Thread로 작성해도된다.