▸JAVA/기본 문법

Thread (스레드)_기본 스레드 사용 [1/3]

코데방 2020. 1. 30.
728x90

[ Thread(스레드) ]

  • 한 프로세스(프로그램 단위)에서 동시에 실행되는 작업 단위
  • 동시에 여러 기능을 사용할 수 있도록 하거나 같은 기능을 여러 명이 동시에 사용할 수 있도록 해줌

프로그램은 항상 절차지향적인 순서대로 실행이 되는데 동시에 여러 절차를 수행할 수 있도록 하는 개념이 스레드의 개념입니다. 연산을 담당하는 CPU의 Core가 한 개라면 여러 스레드의 절차를 돌아가면서 조금씩 수행해 우리가 볼 때는 동시에 여러 로직이 실행되는 것처럼 보이게 됩니다. 4Core CPU라면 4개의 스레드를 동시에 사용해서 연산을 처리하는 것이 가장 좋은 방법이 될 수 있습니다. 만약 8개의 스레드를 사용하게 되면 8개 로직을 동시에 실행할 수 있지만 실질적인 성능이 증가하진 않습니다. 다만 모든 CPU와 스레드가 항상 연산을 수행하고 있는 것이 아니라 유휴시간이 있기 때문에 약간의 성능 증가가 생길 수는 있습니다.

 


 

[ 스레드 생성 - Thread / Runnable ]

 

기본적인 스레드를 사용하는 방법은 두 가지가 있습니다. Thread 클래스를 상속받거나 또는 Runnable 인터페이스를 구현해서 run() 메소드를 오버라이딩한 뒤 Thread 클래스의 인스턴스를 생성해 start() 메소드를 호출하면 됩니다. Runnable 인터페이스는 함수형 인터페이스라 별도 클래스 없이 간단한 로직은 람다식으로 바로 run() 메소드를 구현해서 사용해도 됩니다. 

 

 

아래와 같이 구현하면 되고 결과를 보면 순서대로 실행되지 않고 순서가 섞인 것을 알 수 있습니다.

 

package hs;

public class Main {

	public static void main(String[] args) {

		Thread runnable = new Thread(new ThreadOne());
		ThreadTwo thread = new ThreadTwo();

		runnable.start();
		thread.start();

		// 람다식으로 Runnable 구현
		Runnable lamdba = () -> {
			System.out.println("순서 : 5 - " + Thread.currentThread().getName());
		};

		Thread lam = new Thread(lamdba);
		lam.start();

	}
}

class ThreadOne implements Runnable {

	@Override
	public void run() {

		System.out.println("순서 : 1 - " + "Runnable 구현 클래스");
		System.out.println("순서 : 2 - " + Thread.currentThread().getName());

	}
}

class ThreadTwo extends Thread {

	@Override
	public void run() {

		System.out.println("순서 : 3 - " + "Thread 상속 클래스");
		System.out.println("순서 : 4 - " + Thread.currentThread().getName());

	}
}

 


 

[ 스레드 우선순위 ]

 

여러 개의 스레드를 사용한다면 스레드 간 우선순위를 부여할 수 있습니다. 예를 들어 2개의 스레드만 있으면 스레드 사용 없이 순서대로 돌아가도록 하면 되지만 3개의 스레드 중 2개는 동시에 돌고 1개는 어느 하나의 스레드가 종료된 뒤 수행되도록 하고 싶다면 스레드 간 우선순위를 지정해주면 됩니다. 아래 코드와 같이 setPriority() 메소드를 사용해서 우선순위를 지정해주면 됩니다. 기본은 5이며 1~10까지 우선순위를 지정할 수 있습니다.

 

대부분 PC의 CPU 코어가 4Core 이상이기 때문에 아래 코드의 결과는 계속 순서가 바껴서 나오긴 합니다만 필요 시 아래와 같이 사용하면 됩니다. (코어가 여러 개라 우선순위 상관없이 각 코어에서 한 개씩 가져가서 돌립니다.)

public class Main {

	public static void main(String[] args) {

		Thread a1 = new Thread(new ThreadOne());
		ThreadTwo a2 = new ThreadTwo();
		
		// 우선순위 지정
		a1.setPriority(1);
		a2.setPriority(10);
		
		a1.start();
		a2.start();

	}
}

 

 


 

