13 쓰레드(Thread)

jungnoeun·2022년 6월 3일
0

java

목록 보기
15/22

1) 프로세스와 스레드

프로세스 : 실행중인 프로그램. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.
프로세스는 프로그램을 수행하는데 필요한 데이터와 메모리등의 자원 그리고 쓰레드로 구성되어 있다. 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 쓰레드이다.

멀티태스킹과 멀티쓰레딩

멀티쓰레딩: 하나의 프로세스내에서 여러 쓰레드가 동시에 작업을 수행하는 것이다.

멀티 쓰레딩의 장점

  • CPU의 사용률을 향상시킨다.
  • 자원을 보다 효율적으로 사용할 수 있다.
  • 사용자에 대한 응답성이 향상된다.
  • 작업이 분리되어 코드가 간결해진다.

멀티 쓰레딩의 단점

  • 동기화(syncronization)
  • 교착상태(deadlock)






2) 쓰레드의 구현과 실행

쓰레드를 구현하는 방법

  1. Thread클래스를 상속
class MyThread extends Thread {
	public void run() {/*작업내용*/} //Thread클래스의 run()을 오버라이딩
}
  1. Runnable인터페이스를 구현
class MyThread implements Runnable {
	public void run(){/*작업내용*/} // Runnable인터페이스의 run()을 구현
}
  • Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에 Runnable인터페이스를 구현하는 방법이 일반적이다.
  • Runnable인터페이스를 구현하는 방법은 재사용성(reusability)이 높고 코드의 일관성(consistency)를 유지할 수 있기 때문에 보다 객체지향적인 방법이라고 할 수 있다.
  • 쓰레드를 구현하기 위해서는 위의 두 방법 중 어느것을 선택하든지, 그저 쓰레드를 통해 작업하고자 하는 내용으로 run(){}의 몸통을 채우면 된다.




쓰레드의 인스턴스 생성

  • Thread클래스를 상속받은 경우와 Runnable인터페이스를 구현한 경우의 인스턴스 생성방법이 다르다.
  • Thread클래스를 상속받으면, 자손클래스에서 조상인 Thread클래스의 메서드를 직접 호출할 수 있지만, Runnable을 구현하면 Thread클래스의 static메서드인 currentThread를 호출하여 쓰레드에 대한 참조를 얻어와야 한다.
  • start()가 호출되었다고 해서 바로 쓰레드가 실행되는 것이 아니라 실행대기상테에 있다가 자신의 차례가 되어야 실행된다.
  • static Thread currentThread() - 현재 실행중인 쓰레드의 참조를 반환한다.
  • String getName() - 쓰레드의 이름을 반환한다.
class ThreadEx1 {
	public static void main(String args) {
    	//Thread의 자손클래스의 인스턴스를 생성
		ThreadEx1_1 t1 = new ThreadEx1_1();

		//Runnable을 구현한 클래스의 인스턴스를 생성
		Runnable r = new ThreadEx1_2();
		Thread t2 = new Thread(r); //생성자 Thread(Runnable target)
        //Thread t2 = new Thread(new ThreadEx1_2()); - 위의 두줄을 간단히.
        
		t1.start(); //쓰레드 t1을 실행시킨다.
		t2.start(); //쓰레드 t2를 실행시킨다.
	}
}

class ThreadEx1_1 extends Thread {
	public void run() {
		for(int i=0;i<5;i++) {
			System.out.println(getName()); //조상인 Thread의 getName()을 호출
		}
	}
}

class ThreadEx1_2 implements Runnable {
	public void run() {
		for(int i=0;i<5;i++) {
		//Thread.currentThread() - 현재 실행중인 Thread를 반환한다.
		System.out.println(Thread.currentThread().getName());
		}
	}
}






3) start()와 run()

  • main메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행하는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것이다.
  • 반면에 start()는 새로운 쓰레드가 적업을 실행하는데 필요한 호출스택(call stack)을 생성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫번째로 올라가게 한다.
  • 모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요오하기 때문에, 새로운 쓰레드를 생성하고 실행시킬때마다 새로운 호출스택이 생성되고 쓰레드가 종료되면 작업에 사용된 호출스택은 소면된다.


