본문 바로가기

Language/JAVA

[JAVA] 스레드 개념과 사용방법 쉬운 예시로 이해하기

프로세스 (Process)

 

프로세스는 일반적으로 cpu 의해 메모리에 올려져 실행중인 프로그램을 말하며, 운영체제에서는 실행중인 하나의 애플리케이션프로세스라고 부른다.

사용자가 애플리케이션을 실행하면 운영체제로부터 실행에 필요한 메모리를 할당받아 애플리케이션의 코드를 실행한다.

 

자바 JVM(Java Virtual Machine) 주로 하나의 프로세스 실행되며, 동시에 여러 작업을 수행하기 위해서 멀티 스레드 지원하고 있다.  

 

 

스레드 (Thread)

 

스레드는 프로세스 내에서 실질적으로 작업을 실행하는 단위를 말하며, 자바에서는 JVM(Java Virtual Machine) 의해 관리된다.

java에서 기본적으로 프로그램이 실행되면 1개의 Main 스레드가 동작하고, 별도로 추가 스레드를 생성하면 멀티스레드 환경이 된다.

 

CPU의 코어1개당 1개의 Processor가 동작하며, 모든 프로세스에는 "한 개 이상"의 스레드가 존재한다.

프로세서는 주어진 task(thread)를 빠르게 "순차적"으로 처리하지만 다만 처리 속도가 빠르기에 Multi Tasking이라고 봐도 무방하다.

 

 

💡 멀티 프로세스 VS 멀티 스레드

출처 : whoishoo

 

멀티 스레드 : 하나의 프로세스에서 여러 개의 작업을 하도록 해주는 기능

멀티 프로세스 : 여러 개의 프로세스를 사용하여 병렬적 처리를 있게끔 해주는 기능

 

 

 

 

스레드의 생성

 

방법 1. Thread 클래스를 상속한 객체를 통해 Thread생성

extends 키워드를 사용해 Thread 클래스를 상속받고, run() 메서드를 오버라이딩한다.

*단, 여러 개를 만들었을 때 순서가 보장되지 않는다. 각각 독립적으로 동작함

 

[ ExtendsThreadClass ] 

public class ExtendsThreadClass extends Thread{
    @Override
    public void run() {
        System.out.println("ExtendsThreadClass : " + Thread.currentThread().getName());
    }
}

 

[ main ]

public class MainClass {
    public static void main(String[] args) {
//        쓰레드 생성방법 1. Thrad를 상속한 클래스 객체 실행
        Thread th1 = new ExtendsThreadClass();
//        start가 실행되자마자 run메서드가 자동실행
        th1.start();

        Thread th2 = new ExtendsThreadClass();
//        start가 실행되자마자 run메서드가 자동실행
        th2.start();

        Thread th3 = new ExtendsThreadClass();
//        start가 실행되자마자 run메서드가 자동실행
        th3.start();
    }
}

 

 

 

 

 

방법 2. Runnable 인터페이스를 구현한 객체를 통해 Thread생성

Thread 생성자로 "Runnable객체"를 주입하는 방식

Thread클래스에 Runnable객체가 전달되어 사용자가 정의 run 메서드가 실행된다.

 

implements 키워드를 사용해 Runnable을 상속받고, run() 메서드를 오버라이딩한다.

*Runnable 구현하면 다중상속, 다중구현을 있는 장점이 Thread 상속보다 크다.

 

[ RunnableImplementsClass ]

public class RunnableImplementsClass implements Runnable{
    @Override
    public void run() {
        System.out.println("RunnableImplementsClass : " + Thread.currentThread().getName());
    }
}

 

[ main ]

public class MainClass {
    public static void main(String[] args) {
//        쓰레드 생성방법 2. Runnable 인터페이스를 구현한 객체를 통해 생성
        Thread rt1 = new Thread(new RunnableImplementsClass()); //run메서드를 주입
        rt1.start();

        Thread rt2 = new Thread(new RunnableImplementsClass()); //run메서드를 주입
        rt2.start();

        Thread rt3 = new Thread(new RunnableImplementsClass()); //run메서드를 주입
        rt3.start();
    }
}

 

 

 

(+)

