Java/라이브스터디

자바의 멀티쓰레드 프로그래밍

학습목표

  • Thread 클래스와 Runnable 인터페이스
  • 쓰레드의 상태
  • 쓰레드의 우선순위
  • Main 쓰레드
  • 동기화
  • 데드락

 


 

Thread

Thread란 프로세스 내에서 실행되는 흐름의 단위를 의미하며, MultiThread란 둘 이상의 스레드를 동시에 실행하는 방식을 의미합니다.

Thread를 생성한다는 것은 메인스레드와 동시에 돌아가는 작업을 생성한다는 의미를 가집니다.

 

MultiThread는 작업을 잘게 나눈다음 번갈아가면서 실행하는 것을 의미합니다.

 

새로운 작업 Thread를 만들기 위해서는 해당 스레드에서 실행되어야하는 코드를 제공해야합니다.

  1. Runnable 인터페이스의 객체를 Thread 생성자의 파라미터로 사용하는 방법
  2. Thread 클래스를 상속하여 실행코드를 작성하는 방법

 

2번 방법의 경우, 클래스를 상속받기 때문에 다른 클래스의 상속이 불가능하므로 다른 클래스를 상속받아야 할때는 1번 방법을 사용하는 것이 좋습니다.

 

 

Runnable 인터페이스