1. main메서드에서 쓰레드의 start()를 호출한다.
2. start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는데 사용될 호출스택을 생성한다.
3. 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행한다.
4. 이제는 호출스택이 2개이므로 스케줄러가 정한 순서에 의해서 번간아 가면서 실행된다.




main쓰레드

main쓰레드: main메서드의 작업을 수행하는 쓰레드
프로그램을 실행하면 기본적으로 하나의 쓰레드(일꾼)을 생성하고, 그 쓰레드가 main메서드를 호출해서 작업이 수행되도록 한다.
main메서드가 수행을 마쳐도 다른 쓰레드가 아직 작업을 하고 있는 상태라면 프로그램은 종료되지 않는다.

실행중인 사용자 쓰레드가 하나도 없을때 프로그램은 종료된다.

class ThreadEx2 {
	public static void main(String[] args) throws Exception{
		ThreadEx2_1 t1 = new ThreadEx2_1();
		t1.start();
	}
}

class ThreadEx2_1 extends Thread {
	public void run() {
		throwException();
}

	public void throwException() {
		try {
			throw new Exception();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
}
  • 새로 생성한 쓰레드에서 고의로 예외를 발생시키고 printStrackTrace()를 이용해서 예외가 발생한 당시의 호출스택을 출력하는 예제이다.
  • 호출스택의 첫번째 메서드가 main메서드가 아니라 run메서드인 것을 확인하자.
  • 한 쓰레드가 예외가 발생해서 종료되어도 다른 쓰레드의 실행에는 영향을 미치지 않는다.
  • 예외가 발생한 호출스택에서 main쓰레드의 호출스택이 없는 이유는 main 쓰레드가 종료되었기 때문이다.
    (p.730참고)
class ThreadEx3 {
	public static void main(String[] args) throws Exception{
		ThreadEx3_1 t1 = new ThreadEx3_1();
		t1.run();
	}
}

class ThreadEx3_1 extends Thread {
	public void run() {
		throwException();
}

	public void throwException() {
		try {
			throw new Exception();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
}
  • 위의 예제는 이전 예제와 달리 쓰레드가 새로 생성되지 않았다.
  • 그저 ThreadEx3_1클래스의 run()이 호출되었을 뿐이다.
  • main()쓰레드의 호출스택에 run()쓰레드와 예외쓰레드가 올라간 것을 알 수 있다.
    (p.731참고)






싱글쓰레드와 멀티쓰레드

하나의 스레드로 두 작업을 처리하는 경우는 한 작업을 마친 후에 다른 작업을 시작하지만, 두 개의 쓰레드로 작업하는 경우에는 짧은 시간동안 2개의 쓰레드가 번갈아 가면서 작업을 수행해서 동시에 두 작업이 처리되는 것과 같이 느끼게 한다.


위의 그래프에서 알 수 있듯이 하나의 쓰레드로 두개의 작업을 수행한 시간과 두개의 쓰레드로 두 개의 작업을 수행한 시간은 거의 같다.

오히려 두 개의 쓰레드로 작업한 시간이 싱글 쓰레드로 작업한 시간보다 더 걸리게 되는데 그 이유는 쓰레드간의 작업전환(context switching)에 시간이 걸리기 때문이다.

작업 전환을 할때는 현재 진행중인 작업의 상태, 예를 들면 다음에 실행해야할 위치(PC, 프로그램 카운터)등의 정보를 저장하고 읽어오는 시간이 소요된다.

그래서 싱글 코어에서 단순히 CPU만을 사용하는 계산작업이라면 오히려 멀티쓰레드보다 싱글쓰레드로 프로그래밍하는 것이 효율적이다.

싱글 코어인 경우에는 멀티쓰레드라도 하나의 코어가 번갈아가면서 작업을 수행하는 것이므로 두 작업이 절대 겹치지 않는다. 그러나, 멀티 코어에서는 멀티쓰레드로 두 작업을 수행하면, 동시에 두 쓰레드가 수행될 수 있어서 자원을 놓고 두 쓰레드가 경쟁하게 되는 상황이 일어난다.

두 쓰레드가 서로 다른 자원을 사용하는 작업의 경우에는 싱글쓰레드 프로세스보다 멀티쓰레드 프로세스가 더 효율적이다.






5) 쓰레드 우선순위

쓰레드는 우선순위(priority)라는 속성(멤버변수)를 가지고 있는데, 이 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다.

쓰레드 우선순위 지정하기

쓰레드의 우선순위와 관련된 메서드는 다음과 같다.

void setPriority(int newPriority) 쓰레드의 우선순위를 지정한 값으로 변경한다.
int getPriority() 쓰레드의 우선순위를 반환한다.

public static final int MAX_PRIORITY = 10; // 최대우선순위
public static final int MIN_PRIORITY = 1  // 최소우선순위
public static final int NORM_PRIORITY = 5 // 보통우선순위

쓰레드가 가질 수 있는 우선순위 범위는 1~10이며 숫자가 높을수록 우선순위가 높다.

쓰레드의 우선 순위는 쓰레드를 생성한 쓰레드로부터 상속받는다.

그러므로 main메서드를 수행하는 쓰레드는 우선순위가 5이므로 main메서드 내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 된다.







6) 쓰레드 그룹(thread group)

쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 다루기 위한 것으로, 폴더를 생성해서 관련된 파일들을 함께 넣어서 관리하는 것처럼 쓰레드 그룹을 생성해서 쓰레드를 그룹으로 묶어서 관리할 수 있다.

모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 하기 때문에, 아래와 같이 쓰레드 그룹을 지정하는 생성자를 사용하지 않은 쓰레드는 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 된다.

Class ThreadGroupTest {
	public static void main(String args[]) throws Exeption {
		ThreadGroup main = Thread.currentThread().getThreadGroup();
		ThreadGroup grp1 = new ThreadGroup("Group1");
		ThreadGroup grp2 = new ThreadGroup("Group2");

		//ThreadGroup(ThreadGroup parent, String name)
		ThreadGroup subGrp1 = new ThreadGroup(grp1, "SubGroup1");

		grp1.setMaxPriority(3); //쓰레드 그룹 grp1의 최대 우선순위를 3으로 변경.

		Runnable r = new Runnable() {
			public void run() {
				try{
				Thread.sleep(1000); // 쓰레드를 1초간 멈추게 한다.
				}catch(InterruptedExeption e) {}
			}
		};

		//Thread(ThreadGroup tg, String name)
		new Thread(grp1, r, "th1").start();
		new Thread(subGrp1, r, "th2").start();
		new Thread(grp2, r, "th3").start();

		System.out.println(">>List of ThreadGroup :" + main.getName() 
        + ", Active ThreadGroup: " + main.activeGroupCount() 
        + ", Active Thread : " + main.activeCount());

	
    	main.list();
	}
}







7) 데몬 쓰레드(daemon thread)

데몬 쓰레드는 다른 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)의 직업을 돕는 보조적인 역할을 수행하는 쓰레드이다.

