공부/전자컴퓨터공학

Java 스레드(Thread)란? 자바 기초 배우기

AhJustC 2024. 5. 31. 00:36
반응형
프로세스(Process)와 스레드(Thread)

프로세스는 실행중인 애플리케이션을 의미한다. 프로세느는 데이터, 컴퓨터 자원, 스레드로 구성이 되는데, 스레드는 데이터와 애플리케이션이 확보한 자원을 활용하여 소스 코드를 실행한다. 즉 스레드는 하나의 코드 실행 흐름 이라고 볼 수 있다.

 

메인 스레드(Main thread)

자바 애플리케이션을 실행하면 가장 먼저 실행되는 메서드는 main 메서드이며, 메인 스레드가 main 메서드를 실행시켜준다. 메인 스레드는 main 메서드의 코드를 처음부터 끝까지 차례대로 실행시키며 코드의 끝을 만나거나 return 문을 만나면 실행을 종료한다.

 

멀티 스레드(Multi-Thread)

하나의 프로세스는 여러 개의 스레드를 가질 수 있고 이를 멀티 스레드 프로세스라 한다. 여러개의 스레드를 가진다는 것은 여러 스레드가 동시에 작업을 수행할 수 있음을 의미하며 이를 멀티 스레딩이라고 한다.

작업 스레드 생성과 실행

메인 스레드 외에 별도의 작업 스레드를 활용한다는 것은 작업 스레드가 수행할 코드를 작성하고, 작업 스레드를 생성하여 실행 시키는 것을 의미한다. 그런데 자바는 객체지향 언어이므로 모든 자바 코드는 클래스 안에 작성된다. 따라서 스레드가 수행할 코드도 클래스 내부에 작성해주어야 하며 run() 이라는 메서드 내에 스레드가 처리할 작업을 작성하도록 규정되어 있다.

run() 메서드는 Runnable 인터페이스와 Thread 클래스에 정의되어있다. 따라서 작업 스레드를 생성하고 실행하는 방법은 다음 두 가지가 된다.

 

 

 

 

  • Runnable 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법

먼저 Runnable 인터페이스를 구현한 객체를 만든다. 아래와 같이 임의의 클래스를 만들고, Runnable을 구현하도록 한다.

Runnable에는 run()이 정의되어 있으므로 반드시 run()을 구현해야 한다

public class ThreadExample1 {
    public static void main(String[] args) {

    }
}

// Runnable 인터페이스를 구현하는 클래스
class ThreadTask1 implements Runnable {
    public void run() {

    }
}

그 다음 run() 의 메서드 바디에 새롭게 생성된 작업 스레드가 수행할 코드를 적어주면 된다. 아래와 같이 코드를 작성해보자.

public class ThreadExample1 {
    public static void main(String[] args) {

    }
}

class ThreadTask1 implements Runnable {

    // run() 메서드 바디에 스레드가 수행할 작업 내용 작성
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}

이제 스레드를 생성해보자. Runnable 인터페이스를 구현한 객체를 활용하여 스레드를 생성할 때는 아래와 같이 Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화 한다.

public class ThreadExample1 {
    public static void main(String[] args) {

        // Runnable 인터페이스를 구현한 객체 생성
        Runnable task1 = new ThreadTask1();

        // Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화 하여 스레드를 생성
        Thread thread1 = new Thread(task1);

        // 위의 두 줄을 아래와 같이 한 줄로 축약할 수도 있습니다.
        // Thread thread1 = new Thread(new ThreadTask1());

    }
}

class ThreadTask1 implements Runnable {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}

스레드를 생성하는 것만으로는 run() 내부의 코드가 실행되지 않는다. run() 메서드 내부의 코드를 실행하려면 start() 메서드를 아래와 같이 호출하여 스레드를 실행시켜주어야 한다.

public class ThreadExample1 {
    public static void main(String[] args) {
        Runnable task1 = new ThreadTask1();
        Thread thread1 = new Thread(task1);

        // 작업 스레드를 실행시켜, run() 내부의 코드를 처리하도록 합니다.
        thread1.start();
    }
}

