백기선 자바 라이브 스터디 10: 스레드

Hoyoung Jung·2021년 1월 23일
2

프로세스와 스레드

  • 프로세스: 실행중인 프로그램
  • 스레드: 스케쥴러에 의해 관리될 수 있는 가장 작은 코드 실행의 단위, 일반적으로 OS에서 지원해 준다.
  • https://en.wikipedia.org/wiki/Thread_(computing)
  • 프로세스를 하나 실행하면 리소스 + 메인 스레드가 생성된다.
  • 프로세스 내에서 스레드를 추가 생성하면 리소스 + 메인 스레드 + 추가 스레드 상태가 된다.
  • 동일 프로세스에서 스레드는 각각 자신만의 스택, 레지스터 공간, PC를 가지고 있고, 나머지 자원(힙, 메소드 영역 등)은 공유한다.

Thread 클래스와 Runnable 인터페이스

Thread

  • 스펙에 따르면 자바에서 클래스를 생성하는 유일한 방법은 Thread 클래스의 객체를 만드는 것이라고 한다.
  • 엇 그럼 Runnable은 뭐지? 이상하네... 라고 생각했지만 아래에 답이 있다.
  • 여튼 public void run()을 오버라이드하고 public void start()를 통해 스레드를 생성 및 실행한다.

Threads are represented by the Thread class. The only way for a user to create a thread is to create an object of this class; each thread is associated with such an object. A thread will start when the start() method is invoked on the corresponding Thread object.

public class PrimeThread extends Thread {

    private int num;
    private int prime;

    public PrimeThread(int num) {
        this.num = num;
    }

    public int getPrime() {
        return prime;
    }

    @Override
    public void run() {
        int p = num;
        while(!isPrime(p)) {
            p++;
        }
        prime = p;
    }

    public static void main(String[] args) {
        PrimeThread p = new PrimeThread(5000);
        p.start();
        try {
            p.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(p.getPrime());
    }

    private boolean isPrime(int n) {
        if (n <= 2) return false;
        for (int i  = 2; i < Math.sqrt(n); i++) {
            if (n % i == 0) return false;
        }
        return true;
    }
}

Runnable

  • 스레드에 의해 실행되길 원하는 클래스는 Runnable을 구현하면 된다.
  • JDK 문서에서는 Thread의 서브클래스가 아니면서 스레드에 의해 실행되는 방법을 제공한다고 나와 있다.
  • 위쪽 내용과 서로 상충된다고 생각했는데, 곰곰히 읽어보니 아니었다. 의문이 풀렸다!

The Runnable interface should be implemented by any class whose instances are intended to be executed by a thread. The class must define a method of no arguments called run.
This interface is designed to provide a common protocol for objects that wish to execute code while they are active. For example, Runnable is implemented by class Thread. Being active simply means that a thread has been started and has not yet been stopped.

In addition, Runnable provides the means for a class to be active while not subclassing Thread. A class that implements Runnable can run without subclassing Thread by instantiating a Thread instance and passing itself in as the target. In most cases, the Runnable interface should be used if you are only planning to override the run() method and no other Thread methods. This is important because classes should not be subclassed unless the programmer intends on modifying or enhancing the fundamental behavior of the class.

public class FindPrime implements Runnable{
    private int num;
    private int prime;   

    //생략
    
    public static void main(String[] args) {
        FindPrime p = new FindPrime(9030);
        Thread t = new Thread(p);
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(p.getPrime());
    }
}

쓰레드의 우선순위

  • setPriority() 메소드로 MIN(1), MAX(10) 사이의 값을 지정할 수 있다.
  • 우선순위가 높으면 다른 스레드에 비해 더 많은 실행시간을 얻을 수 있다.
  • 기본값은 5

쓰레드 그룹

  • 쓰레드를 그룹으로 묶어서 다룸
  • 모든 쓰레드는 반드시 하나의 그룹에 속함
  • 지정하지 않을 경우 부모 쓰레드와 같은 그룹에 속하고 같은 우선순위를 상속받는다.

데몬 스레드

  • setDaemon(boolean on)을 이용해서 데몬 쓰레드를 만들 수 있다.
  • 데몬 스레드는 일반 스레드가 종료되기 전까지는 종료하지 않는다고 한다.
  • TODO: 직접 코드 짜서 실행해 보자.

쓰레드 실행 제어

static void sleep(long mills) //현재 스레드를 멈춤
void join()
void join(long mills) //대기 시간 지정 가능
void inturrupt() //WAITING -> RUNNABLE
void stop() // 즉시 종료
void suspend() //일시 종료, 
void resume() //suspend -> resume
static void yield() //실행권을 양보함

쓰레드의 상태

  • NEW
  • RUNNABLE
  • BLOCKED
  • WAITING, TIMED_WATING
  • TERMINATED

Main 쓰레드

