[Java] 스레드

Minit88·2023년 3월 10일
0

[Java]

목록 보기
17/18
post-thumbnail

프로세스와 스레드

프로세스는 실행 중인 애플리케이션을 의미한다. 즉, 애플리케이션을 실행하면 운영체제로부터 실행에 필요한 만큼의 메모리를 할당 받아 프로세스가 된다.

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

메인 스레드

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

멀티 스레드

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


[그림] 멀티스레드

스레드의 생성과 실행

메인 스레드 외에 별도의 작업 스레드를 활용한다는 것은 작업 스레드가 수행할 코드를 작성하고, 작업 스레드를 생성하여 실행시키는 것을 의미한다.

자바는 객체지향 언어이므로 모든 자바 코드는 클래스 안에 작성된다. 따라서 스레드가 수행할 코드도 클래스 내부에 작성해주어야 하며, run()이라는 메서드 내에 스레드가 처리할 작업을 작성하도록 규정된다.

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

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

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

public class ThreadExample1 {
    public static void main(String[] args) {
        //Runnable 인터페이스를 구현한 객체 생성
        Runnable task1 = new ThreadTask1();

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


        //task1.run();
        thread1.start();
        for(int i =0;i<100;i++){
            System.out.println("@");
        }

    }

}

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

Runnable 을 ThreadTask1 클래스을 통해 구현을 하여 run() 메서드를 재정의하였고, Runnable을 구현한 task1 객체를 생성하여 Thread 에 인자로써 전달하여 인스턴스화 한 객체 생성

이때 Thread 의 start() 메서드를 실행하게 되면, 스레드가 실행이 된다.

[그림] 위 코드 출력화면

@와 #은 섞여 있음을 볼 수 있다. 메인 스레드와, thread1 스레드가 동시 실행되고 있다. 즉, 두 스레드는 병렬로 실행되고 있어 두 가지 문자가 섞여서 출력이 된다.

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

public class ThreadExample2 {
    public static void main(String[] args) {
        Thread thread1 = new ThreadTask2();

        thread1.start();

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


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

Thread을 상속받는 ThreadTask2 클래스를 생성해 run() 메서드를 위와 같은 내용으로 재정의하였다.

main 에서는 ThreadTask2을 대입한 tread1 객체를 생성하여 start() 메서드를 실행시켜준다.


[그림] 위 코드 출력 화면

결과 동작은 문자 순서가 다를뿐, 흐름은 두 방법 동일하다.

익명 객체를 사용하여 스레드를 생성하고 실행하기

public class ThreadExample2 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run(){
                for(int i=0;i<100;i++){
                    System.out.println("#");
                }
        }});


                thread1.start();

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

클래스를 생성해 Runnanble을 인자로 받는 thread1 을 생성한다. 이때 Runnable은 익명 구현 객체를 통해 구현되어졌다.


[그림] 위 코드 출력 화면

Thread 익명 자식 객체를 활용한 스레드 생성 및 실행

public class ThreadExample2 {
    public static void main(String[] args) {
        Thread thread1 = new Thread(){
            @Override
            public void run(){
                for(int i=0;i<100;i++){
                    System.out.println("#");
                }
        }};


                thread1.start();

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

익명 자식 객체를 통해 재정의된 Thread() 클래스를 대입해 새로운 객체인 thread1을 생성한다.

[그림] 위 코드 출력화면

스레드의 이름

메인스레드는 "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());
    }
}


[그림] 위 코드 출력 화면

스레드의 이름 설정하기

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

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());
        System.out.println("");
        thread3.setName("thread3");
        System.out.println("thread3.getName() = "+thread3.getName());
    }
}


[그림] 위 코드의 출력화면

스레드의 초기화

앞서 프로세스는 자원,데이터,그리고 스레드로 구성된다는 것을 배웠다. 프로세스는 스레드가 운영 체제로부터 자원을 할당 받아 소스 코드를 실행하여 데이터를 처리한다.

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

예제를 통해 어떤 문제가 발생되는지 알아보면,

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 boolean withdraw(int money){
        if(balance>=money) {
            try{Thread.sleep(1000);} catch(Exception e){}

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

해당 코드를 통해서 1~300 원의 금액을 인출할 때 두 스레드 간의 Accout 참조법에 대해 알아보자.


[그림] 위 코드 출력 결과 중 일부

맨 윗줄과 그 바로 아래 줄을 보면
박자바는 1000원 중 200원을 인출하여 800원이 남아있어 잘 동작하는 것을 확인할 수 있었지만, 김코딩은 1000원 중에 100원을 인출하였음에도 900원이 아닌 800원으로 나타나있음을 알 수 있다.

즉, 두 스레드가 하나의 Account 를 공유하는 상황에 다른 스레드가 끼어들어서 발생하게 된다.

이러한 상황이 발생하지 않게 하는 것을 스레드 동기화라고 한다.

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

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

위 예제에서 발생한 문제를 해결하기 위해서는 withdraw 메서드에 락을 걸어, 두 스레드가 동시에 실행하면 안 되는 영역을 설정해주어야 한다.

즉, withdraw() 메서드를 임계 영역으로 설정해야 한다.
특정 코드 구간을 임계 영역으로 설정할 때에는 synchronized라는 키워드를 사용한다. synchronized키워드는 두 가지 방법으로 사용할 수 있다.

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

위 코드 처럼 임계영역을 특정한 영역으로 설정하려면

Synchronized (객체의 참조) {
	
    임계 영역 지정
}

의 형식대로 설정한다.

임계영역을 설정 한 후의 코드를 재 실행해보면,


위 사진 처럼 정상 동작하고 있음을 알 수 있다.

profile
" To be BE "

0개의 댓글