JAVA - Multi-Thread Programming

Java

Java 공부를 정리 tag


Multi-Thread Programming

Thread

process 내에서 실행되는 단위. - wiki

1개의 프로세스는 적어도 1개의 thread를 가진다. 그리고 하나의 프로세스 안에서 2개 이상의 thread가 동시에 실행될 수 있는데 이를 multi-threading이라고 한다.


Process

컴퓨터의 메모리에 올라가서 실행되는 프로그램을 뜻한다.

여러개의 프로세스를 사용하는 것을 multi-processing이라고 하며, 같은 시간에 여러개의 프로그램을 띄우는 방식을 multi-tasking이라고 한다.


Thread vs Process

javaThread1_1

사진 출처


Multi-Processing, Programming, Threading


Multi-Processing vs Multi-Threading

multi-threading을 더 많이 사용하게 될까?

  1. 자원의 효율성 증가

    multi-processing으로 실행되는 작업을 multi-threading으로 실행할 경우, process를 생성해야하는 system call이 줄어들어서 자원을 효율적으로 관리할 수 있다.

  2. 처리비용 감소

    multi-threading은 stack영역을 제외하고 프로세스 내 메모리를 공유하기 때문에 context-switching이 발생했을 때 overhead가 더 적다. 따라서 전환속도가 더 빠르기에 좋다.

  3. 그러나 동기화 문제를 조심해야 한다.


Java Multi-Thread

Java JDK 8 Concurreny를 보고 정리하는 글입니다.


Concurrency

Even the word processor should always be ready to respond to keyboard and mouse events, no matter how busy it is reformatting text or updating the display. Software that can do such things is known as concurrent software.

Java는 concurrent programming을 염두하며 만들어졌다. 이 문서로 우리는 java.util.concurrent package를 배워볼 것이다.


concurrent programming에서 2개의 실행 단위가 존재한다.


Defining and Starting a Thread

Thread instance를 생성하는 2가지 방법이 있다. 코드는 여기서볼 수 있다.


아니 비슷해보이는데 왜 굳이 2개로 구현할 수 있게 했을까…?

라는 의문이 가장 먼저 들었다. 그리고 난 운영체제에 대해 공부할 때 extends Thread만 있는 줄 알았기에 더 궁금했다.

먼저 Runnable interface를 한번 보도록 하자.

// Runnable.java
package java.lang;

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

그 다음에는 Thread class를 보도록 하자.

// Thread.java
public class Thread implements Runnable {
  ...
    public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }
  ...
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
  ...
}

자세한 내용까지는 잘 모르겠지만 Thread class는 Runnable interface의 구현체라는 사실을 알 수 있었다. 둘로 thread를 생성했을 때 차이점은 ‘구현하느냐’ or ‘상속받느냐’의 차이로 구분할 수 있는데 여기서 Runnable을 일반적으로 쓰이는 이유가 나타난다.

Runnable object, is more general, because the Runnable object can subclass a class other than Thread.

Runnable을 구현하게 되면 Thread무조건 상속받을 필요가 없어지기에 다른 class를 상속받을 수 있게 된다. 따라서 일반적으로 쓰이는데, Thread는 간단한 프로그램을 만들 때 쓰기 쉽다는 장점을 가지고 있다 한다.

… easier to use in simple applications, but is limited by the fact that your task class must be a descendant of Thread.


Pausing Execution with Sleep

Thread.sleep() method 특정 기간동안 현재 실행중인 thread를 중지하게 해준다. 이는 실행 중일 수 있는 다른 application or thread에게 processor의 시간을 사용할 수 있게 해주는 효율적인 방법이다. 그러나 운영체제에 의해 기본으로 제공되는 기능에 의해 제한되기 때문에 중지되는 시간이 정확하다는 보장이 없다.

However, these sleep times are not guaranteed to be precise, because they are limited by the facilities provided by the underlying OS.


Interrupts

An interrupt is an indication to a thread that it should stop what it is doing and do something else.

thread가 interrupt를 받았을 때 어떻게 해야할지는 개발자의 몫이지만 thread를 종료시키는게 일반적이라고 한다.

It’s up to the programmer to decide exactly how a thread responds to an interrupt, but it is very common for the thread to terminate.

thread의 run() method에서 InterruptedException를 자주 던질 수 있는 method가 호출된다면, try-catch를 통해 exception을 처리한다. 제공된 예시 코드를 보도록 하자.

for (int i = 0; i < importantInfo.length; i++) {
    // Pause for 4 seconds
    try {
        Thread.sleep(4000);
    } catch (InterruptedException e) {
        // We've been interrupted: no more messages.
        return;
    }
    // Print a message
    System.out.println(importantInfo[i]);
}

InterruptedException를 던지지 않는 method에 대해서는 아래와 같이 처리해준다고 한다.

for (int i = 0; i < inputs.length; i++) {
    heavyCrunch(inputs[i]);
    if (Thread.interrupted()) { // interrupt 발생하면 return true
        // We've been interrupted: no more crunching.
        return;
    }
}

javaThread1_2