익명객체를 통해 간단하게 생성 및 실행이 가능하다.

 new Thread(new RunnableimplementsClass()).start();
new Thread(() -> System.out.println("익명객체 스레드")).start();

 

 

 

 

 

멀티스레드

 

두 개 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스라 한다. 

멀티 스레드 실행시 코어가 정해진 시간 동안 여러 작업을 번갈아가며 수행하는데, 동시에 실행되는 2개 이상의 쓰레드 때문에 데이터의 동시성 문제가 필연적으로 발생한다.

 

 

📌 멀티스레드의 동시성문제 해결

여러 쓰레드가 동일한 리소스를 공유하여 사용하게 되면 실행의 결과가 동일 리소스에 영향을 주기 때문에 이를 방지하는 동기화 작업을 필수로 수행해야 한다.

 

** 단일스레드 동시성 이슈 테스트 -> 하나의 스레드가 동작하기 때문에 문제없음

[ Library ]

책 재고를 100으로 두고, 스레드가 동작할 때마다 책 재고를 1씩 감소시킴.

재고가 0과 같아지면 "대출 불가" 출력

public class Library {
    static int bookcount = 100;

    static void borrow() {
        if (bookcount > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            bookcount -= 1;
            System.out.println("대출 완료");
            System.out.println("남아 있는 책 수량 : " + bookcount);
        } else {
            System.out.println("대출 불가");
        }
    }
}

 

[ main ]

public class MainClass {
    public static void main(String[] args) {
        //        thread의 동시성 이슈 테스트
//        단일 스레드
        for (int i = 0; i < 1000; i++) {
            Library.borrow();
        }
        System.out.println("최종 책 수량 " + Library.bookcount);
    }
}

 

 

 

** 멀티스레드 동시성 이슈 테스트-> 여러 개의 스레드가 동작하기 때문에 문제발생

[ main ]

Runnable을 익명 객체로 생성.

Library 클래스의 함수를 호출하기 위해 Library.borrow();사용

public class MainClass {
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            Thread th = new Thread(new Runnable() {
                @Override
                public void run() {
                    Library.borrow();
                }
            });
            th.start();
        }
        System.out.println("최종 책 수량 " + Library.bookcount);
    }
}

 

bookcount라는 값에 1000개의 스레드가 독립적으로 접근하며 수정하다보니, 동시성 문제로 인해 재고 관리가 이상하게 되는 것을 볼 수 있다.

 

 

 

💡 어떻게 해결할까?

해결방법 1. 스레드가 접근하는 함수에 syncronized 키워드 사용

syncronized 키워드를 통해, 이미 생성된 스레드들이 동일 메서드에 동시실행하지 못하게 메서드 단에서 막는다.

 

[ Library ]

public class Library {
    static int bookcount = 100;

    static synchronized void borrow() {
        if (bookcount > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            bookcount -= 1;
            System.out.println("대출 완료");
            System.out.println("남아 있는 책 수량 : " + bookcount);
        } else {
            System.out.println("대출 불가");
        }
    }
}

 

 

[ main ]

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

        for (int i = 0; i < 1000; i++) {
            Thread th = new Thread(new Runnable() {
                @Override
                public void run() {
                    Library.borrow();
                }
            });
            th.start();
        }
        System.out.println("최종 책 수량 " + Library.bookcount);
    }
}

 

잘 관리되는 책 재고 수량 !! 동시성 문제 해결 완료.

 

 

해결방법 2. 스레드 생성 시 join메서드 사용

스레드 생성 시 join() 메서드를 사용해, 현재 일을 처리하는 스레드가 종료될 때까지 다른 스레드의 생성을 막는다.

생성 자체를 막기 때문에 synchronized보다는 덜 좋은 방법이다.

 

[ main ]

public class MainClass {
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            Thread th = new Thread(new Runnable() {
                @Override
                public void run() {
                    Library.borrow();
                }
            });
            th.start();
            
//            동시성 이슈 해결
//            방법 2 - join메서드 사용(스레드 생성 시)
            try {
//                join은 한 스레드가 종료될 때까지 다른 스레드의 생성을 막음.
                th.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("최종 책 수량 " + Library.bookcount);
    }
}

 

잘 관리되는 책 재고 수량 !! 동시성 문제 해결 완료222