class ThreadTask1 implements Runnable {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}

 

마지막으로 main 메서드에 아래와 같이 반복문을 추가한 후 코드를 실행해본다.

public class ThreadExample1 {
    public static void main(String[] args) {
        Runnable task1 = new ThreadTask1();
        Thread thread1 = new Thread(task1);

        thread1.start();

        // 반복문 추가
        for (int i = 0; i < 100; i++) {
            System.out.print("@");
        }
    }
}

class ThreadTask1 implements Runnable {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}

출력결과는 아래와 같으며 가독성을 위해 개행을 임의로 추가했다.

/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=51746:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample1
@@@@@@@@@@@######@@@@@############################
@#########@@@@@@@@@@@@@@@@############@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@##@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@###########################################
Process finished with exit code 0

@는 main 메서드의 반복문에서 출력한 문자로 메인 스레드의 반복문 코드 실행에 의해 출력 되었다.

#은 run() 메서드의 반복문에서 출력한 문자로 작업 스레드의 반복문 코드 실행에 의해 출력되었다.

@와 #는 섞여 있다. 즉 메인 스레드와 작업 스레드가 동시에 병렬로 실행되면서 두 가지 문자가 섞여서 출력 된 것이다.

 

  • Thread 클래스를 상속받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법

Thread 클래스를 상속받는 하위 클래스를 만들어준다. Thread 클래스에는 run() 메서드가 정의되어 있으며, 따라서 run() 메서드를 오버라이딩 해주어야 한다.

public class ThreadExample2 {
    public static void main(String[] args) {

    }
}

// Thread 클래스를 상속받는 클래스 작성
class ThreadTask2 extends Thread {
    public void run() {

    }
}

그 다음 run() 메서드 바디에 새롭게 생성될 스레드가 수행할 작업을 작성한다.

public class ThreadExample2 {
    public static void main(String[] args) {

    }
}

class ThreadTask2 extends Thread {

    // run() 메서드 바디에 스레드가 수행할 작업 내용 작성
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}

이제 run() 내의 코드를 실행할 스레드를 생성해보자. 첫 번째 방법과 차이점은 Thread 클래스를 직접 인스턴스화 하지 않는다는 점이다.

public class ThreadExample2 {
    public static void main(String[]args) {

        // Thread 클래스를 상속받은 클래스를 인스턴스화하여 스레드를 생성
        ThreadTask2 thread2 = new ThreadTask2();
    }
}

class ThreadTask2 extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}

마지막으로 첫 번째 방법에서 실습했던 것과 동일하게 start()메서드를 실행시켜 주고, main 메서드에 반복문 코드를 추가한 후 코드를 실행한다.

public class ThreadExample2 {
    public static void main(String[] args) {

        ThreadTask2 thread2 = new ThreadTask2();

        // 작업 스레드를 실행시켜, run() 내부의 코드를 처리하도록 합니다.
        thread2.start();

        // 반복문 추가
        for (int i = 0; i < 100; i++) {
            System.out.print("@");
        }
    }
}

class ThreadTask2 extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}
반응형
익명 객체를 사용하여 스레드를 생성하고 실행하기
Runnable 익명 구현 객체를 활용한 스레드 생성 및 실행
public class ThreadExample1 {
    public static void main(String[] args) {

        // 익명 Runnable 구현 객체를 활용하여 스레드 생성
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.print("#");
                }
            }
        });

        thread1.start();

        for (int i = 0; i < 100; i++) {
            System.out.print("@");
        }
    }
}
Thread 익명 하위 객체를 활용한 스레드 생성 및 실행
public class ThreadExample2 {
    public static void main(String[] args) {

        // 익명 Thread 하위 객체를 활용한 스레드 생성
        Thread thread2 = new Thread() {
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.print("#");
                }
            }
        };

        thread2.start();

        for (int i = 0; i < 100; i++) {
            System.out.print("@");
        }
    }
}

 

