본문 바로가기
Java

Thread-1

by 스르나 2021. 2. 8.
  1. 자바 쓰레드 사용방법
  2. start, run
  3. 쓰레드의 효율성

1. 자바 쓰레드 사용방법

자바에서 쓰레드를 만들 수 있는 방법은 2가지가 있다.

 

1. Thread를 상속받아서 구현

2. Runnable 인터페이스이용하는 방법

 

우선 Thread를 상속받는 방법부터 보자.

 

public class ThreadEx1 extends Thread{

    //Thread의 run 메소드를 구현한다.
    @Override
    public void run(){
        for(int i=0;i<10;i++){
            System.out.println(getName()); //현재 스레드의 이름을 출력
        }
    }
}

 

Thread를 상속받아 구현하는 방법은 Thread의 run 메소드에 쓰레드로 실행시킬 내용을 적어주면 된다.

Thread를 상속받았기 때문에 run이외의 다른 메소드도 직접 오버라이딩해서 수정할 수 있다는 장점이 있다.

 

다음으로는 Runnable 인터페이스를 이용하는 방법이다.

 

public class RunnableEx implements Runnable {
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName());
        }
    }
}

Runnable 인터페이스는 run메소드만 가지고 있기 때문에 run에 쓰레드로 실행시킬 내용을 적어주면 된다.

 

여기서 Runnable은 쓰레드를 위해 만들어진 인터페이스가 아니란것을 람다식을 알고있는 사람은 알겠지만 모르는 사람을 위해서 잠깐 설명하자면 Runnable은 자바의 함수형 프로그래밍을 위해 만들어진 인터페이스이다.

 

사용방법 2가지의 간단한 예시를 보았는데 전자는 객체를 상속하기때문에 다른 객체를 상속할 수 없다는 점에서 보통 쓰레드를 구현할때 Runnable을 이용한다.

 

그럼 2가지 방식의 실행 방법을 보자.

 

// 구현방식 2가지의 실행 방법
public class Ex1 {
    public void start(){
        ThreadEx1 threadEx1=new ThreadEx1();
        Thread threadEx2=new Thread(new RunnableEx());


        threadEx1.start();
        threadEx2.start();
    }
}

실행 방법은 Thread상속 방식 같은 경우 상속받는 후손 클래스를 직접 생성한뒤 start메소드를 호출하면 된다.

Runnable 인터페이스 구현방식은 좀 다른데 우선 Thread객체를 직접 생성하되 new Thread의 생성자에 Runnable을 구현한 구현체의 인스턴스를 넣어 주는 방식이다.

 

각각 생성한 Thread객체의 start메소드를 호출하면 실행이 된다. 실행 결과는 아래와 같다.

 

그런데 쓰레드로 실행시켰으면 우리의 기대는 Thread0,1이 어느정도 번갈아 가며 실행되어야 하는데 결과는 그렇지 않다.

 

이유는 쓰레드로 실행시킨것들의 실행 시간이 워낙짧아 쓰레드 실행 교체(Context Switch)를 하기도전에 끝났기 때문이다.

 

그럼 한번 run의 for문의 반복횟수를 늘려서 확인해보자.

 

public class ThreadEx1 extends Thread{

    //Thread의 run 메소드를 구현한다.
    @Override
    public void run(){
        for(int i=0;i<1000000;i++){
            System.out.println(getName()); //현재 스레드의 이름을 출력
        }
    }
}


public class RunnableEx implements Runnable {
    @Override
    public void run() {
        for(int i=0;i<1000000;i++){
            System.out.println(Thread.currentThread().getName());
        }
    }
}

 

이번에는 1과0이 번갈아 가며 실행이 되었다.

 

 

2. start, run

이번에는 실행을 할때 호출한 start메서드와 run메소드의 차이점을 보면서 쓰레드가 어떤 식으로 실행이 되는지 보자

 

