[java] 자바 스레드를 통한 멀티 태스킹

최재정·2022년 5월 24일
0

java

목록 보기
2/2

스레드란?

스레드는 하나의 작업 실행 단위이다. 하나의 스레드는 하나의 작업밖에 수행할 수 없다.

멀티스레딩

멀티스레딩은 응용 프로그램을 여러개의 작업 단위로 나누고 여러개의 스레드가 각각 하나의 작업을 수행하도록 하는 방법을 말한다.

자바에서 멀티 스레딩

자바에서 멀티 스레딩은 JVM(java virtual marchine)에 의해 작동되는 자바 스레드로 구현된다. JVM은 하나의 자바 응용프로그램을 실행 할 수 있는데 그 프로그램을 실행하면서 자바 스레드는 JVM내부에서 여러개가 동작한다. 만약 응용프로그램이 두 개 이상 필요하다면 그 수만큼의 JVM이 필요하다.
스레드간의 우선순위, 상태, 스레드로 실행되는 프로그램의 메모리 위치 등은 모드 jvm에 의해 관리된다.

자바 스레드를 만드는 법

  1. Tread 클래스를 이용하는 방법
  2. Runnable 인터페이스를 이용하는 방법

Tread 클래스를 이용하는 방법

Tread 클래스는 java.lang에서 불러올 수 있다. Tread 클래스에서 제공하는 메서드는 다음과 같다.
Tread 생성

Thread() // 스레드 객체 생성
Thread(Runnable target) // Runnable 객체를 이용해 스레드 객체 생성
Thread(String name) // 이름을 가진 스레드 객체 생성
Thread(Runnable target, String name) // Runnable 객체를 이용하여 이름이 name인 스레드 객체 생성

그 외 메서드

void run()//반드시 오버라이딩해야하는 스레드 함수로 사용자가 작성한 스레드 코드를 실행시킨다.
void start()//JVM에게 스레드 실행 요청
void interrupt()// 스레드 강제 종료
static void yield()// 스레드 스케줄링이 실행되어 다른 스레드가 먼저 실행되도록 함.
void join()//스레드가 종료할 때가지 기다림
long getId()// 스레드 아이디값 반환
String getname()//스레드 이름값 반환
int getPriority()//스레드 우선순위값 반환
void setPriority()//스레드 우선순위값을 n으로 변경
Thread.State getState()//스레드 상태값 반환
static void sleep()// 밀리초동안 스레드 수면 상태
static Thread currentThread()//현재 실행 중인 스레드 객체의 레퍼런스 값 반환

다양한 메서드를 통해 아이디,이름,우선순위,상태,레퍼런스 값 등 스레드에 관한 다양한 정보들을 얻을 수 있다. 또한 JVM에게 스레드가 특정 상태에 머물도록 요청도 할 수 있다는 것을 알 수 있다.

스레드 크래스를 만들기 위해서는 먼저 Thread 클래스로부터 상속을 받아야한다.
연습 후 추가코드 작성 예정

Runnable 인터페이스를 이용하는 방법

Runnable 인터페이스 또한 Thread와 같은 위치에 존재한다.

데몬 스레드와 일반 스레드

  • 데몬 스레드: JVM이 스스로 필요에 의해 사용되는 스레드
  • 일반 스레드: 응용프로그램에서 생성한 스레드(대표적으로 main함수)

스레드의 상태

스레드는 생성되고 소멸되기까지 여러 상태를 거칠 수 있는데 그 상태는 총6가지로 나뉜다.

  • NEW : start메서드가 실행되기 전 상태. 즉 스레드를 생성만 한 상태이다.
  • RUNNABLE : 스레드가 실행되거나, 실행 준비인 상태이다.
  • WAITING : 스레드가 어떤 object 객체 a에 대해 a.wait()을 호출하고 다른 스레드가 a.notify(),a.notifyAll()을 불러줄때까지 무한 대기하는 상태이다.
  • TIME_WAITING : sleep(int n)을 호출하여 n밀리 초동안 수면을 하는 상태이다.
  • Block : 스레드가 I/O 작업을 요청하고 그 작업이 끝나기를 기다리는 상태이다.
  • TERMINATED - 스레드가 종료한 상태이다.

스레드의 우선순위

jvm은 스케줄링을 통해 무조건 우선순위가 높은 스레드부터 실행시킨다. 스레드의 우선순위는 1~10 사이며 main스레드인 경우 보통값(5)로 생성된다. 자식 스레드는 부모 스레드와 같은 우선순위를 가지고 생성되는데 아래의 메소드를 통해 우선순위를 변경할 수 있다.