스레드의 이름 조회하기

스레드의 이름은 아래와 같이 스레드의 참조값.getName()으로 조회할 수 있다.

public class ThreadExample3 {
    public static void main(String[] args) {

        Thread thread3 = new Thread(new Runnable() {
            public void run() {
                System.out.println("Get Thread Name");
            }
        });

        thread3.start();

        System.out.println("thread3.getName() = " + thread3.getName());
    }
}

출력 결과

/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=52285:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample3
Get Thread Name
thread3.getName() = Thread-0

Process finished with exit code 0

 

스레드의 이름 설정하기

스레드의 이름은 스레드의 참조값.setName() 으로 설정할 수 있다.

public class ThreadExample4 {
    public static void main(String[] args) {

        Thread thread4 = new Thread(new Runnable() {
            public void run() {
                System.out.println("Set And Get Thread Name");
            }
        });

        thread4.start();

        System.out.println("thread4.getName() = " + thread4.getName());

        thread4.setName("Code States");

        System.out.println("thread4.getName() = " + thread4.getName());
    }
}

출력 결과

/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=52282:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample4
Set And Get Thread Name
thread4.getName() = Thread-0
thread4.getName() = Code States

Process finished with exit code 0

 

스레드 인스턴스 주소값 얻기

Thread 클래스의 정적 메서드인 currentThread()를 사용한다.

public class ThreadExample1 {
    public static void main(String[] args) {

        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });

        thread1.start();
        System.out.println(Thread.currentThread().getName());
    }
}

출력 결과

/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=52293:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample1
main
Thread-0

Process finished with exit code 0

 

 

 

 

스레드 동기화란?

싱글 스레드 프로세스는 데이터에 단 하나의 스레드만 접근하므로 문제될 사항은 없으나 멀티 스레드 프로세스의 경우 두 스레드가 같은 데이터를 공유하게 되어 문제가 발생 할 수 있다.

예제를 통해 구체적으로 확인해보자. 확인 전 생소한 문법 요소를 간단히 확인 후 넘어가자.

try { Thread.sleep(1000); } catch (Exception error) {}

  • Thread.sleep(1000);
    • 스레드를 일시 정지시키는 메서드이다.
    • 어떤 스레드가 일시 정지되면, 대기열에서 기다리고 있던 다른 스레드가 실행된다.
    • 또한, Thread.sleep()은 반드시 try … catch문의 try 블록 내에 작성해주어야 한다.
  • try { … } catch ( ~ ) { … }
    • try … catch문은 예외 처리에 사용되는 문법이다.
    • 쉽게 설명하자면, try의 블록 내의 코드를 실행하다가 예외 또는 에러가 발생하면 catch문의 블록에 해당하는 내용을 실행하라는 의미가 된다.
    • Thread.sleep(1000);의 동작을 위해 형식적으로 사용한 문법 요소이다.
public class ThreadExample3 {
    public static void main(String[] args) {

        Runnable threadTask3 = new ThreadTask3();
        Thread thread3_1 = new Thread(threadTask3);
        Thread thread3_2 = new Thread(threadTask3);

        thread3_1.setName("김코딩");
        thread3_2.setName("박자바");

        thread3_1.start();
        thread3_2.start();
    }
}

class Account {

    // 잔액을 나타내는 변수
    private int balance = 1000;

    public int getBalance() {
        return balance;
    }

    // 인출 성공 시 true, 실패 시 false 반환
    public boolean withdraw(int money) {

        // 인출 가능 여부 판단 : 잔액이 인출하고자 하는 금액보다 같거나 많아야 합니다.
        if (balance >= money) {

            // if문의 실행부에 진입하자마자 해당 스레드를 일시 정지 시키고,
            // 다른 스레드에게 제어권을 강제로 넘깁니다.
            // 일부러 문제 상황을 발생시키기 위해 추가한 코드입니다.
            try { Thread.sleep(1000); } catch (Exception error) {}

            // 잔액에서 인출금을 깎아 새로운 잔액을 기록합니다.
            balance -= money;

            return true;
        }
        return false;
    }
}

