자바의 멀티스레드 프로그래밍
유튜브에서 백기선님이 진행하는 온라인 스터디를 진행 중입니다. 아래는 스터디의 링크입니다.
www.youtube.com/watch?v=rPYhY5kFD5k
github.com/whiteship/live-study
이 글은 자바에서의 멀티스레드 프로그래밍을 어떻게 구현하는지 설명하고, 직접 코드를 작성해봅니다. 추가적으로 자바의 스레드를 조작하는 다양한 방법을 소개합니다.
학습할 것
· 메인(Main) 스레드
· Thread 클래스와 Runnable 인터페이스
· 스레드의 우선순위
· 동기화
· 데드락
· 스레드의 상태
· (+추가) 스레드의 이름 사용하기
메인(Main) 스레드
모든 자바 애플리케이션은 메인 스레드가 main() 메소드를 실행하면 시작된다. 메인 스레드는 main() 메소드의 첫 코드부터 아래로 순차적으로 실행하고, main() 메소드의 마지막 코드를 실행하거나 return 문을 만나면 실행이 종료된다.
메인 스레드는 필요에 따라 작업 스레드를 만들어 병렬로 코드를 실행할 수 있다. 즉 멀티 스레드를 생성해서 멀티 태스킹을 수행한다.
싱글 스레드 애플리케이션에서는 메인 스레드가 종료하면 프로세스로 종료된다. 하지만 멀티 스레드 애플리케이션에서는 실행 중인 스레드가 하나라도 있다면, 메인 스레드가 먼저 종료되어도 프로세스는 종료되지 않는다.
Thread 클래스와 Runnable 인터페이스
멀티 스레드로 실행하는 애플리케이션을 개발하려면 몇 개의 작업을 병렬로 실행할지 결정하고, 각 작업별로 스레드를 생성하면 된다.
자바에서는 작업 스레드도 객체로 생성된다. 이를 위해 java.lang.Thread 클래스를 직접 객체화해서 생성하거나, Thread를 상속해서 하위 클래스를 만들어 생성한다.
Thread 클래스로 직접 생성
java.lang.Thread 클래스로 작업 스레드 객체를 직접 생성하려면 Runnable 인터페이스를 매개값으로 갖는 생성자를 호출해야 한다.
Thread thread = new Thread(Runnable target);
Runnable은 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체라고 해서 붙여진 이름이다. Runnable은 인터페이스 타입이기 때문에 구현 객체를 만들어 대입해야 한다. Runnable에는 run() 메소드 하나가 정의되어 있는데, 구현 클래스는 run()을 재정의해서 작업 스레드가 실행할 코드를 작성해야 한다.
class Task implements Runnable{
public void run(){
//스레드가 실행할 코드
}
}
Runnable은 작업 내용을 가진 객체이지 실제 스레드는 아니다. Runnable 구현 객체를 생성한 후, 이것을 매개값으로 해서 Thread 생성자를 호출해야 작업 스레드가 생성된다.
Runnable task = new Task();
Thread thread = new Thread(task);
코드를 더 간결하게 작성하기 위해 Thread 생성자를 호출할 때 Runnable 익명 객체를 매개값으로 사용할 수 있다.
Thread thread = new Thread(new Runnable(){
public void run(){
//스레드가 실행할 코드
}
});
Runnable 인터페이스는 run() 메소드 하나만 정의되어 있다. 즉, 함수형 인터페이스다. 따라서 자바 8 부터는 람다식을 매개값으로 사용할 수 있다.
Thread thread = new Thread(()-> {});
이렇게 만든 작업 스레드에 start() 메소드를 호출하면 작업 스레드는 매개값으로 받은 Runnable의 run() 메소드를 실행한다.
thread.start();
실제 예제를 작성해보자. 다음 예제는 0.5초 주기로 비프(beep)음을 발생시키면서 동시에 프린팅하는 작업이다. 비프음 발생과 프린팅은 서로 다른 작업이므로, 각각 메인스레드와 워크 스레드에서 나누어 작동하게 한다.
import java.awt.*;
public class BeepTask implements Runnable{
public void run(){
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i=0;i<5;i++){
toolkit.beep();
try{
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
public class ThreadExam {
public static void main(String[] args){
Runnable beepTask = new BeepTask();
Thread thread = new Thread(beepTask);
thread.start();
for(int i=0; i<5; i++){
System.out.println("띵");
try{
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
익명 객체로 Runnable 구현
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i=0; i<5; i++){
toolkit.beep();
try{
Thread.sleep(500);
}catch (Exception e){}
}
}
});
람다식으로 Runnable 구현
Thread thread = new Thread(()->{ Toolkit toolkit = Toolkit.getDefaultToolkit();
for(int i=0; i<5; i++){
toolkit.beep();
try{
Thread.sleep(500);
}catch (Exception e){}
}
});
Thread 하위 클래스로 생성
작업 스레드가 실행할 작업을 Runnable로 만들지 않고, Thread의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을 포함시킬 수도 있다.
Thread 클래스를 상속한 후 run 메소드를 재정의(overriding)해서 스레드가 실행할 코드를 작성하자. 작업 스레드 클래스로부터 작업 스레드 객체를 생성하는 방법은 일반적인 객체를 생성하는 방법과 동일하다.
public class WorkerThread extends Thread{
@Override
public void run(){
//스레드가 실행할 코드
}
}
public class ThreadExam {
public static void main(String[] args){
Thread thread = new WorkerThread();
thread.start();
}
}
쓰레드의 우선순위
멀티 스레드는 동시성(Cuncurrency) 또는 병렬성(Parallelism)으로 실행된다.
동시성은 멀티 작업을 위해 하나의 코어에서 멀티 스레드가 번갈아가며 실행하는 설정이다.
병렬성은 멀티 작업을 위해 멀티 코어에서 개별 스레드를 동시에 실행하는 성질이다.
스레드의 개수가 코어의 수보다 많은 경우, 스레드를 어떤 순서에 의해 동시성으로 실행할 것인가 결정해야 한다. 이것을 스레드 스케줄링이라고 한다. 스레드 스케줄링에 의해 스레드들은 아주 짧은 시간 번갈아가며 본인들의 run() 메소드를 조금씩 실행한다.
자바의 스레드 스케줄링은 우선순위(Priority) 방식과 순환 할당(Round-Robin) 방식을 사용한다.
우선순위 방식은 우선순위가 높은 스레드가 실행 상태를 더 많이 가지도록 스케줄링하는 것이다. 순환 할당 방식은 시간 할당량(Time Slice)을 정해서 하나의 스레드를 정해진 시간만큼 실행하고 다시 다른 스레드를 실행하는 방식을 말한다.
스레드 우선방식은 스레드 객체에 우선순위 번호를 부여할 수 있기 때문에 개발자가 코드로 제어할 수 있다. 하지만 순환 할당 방식은 JVM에서 정해지므로 코드로 제어할 수 없다.
우선순위는 1에서 10까지 부여된다. 1이 가장 우선순위가 낮고, 10이 가장 높다. 우선순위를 부여하지 않으면 모든 스레드는 5의 우선순위를 할당받는다. 우선순위를 변경하고 싶다면 Thread 클래스가 제공하는 setPriority() 메소드를 사용한다. 이때 우선순위 매개값으로 1~10까지 값을 직접 주어도 되지만 ,코드 가독성을 높이기 위해 Thread 클래스의 상수를 사용할 수도 있다.
thread.setPriority(Thread.MAX_PRIORITY);
thread.setPriority(Thread.NORM_PRIORITY);
thread.setPriority(Thread.MIN_PRIORITY);
MAX_PRIORITY는 10, NORM_PRIORITY는 5, MIN_PRIORITY는 각각 1의 값을 갖는다. 우선순위가 높은 스레드는 실행 기회를 더 많이 갖기 때문에 우선순위가 낮은 스레드보다 작업을 빨리 끝낸다.
코어의 개수가 많다면 적은 개수의 스레드를 실행할 경우 우선순위 방식이 크게 영향을 미치지 못한다. 예를 들어 쿼드 코어일 경우에는 최소한 5개 이상의 스레드가 실행되어야 우선순위의 영향을 받는다.
동기화
동기화가 필요한 이유
멀티 스레드 프로그램에서는 스레드들이 객체를 공유해서 작업해야 하는 경우가 있다. 이 경우 스레드 A를 사용하던 객체가 스레드 B에 의해 상태가 변경될 수 있기 때문에 스레드 A가 의도했던 것과 다른 결과를 산출할 수 있다.
아래 예제에서는 User1 스레드가 Calculator 객체의 memory 필드에 100을 먼저 저장하고 2초간 일시 정지 상태가 된다. 그동안에 User2 스레드가 memory 필드값을 50으로 변경한다. 2초가 지나 User1 스레드가 다시 실행되어 memory 필드의 값을 출력하면 User2가 저장한 50이 나온다.
public class Calculator {
private int memory;
public int getMemory(){
return memory;
}
public void setMemory(int memory){
this.memory = memory;
try{
Thread.sleep(2000);
}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
}
public class User1 extends Thread{
private Calculator calculator;
public void setCalculator(Calculator calculator){
this.setName("User1");
this.calculator = calculator; //공유 객체를 필드에 저장
}
@Override
public void run(){
calculator.setMemory(100); //공유 객체 Calculator 메모리에 100 저장
}
}
public class User2 extends Thread{
private Calculator calculator;
public void setCalculator(Calculator calculator){
this.setName("User2");
this.calculator = calculator; //공유 객체를 필드에 저장
}
@Override
public void run(){
calculator.setMemory(50); //공유 객체 Calculator 메모리에 100 저장
}
}
public class Main {
public static void main(String[] args){
Calculator calculator = new Calculator();
User1 user1 = new User1();
user1.setCalculator(calculator);
user1.start();
User2 user2 = new User2();
user2.setCalculator(calculator);
user2.start();
}
}
동기화 메소드 및 동기화 블록
스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면, 스레드 작업이 끝날 때까지 객체에 잠금을 걸어 다른 스레드가 사용할 수 없도록 해야 한다.
멀티 스레드 프로그램에서 단 하나의 스레드만 실행할 수 있는 영역을 임계 영역(critical section)이라 한다. 자바는 임계 영역을 지정하기 위해 동기화(synchronized) 메소드와 동기화 블록을 제공한다. 스레드가 객체 내부의 동기화 메소드 또는 블록에 들어가면 즉시 객체에 잠금을 걸어 다른 스레드가 임계 영역 코드를 실행하지 못하도록 한다.
동기화 메소드를 만드려면 메소드 선언에 synchronized 키워드를 붙이면된다. 인스턴스, 정적 메소드 모두 가능하다.
public synchronized void method(){
//임계 영역, 단 하나의 스레드만 실행
}
메소드 전체 내용이 아니라, 일부 내용만 임계 영역으로 만들고 싶다면 다음과 같이 동기화 블록을 만들면 된다.
public void method(){
//여러 스레드가 실행 가능 영역
synchronized(공유객체){ //공유 객체가 자신이면 this를 넣을 수 있다.
//임계 영역, 단 하나의 스레드만 실행
}
//여러 스레드가 실행 가능 영역
}
다음 예제는 이전 예제에서 문제가된 공유 객체인 Calculator를 수정한 것이다. Calculator의 setMemory() 메소드를 동기화 메소드로 만들어서 User1 스레드가 setMemory()를 실행할 동안 User2 스레드가 setMemory() 메소드를 실행할 수 없다.
public class Calculator {
private int memory;
public int getMemory(){
return memory;
}
public synchronized void setMemory(int memory){
this.memory = memory;
try{
Thread.sleep(2000);
}catch (InterruptedException e){}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
}
다음과 같이 동기화 블록으로 만들 수도 있다. 스레드가 동기화 블록으로 들어가면 this(Calculator 객체)를 잠그고, 동기화 블록을 실행한다. 동기화 블록을 모두 실행할 때까지 다른 스레드들은 this의 모든 동기화 메소드 또는 동기환 블록을 실행할 수 없게 된다.
public class Calculator {
private int memory;
public int getMemory(){
return memory;
}
public void setMemory(int memory) {
synchronized (this) {
this.memory = memory;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
}
}
데드락(교착상태)
멀티 스레드에서 데드락이란, 둘 이상의 스레드가 lock을 획득하기 위해 대기하는 상황에서 해당 lock을 잡고 있는 스레드도 다른 lock을 기다리면서 서로 block 상태에 놓이는 것을 말한다. 즉, 두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있는 상황을 말한다.
아래 예제는 Thread1과 Thread2가 서로의 lock을 얻으려고 호출하기 때문에 무한정 락대기 상태 즉, 데드락에 빠지게 된다.
public class DeadlockExam {
public static final Object lock1 = new Object();
public static final Object lock2 = new Object();
public static void main(String args[]){
Thread thread1 = new Thread1();
Thread thread2 = new Thread2();
thread1.start();
thread2.start();
}
}
private static class Thread1 extends Thread{
public void run(){
synchronized (lock1){
System.out.println("Thread1이 lock1을 가지고 있습니다.");
try{
Thread.sleep(10);
}catch (InterruptedException e){}
System.out.println("Thread1이 lock2를 기다리고 있습니다.");
synchronized (lock2){
System.out.println("Thread1이 lock1과 lock2를 가지고 있습니다.");
}
}
}
}
private static class Thread2 extends Thread{
public void run(){
synchronized (lock2){
System.out.println("Thread2가 lock2를 가지고 있습니다.");
try{
Thread.sleep(10);
}catch (InterruptedException e){}
System.out.println("Thread2가 lock1를 기다리고 있습니다.");
synchronized (lock1){
System.out.println("Thread2가 lock1과 lock2를 가지고 있습니다.");
}
}
}
}
}
스레드의 상태
해당 주제는 내용이 길어져 다른 페이지로 분리했습니다.
(+추가)스레드의 이름 사용하기
스레드는 자신의 이름을 가지고 있다. 이 정보는 디버깅할 때 어떤 스레드가 어떤 작업을 하는지 조사할 목적으로 사용될 수 있다.
메인 스레드는 “main”이라는 이름을 가지고 있고, 직접 생성하는 worker 스레드는 자동적으로 “Thread-n”이라는 이름으로 설정된다. Thread-n 대신 다른 이름을 설정하고 싶다면 Thread 클래스의 setName() 메소드로 변경한다.
thread.setName("스레드 이름");
스레드 이름을 알고 싶을 경우에는 getName() 메소드를 호출하면 된다.
thread.getName();
위에 두 메소드는 Thread의 인스턴스 메소드다. 실행하는 현재 스레드의 참조를 Thread의 정적 메소드인 currentThread()로 가져와서 setName(), getName() 메소드를 사용할 수 있다.
Thread thread = thread.currentThread();
아래는 메인 스레드의 참조를 얻어 스레드 이름을 콘솔에 출력하고, 새로 생성한 스레드의 이름을 setName() 메소드로 설정한 후, getName() 메소드로 읽어오는 예제다.
public class ThreadA extends Thread{
public ThreadA(){
setName("ThreadA");
}
public void run(){
for(int i=0; i<2; i++){
System.out.println(getName() + "가 출력한 내용");
}
}
}
public class ThreadB extends Thread{
public ThreadB(){
setName("ThreadB");
}
public void run(){
for(int i=0; i<2; i++){
System.out.println(getName() + "가 출력한 내용");
}
}
}
public class ThreadNameExam {
public static void main(String[] args){
Thread mainThread = Thread.currentThread();
System.out.println("프로그램 시작 스레드 이름: " + mainThread.getName());
Thread threadA = new ThreadA();
System.out.println("작업 스레드 이름: " + threadA.getName());
threadA.start();
Thread threadB = new ThreadB();
System.out.println("작업 스레드 이름: " + threadB.getName());
}
}
출처
이것이 자바다
https://sujl95.tistory.com/63?category=941455
'자바' 카테고리의 다른 글
[Java] Enum, 자바의 열거타입을 알아보자 (0) | 2021.01.29 |
---|---|
[Java] 자바 스레드(Thread)의 상태, 상태를 조절하는 메소드들 (0) | 2021.01.25 |
[Java] 자바의 인터페이스(interface)란? (0) | 2021.01.07 |
[Java] Object 클래스 (0) | 2020.12.26 |
[Java] 자바의 static, 클래스 멤버, 정적 초기화 블록(static block) (0) | 2020.12.20 |
댓글