본문 바로가기

JAVA/혼공자

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

▶ 프로세스
사용자가 어플리케이션을 실행하면, 운영체제로부터 메모리를 할당받아 어플리케이션이 실행되는데, 이를 프로세스라고 한다.
자원의 관점에서 보면, 디스크 안에 있는 프로그램이 메모리에 적재되어 운영체제의 제어를 받는 상태를 의미한다. 
자신만의 자원을 가지기 때문에 프로세스끼리는 서로 독립적이다.

▶ 스레드
프로세스 내부의 실행 흐름을 스레드라고 한다. 
하나의 프로세스는 하나 이상의 실행 흐름을 포함하기 때문에 프로세스는 적어도 하나의 스레드를 가진다. 
자원의 관점에서 보면, 스레드는 프로세스 내부에서 생성되므로 메모리와 파일 등 모든 자원을 프로세스 자원과 공유한다. 
따라서 멀티 스레드 환경에서 한 스레드가 예외를 발생시키면 프로세스 자체가 종료될 수 있다.
이처럼 스레드는 다른 스레드들의 영향을 받는다.

⭐ 정리 ⭐
프로세스 : 실행 중인 하나의 어플리케이션
스레드 : 프로세스 내부의 실행 흐름

 멀티 태스킹과 멀티 스레드
멀티 태스킹 : 두 가지 이상의 작업을 동시에 처리하는 것
이때 멀티 태스킹이 반드시 멀티 프로세스를 의미하진 않는다. 
단일 프로세스에서도 멀티 스레드를 통해 여러 작업이 가능하기 때문이다. 
Ex. 하나의 프로세스에서 멀티 스레드로 멀티 테스킹을 하는 예시 : 영상을 재생하면서 음악을 재생하는 동영상 플레이어.
=> 멀티 스레드의 의미 : 단일 프로세스에서 멀티 테스킹이 가능하게 해줌

멀티 스레드를 이용해 데이터의 병렬 처리 등을 할 수 있으므로 반드시 이해하고 활용할 수 있어야 한다.

▶ 메인 스레드
자바의 모든 어플리케이션은 메인 스레드가 main( ) 메소드를 실행하며 시작한다. 
메인 스레드는 필요에 따라 작업 스레드들을 만들어 병렬로 코드를 실행할 수 있다.
즉, 멀티 스레드를 생성해서 멀티 태스킹을 수행한다.
싱글 스레드 어플리케이션에서는 메인 스레드가 종료되면 프로세스가 종료되지만, 
멀티 스레드 어플리케이션에서는 메인 스레드가 종료되어도 작업 스레드가 실행중이라면 프로세스는 종료되지 않는다.

▶ 작업 스레드 생성과 실행
자바에서는 작업 스레드도 객체로 생성된다. (역시 객체지향 프로그래밍👏)
이때, 스레드를 생성하는 필수 조건은
① Thread 타입 변수에 대입될 수 있어야 함 - Thread 객체 or 하위 클래스 객체
② run( ) 메소드를 오버라이딩 해야 함
이 두가지이다.
따라서 java.lang.Thread 클래스에서 생성자로 직접 객체를 생성하거나,
Thread 클래스를 상속하는 하위 클래스를 만들어 객체를 생성할 수도 있다.

※ 지금부터는 모두 방법이므로 의문 품지 않고 받아들이기!
1️⃣ Thread 생성자로 직접 객체를 생성
Thread 생성자에 Runnable 타입의 매개값을 전달하여 호출해야 한다. 
Runnable은 '작업 스레드가 실행할 수 있는 코드를 가졌다'하여 붙여진 이름이다. 
Runnable은 인터페이스이기 때문에 구현 객체를 만들어 대입해야 한다. 
구현 객체 내부에서는 Runnable의 추상 메소드 run( ) 메소드를 오버라이드해야 한다. 
이를 위해 run( ) 메소드를 오버라이딩하는 익명 구현 객체를 선언하거나,
작업스레드 클래스를 만들고 객체를 전달하여 Thread 생성자의 매개값으로 전달할 수 있다.
=> 정리 : Thread 생성자에 run( ) 메소드가 오버라이딩된 Runnable 구현 객체를 대입하면 된다.

[기본 문법]
Thread thread = new Thread(Runnable() {
    @Override
    public void run() {
        스레드가 실행할 코드;
    }
};


예제) 비프 음을 스피커로 출력하면서, 동시에 모니터로 '띵'을 출력하는 프로세스
-> 메인 스레드만드로는 '동시에' 두 작업을 할 수 없으므로, 작업 스레드를 만들어 줘야 함

 