class ThreadTask3 implements Runnable {
    Account account = new Account();

    public void run() {
        while (account.getBalance() > 0) {

            // 100 ~ 300원의 인출금을 랜덤으로 정합니다.
            int money = (int)(Math.random() * 3 + 1) * 100;

            // withdraw를 실행시키는 동시에 인출 성공 여부를 변수에 할당합니다.
            boolean denied = !account.withdraw(money);

            // 인출 결과 확인
            // 만약, withraw가 false를 리턴하였다면, 즉 인출에 실패했다면,
            // 해당 내역에 -> DENIED를 출력합니다.
            System.out.println(String.format("Withdraw %d₩ By %s. Balance : %d %s",
                    money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> DENIED" : "")
            );
        }
    }
}

위 예제는 두 개의 작업 스레드를 생성하여 1,000원의 잔액을 가진 계좌로부터 100~300원을 인출하고, 인출금과 잔액을 출력하는 예제이다. 위 코드를 실행하면 두 개의 작업 스레드가 생성되며 이 작업 스레드는 Account 객체를 공유하게 된다.

/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=52484:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample3
Withdraw 100₩ By 김코딩. Balance : 600
Withdraw 300₩ By 박자바. Balance : 600
Withdraw 200₩ By 김코딩. Balance : 400
Withdraw 200₩ By 박자바. Balance : 200
Withdraw 200₩ By 김코딩. Balance : -100
Withdraw 100₩ By 박자바. Balance : -100

Process finished with exit code 0

출력 결과를 보면 인출금과 잔액이 다르다. 이는 두 스레드 간에 객체가 공유되기 때문에 발생하는 오류로 값이 왜 이렇게 출력되었는지 추측하기 어렵다. 또한 withdraw()에서 잔액이 인출하고자 하는 금액보다 많은 경우에만 인출이 가능하도록 코드를 작성해 두었음에도 불구하고 if(balance>=money)~ 마치 조건문이 무시된 것처럼 음수의 잔액이 발생하는 것을 확인할 수 있다.이러한 문제가 발생하지 않게 하는 것을 바로 스레드 동기화 라고 한다.

스레드 동기화 적용을 통한 개선을 하기에 앞서 임계영역과 락에 대해서 알아야 한다.

임계 영역(Critical section)과 락(Lock)

임계영역은 오로지 하나의 스레드만 코드를 실행할 수 있는 코드 영역을 의미하며, 락은 임계 영역을 포함하고 있는 객체에 접근 할 수 있는 권한을 의미한다.

withraw() 메서드를 두 스레드가 동시에 실항하지 못하게 해야 한다. 특정 코드 구간을 임계영역으로 설정할 때는 synchronized 라는 키워드를 사용하는데 두 가지 방법으로 사용할 수 있다.

  • 메서드 전체를 임계 영역으로 지정하기
class Account {
	...
	public synchronized boolean withdraw(int money) {
	    if (balance >= money) {
	        try { Thread.sleep(1000); } catch (Exception error) {}
	        balance -= money;
	        return true;
	    }
	    return false;
	}
}
  • 특정한 영역을 임계 영역으로 지정하기
class Account {
	...
	public boolean withdraw(int money) {
			synchronized (this) {
			    if (balance >= money) {
			        try { Thread.sleep(1000); } catch (Exception error) {}
			        balance -= money;
			        return true;
			    }
			    return false;
			}
	}
}

 

아래와 같이 withdraw()의 반환 타입 앞에 synchronized를 붙여준 후 코드를 실행해보자.

public class ThreadExample3 {
    public static void main(String[]args) {

        Runnable threadTask3 = new ThreadTask3();
        Thread thread3_1 = new Thread(threadTask3);
        Thread thread3_2 = new Thread(threadTask3);

        thread3_1.setName("김코딩");
        thread3_2.setName("박자바");

        thread3_1.start();
        thread3_2.start();
    }
}

class Account {
    private int balance = 1000;