void setPriority(int newPriority) 

main 스레드의 생성

main 스레드는 일반 스레드로서 응용 프로그램이 시작되면 가비지 컬렉션 스레드와 함께 실행된다. 위에서 소개한 메서드를 응용하여 메인 스레드의 정보를 확인해보자.

public class void main(String[] args){
  long id = Thread.currentThread().getId();//스레드의 id
  String name = Thread.currentThread.getName();//스레드의 이름
  int priority = Thread.currentThread.getPriority();//우선 순위
  Thread.State s = Thread.currentThread.getState();//스레드의 상태
  
  System.out.println("현재 스레드의 이름 :"+name);
  System.out.println("현재 스레드의 ID :"+id);
  System.out.println("현재 스레드의 우선순위 :"+priority);
  System.out.println("현재 스레드의 상태 :"+s);
}

결과값

현재 스레드의 이름 :main
현재 스레드의 ID :1
현재 스레드의 우선순위 :5
현재 스레드의 상태 : RUNNABLE

스레드의 종료

스레드가 스스로 종료하는 경우
run메서드에서 실행 중간에 리턴하거나 완전히 실행하고 리턴할 때

다른 스레드에 의해 강제로 종료되는 경우
1. interrupt메서드를 이용하기
해당 스레드를 생성한 다른 스레드에서 interrupt메서드를 호출하면 된다. 단 강제로 종료시킬 스레드에는 try-catch문을 작성하고 interruptedException 예외를 받을 수 있어야한다.

//종료할 스레드를 생성한 스레드의 메서드 내에서
.........
종료할 스레드의 객체.interrupt();
.........
//종료할 스레드 run 메서드 내에서
..........
try{
..........
}
catch(interruptedException){
.........
return;
}
  1. flag이용하기
    종료의 주체가 되는 스레드에 boolean type의 flag 필드와 finish 메서드를 만들고 run 메서드에 주기적으로 flag를 검사하면 된다. 스레드 객체 생성 시 flag의 상태는 false로 두고 다른 스레드에서 finish메서드를 호출하면 true로 바꾸어 flag검사 시 true값을 확인하고 스레드를 종료시키는 방식이다.

스레드 동기화

다수의 스레드가 동일한 값에 접근하는 경우 예기치 못한 결과가 발생할 수 있다. 이러한 경우를 고려하여 내놓은 해결책이 스레드 동기화이다. 스레드 동기화는 synchoronized 키워드를 통해 특정 영역을 동기화 영역으로 지정한다. 이 영역을 임계 영역이라고 하는데 임계 영역은 메서드 전체가 될 수도 있고 임의의 코드 블럭일 수도 있다. 임계영역으로 지정된 부분은 스레드가 접근하면 lock이 걸리며 코드가 끝날때까지 풀리지않는다. 때문에 먼저 접근한 스레드가 이 영역에 나올때까지 다른 스레드는 대기할 수밖에 없다. 즉, 임계 영역은 먼저 접근한 스레드가 독점적으로 소유할 수 있다는 것이다.

  • 메소드 전체가 임계영역이 될때: 메소드 앞에 synchoronized키워드를 붙여 선언
  • 코드 내에서 특정 부분을 synchoronized로 감싸기
synchoronized(this){
.....
}

사용자는 synchoronized에 this는 해당 코드를 소유하고 있는 객체 자기 자신을 말하는 것이며 만약 다른 객체에 락을 걸고 싶다면 해당 객체의 레퍼런스를 인자에 넣으면 된다.
synchoronized의 인자에 들어간 객체는 블럭이 끝나기 전까지 lock상태가 되며 다른 스레드들은 해당 객체에 접근이 불가능할뿐더러 그 블럭 내부 코드에도 접근하지 못한 채 대기상태에 머물게 된다.또한 lock된 객체가 다른 메서드 synchoronized블럭의 인자라면 그 메서드에 접근한 스레드들도 lock이 풀릴때까지 블럭 내부에 접근할 수 없다.

예제

package ThreadTest;
import java.lang.Thread;
import java.util.Scanner;

public class ThreadTest {
	private static final long sleeptime = 1000;
	private static int count = 0;
	private static int num = 10;
	private String state = "state";
	private final Object ob = new Object();
    private Scanner sc = new Scanner(System.in);


	// ob를 하든, this를 하든 결국 두 스레드가 똑같은 메서드에 접근했을때 먼저 접근한 스레드가 lock을 하기때문에 뒤에 들어온 스레드는
	// 기다릴수밖에 없음
	public void Case1print(String s) {
		synchronized (ob) {
			try {
				count += 1;
				System.out.println("test스레드에 " + s + "접근" + ", 횟수:" + count);
				Thread.sleep(sleeptime);
			} catch (InterruptedException e) {
				
				e.printStackTrace();
			}
		}
	}

