인생을 코딩하다.

[Java] Thread 본문

Java

[Java] Thread

Hyung1 2020. 11. 17. 13:00
728x90
반응형

Process

  • 실행중인 프로그램
  • OS으로부터 메모리를 할당 받음

Thread 

  • 실제 프로그램이 수행되는 작업의 최소 단위
  • 하나의 프로세스는 하나 이상의 Thread를 가지게됨
  • Thread는 CPU를 점유해서 돌아가게 되는데, CPU를 점유할 수 있는 것은 스케쥴러라는 것이 있다.
  • 스케쥴러가 쓰레드의 CPU를 할당을 해서 쓰레드가 수행 되도록 역할을 한다.
  • java 명령어를 사용하여 클래스를 실행시키는 순간 자바 프로스세가 시작되고, main() 메소드가 수행되면서 하나의 쓰 레드가 시작되는 것이다.
  • 아무런 쓰레드를 생성하지 않아도 JVM을 관리하기 위한 여러 쓰레드가 존재한다. 예를 들면 자바의 쓰레기 객체를 청소하는 GC 관련 쓰레드가 여기에 속한다. 

그런데 왜 Thread를 만들었을까?

프로세스가 하나 시작하려면 많은 자원이 필요하다. 만약 하나의 작업을 동시에 수행하려고 할 때 여러개의 프로세스를 띄어서 실행 하면 각각 메모리를 할당하여 주어야만 한다. JVM은 기본적으로 아무런 옵션 없이 실행하면 OS마다 다르지만, 적어도 32MB ~ 64MB의 물리 메모리를 점유한다. 그에 반해서, 쓰레드를 하나 추가하면 1MB 이내의 메모리를 점유한다. 그래서, 쓰레드를 "경량 프로세스"라고도 부른다.

 

그리고, 요즘은 두 개 이상의 코어가 달려있는 멀티 코어 시대다. 대부분의 작업은 단일 쓰레드로 실행하는 것보다는 다중 쓰레도로 실행하는 것이 더 빠른 시간에 결과를 제공해준다.

 

쓰레드들이 공유하는 자원 = 쉐어드 리소스

이런 자원의 영역을 critical section 이라고 한다.

 

쓰레드에서는 쉐어드 리소스가 발생할 수 있다. 공유 자원을 동시에 쓰면 문제가 발생할 수 있다.

예를 들면 한 쪽에서는 값을 더하고, 한 쪽에서는 값을 빼고 업데이트 되지 않은 상태에서 연산이 중복으로 일어나면 문제가 될 수있다. 그래서 동기화(싱크로나이즈)로 순서를 맞춰줘야한다. 동기화가 어떻게 되는지 살펴보겠다.

 

Thread 구현하기

쓰레드를 생성하는 것은 크게 두 가지 방법이 있다. 하나는 Runnable 인터페이스를 사용하는 것이고, 다른 하나는 Thread 클래스를 사용하는 것이다. Thread 클래스는 Runable 인터페이스를 구현한 클래스이므로, 어떤것을 적용하느냐의 차이만 있다. 모두 java.lang 패키지에 있다.

 

Runnable 인터페이스 구현

