자바 스터디 10주차

김성훈·2022년 10월 15일
0

백기선 자바 스터디

목록 보기
10/15
post-thumbnail

자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.


학습할 것 (필수)

Thread 클래스와 Runnable 인터페이스
쓰레드의 상태
쓰레드의 우선순위
Main 쓰레드
동기화
데드락


Thread

어떠한 프로그램내에서 특히 프로세스 내에서 실행되는 흐름의 단위.

  • JVM을 사용하면 애플리케이션이 동시에 실행되는 여러 스레드를 생성할 수 있다.

  • 모든 스레드에는 우선 순위가 있다.

  • 우선 순위가 높은 스레드가 우선 순위가 낮은 스레드보다 우선적으로 실행된다.

  • 실행중에 멈출 수 있으며 동시에 수행이 가는ㅇ하다.

  • 프로세스 내의 명령어 블록으로 시작점과 종료점을 가진다.

Thread class

JDK에서 지원하는 java.lang.Thread 를 제공한다.

생성자

Thread()
  
Thread(String name){ // 스레드 이름
}
  
Thread(Runnable r){ // 인터페이스 객체
  
}

Thread(Runnable r, String name){ // 인터페이스 객체와 스레드 이름
  
}

메소드

Thread thread = new Thread();

thread.sleep(1);  //   msec에 지정된 밀리초 동안 대기
 throws Interrupted Exception 
   
thread.getName(String s); // 스레드의 이름 가져오기
   
thread.setName(String s); // 스레드 이름 설정
   
thread.start(); // 스레드를 시작 run() 메소드 호출   

thread.getPriority(); // 스레드 우선 순위를 반환

thread.setPriority(1); //스레드 우선 순위 설정

thread.isAlive(); // 스레드가 시작되었고 아직 끝나지 않았으면 true 끝났으면 false 반환
 
thread.join(); // 스레드가 끝날 때 까지 대기

thread.run(); // 스레드가 실행할 부분 기술 (오버라이딩)

thread.suspend(); // 스레드 일시정지 resume()에 의해 다시시작 할 수 있음
 
thread.resume(); // 일시 정지된 스레드를 다시 시작

Thread.yield(); // 다른 스레드에게 실행 상태를 양보하고 자신은 준비 상태로 

Thread 생성

스레드를 생성하는 2가지 방법

  1. 직접 상속 받아서 스레드 생성하기
  2. Runnable 인터페이스를 구현해서 생성

Thread클래스 이용

Thread 클래스로 부터 제공되는 run() 메소드를 오버라이딩 해서 사용

ex)
public class ThreadA extends Thread {
    @Override
    public void run() {
        try {
            for (int i = 0; i < 10; i++) {
                // 스레드 0.5초동안 대기
                Thread.sleep(5000);
                System.out.println("Thread : " + i);
            }
        } catch (InterruptedException e) {
            System.out.println(e);
        }
    }

  public static void main(String[] args) {

        ThreadA threadA = new ThreadA();
        ThreadA threadA1 = new ThreadA();

        threadA.run();
        threadA1.run();
        
        threadA.start();
        threadA1.start();
    }
}

//run                    //start
Thread : 0             Thread : 0
Thread : 1             Thread : 0 
Thread : 2             Thread : 1
Thread : 3             Thread : 1
Thread : 4             Thread : 2
Thread : 5             Thread : 2
Thread : 6             Thread : 3
Thread : 7             Thread : 3
Thread : 8             Thread : 4
Thread : 9             Thread : 4
Thread : 0             Thread : 5
Thread : 1             Thread : 5
Thread : 2             Thread : 6
Thread : 3             Thread : 6
Thread : 4             Thread : 7
Thread : 5             Thread : 7
Thread : 6             Thread : 8 
Thread : 7             Thread : 8
Thread : 8             Thread : 9
Thread : 9             Thread : 9
  
  

만약에 현재의 클래스가 이미 다른 클래스로부터 상속 받고 있다면 Runnable 인터페이스를 이용하여 스레드를 생성 할 수 있다.