2️⃣ Thread 클래스의 하위 클래스로 객체 생성
Thread를 상속하고, run( ) 메소드를 오버라이딩하는 하위 클래스를 만들어 Thread 타입 변수에 대입하면 된다.
첫번째 방법과 마찬가지로, 코드의 절약을 위해 익명 상속 객체를 만들거나 
가독성을 위해 클래스로 분리하여 작성할 수도 있다.
=> 정리 : Thread 타입 변수에 run( ) 메소드가 오버라이딩된 Thread를 상속하는 구현 객체를 대입하면 된다.

[기본 문법]
Thread thread = new Thread() {
    @Override
    public void run() {
        스레드가 실행할 코드;
    }


작업 스레드는 스레드 객체의 start( ) 메소드를 호출해야 실행된다. 
start( ) 메소드가 호출되어 실행 가능 상태가 되고 이후 run( ) 메소드가 호출되면서 작업이 실행되는 것이다.

 

▶ Thread 클래스가 제공하는 메소드

스레드의 이름이 큰 역할을 하는 것은 아니지만, 디버깅할 때 어떤 스레드가 어떤 작업을 하는지 조사할 목적으로 가끔 사용된다.
메인 스레드는 'main'이라는 이름을,  setName으로 이름을 지정하지 않은 작업 스레드는 'Thread-n'이라는 이름을 갖는다.

public class ThreadNameExample {
	public static void main(String[] args) {
		// currentThread - 현재 실행중인 스레드 객체의 참조값 반환
		Thread thread = Thread.currentThread();
		System.out.println(thread.getName()); // main
		
		ThreadA threadA = new ThreadA();
		System.out.println(threadA.getName()); // ThreadA
	}

}

class ThreadA extends Thread{
	ThreadA(){
		setName("ThreadA");
	}
	
	@Override
	public void run() {
	}
}

 

▶ 동기화 메소드
앞서 배운 것처럼, 프로세스는 각자의 메모리를 할당받으므로 서로 독립적이지만, 
한 프로세스 내부에 있는 멀티 스레드는 자원을 공유하므로 서로에게 영향을 줄 수 있다.
특히 객체를 공유하여 작업하는 경우, A 스레드가 작업하던 객체를 도중에 B가 수정하면, 
A 스레드가 의도했던 것과 다른 결과가 나올 수 있다. 
이런 문제를 방지하기 위해 자바에서는 동기화 메소드를 제공한다.

예를들어, A 스레드가 result를 100으로 수정하고 다른 일을 하다 result를 출력하는 동작을 한다고 하자. 
이때 중간에 B 스레드가 result를 50으로 수정한다면, A 스레드는 의도하지 않은 결과를 받는 문제가 발생한다.

동기화 메소드의 원리 :
어떤 스레드의 작업이 끝날 때까지 객체의 메소드에 잠금을 걸어서 다른 스레드가 사용하지 못하게 하는 것

멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계영역이라고 한다. 
동가화 메소드는 그 자체가 임계영역이 되어, 
한 스레드가 동기화 메소드를 실행하는 도중에는 다른 스레드가 동기화 메소드를 실행하지 못하게 한다. 
따라서 result를 수정하는 내용이 있는 모든 메소드를 동기화 메소드로 선언한다면,
위의 예시에서 중간에 result가 수정되는 것을 막을 수 있다.
또한 객체에서 동기화 메소드를 여러개 선언해둔 경우, A 스레드가 동기화 메소드1을 실행하고 있다면, 
B 스레드는 동기화 메소드1 뿐 아니라 해당 객체의 모든 동기화 메소드를 사용할 수 없다. 
(반면 일반 메소드는 언제든 사용 가능하다)

동기화 메소드는 메소드 선언부에 synchronized 키워드를 붙임으로서 선언할 수 있다.

=> 동기화 메소드의 의의 : 멀티 스레드에서 공유하는 객체를 동시에 수정하는 것을 막아준다.

 

MainThreadExample.java

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

 

Calculator.java

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(Exception e) {}
		System.out.println(Thread.currentThread().getName()+" 메모리는 "+this.memory);
	}
}

 

User1.java

public class User1 extends Thread{
	public Calculator cal;
	
	public void setCalculator(Calculator cal) {
		this.setName("User1");
		this.cal = cal;
	}
	
	@Override
	public void run() {
		cal.setMemory(100);
	}
}

 

User2.java

public class User2 extends Thread{
	public Calculator cal;
	
	public void setCalculator(Calculator cal) {
		this.setName("User2");
		this.cal = cal;
	}
	
	@Override
	public void run() {
		cal.setMemory(50);
	}
}