Class Test
{
	t = new Thread(new Anything());
    t.start();
}
Class Anything implements Runnable 
{
	public void run(){
    	'''
   }
}

자바는 다중 상속이 허용되지 않으므로 이미 다른 클래스를 상속한 경우 thread를 만들려면 Runnable interface를 implements 하도록 한다.

 

방법 1 - Thread를 extends -> 

package thread;

class MyThread extends Thread{

    public void run() { // 쓰레드가 start되면 run() 실행이 된다.
        int i;

        for (i = 0; i <= 200; i++) {
            System.out.print(i + "\t");

            try {
                sleep(100);  // Thread 클래스의 static method
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadTest {

    public static void main(String[] args) {
		
        // Thread를 바로 extends하여 실행하는 방법
        System.out.println("start");
        MyThread th1 = new MyThread();
        MyThread th2 = new MyThread();

        th1.start();
        th2.start();
        System.out.println("end");
    }
}

 

방법 2 Runnnable를 implements 

package thread;

class MyThread implements Runnable{

    public void run() { // 쓰레드가 start되면 run() 실행이 된다.
        int i;

        for (i = 0; i <= 200; i++) {
            System.out.print(i + "\t");

            try {
                Thread.sleep(100);  // Thread 클래스의 static method
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadTest {

    public static void main(String[] args) {
		
        // Thread 객체를 만들어서 Thread 인스턴스에 Runnable 객체를 넣어서 하는 방법
        System.out.println("start");
        MyThread runner1 = new MyThread();
        Thread th1 = new Thread(runner1);
        th1.start();

        MyThread runner2 = new MyThread();
        Thread th2 = new Thread(runner2);
        th2.start();

        System.out.println("end");
    }
}

  • 쓰레드가 수행되는 우리가 구현하는 메소드는 run() 메소드다.
  • 쓰레드를 시작하는 메소드는 start() 이다.
  • 위의 두 클래스에서 쓰레드는 총 3개가 돌아간다. main, th1, th2
  • main이 하는일 -> start찍고 쓰레드 2개 만들어서 뛰어놓고 end찍는게 끝
  • 쓰레드가 다른 클래스를 확장할 필요가 있을 경우에는 Runnalbe 인터페이스를 구현하면 되며, 그렇지 않은 경우에는 Thread 클래스를 사용하는 것이 편하다.

Threat 클래스의 생성자를 살펴보자

생성자 설명
 Thread() 새로운 쓰레드를 생성한다.
Thread(Runnable target) 매개변수로 받은 target 객체의 run 메소드를 수행하는 쓰레드를 생성한다.
Thread(Runnable target, String name)
매개 변수로 받은 target 객체의 rin() 메소드를 수행하고,  name이 라는 이름을 갖는 쓰레드를 생성한다.
Trhead(String name) name이라는 이름을 갖는 쓰레드를 생성한다.
Thread(ThreadGroup group, Runnable target) 매개 변수로 받은 group 쓰레드 그룹에 속하는 target 객체의 run()메소드를 수행하는 쓰레드를 생성한다.
Thread(ThreadGroup group, Runnable target, String name) 배개 변수로 받은 group의 쓰레드 그룹에 속하는 target 객체의 run() 메소드를 수행하고, name라는 이름을 갖는 쓰레드를 생성한다.
Thread(ThreadGroup group, Runnable target, String name, long stackSize) 매개 변수로 받은 group의 쓰레드 그룹에 속하는 target 객체의 run() 메소드를 수행하고, name이라는 이름을 갖는 쓰레드를 생성한다. 단 해당 쓰레드의 스택의 크기는 stackSize만큼만 가능하다.
Thread(ThreadGroup group, String name) 매개 변수로 받은 group의 쓰레드 구룹에 속하는 name이라는 갖는 쓰레드를 생성한다.

Thread의 이름을 지정해주고 싶다면?

NameThread라는 클래스가 있으며, Thread 클래스를 확장했다. 이렇게 아무런 조치를 취하지 않으면, 아무 매개 변수도 없는 Thread() 생성자를 사용하는 것과 동일하다.

public class NameThread extends Thread {
        public NameThread() {
            
        }
        public void run() {
            
        }
}

 

만약 쓰레드의 이름을 "Threadname"으로 지정하고 싶다면, 이 NameThread의 생성자는 다음과 같이 변경하면 된다.

public NameThread() {
        super("ThreadName");
}

그러면, Thread(String name)을 호출한 것과 동일한 효과를 보게 되는 것이다. 한 걸음 더 나아가서, 이렇게 코드에 "ThreadName"이라고 지정해주면, 이 쓰레드 객체를 몇십개를 만들어도 "ThreadName"이라는 동일한 이름을 가지게 된다. 이러한, 단점을 피하려면 어떻게 해야할까?

public NameThread(String name) {
        super(name);
}

다음과 같이 변경하면 된다.

 

쓰레드 객체를 생성할 때 매개 변수를 받고, 인스턴스 변수로 사용할 수도 있다.

public class NameCalcThread extends Thread {
        private int calcNumber;
        public NameCalcThread (String name, int clacNumber) {
            super(name);
            this.calcNumber = clacNumber;
        }
        public void run() {
            calcNumber++;
        }
}

이렇게 사용하면, calcNumber라는 갑을 동적으로 지정하여 쓰레드를 시작할 수 있다.

쓰레드의 생성자들에 대해서 살펴봤으니, 이제는 Thread 클래스의 주요 메소드들을 살펴보자.

 

많이 사용되는 sleep()

Thread 클래스에는 deprecated 된 메소드도 많고, static 메소드도 많이 있다. deprecated 된 메소드는 "더 이상 사용하지 않는 것" 이라는 의미다. 그리고, static 메소드는 개겣를 생성하지 않아도 사용할 수 있는 메소드를 말한다. 다시 말해서 Thread 메소드는 대부분 해당 쓰레드를 위해서 존재하는 것이 아니라, JVM에 있는 쓰레드를 관리하기 위한 용도로 사용한다. sleep() 는 항상 try~catch로 묶어주어야하고 적어도 InterruptedException으로 catch해주어야 한다.

 

Thread 클래스의 주요 메소드를 살펴보자

리턴 타입 메소드 이름 및 매개 변수 설명
void run() 더 이상 설명이 필요없는 여러분들이 구현해야 하는 메소드다.
long getId() 쓰레드의 고유 id를 리턴한다. JVM에서 자동으로 생성해준다.
String getName() 쓰레드의 이름을 리턴한다.
void setName(String name) 쓰레드의 이름을 지정한다.
int getPriority() 쓰레드의 우선 순위를 확인한다.
void setPriority(int newPriority) 쓰레드의 우선 순위를 지정한다.
boolean isDaemon() 쓰레드가 데몬인지 확인한다.
void setDaemon(boolean on) 쓰레드를 데몬으로 설정할지 아닌지를 설정한다.
StackTraceElement[] getStackTrace() 쓰레드의 스택 정보를 확인한다.
Thread.State getState() 쓰레드의 상태를 확인한다.
ThreadGroup getThreadGroup() 쓰레드의 그룹을 확인하다.

데몬 쓰레드?

어떤 쓰레드를 데몬으로 지정하면, 그 쓰레드가 수행되고 있든, 수행되지 않고 있든 상관 없이 JVM이 끝날 수 있다. 단, 해당 쓰레드가 시작하기(start() 메소드가 호출되기) 전에 데몬 쓰레드로 지정되어야만 한다.

DaemonThread thread = new DaemonThread();
thread.setDaemon(true); // 데몬쓰레드로 지정
thread.start();

쓰레드가 시작한 다음에는 데몬으로 지정할 수 없다. 데몬 쓰레드로 지정하면 데몬 쓰레드는 해당 쓰레드가 종료되지 않아도 다른 실행중인 일반 쓰레드가 없다면 멈춰버린다. 그런데 이런 데몬 스레드를 왜 만들었을까?

 

예를 들면, 모니터링하는 쓰레드를 별도로 띄어 모니터링하다가, 주요 쓰레드가 종료되면 관련된 모니터링 쓰레드가 종료되어야 프로세스가 종료될 수 있다. 그런데, 모니터링 쓰레드를 데몬 쓰레드로 만들지 않으면 프로세스가 종료될 수 없게 된다. 이렇게 부가적인 작업을 수행하는 쓰레드를 선언할 때 데몬 쓰레드를 만든다.

 

쓰레드와 관련이 많은 Sumchronized

어떤 클래스나 메소드가 쓰레드에 안전하려면, synchronized를 사용해야만 한다. 은행에서 직원이 한 창구에서 한 고객의 요청만 처리한다. 여러 쓰레드가 한 객체에 선언된 메소드에 접근하여 데이터를 처리하려고 할 때 동시에 연산을 수행하여 값이 꼬이는 경우가 발생할 수 있다. 단 메소드에서 인스턴스 변수를 수정하려고 할 때에만 이러한 문제가 생긴다. 매개 변수나 메소드에서만 사용하는 지역변수만 다루는 메소드는 전혀 synchronized로 선언할 필요가 없다.

  • 메소드 자체를 synchronized로 선언하는 방법 (synchronized method)
    public synchronized void plus(int value){
        amount += value;
    }

 

하지만, 이렇게 하면 성능상 문제점이 발생할 수 잇다. 예를 들어 어떤 클래스에서 30줄짜리 메소드가 있다고 가정하자. 그 클래스에서 amount라는 인스턴스 변수가 있고, 30줄짜리 메소드에서 amount라는 변수를 한 줄에서만 다룬다. 만약 해당 메소드 전체를 synchronized로 선언한다면, 나머지 29줄의 처리를 할 때  필요없는 대기시간이 발생하게 된다. 이러한 경우는 메소드 전체를 감싸면 안 되며, amount라는 변수를 처리하는 부분만 synchronized 처리를 해주면 된다.

 

  • 다른 하나는 메소드 내의 특정 문장만 synchronized로 감싸는 방법(synchronizedstatements)
    public void plus(int value){
        synchronized(this){
            amount += value;
        }
    }

이렇게 하면 synchronized(this) 이후에 있는 중괄호 내에 있는 연산만 동시에 여러 쓰레드에서 처리하지 않겠다는 의미히다. 소괄호 안에 this가 있는 부분에는 잠금 처리를 하기 위한 객체를 선언한다. 여기는 그냥 this라고 지정했지만, 일반적으로는 다음과 같이 별도의 객체를 선언하여 처리한다. 여기서는 그냥 this라고 지정했지만, 일반적으로는 다음과 같이 별도의 객체를 선언하여 사용한다.

 

Object lock = new Object();
public void plus(int value){
        synchronized(lock){
            amount += value;
        }
    }

synchronized를 사용할 때에는 하나의 객체를 사용하여 블록 내의 문장을 하나의 쓰레드만 수행하도록 할 수 있다. 만약 블록에 들어간 쓰레드가 일을 다 처리하고 나오면, 문지기는 대기하고 있는 다른 쓰레드에게 기회를 준다.

 

    // 이러한 객체는 하나의 클래스에서 2개 이상 만들어 사용 할 수도 있다.
    private int amount;
    private int interest;
    private Object interstLock = new Object();
    private Object amountLock = new Object();
    public  void addInterest(int value){
        synchronized(interstLock) {
            interest += value;
        }
    }
    public void plus(int value){
        synchronized(amountLock){
            amount += value;
        }
    }

두 개의 별도 lock 객체를 사용하면 보다 효율적인 프로그램이 된다.

쓰레드를 통제하는 메소드들

리턴 타입 메소드 이름 및 매개 변수 설명
Thread.State getState() 쓰레드의 상태를 확인한다.
void join() 수행중인 쓰레드가 중지할 때까지 대기한다.
void join(long millis) 매개 변수에 지정된 시간만큼(1/1,000초) 대기한다.
void join(long millis, int nanos) 첫 번째 매개 변수에 지정된 시간(1/1,000초) + 두 번째 매개 변수에 지정된 시간(1/1,000,000,000초)만큼 대기힌다.
void interrupt() 수행중인 쓰레드에 중지 요청을 한다.

먼저 getState() 메소드에서 리턴하는 Thread.State에 대해서 알아보자. 자바의 Thread 클래스에는 State라는 enum 클래스가 잇다. 이 클래스에 선언되어 잇는 상수들의 목록은 다음과 같다.

상태 의미
NEW 쓰레드 객체는 생성되엇지만, 아직 시작되지는 않은 상태
RUNABLE 쓰레드가 실행중인 상태
BLOCKED 쓰레드가 실행 중지 상태이며, 모니터 락이 풀리기를 기다리는 상태
WATTING 쓰레드가 대기중인 상태
TIMED_WATTING 특정 시간만큼 쓰레드가 대기중인 상태
TERMINATED 쓰레드가 종료된 상태

이 클래스는 public static으로 선언되어 있다. 다시 말하면, Thread.Stated.State.New와 같이 사용할 수 있다는 의미다. 그리고, 어떤 쓰레드이건 간에 "NEW -> 상태 -> TERMINATED"의 라이프 사이클을 가진다. 여기서 "상태"에 해당하는 것은 NEW와 TERMINATED를 제외한 모든 다른 상태를 의미한다. 쓰레드 생태를 다이어그램으로 표시하면 다음과 같다.

출처 : https://www.uml-diagrams.org/java-thread-uml-state-machine-diagram-example.html?context=stm-examples

Thread의 상태 확인을 위한 메소드

리턴 타입 메소드 이름 및 매개 변수 설명
void checkAcces() 현재 수행중인 쓰레드가 해당 쓰레드를 수정할 수 있는 권한이 있는지를 확인한다. 만약 권한이 없다면 Secu-rityException이라는 에외를 발생시킨다
boolean isAlive() 쓰레드가 살아 있는지를 확인한다. 해당 쓰레드의 run() 메소드가 종료되었는지 안 되었는지를 확인하는 것이다.
boolean isInterrupted() run() 메소드가 정상적으로 종료되지 않고 inter-rupt() 메소드의 호출을 통해서 종료되었는지 확인 하는 데 사용한다.
static boolean interrupted() 현재 쓰레드가 중지되었는지를 확인한다.

interrupted() 메소드는 잘 살펴보면 static 메소드다. 따라서, 현재 쓰레드가 종료되었는지를 확인할 때 사용한다. isInterrupted() 메소드는 다른 쓰레드에서 확인할 때 사용되고, interrupted() 메소드는 본인의 쓰레드를 확인할 때 사용된다는 점이다. JVM에서 사용되는 쓰레드의 상태들을 확인하기 위해서는 Thread 클래스의 static 메소드들을 알아야만 한다.

주요 static 메소드의 간단한 목록만 살펴보고 넘어가자

리턴 타입 메소드 이름 및 매개 변수 설명
static int activeCount() 현재 쓰레드가 속한 쓰레드 그룹에 쓰레드 중 살아 있는 스레드의 개수를 리턴한다.
static Thread currentThread() 현재 수행중인 쓰레드의 객체를 리턴한다.
static void dumpStack() 콘솔 창에 현재 쓰레드의 스택 정보를 출력한다.

Object 클래스에 선언된 쓰레드와 관련있는 메소드들

Thread 클래스에 선언된 메소드 외에 쓰레드의 상태를 통제하는 메소드가 있다. Object 클래스들에 선언되어 있는 메소드들이다. 앞에서 Object 클래스 설명할 때 살짝 설명만 하고 넘어갔었던 그 메소드들이다.

리턴타입 메소드 이름 및 매개 변수 설명
void wait() 다른 쓰레드가 Object 객체에 대한 notify() 메소드나 notifyAll() 메소드를 호출할 때까지 현재 쓰레드가 대기하고 있도록 한다.
void wait(long itmeout) wait() 메소드와 동일한 기능을 제공하며, 매개 변수에 지정한 시간만큼 대기한다. 즉, 매개 변수 시간을 넘어 섰을 때에는 현재 쓰레드는 다시 깨어난다. 여기서의 시간은 밀리초로 1/1000초 단위다. 만약 1초간 기다리게 할 경우에는 1000을 매개 변수로 넘겨주면 된다.
void wait(ling timeout, int nanos) wait() 메소드와 동일한 기능을 제공한다. 하지만, wait(timeout)에서 밀리초 단위의 대기 시간을 기다린다면, 이 메소드는 보다 자세한 밀리초 + 나노초(1/1,000,000,000초) 만큼만 대기한다. 뒤에 있는 나노초의 값은 0~999,.999 사이의 값만 지정할 수 있다.
void notify() Object 객체의 모니터에 대기하고 있는 단일 쓰레드를 깨운다.
void notifyAll() Object 객체의 모니터에 대기하고 있는 모든 쓰레드를 깨운다.

wait() 메소드를 사용하면 쓰레드가 대기 상태가 되며, notify()나 notifyAll() 메소드를 사용하면 쓰레드의 대기 상태가 해제된다.

volatile

volatile에 관해 보기!

 

내용이 길어 따로 포스팅 해두었습니다.

 

ThreadLocal

ThreadLocal에 관해 보기!

 

내용이 길어 따로 포스팅 해두었습니다.

 

 

 

참고 자료 : 자바의 신 / 이상민

728x90
반응형
Comments