bdfgdfg

쓰레드 본문

웹프로그래밍/Java

쓰레드

marmelo12 2023. 7. 29. 15:33
반응형

흔히 쓰레드라는 말이나오면 프로세스와 같이 설명이 된다.

프로세스란 하드에 있는 프로그램이 메모리에 올라와 실행상태에 놓이게 되는 것. 즉 실행중인 프로그램을 의미하며

쓰레드는 프로세스의 자원을 이용해 실제로 작업(코드 실행)을 수행하는 것이 바로 쓰레드.

 -> 그렇기에 기본적으로 모든 프로세스는 최소한 하나의 쓰레드를 가진다.

 

쓰레드는 프로그래머에 의해 둘 이상의 쓰레드가 존재할 수 있고 이를 멀티쓰레드 환경이라고 한다.

멀티 쓰레드의 장점은 이론상 멀티코어 CPU상에서 하나의 코어 = 쓰레드 이므로 CPU사용률이 향상된다.

 -> 다만 쓰레드는 스케쥴링 대상이므로(컨텍스트 스위칭) 너무 많은 쓰레드 생성 시 오히려 성능이 더 저하될 수 있다.

 -> 또한 데이터의 경쟁상태(race condition) 및 교착상태(DeadLock)등의 문제가 발생할 수 있어 코드의 복잡성도 높아진다. (쓰레드는 개별적인 스택메모리를 가지지만 그 외의 힙영역, Method Area(Static 변수)등을 공유한다는것도 잊지말자

 

자바에서 쓰레드를 간단하게 쓰는법을 보자.

자바에선 쓰레드를 구현하는 방법은 Thread 클래스를 상속받거나 Runnable 인터페이스를 구현하는법 두가지가 존재한다.

 -> 보통은 다중상속을 허용하지 않는 자바에서 클래스를 상속받기보단 Runnable 인터페이스를 구현하는게 일반적이라고 한다.

class MyThread implements Runnable
{
	public void run()
	{
		
	}
}

 Runnable인터페이스는 오로지 run만 정의되어 있는 간단한 인터페이스이고, 해당 쓰레드가 처리할 내용(몸통)을 run함수안에 구현해주면 된다.

import java.io.FileInputStream;
import java.util.*;
import java.time.*;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;

class MyThread implements Runnable
{
	public void run()
	{
		for(int i = 0; i < 10; ++i)
		{
			System.out.println("Hello~~ ThreadNum : " + Thread.currentThread().getName());
		}
	}
}

public class Hello {	
	public static void main(String[] args) 
	{
		Runnable r = new MyThread();
		Thread t = new Thread(r);
		
		t.start();;
		r.run(); // 메인쓰레드가 직접 호출
		try
		{
			Thread.currentThread().join();
		}
		catch( Exception e)
		{
			e.printStackTrace();
		}
	}
}
 

Hello~~ ThreadNum : main

Hello~~ ThreadNum : main

Hello~~ ThreadNum : main

Hello~~ ThreadNum : main

Hello~~ ThreadNum : main

Hello~~ ThreadNum : main

Hello~~ ThreadNum : main

Hello~~ ThreadNum : main

Hello~~ ThreadNum : Thread-0

Hello~~ ThreadNum : Thread-0

Hello~~ ThreadNum : Thread-0

Hello~~ ThreadNum : Thread-0

Hello~~ ThreadNum : Thread-0

Hello~~ ThreadNum : Thread-0

Hello~~ ThreadNum : Thread-0

Hello~~ ThreadNum : main

Hello~~ ThreadNum : main

Hello~~ ThreadNum : Thread-0

Hello~~ ThreadNum : Thread-0

Hello~~ ThreadNum : Thread-0

재밌는건 호출할 때마다 출력결과가 조금씩 다를것이다.

그 이유가 바로 쓰레드가 스케쥴링에 의해 컨텍스트 스위칭을 하기때문.

 

쓰레드는 해당 메소드가 return되면 종료되기에, 보통은 무한히 반복되는 환경에서 쓰레드를 계속해서 돌리고 특정 시점 혹은 프로세스 종료시에 해당 쓰레드가 종료되게끔 유도하는게 일반적이다.

 

쓰레드와 관련된 여러 메소드

sleep : 지정된 시간동안 쓰레드를 일시정지

join : 지정된 시간동안 지정된 쓰레드가 종료되기를 기다린다(기다리는 대상은 join을 호출한 쓰레드).

interrupt : sleep이나 join에 의해 일시정지 상태인 쓰레드를 깨워서 실행대기 상태로 만든다. 

stop : 쓰레드를 즉시 종료

suspend : 쓰레드를 일시정지 시킨다. resume호출 시 다시 실행 대기상태가 된다.

resume : suspend에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만든다.

yield : 실행중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 대기상태에 빠진다.

wait : 

 

쓰레드와 관련된 여러 상태들

NEW : 쓰레드가 생성되었지만 아직 start()가 호출되지 않은 상태

RUNNABLE : 실행 중 또는 실행가능한 상태

BLOCKED : 동기화 블럭에 의해 일시정지상태(lock)

WAITING,TIMED_WAITING : 쓰레드의 작업이 종료되진 않았지만 실행가지 않은 일시정지 상태

TERMINATED : 스레드의 작업이 종료된 상태

 

데몬 쓰레드

쓰레드라는 점은 동일하지만 데몬 쓰레드는 보조적인 역할에 가깝다.

일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동종료되며 그 외에는 다른점이 없다.

설정도 매우간단하다. 단순 setDaemon메소드를 호출 시 해당 쓰레드는 데몬 쓰레드로 변경된다.

다만 setDaemon메소드는 반드시 start를 호출하기전에 실행되어야함.

 

쓰레드의 동기화

멀티쓰레드 환경에서 동기화는 매우 중요하다. 공유되는 데이터간에 여러 쓰레드가 동시에 접근할 시 의도치 않은 일이 발생하기 때문.

공유되는 객체 A가 존재하고 멤버변수로 int형 변수 A가 10으로 초기화 되어 있다고 해보자.

A,B쓰레드가 동시에 A변수에 접근하고, A는 기존의 값 +10을 저장하며 B쓰레드는 기존의 값 +20을 저장한다.

 

원하는 결과는 A변수에 +10, +20이 각각 수행되고, 최종값으로는 40이라는 값이 되길 바란다.

다만 쓰레드는 스케쥴링 대상이며 컨텍스트 스위칭을 통해 A쓰레드가 작업도중 자신에게 부여된 실행시간을 다 소모하여 실행 대기상태로 놓이고, 다른 쓰레드가 실행상태에 놓일 때(컨텍스트 스위칭)

만약 A쓰레드가 기존의 값(10)+10을 수행하던 도중 컨텍스트 스위칭으로 B쓰레드가 작업을 진행하여 +20을 저장해 A는 30이 되지만, 나중에 다시 A쓰레드가 꺠어나 기존 값 = 기존값(10) + 10을 수행하면 A변수는 최종적으로 20이 저장이 된다.

 -> 그렇기에 여러 쓰레드가 동시다발적으로 접근가는한 코드 영역을 임계영역이라 하며, 해당 영역을 Lock을 통해 하나의 쓰레드만 접근가능하도록 코드처리가 되어야한다.

 -> 이렇게 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 만는것을 쓰레드의 동기화(synchronization)라고함.

import java.io.FileInputStream;
import java.util.*;
import java.time.*;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;

class MyThread implements Runnable
{
	public void run()
	{
		for(int i = 0; i < 10000; ++i)
		{
			(TmpClass.a)++;
		}
	}
}

class TmpClass
{
	public static int a = 0;
}

public class Hello {	
	public static void main(String[] args) 
	{
		
		Runnable r = new MyThread();
		Thread t = new Thread(r);
		
		t.start();
		
		for(int i = 0; i < 10000; ++i)
			(TmpClass.a)++;
		
		try
		{
			t.join();
			
			System.out.println(TmpClass.a);
		}
		catch( Exception e )
		{
			e.printStackTrace();
		}
	}
}

위 코드는 메인쓰레드와, 별도로 생성한 쓰레드가 TmpClass에 있는 static변수를 10000번씩 각각 증가시키는 코드이다.

즉 원하는 결과는 TmpClass의 a변수가 20000이라는 값을 가지길 원하지만 결과는.

11897

11568

이렇게 이상한값이 튀어나온다. 심지어 매번 실행시마다 결과도 달라진다.

 

이제 동기화를 통해서 정상적인 값이 나오게끔 처리해보자.

 

1.synchronized를 이용한 동기화

import java.io.FileInputStream;
import java.util.*;
import java.time.*;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;

class MyThread implements Runnable
{
	public void run()
	{
		for(int i = 0; i < 10000; ++i)
		{
			TmpClass.Plus(); // synchronized.즉 동기화 된 메소드를 호출
		}
	}
}

class TmpClass
{
	public static int a = 0;
	
	// 동기화 처리가된 메소드
	public static synchronized void Plus()
	{
		a++;
	}
}

public class Hello {	
	public static void main(String[] args) 
	{
		
		Runnable r = new MyThread();
		Thread t = new Thread(r);
		
		t.start();
		
		for(int i = 0; i < 10000; ++i)
			TmpClass.Plus(); // synchronized.즉 동기화 된 메소드를 호출
		
		try
		{
			t.join();
			
			System.out.println(TmpClass.a);
		}
		catch( Exception e )
		{
			e.printStackTrace();
		}
	}
}

결과는

20000

여러번 실행해보아도 정확한 결과값이 떨어진다.

메소드에 synchronized를 지정할 시 해당 메소드 전체가 임계영역이라 보고, 메소드 자체에 진입할 때 하나의 쓰레드만 접근하도록 보장이 된다는 의미.

 

다른 방법도 존재한다.

class TmpClass
{
	public static int a = 0;
	public static Object lock = new Object();
	
	// 동기화 처리가된 메소드
	public static void Plus()
	{
		synchronized( lock ) // 객체의 참조변수만 올 수 있다.
		{
			a++;
		}
	}
}

둘 방법 모두 lock의 반납이 블럭을 벗어나면 자동으로 이루어진다(RAII)

가능한 메소드 전체에 걸기보단 공유되는 데이터만 감싸는 방식이 더 효율적이다.

 

volatile

CPU가 데이터를 연산하는 방식은 메모리에 있는 데이터(현재 접근하려는 데이터의 근처에있는 데이터도)들이 캐시에 올라온후, 실제 CPU 레지스터에 올라가 연산을 수행한다.

 -> 즉 cpu는 먼저 캐시를 확인 후 데이터가 없다면 Main Memory에서 값을 읽어오는 방식.

 

여기서 문제는 cpu에서는 최적화를 위해 실제 연산이 끝난값을 바로 Main Memory에 Write하는게 아닌, 캐시에 Write를 해두고, 일정시간 후에 Main Memory에 반영이 된다.

그 반대로, 캐시에 올라온 변수가 Main Memory에는 다른 쓰레드에 의해 값이 갱신되었지만 캐시에 존재하는 변수는 해당 값을 갱신하지 않으므로 정보의 불일치성이 발생.

 

volatile은 이러한 문제를 해결해준다.(고급진 말로 가시성 보장)

volatile 키워드를 붙인 변수는 Main Memory에 바로 Read&Write를 한다는 보장을 해준다. (최적화면에서는 안좋음)

 -> synchronized같은 키워드는 volatile의 기능을 이미 같이 수행중.

 

다만 이것은 가시성이 보장이 되는거지 동기화가 보장이 된다는 의미는 아니다.

 -> 메인 메모리에 바로 반영을 한다하더라도, 하나의 쓰레드에서 연산중에 다른 쓰레드가 연산을 해버리면 결국 정합성이 깨져버림.

 

반응형

'웹프로그래밍 > Java' 카테고리의 다른 글

스트림(Stream)  (0) 2023.07.30
람다  (0) 2023.07.30
제네릭스 & 어노테이션  (0) 2023.07.29
자바의 컬렉션(Collection)  (0) 2023.07.27
Calendar와 Date 그리고 java.time  (0) 2023.07.27
Comments