[ 동기화 처리 - synchronized ]

 

하나의 객체를 여러 스레드로 나눠서 실행할 수도 있습니다. 이 때 한 자원을 여러 스레드에서 편집하면서 오류가 발생할 수 있습니다. 예를 들어 아래 코드에서 보면 필드 값은 1번 스레드에서만 증가시키도록 되어 있습니다. 하지만 2번 스레드에서도 증가된 값이 생깁니다. 이는 공유된 자원을 각 스레드에서 동시에 사용하면서 발생하는 문제입니다.

 

1번 스레드에서 a를 증가시키고 출력을 하기 전에 2번 스레드에서 이미 a를 가져와서 메모리에 저장했는데 출력은 1번 스레드가 먼저 하는 등 내부적으로 순서가 뒤죽박죽이 돼서 아래와 같은 문제가 발생할 수 있는 것이죠.

 

package hs;

public class Main {

	public static void main(String[] args) {

		ThreadTest test = new ThreadTest();
		Thread a1 = new Thread(test, "Thread-1");
		Thread a2 = new Thread(test, "Thread-2");

		a1.start();
		a2.start();
	}
}

class ThreadTest implements Runnable {

	int a;

	@Override
	public void run() {

		String str = Thread.currentThread().getName();

		// 1번 스레드일 경우에만 a값을 증가시킴
		if (str.equals("Thread-1")) {

			for (int i = 0; i < 5; i++) {
				a++;
				System.out.println(str + " : " + a);
			}

		} else {

			for (int i = 0; i < 5; i++) {
				System.out.println(str + " : " + a);
			}
		}
	}
}

 

동기화 문제를 해결하기 위한 장치가 synchronized 입니다. 메소드에 붙여서 한 스레드가 메소드를 사용할 때 다른 스레드가 사용하지 못하게 할 수도 있고, 값을 가진 객체에 사용해서 한 스레드가 객체를 사용할 때 다른 스레드가 접근하지 못하도록 할 수 있습니다. 즉, synchronized 블록 안에 있는 내용을 먼저 점유한 스레드가 블록 안의 내용을 모두 완료한 뒤 다른 스레드가 다시 해당 블럭을 다시 점유할 수 있다는 것입니다. 

 

먼저 아래는 메소드에 동기화 처리를 한 코드입니다. 메소드 동기화라고 합니다. 

 

package hs;

public class Main {

	public static void main(String[] args) {

		ThreadTest test = new ThreadTest();
		Thread a1 = new Thread(test, "Thread-1");
		Thread a2 = new Thread(test, "Thread-2");

		a1.start();
		a2.start();
	}
}

class ThreadTest implements Runnable {

	int a;

	@Override
	public void run() {

		plus();

	}

	public synchronized void plus() {

		String str = Thread.currentThread().getName();

		// 1번 스레드일 경우에만 a값을 증가시킴
		if (str.equals("Thread-1")) {

			for (int i = 0; i < 5; i++) {
				System.out.println(str + " : " + a++);
			}

		} else {

			for (int i = 0; i < 5; i++) {
				System.out.println(str + " : " + a);
			}
		}
	}
}

 

 

아래는 값을 가진 a 필드를 동기화 처리한 코드입니다. 블록 동기화라고 합니다. synchronized의 매개변수에는 원시타입 변수를 넣을 수 없고 객체(참조변수)만 가능합니다. 따라서 아래 코드에서는 int형이 아닌 Integer 타입으로 사용해줘야 합니다. 한 스레드에서 a 객체를 사용하고 있으면 다른 스레드에서는 a객체를 사용하지 못하고 끝날 때까지 기다려야 한다는 의미입니다. 예시 코드에서는 간단한 상황으로 짰지만 만약 하나의 자원을 공유하는 여러 클래스가 있다거나 여러 메소드가 있을 경우 메소드에 동기화 처리를 할 수 없고 객체 자체에 동기화 처리를 해야하는 상황이 발생합니다.

 

package hs;

public class Main {