Runnable 인터페이스는 JDK 라이브러리 인터페이스 이고 run() 메소드만 정의되어 있다.

public class RunnableTest implements Runnable
{
    public static void main(String[] args) {
        {
            try    // 인터럽트 예외처리
            {
                for (int i=0 ; i<10 ; i++)
                {
                    // 대기시간 0.2초
                    Thread.sleep(200);
                    System.out.println("스레드 :" + i);
                }

            }catch(InterruptedException e )
            {
                e.printStackTrace();
            }

        }
    }

    @Override
    public void run() {

    }
    // Runnable 인터페이스의 run()오버라이딩

}

// 
스레드 :0
스레드 :1
스레드 :2
스레드 :3
스레드 :4
스레드 :5
스레드 :6
스레드 :7
스레드 :8
스레드 :9
public class Thread2 {
    public static void main(String[] args) {
        RunnableTest runnableTest = new RunnableTest();
        RunnableTest runnableTest1 = new RunnableTest();

        Thread thread = new Thread(runnableTest);
        Thread thread1 = new Thread(runnableTest1);
        thread.start();
        thread1.start();
    }
}

// 
스레드 :0
스레드 :0
스레드 :1
스레드 :1
스레드 :2
스레드 :2
스레드 :3
스레드 :3
스레드 :4
스레드 :4
스레드 :5
스레드 :5
스레드 :6
스레드 :6
스레드 :7
스레드 :7
스레드 :8
스레드 :8
스레드 :9
스레드 :9

run() 메소드가 종료하면 스레드는 종료된다.

  • 스레드를 계속 실행시키고 싶으면 run() 메소드를 무한루프 속에서 실행되어야 한다.

한번 종료한 스레드는 다시 시작시킬 수 없다.

  • 스레드 객체를 다시 생성해야 한다.

한 스레드에서 다른 스레드를 강제 종료할 수 있다

Thread의 상태 6가지

  1. NEW
  • 스레드가 생성되었지만 스레드가 아직 실행할 준비가 되지 않음.
  1. RUNNABLE
  • 스레드가 실행되고 있거나 실행준비되어 스케쥴링울 기다리는 상태
  1. WAITING
  • 다른 스레드가 notify(), notifyAll() 을 불러주기를 기다리고 있는 상태(동기화)
  1. TIMED_WATING
  • 스레드가 sleep(n) 호출로 인해 n 밀리초동안 잠을 잠고 있는 상태
  1. BLOCK
  • 스레드가 I/O 작업을 요청하면 자동으로 스레드를 BLOCK 상태로 만든다.
  1. TERMINATED
  • 스레드가 종료된 상태

스레드 상태는 JVM에 의해 기록되고 관리된다.

img

Thread 우선순위

멀티 스레드 프로그래밍에서 순위를 정하는 것을 스레드 스케줄링 이라고 한다.

스레드 스케줄링 방법에는 우선순위(Priority) 스케줄링과 라운드 로빈(Round-Robin) 이 있다.

우선순위 스케줄링

  • 스레드의 우선순위가 높은 순으로 실행 상태를 더 많이 차지하는 스케줄링 방식으로 개발자가 setPriority() 메소드를 사용하여 우선순위를 설정할 수 있다.

    라운드 로빈

  • Time Slice를 정해서 그 시간만큼만 스레드가 실행되도록 하고 이후에는 다른 스레드가 실행되는 스케줄링 방식으로 JVM에 의해 결정되기 때문에 개발자가 임의로 수정이 불가능 하다.

우선 순위를 지정하기 위한 상수

  • static final int MAX_PRIORITY : 우선순위 10 - 가장 높은 순위

  • static final int MIN_PRIORITY : 우선순위 1 - 가장 낮은 우선 순위

  • static final int NORM_PRIORITY : 우선순위 5 - 보통의 우선 순위

main() 스레드의 우선 순위 값은 5이다.