우선 간단하게 한 줄로 설명을 하면 start메소드는 쓰레드를 실행시키는 메소드이고, run메소드는 start에 의해 실행될 메소드이다.

 

그럼 start메소드가 실행이 될때 어떤일이 일어날까 한번 알아보자.

 

우선 자바의 호출스택을 알아야한다.

 

호출스택이란 실행을 할 함수들이 들어가는 스텍이다.

 

자바의 main메소드를 실행시키면 가장먼저 Main스택이 생성이 되고 이 Main스택에 main메소드가 들어간다. 그리고 main 메소드에서 실행되는 함수들이 Main스택에 쌓이고 실행이 완료되면 스택에서 나간다.

 

한번 예시를 보자

 

public class StackEx {
    
    public void method1(){
        method2();
        System.out.println("메소드 2 실행!");
    }
    
    private void method2(){
        System.out.println("메소드 1 실행!");
    }
}

위 코드는 method1이라는 메소드가 method2를 호출하는 간단한 클래스이다. 이 StackEx라는 클래스를 main메소드에서 실행을 시킨다면 아래와 같이 호출스택이 변한다.

 

 

public class Main {
    public static void main(String[] args) {
        StackEx stackEx=new StackEx();

        stackEx.method1();
    }
}

 

 

호출스택은 우선 main메소드가 들어가고, 그다음 실행시킨 new Stack생성자 메소드가 올라간다. new Stack으로 생성자 메소드가 실행이 완료된다면 호출스택에서 해당 메소드는 빠진다.

 

그다음에는 method1메소드가 호출되어서 Main스택에 들어가고 method1에서 호출한 method2가 Main스택에 들어간다.

 

Main스택에서 method2가 완료된다면 위에서부터 차례로 빠진다.

 

자, 여기서 보면 해당 스택에서는 한가지 메소드만 처리할 수 있다는 것을 알 수 있다.

 

그럼 쓰레드가 어떤식으로 실행이 되는지 한번보자.

 

쓰레드는 호출스택을 추가로 생성하는 것부터 시작한다.

 

우리의 바 프로그램은 처음에 Main스택이 생성되는데, 실행도중 쓰레드가 실행된다면 해당 쓰레드를 실행 시킬 호출스택을 추가로 만든다.

 

그리고 해당 쓰레드에의해 실행되는 메소드들은 해당 호출스택에 들어간다.

 

한번 예시를 보자.

 

public class Main {
    public static void main(String[] args) {
        
        ThreadEx1 threadEx1=new ThreadEx1();
        threadEx1.start();
        
        
        StackEx stackEx=new StackEx();
        stackEx.method1();
    }
}

위 코드를 보면 기존에 만들어두었던 ThreadEx1 쓰레드를 실행시키고 StatckEx를 실행시켰다. 그럼 호출스택은 아래와 같이 된다.

 

우선 new ThreadEx로 생성과 start까지는 Main스택에서실행 되지만 start에서 쓰레드가 실행되어서 새로운 호출 스택이 만들어지고 그 호출스택에는 쓰레드가 실행시킬 run메소드가 스택에 가장 먼저 들어간다. 그리고 run안에서의 메소드들은 모두 ThreadEx Stack에 들어간다.

 

그리고 Main스택에는 다음에서 실행시킬 new StackEx생성자가 들어가는 것이다.

 

자 이제 쓰레드와 스택에 좀 이해가 됐을것이다.

 

 

3. 쓰레드 효율성

 

이번에는 쓰레드가 항상 빠른지에 대해 알아볼것이다.

 

쓰레드의 효율성을 따지는것에는 여러가지 지표들이 있지만 우리가 가장 크게 신경써야 하는것은 위에서 한번 언급한 Context Switch가 있고, 다음으로는 자원의 점유가 있다.

 

우선 Context Switch를 먼저 설명해보겠다. Context Switch는 한 프로세스에서 여러 쓰레드들이 있을때 현재 실행중인 쓰레드를 중지하고 다른 쓰레드로 바꾸는 것을 의미한다.

 