interrupted()method에 대한 설명을 보면 위와 같다. 따라서 예외를 안던지는 method에 대해 thread 내에서 주기적으로 확인을 해주는 작업을 거쳐야 한다.

Interrupt mechanism은 interrupt status라는 내부 flag로 실행된다. Thread.interrupt를 호출하게 되면, 해당 flag를 1로 만든다. thread가 Thread.interrupted() static method로 interrupt가 발생했는지 확인할 때 해당 flag가 0이 된다.

javaThread1_3


start() method

Thread 객체를 생성하면 start()라는 method로 해당 thread 객체를 시작상태로 만들 수 있다. 근데 그렇다면 왜 run을 굳이 한번 더 구현하는 것일까?

그 이유는 start()method를 호출해야 새로운 thread를 생성하고 run()method를 호출하기 때문이다. 다음 그림을 보도록 하자.

javaThread1_5

run()method를 바로 호출한다고 가정해보자. 그렇게되면, main stackmain()위에 바로 run()method가 쌓인다. 그렇게 되면 Multi-Thread로 실행시키는게 아닌, main thread하나로 실행시키는 것과 마찬가지이다.


진짜 새로운 Thread만들까…?

// Thread.java
...
public synchronized void start() {
    ...

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

private native void start0();
...

Thread.javastart()method에 관한 내부 코드이다. start0이라는 native method를 호출하는 것을 볼 수 있는데 여기서 새로운 thread를 생성해주는 것이다. 그리고 start()가 호출되는 순간 해당 thread가 시작되는 것이 아니다. OS스케쥴러가 실행순서를 결정하기 때문에 start()는 ‘실행가능한 상태가 되게 해주는’ 역할을 한다고 볼 수 있다.


joins() method

The join method allows one thread to wait for the completion of another.

현재 실행되고 있는 A thread를 B thread가 끝날 때까지 기다리게 하고 싶다면,

B.joins();

를 써주면 된다. A는 B가 끝날 때까지 기다리게 된다. joins()method를 overload한 method를 만든다면, 개발자가 직접 기다리는 시간을 지정할 수 있게 된다.


Synchronization

Threads는 field와 참조하는 object reference field 에 대한 접근을 공유하여 통신한다. 이러한 통신은 효율적이지만, 2가지 문제를 만들 수 있다.

위 2개의 오류를 해결하기 위해 synchronization을 쓴다고 했는데 이는 또 thread contention문제를 발생시킬 수 있다.


Synchronized Methods

Java에서는 synchronization idiom으로 synchronized methodssynchronized statements를 제공한다. 먼저 synchronized methods 에 대해 알아보도록 하자.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class SynchronizationMethod {
	public static void main(String[] args) throws InterruptedException {
		ExecutorService executorService = Executors.newFixedThreadPool(10); // Thread 10개 생성

		Counter counter = new Counter();

		for (int i = 0; i < 1000; i++) {
			executorService.submit(() -> counter.increment()); // counter.increment() 라는 행위를 multi-threading 으로 처리하겠다는 코드
		}

		executorService.shutdown();
		executorService.awaitTermination(60, TimeUnit.SECONDS);

		System.out.println("counted value = " + counter.getValue());
    /**
		 * Counter class 의 increment() method 는 thread 가 공통으로 접근하는 critical-section 이다.
		 * 따라서 해당 method 에 synchronized keyword 를 추가하여 선언해주면,
		 * 해당 method 에는 한번에 1개의 thread 만 접근이 가능해진다. => interleaving 제거
		 */
	}

	static class Counter {
		int value = 0;

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

		public int getValue() {
			return value;
		}
	}
}
  1. 동일 객체에 대해 synchronized method의 호출들이 interleave 될 수 없게 된다. 한 thread가 객체의 synchronized method를 실행할 때, 동일한 객체에 대해 synchronized method를 호출하는 다른 모든 thread들은 실행을 일시 중단한다.
  2. 이어서 발생하는 동일 객체에 대한 synchronized method의 호출들은 자동적으로 happens-before 관계를 수립하게 된다. 따라서 모든 thread들에게 객체의 변경사항들을 알 수 있게 해준다.

그리고 constructor에 sychronized 키워드를 붙이는건 의미가 없다고 한다. Syntax error 가 발생한다고도 한다.

→ 객체가 생성될 때는 오직 ‘해당 객체를 생성하는 thread만 생성자에 접근’할 수 있기 때문에 있어도 의미가 없다.

Synchronized methodsthread interferencememory consistency errors를 예방할 수 있는 좋은 전략이다. 효과적이지만, liveness라는 문제를 발생시킬 수 있다. 이는 향후 포스팅에서 보도록 하자.


Rerference

Thread란? - wiki

Daemon Thread vs Normal Thread - medium

Java JDK 8 Concurrency - Java docs

Java 8 API

Process vs Thread

Multi-Programming/Processing/Threading

Thread Interference error, Memory Consistency error

Thread Interference error, Memory Consistency error - example code

Thread Interference

Synchronization methods

start() vs run() method

start() vs run() method - youtube