public class PriorityTest extends Thread{
    PriorityTest(String name) {
        super(name);

    }
    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            try {
                Thread.sleep(1000);

                System.out.println(getName() + i + "번째");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
    }
    public static void main(String[] args) {

        PriorityTest priorityTest = new PriorityTest("우선 순위가 낮은 스레드");
        PriorityTest priorityTest1 = new PriorityTest("우선 순위가 높은 스레드");
        priorityTest.setPriority(Thread.MAX_PRIORITY);
        priorityTest1.setPriority(Thread.MIN_PRIORITY);

        priorityTest.run();
        priorityTest1.run();

    }
}


// 
우선 순위가 낮은 스레드0번째
우선 순위가 낮은 스레드1번째
우선 순위가 낮은 스레드2번째
우선 순위가 낮은 스레드3번째
우선 순위가 낮은 스레드4번째
우선 순위가 낮은 스레드5번째
우선 순위가 낮은 스레드6번째
우선 순위가 낮은 스레드7번째
우선 순위가 낮은 스레드8번째
우선 순위가 낮은 스레드9번째
우선 순위가 높은 스레드0번째
우선 순위가 높은 스레드1번째
우선 순위가 높은 스레드2번째
우선 순위가 높은 스레드3번째
우선 순위가 높은 스레드4번째
우선 순위가 높은 스레드5번째
우선 순위가 높은 스레드6번째
우선 순위가 높은 스레드7번째
우선 순위가 높은 스레드8번째
우선 순위가 높은 스레드9번째

Main Thread

    public static void main(String[] args) {
          System.out.println("내배캠 포에버"); 
    }

모든 자바 어플리케이션에서 메인 스레드는 다음과 같은 main 메소드를 통해서 실행하게 된다.

메인 메소드가 실행되면 코드 한줄 한줄 순차적으로 시작하게 되고, return 을 만나거나, main 메소드의 끝이오면 종료하게 된다.

    public static void main(String[] args) {
        System.out.println("내배캠 포에버");
        for (int i = 0; i <10 ; i++) {
            System.out.println("우리의 꿈" + i);
        }
        return;
    }

이런 main 메소드만 존재하는 상황을 싱글 스레드 어플리케이션 이라고 한다.

메인 스레드가 종료되면 프로세스 자체도 종료된다, 정확히는 프로세스 내에 스레드가 먼저 다 종료되어야 메인 스레드도 종료된다.

이런 메인 스레드 구조에서 개발자들은 작업 스레드를 여러개 생상하여, 멀티 스레드를 구성할 수 있다.

image

다음과 같이 main 스레드가 끝나고도 Thread2 가 끝나야 프로세스가 종료된다, 하지만 데몬 스레드는 예외

Daemon Thread

데몬 스레드는 main 스레드를 보조하는 친구를 말한다.

보조를 하는 역할이기 때문에, 메인 스레드가 종료되면 데몬 스레드도 강제적으로 종료된다.

예를 들어 우리가 개발하면서 크롬을 사용하는데 여기서 , 유튜브, 웹툰 , 문서 같은것을 볼 수 있다

여기서 크롬은 메인 메서드 라고 볼 수 있고, 유튜브, 웹툰 같은 것들을 데몬 스레드 라고 볼 수 있다.

  public static void main(String[] args) {



        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 10; i++){
                    System.out.println("유튜브 영상 실행중" + Thread.currentThread().getName());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.setDaemon(true); // 데몬 쓰레드 설정
        thread.setDaemon(false); // 데몬 쓰레드 설정
        thread.start();
        System.out.println("메인 메소드 종료");
    }
    
    
    메인 메소드 종료
    
메인 메소드 종료
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
    
    
    

Thread start,run 차이

     public static void main(String[] args) {

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 10; i++){
                    System.out.println("유튜브 영상 실행중" + Thread.currentThread().getName());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
//        thread.setDaemon(true); // 데몬 쓰레드 설정
        thread.start();
        thread.run();
        System.out.println("메인 메소드 종료");
    }
    start
    
메인 메소드 종료
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0
유튜브 영상 실행중Thread-0


  run 
  
유튜브 영상 실행중main
유튜브 영상 실행중main
유튜브 영상 실행중main
유튜브 영상 실행중main
유튜브 영상 실행중main
유튜브 영상 실행중main
유튜브 영상 실행중main
유튜브 영상 실행중main
유튜브 영상 실행중main
유튜브 영상 실행중main
메인 메소드 종료
    

