Thread

김준영·2023년 3월 9일
0

Code States

목록 보기
8/33

스레드? 프로세스?


어떤 애플리케이션이 실행되면 운영체제가 해당 애플리케이션에게 메모리를 할당해주며 애플리케이션이 실행되는데, 이처럼 실행 중인 애플리케이션을 프로세스라고 하며, 프로세스 내에서 실행되는 소스 코드의 실행 흐름을 스레드라고 한다.

단 하나의 스레드를 가지는 프로세스를 싱글 스레드 프로세스, 여러 개의 스레드를 가지는 프로세스를 멀티 스레드 프로세스라고 한다.

어떤 프로세스가 멀티 스레드로 동작하는 것은 해당 애플리케이션이 동시 작업을 할 수 있다는 것을 의미한다. 즉, 여러 코드를 각 스레드에 분배하여 동시에 실행시킬 수 있다.

프로세스

데이터, 컴퓨터 자원, 그리고 스레드로 구성된다.

스레드

데이터와 애플리케이션이 확보한 자원을 활용하여 소스 코드를 실행한다.
즉, 스레드는 하나의 코드 실행 흐름이라고 볼 수 있다.

메인 스레드

어떤 자바 애플리케이션의 소스 코드가 싱글 스레드로 작성되면, 그 애플리케이션이 실행되어 프로세스가 될 때 메인 스레드만 가지는 싱글 스레드 프로세스가 될 것이다.

멀티 스레드

여러 개의 스레드를 가진다는 것은 여러 스레드가 동시에 작업을 수행할 수 있음을 의미하며, 이를 멀티 스레딩이라고 한다.

자바에서 스레드 생성과 실행


run()이라는 메서드 내에 스레드가 처리할 작업을 작성하도록 규정되어져 있다.
run() 메서드는 Runnable 인터페이스와 Thread 클래스에 정의되어 있다.

  1. Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
  2. Thread 클래스를 상속 받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법

1번 방법

public class ThreadExample1 {
    public static void main(String[] args) {
    
     // Runnable 인터페이스를 구현한 객체 생성
        Runnable task1 = new ThreadTask1();

        // Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화하여 스레드를 생성
        Thread thread1 = new Thread(task1);

        // 위의 두 줄을 아래와 같이 한 줄로 축약할 수도 있습니다. 
        // Thread thread1 = new Thread(new ThreadTask1());
        
        thread1.start();

    }
}

// Runnable 인터페이스를 구현하는 클래스
class ThreadTask1 implements Runnable {
    // run() 메서드 바디에 스레드가 수행할 작업 내용 작성
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}

Runnable을 implements한 클래스는 run() 메서드를 작성해줘야 하며, 작성 후 Runnable 인터페이스를 구현한 객체를 활용하여 스레드를 생성할 때에는 Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화 한다.

또한 실행시키위해 start() 메서드를 호출하여 스레드를 실행시켜준다.

2번 방법

public class ThreadExample2 {
    public static void main(String[]args) {

        // Thread 클래스를 상속받은 클래스를 인스턴스화하여 스레드를 생성
        ThreadTask2 thread2 = new ThreadTask2();
        thread2.start();
    }
}

class ThreadTask2 extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}

1번 방식과 차이점은, Thread 클래스를 직접 인스턴스화하지 않는다는 점이다.

익명 객체를 사용하여 스레드 생성 & 실행

Runnable 익명 구현 객체를 활용한 스레드 생성 및 실행

public class ThreadExample1 {
    public static void main(String[] args) {
				
        // 익명 Runnable 구현 객체를 활용하여 스레드 생성
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.print("#");
                }
            }
        });

        thread1.start();

        for (int i = 0; i < 100; i++) {
            System.out.print("@");
        }
    }
}

Thread 익명 하위 객체를 활용한 스레드 생성 및 실행

