[Java] Concurrency

immanuelk1m·2023년 6월 7일
0

Java

목록 보기
9/9
post-thumbnail

Concurrency : 동시성

a program or algorithm can be performed simultaneously regardless of order

어떤 프로그램이나 알고리즘이 순서에 상관없이 동시에 수행가능함을 의미

보통 1~50까지 더하면 1부터 50까지 차례대로 1+2+3+4 ... + 50 을 진행하겠지만 동시성을 고려한다면
(a) 1 + 2 + 3 + 4 .. + 10
(b) 11+ 12 + 13 + 14 .. + 20
...
(e) 41+ 42 + 43 + 44 .. + 50
(a),(b),(c),(d),(e) 각 작업을 동시해 수행해 1부터 50까지의 합을 더한다.

Process

A self-contained execution environment
A complete, private set of basic run-time resources

프로세스는 독립 실행환경으로, 프로세스는 외부에 노출안되는 런타임 자원의 집합

쉽게 말하면 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램으로 '메모리에 올라와 실행되고 있는 프로그램의 인스턴스'을 의미한다.

Threads & Thread programming

Threads

'lightweight processes'
a subprocess with lightweight with the smallest unit of processes

프로세스가 할당받은 자원을 이용하는 실행의 단위
프로세스의 특정한 수행경로라고 보면 된다

Two ways to define and start threads

threads를 실행하는 방법으로는 Runnable interface를 사용하는 방법과
Thread class를 사용하는 방법 2가지가 있다.

Runnable interface

public class HelloRunnable implements Runnable 
{
	public void run()
    {
    	System.out.println("Hello from a thread!");
  	}
  	public static void main(String args[]) 
	{
  		Thread myThread = new Thread(new HelloRunnable ());
  		myThread.start();
	}
}

Thread class

public class HelloThread extends Thread 
{
  public void run()
  {
  	System.out.println("Hello from a thread!");
  }
  public static void main(String args[]) 
  {
  	HelloThread myThread = new HelloThread();
  	myThread.start();
  }
}

Runnable interface vs Thread Class

Runnable 방식이 더 일반적인 방법으로, thread class가 아닌 다른 class를 이미 상속받은 기존 클래스가 threads를 추가할 수 있다.

Thread Class 방식은 간단한 Application을 만들 때 유용하다.
하지만 class는 하나만 상속 가능하므로 다른 class는 Extend 할 수 없다.

Start( ) / Run( )

둘 다 thread를 실행하기 위한 method라는 점에서는 같지만,

run()는 run을 호출한 thread에서 작업이 처리되고 코드가 완료해야 다음 코드로 이동하기 때문에 병렬 작업이 불가능하다.

start()는 코드의 완료 여부와 상관없이 호출한 thread를 수행하기 때문에 멀티 스레드를 사용하는 경우에는 start를 사용해 줘야 한다.

Start를 두 번 호출하면 안 되는 이유

IllegalThreadStateException: Thread 클래스는 한 번 시작된 스레드를 다시 시작할 수 없도록 설계되었다. start() 메서드를 호출하여 스레드를 시작하면 해당 스레드는 실행 중인 상태로 전환된다. 이미 실행 중인 스레드에 대해 start()를 다시 호출하면 IllegalThreadStateException이 발생한다.

Thread 수명 주기: Thread 클래스의 Thread 인스턴스는 일회용이며, 한 번 실행을 마치면 재사용할 수 없다. Thread가 실행을 완료하면 종료되고, 다시 시작하려면 새로운 Thread 인스턴스를 생성해야 한다.

Sleep & Interrupts

지정된 시간동안 현재 thread를 일시 중단

try
{
	Thread.sleep(1000); // 1초 정지
}
catch(InterruptedException e) 
// thread.interrupt() 중지 시 exception 발생
{
	e.printStackTrace();
}

Joins

start() method를 호출하여 Thread를 시작한 후에 join() method를 호출하면, 호출한 Thread는 join() method가 반환될 때까지 기다린다.
호출한 Thread는 다른 Thread의 실행이 완료될 때까지 블록된다.

상세 예시

Thread Pool

Thread object use a significant amount of memory.
In a large-scale application, allocating and deallocating many thread objects creates a significant memory management overhead.

프로그램의 실행흐름 도중에 Thread를 여러 개 만들다 보면 기존의 thread가 다뤄지던 위치와 동떨어진 위치에 새로 할당을 하게 되고 이에 따라 추가적으로 시간,메모리,자원이 사용되는 현상이 발생한다.

따라서 이러한 Overhead 문제를 해결하기 위해 thread 개수를 미리 할당하는데, 이를 thread Pool이라고 한다.

ExecutorService class

  • Provides methods to manage termination of asynchronous task
    비동기 작업의 종료를 관리하는 방법을 제공
  • the fixed number of threads operates and others are waiting in a queue
    정해진 숫자의 thread가 Queue(FIFO)로 실행
  • ExecutorService must call shutdown()
    반드시 shutdown()를 호출해 더 이상 새로운 thread를 받지 않음

Using Thread pool by

package edu.handong.csee.java.examples.thread.pool;

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

public class MainToTestThreadPool 
{

	public static void main(String[] args) 
    {
		MainToTestThreadPool runner = new MainToTestThreadPool();
		runner.doMyJob();
	}