start 메서드는 출력결과로 Thread인스턴스 1개가 출력된 것을 볼 수 있다.

runt 메서드는 결과가 다르게 출력 되었다 결과가 멀티쓰레드가 아니라 싱글쓰레드로 동작된 걸 알 수 있다.

start() 메서드는 Thread를 새로 생성하여 start하고 start 하게 되면 run() 메소드가 실행된다.

run() 메소드는 단순히 Runnable 타입의 멤버변수 target의 run()메서드를 호출하기 때문에, Thread도 새로 생성하지 않고 싱글쓰레드에서 동작된다.

startrun
native 영역에서 새로운 Thread가 생성되며 Thread가 시작되면 run() 메서드가 실행된다.Thread가 생성되지 않으며 그냥 run() 메서드만 실행된다.
동일한 객체에서 두번이상 호출 시 IllegalThreadStateException 예외가 발생된다.호출수에 제한없이 계속 호출할 수 있다.
멀티쓰레드로 동작한다.싱글쓰레드로 동작한다.

스레드 동기화

싱글 스레드, main 스레드 한개로 구성된 어플리케이션에서는 문제가 되진 않지만, 만약에 멀티 스레드 상황에서 객체를 생성 한다면, 여러개의 스레드가 객체를 건들 수 있는 상황이 생길수도 있다.

스레드 A를 사용하던 객체가 만약에 스레드 B에 의해 객체의 값이 변경 될 수 있다는 뜻이다.

이런 상황에서 지금 사용중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드의 작업이 끝날때까지 객체에 잠금을 걸어서 다른 스레드가 사용할 수 없게 해야한다.

임계영역: 멀티 스레드에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계영역 이라고 한다.

자바는 이러한 임계영역을 지정하기 위해서 동기화 메서드와 동기화 블록을 제공한다.
동기화 메소드를 만드는 방법은 메소드 선언에 synchronized 키워드를 붙이면 된다.



public class App {

    public static void main(String[] args) {
        System.out.println("게임시작");
        SungHoon sungHoon = new SungHoon();
        God1 god1 = new God1();
        god1.setSungHoon(sungHoon);
        god1.start();


        God2 god2 = new God2();
        god2.setSungHoon(sungHoon);
        god2.start();

    }

    public static class SungHoon extends Thread{
        String job = "";

        public synchronized void setJob(String job){
            this.job = job;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            System.out.println(Thread.currentThread().getName() + "현재 직업 : " + this.job);
        }

        public String getJob(){
            return job;
        }
    }

    public static class God1 extends Thread{
        private SungHoon sungHoon;

        public void setSungHoon(SungHoon sungHoon){
            this.setName("God1");
            this.sungHoon = sungHoon;
        }

        @Override
        public void run() {
            sungHoon.setJob("검사");
        }
    }

    public static class God2 extends Thread{
        private SungHoon sungHoon;

        public void setSungHoon(SungHoon sungHoon){
            this.setName("God2");
            this.sungHoon = sungHoon;
        }

        @Override
        public void run() {
            sungHoon.setJob("농부");
        }
    }

}

게임시작
God1현재 직업 : 농부
God2현재 직업 : 농부


게임시작
God1현재 직업 : 검사
God2현재 직업 : 농부

데드락

멀티 쓰레드 프로그래밍에서 동기화를 통해 락을 획득하여 동일한 자원을 여러 곳에서 함부로 사용하지 못하도록 하였다.

하지만 두 개의 쓰레드에서 서로가 가지고 있는 락이 해제되기를 기다리는 상태가 생길 수 있으며 이러한 상태를 교착상태(deadlock) 이라고 한다.

교착상태가 되면 어떤 작업도 실행되지 못하고 서로 상대방의 작업이 끝나기만을 바라는 무한정 대기 상태이다.

데드락 발생 조건

상호 배제 (Mutual Exclusion) : 한 자원에 대해 여러 쓰레드 동시 접근 불가