	public void Case2print1(String s) {
		synchronized (this) {
			state = s;
			System.out.println(state);
			try {
				Thread.sleep(sleeptime);
			} catch (InterruptedException e) {
	
				e.printStackTrace();
			}
		}
	}
	
	public void Case2print2(String s) {
		synchronized (this) {
			state = s;
			System.out.println(state);
			try {
				Thread.sleep(sleeptime);
			} catch (InterruptedException e) {

				e.printStackTrace();
			}
		}
	}
	
	public synchronized void printnum(String s, int n){
		if(num>0){
		num+=n;
		System.out.println(s+"and num: "+num);
	}
	}

	

	public void Case1(ThreadTest test1) {
		new Thread(() -> {
			for (int i = 0; i < 10; i++) {
				test1.Case1print("Thread1");
			}
		}).start();

		new Thread(() -> {
			for (int i = 0; i < 10; i++) {
				test1.Case1print("Thread2");
			}
		}).start();

	}


	//이름만 다르고 내용이 같은 메서드 2개에  스레드가 각각 접근하여도 동일한 객체에 대해 lock을 걸기 때문에 
	//먼저 접근한 스레드가 먼저 실행될 수밖에 없다. 만약 서로 다른 객체에 대해 lock을 건다면 두 스레드는 서로 영향을 주지 않고 각자의 일을 할
	//것이다. 그러나 이 메서드에서는 state라는 동일한 값에 접근하기 때문에 그렇게 변경을 하면 스레드의 충돌이 일어날 것이다. 
	public void Case2(ThreadTest test1) {
		new Thread(() -> {
			for (int i = 0; i < 10; i++) {
				test1.Case2print1("Thread1");
			}
		}).start();

		new Thread(() -> {
			for (int i = 0; i < 10; i++) {
				test1.Case2print2("Thread2");
			}
		}).start();

	}
	//static 변수 num이 0보다 작아질 때까지 두 스레드가 printnum함수를 호출하여 num에 각각 +1,-2를 함.
	public void Case3(ThreadTest test1) {
		new Thread(() -> {
			while(num>0){
				printnum("Thread1", 1);
			}
		}).start();

		new Thread(() -> {
			while(num>0){
				printnum("Thread2", -2);
			}
		}).start();
	}

	public static void main(String[] args) {
		ThreadTest test1 = new ThreadTest();
        System.out.print("choice test(Case 1,2,3) >>");
        int choice = test1.sc.nextInt();
        switch(choice){
            case 1:
            System.out.println("Test Case1 start!");
            test1.Case1(test1);//두 스레드가 synchronized 블록을 가진 동일한 메서드에 접근했을 때
            break;
            case 2:
            System.out.println("Test Case2 start!");
            test1.Case2(test1);//두 스레드가 synchronized 블록을 가진 서로 다른 두개의 메서드에 접근했을 때 
            break;
            case 3:
            System.out.println("Test Case3 start!");
            test1.Case3(test1);//두 스레드가 동일한 synchronized 메서드에 접근했을 때
            break;
        }
    }

	
}

github_link

wait,notify,notifyAll메서드를 이용한 스레드 동기화

스레드가 임계 영역을 사용하다가 필요에 의해 잠시 일시 정지 상태에 전환하면 대기하던 다른 스레드가 해당 영역을 사용하고 나중에 먼저 들어왔던 일시 정지 상태의 스레드를 깨우는 식의 메커니즘이 필요할때 사용된다.
wait() : wait에 접근한 스레드를 일시 정지 상태로 전환시킴. 이 상태에 들어선 스레드는 wait가 호출한 대기실에서 기다리게 된다.
notify : 하나의 스레드를 일시 정지 상태에서 실행 대기 상태로 전환시킴. 일시 정지 상태에 있는 스레드들 중 하나를 랜덤으로 전환시키기 때문에 원하는 스레드를 선택할 수 없다.
notifyAll : 일시 정지 상태에 있는 모든 스레드를 실행 대기 상태로 전환시킴. 이렇게 되면 jvm의 스케줄링을 통해 원하는 스레드를 깨울 수 있다.

  • 해당 메서드들은 object에서 정의된 메서드들이므로 모든 객체에서 적용이 가능하다.
  • synchoronized 영역 내에서만 사용이 가능하다.

참고자료
1. 명품 java programming
2. https://javafactory.tistory.com/1535

profile
초짜 개발자의 개발일지

0개의 댓글