[Java] 스레드

최우형·2023년 3월 12일
1

Java

목록 보기
17/24

📌프로세스(Process)와 스레드(Thread)

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

스레드는 하나의 코드 실행 흐름이라고 볼 수 있다.

메인 스레드(Main-Thread)

자바 애플리케이션을 실행하면 가장 먼저 실행되는 메서드는 main 메서드이다. 메인 스레드가 main 메서드를 실행시켜준다.

멀티 스레드(Multi-Tread)

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


📌스레드의 생성과 실행

작업 스레드 생성과 실행

작업 스레드가 수행할 코드를 작성하고, 작업 스레드를 생헝하여 실행시키는 것을 의미한다.

  • 첫 번째 방법
    • Runnalbe 인터페이스를 구현한 객체에서 run()을 구현하여 스레드를 생성하고 실행하는 방법
  • 두 번째 방법
    • Thread 클래스를 상속받은 하위 클래스에서 run()을 구현하여 스레드를 생성하고 실행하는 방법

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

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

    }
}

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

    }
}

Runnable 에는 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("#");
        }
    }
}

run() 메서드 바디에 새롭게 생성된 작업 스레드가 수행할 코드를 적어주면 된다.

이제 스레드를 생성한다.

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("#");
        }
    }
}

이와 같이 스레드를 생성할 때는 Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화 한다.

하지만 스레드를 생성하고 실행하려면 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("#");
        }
    }
}

이렇게 나온다.

결과 값

@@@@@@@@@@@######@@@@@############################
@#########@@@@@@@@@@@@@@@@############@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@##@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@###########################################
Process finished with exit code 0
// 가독성을 위해 50개 마다 개행을 임의로 추가함.

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

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("#");
        }
    }
}

이제 스레드를 생성해보자. 첫 번째 방법과의 차이점은, 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("@");
        }
    }
}

📌스레드의 이름

메인 스레드는 main 이라는 이름을 가지며, 그 외 추가적으로 생성한 스레드는 기본적으로 Thread-n 이라는 이름을 가진다.

스레드의 이름 조회하기

스레드의_참조값.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());
    }
}

출력결과

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());
    }
}

출력값

Set And Get Thread Name
thread4.getName() = Thread-0
thread4.getName() = Code States

Process finished with exit code 0

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

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());
    }
}

출력값

main
Thread-0

Process finished with exit code 0

📌스레드의 동기화

멀티 스레드 프로세스의 경우, 두 스레드가 동일한 데이터를 공유하게 되어 문제가 발생할 수 있다.

//예제
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" : "")
            );
        }
    }
}

라는 코드를 실행시켜보면

Withdraw 100By 김코딩. Balance : 600 
Withdraw 300By 박자바. Balance : 600 
Withdraw 200By 김코딩. Balance : 400 
Withdraw 200By 박자바. Balance : 200 
Withdraw 200By 김코딩. Balance : -100 
Withdraw 100By 박자바. Balance : -100 

Process finished with exit code 0

라고 코드가 나온다.

첫 번째 줄에서 100원을 사용했지만 잔여금액이 600원이 나왔다. (오류)

그 이유는 thread.sleep을 통해 1000ms 가 멈춰있는 동안 다른 코드가 와서 돈을 사용했기 때문에이다.

그래서 이런 상황이 발생하지 않게 하는 것이 바로 스레드 동기화이다.

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

임계 영역은 오로지 하나의 스레드만 코드를 실행할 수 있는 코드 영역이다.

락은 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미한다.

1. 메서드 전체를 임계 영역으로 지정하기

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;
	}
}

2. 특정한 영역을 임계 영역으로 지정하기

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;
			}
	}
}

그러면 다시 기존 코드로 와서 수정을 하면

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" : "")
            );
        }
    }
}

출력값

Withdraw 100By 김코딩. Balance : 900 
Withdraw 100By 박자바. Balance : 800 
Withdraw 200By 김코딩. Balance : 600 
Withdraw 300By 박자바. Balance : 300 
Withdraw 300By 김코딩. Balance : 0 
Withdraw 100By 박자바. Balance : 0 -> DENIED

이렇게 정상출력이 된다.

profile
프로젝트, 오류, CS 공부, 코테 등을 꾸준히 기록하는 저만의 기술 블로그입니다!

0개의 댓글