	public static void main(String[] args) {

		ThreadTest test = new ThreadTest();
		Thread a1 = new Thread(test, "Thread-1");
		Thread a2 = new Thread(test, "Thread-2");

		a1.start();
		a2.start();
	}
}

class ThreadTest implements Runnable {

	Integer a = 0;

	@Override
	public void run() {

		String str = Thread.currentThread().getName();

		// a 객체에 대한 동기화 처리
		synchronized (a) {

			// 1번 스레드일 경우에만 a값을 증가시킴
			if (str.equals("Thread-1")) {

				for (int i = 0; i < 5; i++) {
					System.out.println(str + " : " + a++);
				}

			} else {

				for (int i = 0; i < 5; i++) {
					System.out.println(str + " : " + a);
				}
			}
		}
	}
}

 

 


 

 

[ 스레드 상태값 ]

 

위에서 스레드를 많이 만든다고 무한정 동시 처리할 수는 없다고 했습니다. 실제 연산을 수행하는 CPU의 코어 갯수가 한정적이기 때문입니다. 또 Sleep() 메소드와 같이 스레드를 잠시 멈추는 경우도 있고 위의 사례와 같이 동기화 처리를 위해 잠시 스레드가 멈춰서 놀고 있기도 합니다. 이러한 현재 스레드의 상태를 볼 수 있는 값을 모아둔 Thread.State 열거형(Enum) 클래스가 있습니다. Thread 클래스의 getState() 메소드로 현재 스레드 상태를 볼 수 있습니다. 

 

상태값 상태
NEW  스레드 객체는 생성되었지만 start() 메소드가 호출되지 않은 상태
RUNNABLE  start() 메소드가 호출되었고 JVM에 의해 선택되어 실행될 수 있는 상태
BLOCKED  실행 대기 상태. JVM이 RUNNABLE 상태로 변경시킬 수 있음
WAITING  실행 대기 상태. 다른 스레드에 의해 RUNNABLE 상태로 변경됨
TIME_WAITING  실행 대기 상태. 일정 시간이 지나면 RUNNABLE 상태로 변경됨 (sleep 사용)
TERMINATED  스레드 실행 종료

 

 

간단한 예시 코드입니다. 객체에 대한 동기화 처리를 하고 두 개의 스레드로 작업하면 아래와 같이 하나의 스레드가 작업할 때 다른 스레드는 BLOCKED 처리되는 것을 볼 수 있습니다.

 

package hs;

public class Main {

	public static void main(String[] args) {

		Sync s = new Sync();
		Thread t1 = new Thread(new Thread1(s), "Thread-1");
		Thread t2 = new Thread(new Thread2(s, t1), "Thread-2");

		t1.start();
		t2.start();

	}
}

// 데이터를 가진 객체
class Sync {

	int a;

}

//1번 스레드로 실행시킬 클래스
class Thread1 implements Runnable {

	Sync s;

	Thread1(Sync s) {

		this.s = s;
	}

	@Override
	public void run() {

		synchronized (s) {

			for (int i = 0; i < 2; i++) {
				s.a++;
				System.out.println("Thread 1 - " + s.a);
			}
		}
	}
}

// 2번 스레드로 실행시킬 클래스
class Thread2 implements Runnable {

	Sync s;
	Thread t;

	Thread2(Sync s, Thread t) {

		this.s = s;
		this.t = t;
	}

	@Override
	public void run() {

		synchronized (s) {

			for (int i = 0; i < 2; i++) {
				s.a++;
				System.out.println("Thread-2 - " + s.a);

				// 다른 스레드의 현재상태
				System.out.println("Thread-2 실행중 : " + t.getName() 
				                                 + " = " + t.getState());
			}
		}
	}
}

 

728x90

'▸JAVA > 기본 문법' 카테고리의 다른 글

Thread (스레드)_스레드풀 [3/3]  (2) 2020.02.01
Thread (스레드)_스레드 제어 [2/3]  (2) 2020.01.30
람다식(Lamdba Expressions)  (2) 2019.12.23
제네릭(generic)  (2) 2019.12.23
직렬화와 역직렬화 (Serializable)  (2) 2019.12.20

댓글

💲 추천 글