점유와 대기 (Hold and Wait) : 자원을 가지고 있는 상태에서 다른 쓰레드가 사용하고 있는 자원 반납을 기다리는 것

비선점 (Non Preemptive) : 다른 쓰레드의 자원을 실행 중간에 강제로 가져올 수 없음

환형대기 (Circle Wait) : 각 쓰레드가 순환적으로 다음 쓰레드가 요구하는 자원을 가지고 있는 것

위 4가지 조건을 모두 충족해야지 데드락이 발생한다.

반대로 보면 위 4가지 중 하나라도 충족하지 않으면 데드락을 해결할 수 있다는 말이다.


public class DeadLock {
    public static Object object1 = new Object();
    public static Object object2 = new Object();

    public static void main(String[] args) {

        FirstThread thread1 = new FirstThread();
        SecondThread thread2 = new SecondThread();

        thread1.start();
        thread2.start();

    }

    private static class FirstThread extends Thread{
        @Override
        public void run() {
            synchronized (object1){
                System.out.println("First Thread has object1's lock");

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("First Thread want to have object2's lock. so wait");

                synchronized (object2){
                    System.out.println("First Thread has object2's lock too");
                }
            }
        }
    }

    private static class SecondThread extends Thread{
        @Override
        public void run() {
            synchronized (object2){
                System.out.println("Second Thread has object2's lock");

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Second Thread want to have object1's lock, so wait");

                synchronized (object1){
                    System.out.println("Second Thread has object1's lock too");
                }
            }
        }
    }
}

First Thread has object1's lock
Second Thread has object2's lock
First Thread want to have object2's lock. so wait
Second Thread want to have object1's lock, so wait

상호 배제 (Mutual Exclusion) : object1과 object2 객체에 대해서 동시에 쓰레드가 사용할 수 없도록 하였다.

점유와 대기 (Hold and Wait) : 첫번째 스레드는 object1에 대한 락을 가지고 있으면서 object2에 대한 락을 원하고, 두번째 스레드는 object2에 대한 락을 가지고 있으면서 object1의 락을 획득하기를 원한다.

비선점 (Non Preemptive) : 스레드의 우선순위의 기본값은 NORM_PRIORITY로 동일하기 설정되어 있다.

환형대기 (Circle Wait) : 첫번째 스레드는 두번째 스레드의 object2 객체의 락을 대기하고 두번째 스레드는 첫번째 스레드의 object1 객체의 락을 대기하고 있다.

실행하면 데드락이 발생해 아무런 실행도 하지 못하고 있다.


public class DeadLock {
    public static Object object1 = new Object();
    public static Object object2 = new Object();

    public static void main(String[] args) {

        FirstThread thread1 = new FirstThread();
        SecondThread thread2 = new SecondThread();

        thread1.start();
        thread2.start();

    }

    private static class FirstThread extends Thread{
        @Override
        public void run() {
            synchronized (object1){
                System.out.println("First Thread has object1's lock");

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("First Thread want to have object2's lock. so wait");

                synchronized (object2){
                    System.out.println("First Thread has object2's lock too");
                }
            }
        }
    }

    private static class SecondThread extends Thread{
        @Override
        public void run() {
            synchronized (object1){
                System.out.println("Second Thread has object2's lock");

                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Second Thread want to have object1's lock, so wait");

                synchronized (object2){
                    System.out.println("Second Thread has object1's lock too");
                }
            }
        }
    }
}
First Thread has object1's lock
First Thread want to have object2's lock. so wait
First Thread has object2's lock too
Second Thread has object2's lock
Second Thread want to have object1's lock, so wait
Second Thread has object1's lock too

아까 말했듯이 데드락은 4개의 조건 중 하나라도 충족되지 않게 하면 해결할 수 있다. 위에 조건은 환경대기 조건을 만족하지 않기 때문에 데드락이 발생하지 않는다.

<출처>

https://catch-me-java.tistory.com/47
https://math-coding.tistory.com/173

profile
"한 명이 걷는 천 걸음 보다 천 명이 함께 걷는 한 걸음이 성공의 시작이고 완성이다"

0개의 댓글