public interface Runnable {
    /**
     * When an object implementing interface {@code Runnable} is used
     * to create a thread, starting the thread causes the object's
     * {@code run} method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method {@code run} is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

Runnable 인터페이스는 run() 이라는 추상메소드 하나로만 이루어져있습니다.

 

 

public class subRunnable implements Runnable{
    @Override
    public void run() {
        for(int i = 0; i < 100; i++){
            System.out.println("스레드 2 : " + i);
        }
    }

    public static void main(String[] args) {
        new Thread(new subRunnable()).start();
        
        for(int i = 0; i < 100; i++){
            System.out.println("스레드 1 : " + i);
        }
    }
}


=== 출력 결과 === 
스레드 2 : 0
스레드 1 : 0
스레드 2 : 1
스레드 2 : 2
스레드 1 : 1
스레드 2 : 3
스레드 1 : 2
스레드 2 : 4
.
.
.

Runnable 인터페이스를 상속한 클래스내에 run()을 재정의하고 해당 클래스의 객체를 Thread 생성자의 파라미터로 제공할 수 있습니다.

 

 

Thread 클래스

Thread클래스는 기본적으로 Runnable 인터페이스를 상속하고있습니다.

 

public class subThread extends Thread{

    @Override
    public void run(){
        for(int i = 0; i < 100; i++){
            System.out.println("스레드 2 : " + i);
        }
    }
    public static void main(String[] args) {
        new subThread().start();

        for(int i = 0; i < 100; i++){
            System.out.println("스레드 1 : " + i);
        }
    }
}

== 출력 결과 ==

스레드 2 : 0
스레드 2 : 1
스레드 1 : 0
스레드 2 : 2
스레드 1 : 1
스레드 1 : 2
스레드 2 : 3
.
.
.

Thread 클래스를 extends하는 클래스내에서 run()을 재정의하여 사용할 수 있습니다.

 

 

Therad의 상태

스레드의 상태는 총 6가지가 존재합니다.

 

상태 설명
NEW 스레스가 생성은 되었지만 아직 실행되지 않은 상태
RUNNABLE 스레드가 JVM에서 실행중인 상태
BLOCKED monitor lock을 기다리며 block된 상태
WAITING 다른 스레드에서 특정 작업을 수행 할 때까지 무기한 대기중인 상태
TIMED_WAITING 지정된 시간동안 다른스레드의 작업을 기다리는 상태
TERMINATED 종료된 스레드

 

public class subThread extends Thread{
    @Override
    public void run(){
        System.out.println(getState());
    }
    public static void main(String[] args) {
        new subThread().start();
    }
}

=== 출력 결과 ===
RUNNABLE

현재 스레드의 상태는 getState()를 통하여 확인할 수 있습니다.

 

스레드의 상태제어

sleep

public class subThread extends Thread{
    @Override
    public void run(){
        try {
            Thread.sleep(10000);
            System.out.println("10초 후..");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        new subThread().start();
    }
}

== 출력 결과 ==

10초 후..

sleep 메소드는 입력한 시간동안 스레드를 TIMED_WAITING 상태로 만들 수 있습니다.

 

join

public class subThread extends Thread{
    static subThread thread01 = new subThread();
    static Thread main;
    @Override
    public void run(){
        System.out.println(this.getName() + " : " + this.getState());
        System.out.println(main.getName() + " : " + main.getState());
    }
    public static void main(String[] args) throws InterruptedException {
        main = Thread.currentThread();
        System.out.println(main.getName() + " : " + main.getState());
        thread01.start();
        thread01.join();

        System.out.println(main.getName() + " : " + main.getState());
    }
}

== 출력 결과 ==
main : RUNNABLE
Thread-0 : RUNNABLE
main : WAITING
main : RUNNABLE

join을 실행한 스레드는 호출한 스레드가 종료되기 전 까지 WAITING상태가 됩니다.
즉, 특정 스레드의 종료 이전에는 현재 스레드가 종료되지 않습니다.

 

join(long millis)와 같이 원하는 시간동안 기다리는 것도 가능합니다.

 

public class subThread extends Thread{
    static subThread thread01 = new subThread();
    static Thread main;
    @Override
    public void run(){
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(this.getName() + " : " + this.getState());
        System.out.println(main.getName() + " : " + main.getState());
    }
    public static void main(String[] args) throws InterruptedException {
        main = Thread.currentThread();
        System.out.println(main.getName() + " : " + main.getState());
        thread01.start();
        thread01.join(1000);

        System.out.println(main.getName() + " : " + main.getState());
    }
}

== 출력 결과 ==

main : RUNNABLE
main : RUNNABLE
Thread-0 : RUNNABLE
main : TERMINATED

위의 예제의 경우 1초 동안 thread01의 종료를 기다리는데, thread01 내에서 sleep(1000)을 통하여 1초간 스레드를 TIME_WAITING 상태로 만듭니다.

 

main 스레드에서는 약속한 1초동안 기다렸으나 thread01이 종료되지 않았으므로 계속해서 진행하여 스레드를 종료시킵니다.

 

이후에 thread01에서 main 스레드의 상태를 출력하면 이미 종료되었으므로 TERMINATED가 출력됩니다.

 

interrupt

public class subThread extends Thread{
    static subThread thread01 = new subThread();
    static Thread main;
    @Override
    public void run(){
        if(Thread.interrupted()){
            System.out.println("interrupted");
        }else{
            System.out.println("...");
        }
    }
    public static void main(String[] args) throws InterruptedException {
        main = Thread.currentThread();
        System.out.println(main.getName() + " : " + main.getState());
        thread01.start();
        thread01.interrupt();

        System.out.println(thread01.getName() + " : " + thread01.getState());
        System.out.println(main.getName() + " : " + main.getState());
    }
}

=== 출력 결과 ===

main : RUNNABLE
interrupted
Thread-0 : RUNNABLE
main : RUNNABLE

스레드의 현재 수행중인 작업을 중지하고 다른 작업을 수행시켜야 할 때 사용가능합니다.

정적 메소드인 Thread.interruped()와 정적 메소드가 아닌isInterrupted()를 통하여 수행할 작업을 지정할 수 있습니다.

 

 

public class subThread extends Thread{
    static subThread thread01 = new subThread();
    static Thread main;
    @Override
    public void run() {
        try {
            this.sleep(1000);
        } catch (InterruptedException e) {
            System.out.println("Interrupt Occur");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        main = Thread.currentThread();
        System.out.println(main.getName() + " : " + main.getState());
        thread01.start();
        thread01.interrupt();

        System.out.println(thread01.getName() + " : " + thread01.getState());
        System.out.println(main.getName() + " : " + main.getState());
    }
}

=== 출력 결과 ===

main : RUNNABLE
Thread-0 : RUNNABLE
Interrupt Occur
main : RUNNABLE

만일 해당 스레드가 sleep() 이나 wait() 메소드를 수행중이라면 InterruptedException 예외를 발생시킵니다.

 

 

스레드 간섭

두 개 이상의 스레드가 하나의 변수에 접근할 때, 원하는대로 값이 변화하지 않는 것을 의미합니다.

 

public class Counter {
    private int cnt = 0;

    public void increment(){
        cnt++;
    }

    public void decrement(){
        cnt--;
    }

    public int value(){
        return cnt;
    }
}
public class SUBMAIN extends Thread{
    Counter counter = new Counter();

    SUBMAIN(Counter counter){
        this.counter = counter;
    }

    @Override
    public void run(){
        for(int i = 0; i < 2000; i++){
            counter.increment();
            counter.decrement();
            System.out.println(this.getName() + " : " + counter.value());

            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        new SUBMAIN(counter).start();
        new SUBMAIN(counter).start();
        new SUBMAIN(counter).start();
        new SUBMAIN(counter).start();
    }
}

=== 출력 결과 ===
Thread-3 : 0
Thread-0 : 0
Thread-2 : 0
Thread-1 : 0
Thread-0 : 1
Thread-2 : 1
Thread-3 : 1
Thread-1 : 1
Thread-1 : -2
Thread-2 : -2
.
.
.

Thread-2 : -6
Thread-1 : -6
Thread-0 : -6
Thread-3 : -6
Thread-1 : -6
Thread-2 : -6
Thread-0 : -6
.
.
.

개발자가 의도한대로 작동했다면 항상 0이 출력되어야 하지만 스레드간의 간섭이 발생합니다.

 

스레드의 우선순위

모든 스레드는 우선순위를 가지고 있습니다.
높은 우선순위의 스레드는 낮은 우선순위의 스래드보다 우선적으로 실행되며, 새로 생성된 스레드의 우선순위는 5가 됩니다.

우선순위를 설정하기 위해서는 setPriority()를 이용해야하며, 설정된 우선순위를 알기 위해서는 getPriority()를 호출하면 됩니다.

 

public class SUBMAIN extends Thread{

    @Override
    public void run(){
    }

    public static void main(String[] args) {
        SUBMAIN submain = new SUBMAIN();
        SUBMAIN submain1 = new SUBMAIN();

        submain.setPriority(3);

        System.out.println(submain1.getName() + " : " +submain1.getPriority());
        System.out.println(submain.getName() + " : " +submain.getPriority());

    }
}

=== 출력 결과 ===
Thread-1 : 5
Thread-0 : 3

스레드의 우선순위 최대값은 Thread클래스에 정의되어 있으며 각각은 다음과 같습니다.

  • static final int MAX_PRIORITY : 10
  • static final int MIN_PRIORITY : 1
  • static final int NORM_PRIORITY : 5

 

Main 스레드

메인 스레드는 프로그램의 동작 시 기본적으로 실행되는 스레드를 의미합니다.
해당 스레드의 우선순위는 5이고 Thread.currentThread() 를 통하여 해당 스레드에 접근할 수 있습니다.

 

public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());
    System.out.println(Thread.currentThread().getState());
    System.out.println(Thread.currentThread().getPriority());
}

=== 출력 결과 ===
main
RUNNABLE
5

 

synchronized

public class Counter {
    private int cnt = 0;

    public synchronized void increment(){
        cnt++;
    }

    public synchronized void decrement(){
        cnt--;
    }

    public synchronized int value(){
        return cnt;
    }
}

public class SUBMAIN extends Thread{
    Counter counter = new Counter();

    SUBMAIN(Counter counter){
        this.counter = counter;
    }

    @Override
    public void run(){
        for(int i = 0; i < 2000; i++){
            counter.increment();
            counter.decrement();
            System.out.println(this.getName() + " : " + counter.value());

            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        new SUBMAIN(counter).start();
        new SUBMAIN(counter).start();
        new SUBMAIN(counter).start();
        new SUBMAIN(counter).start();
    }
}

=== 실행 결과 ===
Thread-1 : 0
Thread-0 : 0
Thread-1 : 0
Thread-2 : 0
Thread-0 : 0
Thread-3 : 0
Thread-1 : 0
Thread-3 : 0
Thread-0 : 1
Thread-2 : 1
Thread-1 : 0
Thread-0 : 0
.
.
.
.

synchronized키워드는 공유변수에 대한 여러 스레드의 동시 접근을 차단합니다.

하나의 공유 변수에 여러 스레드가 동시에 접근하지 못하므로, 좀 더 원하는 결과에 근접한 값이 출력됩니다.

 

public class Counter {
    private int cnt = 0;

    public void increment(){
        synchronized (this) {
            cnt++;
        }
    }

    public void decrement(){
        synchronized (this) {
            cnt--;
        }
    }

    public int value(){
        synchronized (this) {
            return cnt;
        }
    }
}

메소드내의 특정 Statements를 synchronized로 감싸서 해당 부분에만 적용시키는 것도 가능합니다.

 

deadlock

Deadlock( 교착 상태 )란, 두 프로세스에 대하여 다음과 같은 상태를 말합니다.

  • Process01 : A를 소유한채로 B를 요구
  • Process02 : B를 소유한채로 A를 요구

예제 코드를 보면서 생각해봅시다.

public class SUBMAIN implements Runnable{

    @Override
    public void run() {

    }

    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public void bow(Friend bower) {
            synchronized (this) {
                System.out.format("%s: %s" + "  has bowed to me!%n", this.name, bower.getName());
                bower.bowBack(this);
            }
        } 
        public synchronized void bowBack(Friend bower) {
            synchronized(this) {
                System.out.format("%s: %s has bowed back to me!%n", this.name, bower.getName());
            }
        }
    }

    public static void main(String[] args) {
        final Friend alphonse = new Friend("Alphonse");
        final Friend gaston = new Friend("Gaston");
        new Thread(new Runnable() {
            public void run() { alphonse.bow(gaston); }
        }).start();
        new Thread(new Runnable() {
            public void run() { gaston.bow(alphonse); }
        }).start();
    }
}

소스 출처 : https://docs.oracle.com/javase/tutorial/essential/concurrency/deadlock.html

 

Deadlock (The Java™ Tutorials > Essential Classes > Concurrency)

The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't take advantage of improvements introduced in later releases and might use technology no longer available. See Java Language Changes for a summary of updated

docs.oracle.com

Deadlock 발생 과정

  1. alphonse.bow(gaston);의 실행과 함께 synchronized키워드로 인하여 alphonse에 대한 동시접근을 금지시킵니다.
  2. gaston.bos(alphonse);의 실행과 함께 synchronized키워드로 인하여 gaston에 대한 동시접근을 금지시킵니다.
  3. 1.에서 bower.bowBack(this); 즉, gaston.bowBack(alphonse)를 호출하지만 gaston은 이미 동시접근이 불가능합니다.
  4. 2.에서 bower.bowBack(this); 즉, alphonse.bowBack(gaston)를 호출하지만 alphonse은 이미 동시접근이 불가능합니다.

 

 

 

 

참고자료 및 사이트