이 Context Switch가 쓰레드의 효율성을 따질때 중요한 이유는 Context Switch를 할때 컴퓨터는 상당히 많은 일을 해야하기 때문이고, Context Switch시간은 생각보다 길다. 그렇기 때문에 단순한 계산만 있는 경우는 쓰레드를 통해 계산하는 것보단 싱글쓰레드로 해결하는 것이 보다 효율적일때가 있다.

 

다음으로는 자원의 점유라는 것이있는데 쓰레드같은 경우 어찌됐든 한 프로세스 안에서 실행이 되기때문에 해당 프로세스의 자원을 이용한다. 이때 여러 쓰레드가 한 자원을 동시에 사용중이라면 현재 자원을 사용중인 쓰레드가 작업을 완료하던가, Context Switch가 일어나서 순서가 바뀔때 까지 기다려야한다.

그렇다는 것은 한 쓰레드는 지금당장 자원을 소유할 수 있다면 작업이 금방 완료되는데도 불구하고 다른 쓰레드가 자원을 사용중이여서 작업을 완료하지 못하는 경우가 생길 수 있다.

여기서 알 수 있는점은 쓰레드는 가급적이면 각자 독립된 자원을 사용할 경우에 보다 효율적이라는 것을 알 수 있다.

 

그럼 위의 2가지 모두가 포함된 문제를 보자.

 

 

public class Ex2 {
    public void startEx1(){
        long start=System.currentTimeMillis();
        for(int i=0;i<100000;i++){
            System.out.println("hello");
        }

        for(int i=0;i<100000;i++){
            System.out.println("world");
        }

        System.out.println("작업 완료시간"+(System.currentTimeMillis()-start));
    }

    public void startEx2(){
        long start=System.currentTimeMillis();
        Thread thread1=new Thread(()->{
            for(int i=0;i<100000;i++){
                System.out.println("hello");
            }
            System.out.println("작업1 완료시간: "+(System.currentTimeMillis()-start));
        });

        Thread thread2=new Thread(()->{
            for(int i=0;i<100000;i++){
                System.out.println("world");
            }
            System.out.println("작업2 완료시간: "+(System.currentTimeMillis()-start));
        });

        thread1.start();
        thread2.start();
    }
}

자 Ex2의 startEx1 메소드는 쓰레드를 사용하지 않고 100000번의 반복회수를 가진 for문을 2번 돌리는 일을하고,

 

startEx2는 쓰레드를 이용해 100000번의 for문을 각각 실행시킨다.

 

그럼 실행과정은 아래와 같다.

 

아무래도 쓰레드를 사용하는쪽이 병렬로 실행되어 보다 더 빠를것 같지만 실상은 그렇지 않다.

 

실행결과를 보자.

 

 

 

위가 startEx1의 결과이고 밑이 startEx2의 결과이다. 오히려 쓰레드를 사용하지 않은쪽이 더 빠르게 끝났다.

 

이유는 위에서 설명한 효율성 문제 2가지 모두 포함이된다.

 

우선 싱글쓰레드인 startEx1은 Context Switch과정이 없다. 반면 startEx2는 Context Swtich가 자주 일어나서 시간이 추가가 되었다.

 

그다음으로는 자원인데 코드를 보면 공유하는 자원이 없다. 하지만 코드가 전부가 아니다. 우리가 보는 화면에 출력해주는 것또한 공유된 자원이다. 즉, System.out.println의 호출에서 공유된 자원이 있끼때문에 쓰레드는 대기 시간이 추가가 된것이다.

 

위처럼 간단한 것에는 오히려 쓰레드를 사용하는 것이 보다 효율성이 떨어진다.

 

 

 

'Java' 카테고리의 다른 글

Thread-3  (0) 2021.02.12
Thread-2  (0) 2021.02.11
Interface  (0) 2020.09.21
Generic  (0) 2020.07.21
Stream  (0) 2020.07.15