public class ThreadExample2 {
    public static void main(String[] args) {

        // 익명 Thread 하위 객체를 활용한 스레드 생성
        Thread thread2 = new Thread() {
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.print("#");
                }
            }
        };

        thread2.start();

        for (int i = 0; i < 100; i++) {
            System.out.print("@");
        }
    }
}

스레드의 이름


메인 스레드는 main이라는 이름을 가지며, 그 외에 추가적으로 생성한 스레드는 기본적으로 Thread-n이라는 이름을 가진다.

public class ThreadExample3 {
    public static void main(String[] args) {

        Thread thread3 = new Thread(new Runnable() {
            public void run() {
                System.out.println("Get Thread Name");
            }
        });

        thread3.start();

        System.out.println("thread3.getName() = " + thread3.getName());
        
        thread4.setName("hihi");

        System.out.println("thread4.getName() = " + thread4.getName());
    }
}

스레드 인스턴스의 주소값 얻기

public class ThreadExample1 {
    public static void main(String[] args) {

        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });

        thread1.start();
        System.out.println(Thread.currentThread().getName());
    }
}

스레드의 동기화


동기화란 두 스레드가 동일한 데이터를 공유하게 되어 문제가 발생할 수 있는데 이것을 막아주는 것이다.

임계 영역과 락

임계 영역은 오로지 하나의 스레드만 코드를 실행할 수 있는 코드 영역을 의미.
락은 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미.

메서드의 반환 타입 좌측에 synchronized 키워드를 작성하면 메서드 전체를 임계 영역으로 설정할 수 있다.

이렇게 메서드 전체를 임계 영역으로 지정하면 메서드가 호출되었을 때, 메서드를 실행할 스레드는 메서드가 포함된 객체의 락을 얻는다.

특정 영역을 임계 영역으로 지정하려면 아래와 같이 synchronized 키워드와 함께 소괄호(()) 안에 해당 영역이 포함된 객체의 참조를 넣고, 중괄호({})로 블럭을 열어, 블럭 내에 코드를 작성하면 됩니다.

class Account {
	...
	public boolean withdraw(int money) {
			synchronized (this) {
			    if (balance >= money) {
			        try { Thread.sleep(1000); } catch (Exception error) {}
			        balance -= money;
			        return true;
			    }
			    return false;
			}
	}
}

스레드의 상태와 실행 제어


앞서 말한 start()는 스레드를 실행시키는 메서드는 아니다.

실행 대기 상태로 만들어주는 메서드이며, 운영체제가 적절한 때에 스레드를 실행시켜준다.

스레드 실행 제어 메서드

sleep(long milliSecond) : milliSecond 동안 스레드를 잠시 멈춘다.

sleep()은 Thread의 클래스 메서드이다. 사용할 땐 ex -> Thread.sleep(1000);

실행 상태에서 일시 정지 상태로 전환한다.

인자로 전달한 시간 만큼의 시간이 경과한 경우, interrupt()를 호출한 경우 실행 대기 상태로 복귀한다.

위의 두 가지 경우 중 interrupt()를 호출하여 스레드를 실행 대기 상태로 복귀시키고자 한다면 반드시 try … catch 문을 사용해서 예외 처리를 해주어야 한다. interrupt()가 호출되면 기본적으로 예외가 발생하기 때문이다.

Interrupt() : 일시 중지 상태인 스레드를 실행 대기 상태로 복귀시킨다.

interrupt()는 sleep(), wait(), join()에 의해 일시 정지 상태에 있는 스레드들을 실행 대기 상태로 복귀시킵니다.

yield() : 다른 스레드에게 실행을 양보한다.

public void run() {
		while (true) {
				if (example) {
						...
				}
				else Thread.yield();
		}
}

join() : 다른 스레드의 작업이 끝날 때까지 기다린다.

join()은 특정 스레드가 작업하는 동안에 자신을 일시 중지 상태로 만드는 상태 제어 메서드이다.

wait(), notify() : 스레드 간 협업에 사용된다.

profile
ㅎㅎ

0개의 댓글