  • 자바 프로그램 실행시 기본으로 실행되는 스레드
    public static void main(String[] args) {
        Thread t = Thread.currentThread();
        System.out.println(t.getName());
        System.out.println(t.getThreadGroup().getName());
        System.out.println(t.getPriority());
    }
  • 결과: main, main, 5

동기화

  • synchronized 사용.
  • 자바의 객체는 기본적으로 monitor를 가지고 있고 thread는 monitor를 lock/ unlock할 수 있다.
  • 객체만 모니터를 가지고 있으므로 primitive에 대한 동기화는 제공하지 않는다.
  • 실험적으로 래퍼 클래스 사용시에도 동기화가 되지 않는 것을 확인 (정확한 팩트 체크 필요함)
  • synchronized를 사용하면 객체의 참조를 를 보고 락 상태라면 실행을 멈춘다.

a thread can lock or unlock. Only one thread at a time may hold a lock on a monitor. Any other threads attempting to lock that monitor are blocked until they can obtain a lock on that monitor. A thread t may lock a particular monitor multiple times; each unlock reverses the effect of one lock operation.

The synchronized statement (§14.19) computes a reference to an object; it then attempts to perform a lock action on that object's monitor and does not proceed further until the lock action has successfully completed. After the lock action has been performed, the body of the synchronized statement is executed. If execution of the body is ever completed, either normally or abruptly, an unlock action is automatically performed on that same monitor.

A synchronized method (§8.4.3.6) automatically performs a lock action when it is invoked; its body is not executed until the lock action has successfully completed. If the method is an instance method, it locks the monitor associated with the instance for which it was invoked (that is, the object that will be known as this during execution of the body of the method). If the method is static, it locks the monitor associated with the Class object that represents the class in which the method is defined. If execution of the method's body is ever completed, either normally or abruptly, an unlock action is automatically performed on that same monitor.

  • 성능향상을 위해 wait()와 notify()를 사용할 수 있다고 한다. 많이 좋아질까?
  • wait()를 호출하면 락을 풀고 대기상태가 되고, notify()나 notifyAll()을 이용해 깨울 수 있다.
void synchronized foo() {
    //critical code here
}

void foo2() {
	Info a;
	synchronized(a) {
    	//critical code here
    }
  • 정확한 원인은 기억이 안 나는데 메소드에 사용시 원하는 결과가 나오지 않는 경우도 있었다. 실수였던 걸까?

동기화 실패 코드 및 성공 코드

  • 인스턴스 변수를 사용하면 되지만 테스트를 위해 억지로 static 객체를 하나 만들었다.
  • 아래 코드는 정상 작동하지 않는데 왜 그런 걸까요?
package net.honux;

public class SumTest extends Thread {

    private static Info sum;
    private long curr;
    private long start;
    private long end;

    public SumTest(long start, long end, String name) {
        this.start = start;
        this.curr = start;
        this.end = end;
        System.out.println(start + " to " + end);
        this.setName(name);
        sum = new Info(0);
    }

    @Override
    public void run() {
        calc();
    }

    private synchronized void calc() {
        for (long i = start; i <= end; i++) {
                sum.add(i);
        }
        System.out.println(this.getName() + " ended");
    }

    public static void main(String[] args) {
        long l = 10000L;
        final int NUM_THREAD = 5;
        long step = l / NUM_THREAD;
        SumTest[] t = new SumTest[NUM_THREAD];
        for (int i = 0; i < 5; i++) {
            t[i] = new SumTest(i * step + 1, (i + 1) * step, "T" + i);
        }

        for (var curr : t) {
            curr.start();
        }

        try {
            for (var curr : t) {
                curr.join();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(sum);
    }
}

class Info {
    public long value;
    Info(long v) {
        this.value = v;
    }

    public void add(long v) {
        value += v;
    }

    @Override
    public String toString() {
        return "Info{" +
                "value=" + value +
                '}';
    }
}

성공코드 1

  • 블록을 사용하면 성공한다. 왜?
    private void calc() {
        for (long i = start; i <= end; i++) {
            synchronized (sum) {
                sum.add(i);
            }
        }
        System.out.println(this.getName() + " ended");
    }

성공코드 2

  • 문득 생각나서 테스트해 보니 Info.add()를 synchrhonized하면 성공한다.
  • 갑자기 처음 코드가 뭐가 잘못인지 알 것 같다!

/*  수정된 calc() 메소드 
    private void calc() {
        for (long i = start; i <= end; i++) {
                sum.add(i);
        }
        System.out.println(this.getName() + " ended");
    }*/
    
class Info {
    public long value;
    Info(long v) {
        this.value = v;
    }

    public synchronized void add(long v) {
        value += v;
    }

    @Override
    public String toString() {
        return "Info{" +
                "value=" + value +
                '}';
    }
}

데드락

  • 기본적인 데드락 이론과 같은 이유로 데드락이 발생할 수 있다.
  • 동기화를 사용하고 자원의 순환 참조가 생기면 데드락 발생 가능.

참고자료

profile
주짓수를 좋아하는 개발자

0개의 댓글