	private void doMyJob() 
    {
		long to = 10000000;
        // Thread를 추적하기 위한 Thread ArrayList
		ArrayList<SumPartiallyThread> sumRunners = new ArrayList<SumPartiallyThread>();

        // 내 컴퓨터에 있는 CPU 코어 개수
		int numOfCoresInMyCPU = Runtime.getRuntime().availableProcessors();
		System.out.println("The number of cores of my system: " + numOfCoresInMyCPU);
		
        // ThreadPool을 생성
		ExecutorService executor = Executors.newFixedThreadPool(numOfCoresInMyCPU);

		for(long i=0; i<to/1000000; i++) 
        {
            // SumPartiallyThread Class를 인스턴스화하여 작업 생성
			Runnable worker = new SumPartiallyThread((i*1000000)+1, (i+1)*1000000);
			// ThreadPool에 작업을 제출 및 실행
            executor.execute(worker);
            // Thread 객체를 sumRunners 리스트에 추가
			sumRunners.add((SumPartiallyThread)worker);
		}
		// 더 이상 새로운 작업을 수락하지 않음
		executor.shutdown();
		
        //  모든 작업이 완료될 때까지 기다림
        // isTerminated() : 스레드 풀의 모든 작업이 완료 여부를 반환
		while (!executor.isTerminated()) 
        {}
        
        // 작업 객체의 결과를 사용하여 전체 합을 계산
		long grandTotal = 0;
		for(SumPartiallyThread runner:sumRunners) 
        {
			grandTotal += runner.getTotalSum();
		}
		

		System.out.println("Grand Total = " + grandTotal);

	}

}

thread interference

when multiple threads try to read and write shared data at the same time

여러 스레드가 공유 데이터를 동시에 읽고 쓰려고 할 때 발생하는 문제

counter class는 c 값을 +1 증가하거나 -1 감소시킨다.

class Counter 
{
  private int c = 0;
  public void increase() 
  {
  	c++;
  }
  public void decrease() 
  {
  	c--;
  }
  public int getValue() 
  {
  	return c;
  }
}

Thread A,B가 Counter class를 동시에 수정하게 되면 c 값이 꼬이게 된다. 이를 thread interference라고 한다.

A는 C를 증가하고 B는 C를 감소한다고 했을 때,
A->B 순서로 진행 된다면, c값이 1에서 -1로 바뀌어 A의 실행이 무의미 해지게 된다.

Synchronization to avoid thread interference

thread interference 문제를 해결하기 위한 방법은 2가지가 있다.
객체 전체를 Lock을 걸어 지정해서 하나의 thread만 참조가능하게 하는 방식이다.

Synchronized method

가장 먼저 synchronized keyword를 사용하는 방법이다.

public class SynchronizedCounter 
{
  private int c = 0;
  public synchronized void increase() 
  {
  	c++;
  }
  public synchronized void decrease() 
  {
  	c--;
  }
  public int getValue() 
  {
  	return c;
  }
}

c++; 은 c = c+1; 과 같은데 여기에는 3가지 과정이 생략되어있다.
먼저 c값을 가져오고, c값을 증가하고, c값을 다시 저장한다.
그러면 이 3가지 과정을 A,B가 같이 진행한다면 총 8가지(222) 시나리오가 나오게 된다.

아래는 시나리오 중 하나이다.

Thread A: 현재 c 의 값을 가져온다 (a)
Thread B: 현재 c 의 값을 가져온다 (b)

Thread A: 불러온 c의 값에 1을 더한다; c = 1 (a')
Thread B: 불러온 c의 값에서 1을 뺀다; c = -1 (b')

Thread A: 더해진 값을 c에 저장한다; c = 1 (a'')
Thread B: 뺀 값을 c에 저장한다; c = -1 (b'')

Methods 자체에 Syncronized를 설정 함으로 여러 Thread에서 해당 Method가 하나의 Thread에서 다뤄지는 것처럼 한다.
따라서 만약 A가 increase() 작업을 하고 있다면 (Locked by A), A가 이미 Counter를 사용 중이므로 B가 decrease() 작업을 하지못한다.

쉽게말하면 누가 화장실에서 똥싸고 문 잠궈 놨는데 거기 같이 들어가서 똥싸지 못하는 것과 같다.

+) 생성자는 Syncronized를 하지 못한다.

Synchronized statements

synchronized statements은 지정된 객체의 Lock을 획득한 후 코드 블록을 실행한다.

쉽게 말해 객체 전체를 Lock 거는 것이 아니라 객체의 일부분 지정된 코드 블록을 동기화한다는 것이다.

public class MsLunch 
{
    private long c1 = 0;
    private long c2 = 0;
    
    // lock1 획득
    private Object lock1 = new Object();
    // lock2 획득
    private Object lock2 = new Object();

    public void inc1() 
    {
        synchronized(lock1) 
        {
            c1++;
        }
    }

    public void inc2() 
    {
        synchronized(lock2) 
        {
            c2++;
        }
    }
}

thread가 해당 class를 호출하게 되면 lock1,2를 얻게 되고,
이전 thread가 synchronized block을 마치게 되면 lock을 사용해 해당 block을 실행한다.

Synchronized Method vs Synchronized Statements

Method : 메서드 전체가 임계 영역으로 지정되어 모든 코드가 동기화
Statements : 특정 코드 블록만을 동기화하고, 다른 코드는 비동기적으로 실행
Method : 객체의 모니터 락을 사용하여 동기화
Statements : 지정된 모니터 락을 사용하여 동기화

Method : 메서드 단위로 동기화를 수행하여 메서드의 실행 시간이 길 경우 다른 스레드들이 오랜 시간 대기
Statements : 특정 코드 블록만을 동기화하므로 다른 스레드들은 해당 블록이 실행되지 않으면 비동기적으로 실행

Reference of Synchronized 1
Reference of Synchronized 2

profile
개발 새발

0개의 댓글