    public int getBalance() {
        return balance;
    }

    public synchronized boolean withdraw(int money) {
        if (balance >= money) {
            try {
                Thread.sleep(1000);
            } catch (Exception error) {
            }
            balance -= money;
            return true;
        }
        return false;
    }
}

class ThreadTask3 implements Runnable {
    Account account = new Account();

    public void run() {
        while (account.getBalance() > 0) {
            int money = (int)(Math.random() * 3 + 1) * 100;
            boolean denied = !account.withdraw(money);
            System.out.println(String.format("Withdraw %d₩ By %s. Balance : %d %s",
                    money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> DENIED" : "")
            );
        }
    }
}

출력 결과는 다음과 같다.

/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=52534:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample3
Withdraw 100₩ By 김코딩. Balance : 900
Withdraw 100₩ By 박자바. Balance : 800
Withdraw 200₩ By 김코딩. Balance : 600
Withdraw 300₩ By 박자바. Balance : 300
Withdraw 300₩ By 김코딩. Balance : 0
Withdraw 100₩ By 박자바. Balance : 0 -> DENIED

Process finished with exit code 0

Thread Pool

동시 작업 처리가 많아지면 스레드 수가 증가하고 스래드 생성과 스케줄링으로 인한 메모리 사용량이 늘어나면서 애플리케이션의 성능을 저하시킨다. 스레드의 무분별한 증가를 방지하려면 스레드 풀을 사용해야한다. 스레드 풀은 작업 처리에 사용되는 스레드 수를 정해놓는다. 큐에 들어오는 작업이 들어오면 스레드 풀 안에 스레드가 하나씩 맡아서 처리한다.

스레드풀 생성

ExecutorService(스레드 풀) 구현객체는 Executors 클래스 메서드로 생성할 수 있다.

메서드 초기스레드 수 코어스레드 수 최대스레드 수
newCachedThreadPool() 0 0 Integer.MAX_VALUE
newFixedThreadPool(int num) 0 num num

 

스레드풀 종료

스레드풀은 main 스레드가 종료되어도 작업을 처리하기 위해 계속 실행 상태로 남아있습니다. 애플리케이션을 종료하기 위해서는 스레드풀을 종료해야 한다.

메서드 리턴타입 설명
shutdown() void 작업 큐에 남아있는 모든 작업을 처리한 뒤 종료
shutdownNow() List 작업 큐에 남아있는 작업과 상관없이 종료, 처리 못한 작업(Runnable) 목록을 리턴
awaitTermination(long timeout, TimeUnit unit) boolean shotdown() 메소드 호출 후, 모든 작업 처리를 timeout 시간안에 처리하면 true, 처리 못하면 작업 스레드들을 interrupt()하고 false 리턴

 

스레드풀 예시코드
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class Main {
	public static void main(String[] args) {
    //최대 6개의 스레드를 가진 스레드풀 생성
		ExecutorService executorService = Executors.newFixedThreadPool(6);

		for(int i = 0; i < 10; i++){
			Runnable runnable = new Runnable() {

				@Override
				public void run() {
					ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
					//스레드풀 갯수 확인
					int poolSize = threadPoolExecutor.getPoolSize();
					//스레드 풀에 있는 해당 스레드이름 확인
					String threadName = Thread.currentThread().getName();
					System.out.println("스레드풀 갯수:" + poolSize + "스레드 이름: "+threadName);
				}
			};

			//스레드풀 작업 처리 요청
			executorService.execute(runnable);
			//executorService.submit(runnable);

			//콘솔 출력을 위해 main스레드에 10ms sleep을 걸어둠.
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		//스레드풀 종료
		executorService.shutdown();
	}
}
반응형