(ex → 가비지 컬렉터, 워드프로세서의 자동 저장, 화면 자동 갱신)

일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동종료 되는데, 그 이유는 데몬 쓰레드는 일반 쓰레드의 보조역할을 수행하므로 일반 쓰레드가 모두 종료되고 나면 데몬 쓰레드의 존재의 의미가 없기 때문이다.

데몬 쓰레드는 무한루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.

  • boolean isDaemon() → 쓰레드가 데몬 쓰레드인지 확인한다.
  • void setDaemon(boolean on) → 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경한다.
public class DaemonThread implements Runnable{
    static boolean autoSave = false;

    public static void main(String[] args) {
        Thread t = new Thread(new DaemonThread());
        t.setDaemon(true);
        t.start();

        for(int i = 1 ; i <= 10 ; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                //TODO: handle exception
                
            }
            System.out.println(i);

            if(i == 5) {
                autoSave = true;
            }
        }

        System.out.println("System out");
    }    
    
    @Override
    public void run() {
        // TODO Auto-generated method stub
        while(true) {
            try {
                Thread.sleep(3 * 1000);
            } catch (InterruptedException e) {
                //TODO: handle exception

            }

            if(autoSave) {
                autoSave();
            }
        }
    }    
    
    public void autoSave() {
        System.out.println("save!");
    }
}

만일 해당 쓰레드를 데몬 쓰레드로 설정하지 않았다면, 이 프로그램은 강제종료하지 않는 한 영원히 종료되지 않을 것이다.

profile
개발자

0개의 댓글