JAVA thread

자바의 스레드란?

  • 프로세스(공장)
    • 동작하고 있는 프로그램을 프로세스
    • 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것
  • 스레드(일꾼)
    • 실제로 작업을 수행하는 주체로 프로세스내에 스레드가 존재
      • 별도의 레지스터와 별도의 스택 을 갖고, 힙 영역은 공유함
    • 어떻게 사용할까
  • 싱글 스레드
  • 멀티 스레드
    • 장점
      • 자원을 효율적으로 사용
      • 응답성이 향상
        • 오래걸리는 작업을 다른 스레드가 작업하면서 기다릴 필요가 없어진다.
    • 단점
      • 동기화, 교착상태(기아, dead-lock), 쓰레드를 관리해야한다.
      • 불확실성이 존재
      • os에 종속적인 스케줄러

스레드가 작동하는 방식은?

  • 크게 두가지가 존재
    • Concurrent(동시성)
      • 논리적으로 여러작업을 처리하는것
        • 이거했다가 저거했다가
      • 실제로 물리적으로 동시적 실행은 안함
    • Parallel(병렬성)
      • 물리적으로 스레드가 병렬적으로 수행하는것
  • JAVA는 concurrent하게 작동한다.

구현방법 1 runnable 인터페이스

class Thread1 implements Runnable{
  @Override
  public void run() {
    //수행될 내용 입력
    for (int i = 0; i < 100; i++) {
      System.out.println("나는2");
    }
  }
}
//스레드1
Runnable r = new Thread1();//runnable 객체로 받는다.
//객체의 다형성으로 Thread1 r = new Thread1();도 작동된다.
Thread t1 = new Thread(r);//Thread를 생성함면서 생성자 매개변수로 runnable 객체를 넘겨준다.
//한줄로 표현
  //Thread t1 = new Thread(new Thread1());

t1.start();//스레드 실행

///람다로 바로 실행하는 방법

new Thread(new Runnable() {
      @Override
      public void run() {
        //스레드로 실행할 것
        for (int i = 0; i < 150; i++) {
          System.out.print("1");
        }
      }
    }).start();


//lambda형태
new Thread(()->System.out.println("시작")).start();

구현방법 2 Thread 클래스 상속

class Thread2 extends Thread {
  @Override
  public void run() {
     //수행될 내용 입력
    for (int i = 0; i < 100; i++) {
      System.out.println("나는1");
    }
  }
}
Thread2 t2 = new Thread2();
t2.start();
  • 인터페이스를 이용한 방식이 좀더 낫다고 한다.
    • java는 단일 상속이고, 인터페이슨 다중 구현이 되기 때문에 좀더 유연하게 구현이 된다고 한다.
  • 둘다 run이라는 함수를 작성해야 한다는 점은 같다.

자바의 콜 스택(스택의 방식)

Untitled

  • start()가 수행하면서 스레드가 만들어지고, 새로운 스택을 만든다.
  • 스레드에서 수행하고 싶은 run()을 스레드 스택에 추가된다.
    • main의 start는 할일을 다해서 종료된다.
  • 이후 main함수와 run함수가 서로 번갈아 가면서 수행된다.

어떻게 수행되는지 확인해보기1

//스레드1
Thread1 t1 = new Thread1();

//스레드2
Runnable r = new Thread2();
Thread t2 = new Thread(r);

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

t1.start(); //스레드 1시작

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

t2.start();//스레드 2시작

for (int i = 0; i < 20; i++) {
  System.out.println("main3");
}
  • 작동 되는 그림 확인

Untitled

  • 멀티 스레드가 끝나는 시간을 보면 싱글스레드보다 오래 걸리는 것 을 확인이 가능하다.

    • context switching이 있기 때문에
  • 결과확인(실행할때마다 약간 다름)
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main1
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main2
    main3
    나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1main3
    main3
    main3
    main3
    나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는1나는1나는1나는1나는1나는1나는1나는1main3
    main3
    main3
    main3
    main3
    main3
    main3
    main3
    main3
    main3
    main3
    main3
    main3
    main3
    main3
    나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는2나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1나는1
    
  • 실행 순서는 OS의 스케쥴러에 따라 다르다고 한다.
    • 스레드는 os에 종속적이다. java가 os에 종속적이지 않도록 jvm위에서 독립적으로 작동한다고 하지만, 스레드는 os에 종속되어 작동되는 것이라고 한다.
  • 메인이 전부 실행했다고 하더라도, 스레드가 남아있다면 종료되지 않는다.

스레드로 입력 응용

public class Thread3 extends Thread {
  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      System.out.print("_");
      try {sleep(1000);} catch(Exception e) {}
      //1초마다 '_' 출력
    }
  }
}

//-------------------------------------------

//스레드시작
Thread3 t3 = new Thread3();
t3.start();

//스레드 없이 아래 코드를 실행한다면 강제로 10초 정지됨
//for (int i = 0; i < 10; i++) {
//		System.out.print("_");
//		try {Thread.sleep(1000);} catch(Exception e) {}
//}

Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
System.out.println("입력하신 값은"+n);
  • 위의 방식을 통해 중간에 입력도 가능하다.
    • 싱글 스레드에서 입력을 받는것을 대기하지만, 스레드를 이용해 동시에 어떤 작업이 가능해 진다.

스레드의 메소드

  • 스레드 우선순위
    • t1.setPriority(n);
    • t1.getPriority();
    • 1~10으로 지정 가능하며(JVM기준), 숫자가 높을수록 우선순위가 높다.
      • 기본은 5
    • 절대적이지 않고 희망사항으로 os스케줄러에게 요청한다.
      • 실제 java의 스레드는 os스케줄러에 종속적이기 때문에
  • 스레드 그룹
    • 스레드를 그룹으로 관리
    • 따로 지정해주지 않으면 main스레드 그룹에 속함
    • 자신을 생성한 스레드의 그룹과 우선순위를 상속받는다.
    • Thread(ThreadGroup,Runnable,..)로 스레드 생성시 생성자로 지정 가능 unnamed(1).png
    • 그룹단위로 실행한 갯수, 실행중인 갯수 등을 통해 비슷한 스레드들을 관리하는데 사용한다.

데몬 스레드

  • 일반 스레드의 작업을 돕는 보조적인 역할
  • 일반 스레드가 종료되면 자동적으로 종료
    • gc, 자동저장, 화면 자동갱신
  • 무한 루프를 이용해 대기하면서 특정조건이 있으면 수행
    • 무한루프? → 데몬 스레드는 일반스레드가 종료되면 자동적으로 종료되기 때문에 괜찮다고한다.
public void run(){
  while(true){
    //데몬 스레드가 하고 싶은일
  }
}
  • 메소드
    • t1.isDaemon(); 데몬스레드 여부
    • t1.setDaemon(데몬스레드로 지정true , false );
      • true면 데몬스레드로 변경
      • false면 사용자 스레드로 변경
      • start()전에 데몬으로 할지 정해야 한다.
public class threadTEST_daemon implements Runnable{

  static boolean is4sec = false;
  public static void main(String[] args) {
    Thread t = new Thread(new threadTEST_daemon());
    t.setDaemon(true); // 데몬으로 지정
    t.start();

    for (int i = 0; i < 10; i++) {
      try {Thread.sleep(1000);} catch(Exception e) {}
      System.out.print(i);
      if(i==4) {
        is4sec = true;//특정 flag를 true로 지정
      }
    }

    System.out.println("끝!");
  }

// is4sec이 true가 되면 go4sec가 수행하는 데몬 스레드
  @Override
  public void run() {
    while(true) {
      //너무 빠르면 제대로 수행이 안되는것 같다.
      try {Thread.sleep(500);} catch(Exception e) {}

      if(is4sec) { // 특정 flag로 boolean을 이용해 만들어서 수행이 되도록 작성
        System.out.println("데몬이야");
        go4sec();//특정 함수 실행
        is4sec = false;
      }
    }
  }

  public void go4sec() {
    System.out.println("와 4초야!");
  }


}

스레드의 상태

  • 생성
    • new
  • 실행중/실행가능
    • runnable
  • 일시정지
    • block
      • 동기화블럭에 의해 일시정지
    • waiting
  • 작업종료
    • terminated

Untitled

  • 생성되고, 실행대기에서부터 순서대로 실행되면서 스레드가 실행된다.
  • 스레드가 할일을 다하거나, stop()을 만나면 소멸이 된다.
  • 잠시 일시정지로 넘어가서 줄서기를 안하고 일시정지 상태로 가기도 한다.
    • suspend, sleep, wait, join
  • 일시 정지 상태에서 다시 줄서기로 복귀
    • time-out, resume, notify, interrupt

실행 제어 메소드

  • suspend()
    • 일시정지
    • 다시 복귀는 resume()
  • wait()
    • 일시정지
    • 다시 복귀는 notify()
  • join()
    • 다른 스레드 기다리기
  • sleep()
    • static - 자기 자신에게만 호출 가능
    • 특정 시간 일시 정지
  • yield()
    • static - 자기 자신에게만 호출 가능
    • 양보
    • 자신에게 주어진 시간을 다른 스레드에게 양보, 자신은 일시정지
  • interrupt()
    • join, sleep로 일시정지인 스레드를 깨우기

sleep()

  • 잠시 일시정지
  • 시간이 다되거나, interrupted가 오면 깬다.
    • time up
    • interrupted
  • 특정 스레드를 지정하는게 X, 현재 자신 스레드
    try{
      Thread.sleep();
    }catch(InterruptedException e){}
    

interrupt()

  • 대기상태인 스레드를 실행대기 runnable로 만든다.
  • sleep(), join(), wait()
    • interrupt() - 인터럽트 상태를 true로
    • isInterrupted() - 인터럽트 상태만 확인
    • interrupted() - 상태를 알려주고, false로 초기화
      • static으로 현재 자기자신 스레드만 지정가능
      • th1.interrupted와 같이 사용은 X
    • true는 중간에 인터럽트로 깨웠다는 상태라고 한다.
class Thread_4 extends Threads{
public void run(){
  ...

  //다운로드가 끝나거나, 중간에 인터럽트가 되었을때 반복문에서 나온다
  while(download &&!interrupted()){
  }
  ..
  System.out.println("다운로드끝");
}
}

...
Thread_4 t4 = new Thread_4();
t4.start();

t4.interrupt(); //이걸 실행하는 순간 run내부의 interrupted()가 true가 된다.

join()

  • 지정된 시간동안 특정 쓰레가 작업하는것을 기다리기
...

try{
  th1.join();
  th2.join();
}catch(InterruptedException e){} // th1과 th2가 끝 or interrupted가 발생하면 빠져나온다.

'''
  • 예 - gc가 실행하는 예제
class ThreadEx20 {
  public static void main(String args[]) {
    ThreadEx20_1 gc = new ThreadEx20_1();
    gc.setDaemon(true);
    gc.start();

    int requiredMemory = 0;

    for(int i=0; i < 20; i++) {
      requiredMemory = (int)(Math.random() * 10) * 20;

      // 필요한 메모리가 사용할 수 있는 양보다 크거나 전체 메모리의 60%이상을
            // 사용했을 경우 gc를 깨운다.
      if(gc.freeMemory() < requiredMemory || gc.freeMemory() < gc.totalMemory() * 0.4) {
        gc.interrupt();	// 잠자고 있는 쓰레드 t1을 깨운다.

        //gc를 깨우고 잠시 일하는 시간을 얻어야 한다. 이런식의 약간의 딜레이가 필요할때 join을 사용
        try{
            gc.join(100);//gc가 실행되는 동안 기다려주기 0.1초 정도 대기
        } catch(interruptedException e){}
      }

      gc.usedMemory += requiredMemory;
      System.out.println("usedMemory:"+gc.usedMemory);
    }
  }
}

...
  public void run() {
    while(true) {
      try {
        Thread.sleep(10 * 1000);	// 10초를 기다린다.
      } catch(InterruptedException e) {
        System.out.println("Awaken by interrupt().");
      }

      gc();	// garbage collection을 수행한다.
      System.out.println("Garbage Collected. Free Memory :" + freeMemory());
    }
  }

...

yield()

  • 양보
  • 남은 시간을 다음 쓰레드에게 양보, 자신은 실행 대기
    • static으로 자기자신에게만 사용가능
  • busy-waiting
    • 아직stop은 안되어있지만, suspended가 되었으면 단순히 주어진 시간동안 계속 대기된다.
    • suspended가 되었을대 조건을 추가하여 Thread.yield()를 통해 현재 실행중인 스레드를 넘기도 다음 스레드가 실행되도록 해준다.
  • yield()와 interrupt()를 적절히 사용하면, 응답성과 효율을 높일 수 있다.
  • os스케줄러에게 통보를 하는것으로 실제로 작동한다는 보장은 없다.

deprecated 메소드

  • 아래의 메소드는 교착상태를 발생 시킬 수 있다고 한다. Untitled

    suspend()

    • 실행을 일시정지

    resume()

    • 재개
    • suspend()에 의해 일시정지된 스레드를 실행대기(runnable)로

    stop()

    • 완전 정지
    //duplicate되었으니 유사하게 boolean을 이용해 만드는것이 가능하다.
    class MyThread implements Runnable {
      volatile boolean suspended = false; //volatile
      volatile boolean stopped = false;
    
      Thread th;
      public MyThread(String name) {
        th = new Thread(this, name);
      }
    
      void start() {
        th.start();
      }
    
      void stop() {
        stopped = true;
      }
      void suspend() {
        suspended = true;
      }
    
      void resume() {
        suspended = false;
      }
    
    
    
      public void run() {
        while(!stopped) {
          if(!suspended) {
            System.out.println(Thread.currentThread().getName());
            try {
              Thread.sleep(1000);
            } catch(InterruptedException e) {}
          }
        }
      } // run()
    }
    
    • volatile 는 복사본이 아닌 직접 확인하도록 하는것이라고 한다.
      • cpu에서 자주 바뀌는 변수를 복사해서 확인하다 보면 문제가 발생한다.
      • 자주 바뀌기 때문에 원본을 확인하는 그런 표시라고 한다.
      • 좀더 확인이 필요…

스레드의 동기화

  • 멀티 스레드는 다른 스레드의 작업에 영향을 미칠 수 있다.
  • 동기화
    • 다른 스레드가 간섭하지 못하게 막는것
  • 임계영역
    • 간섭받지 않아야 하는 영역
    • 한번에 한개의 스레드만 접근
    • 임계영역에 들어가는 키

synchronized

  • 임계 영역으로 지정
    • 많으면 성능이 떨어진다. 최소한으로 있는게 좋다고 한다.
  • 메서드 전체, 특정한 영역 지정이 가능
public synchronized void 임계영역함수(){

}

synchronized(객체 참조 변수){

}

wait(), notify()

  • 동기화의 효율을 높이기 위해
    • synchronized는 한번의 하나의 스레드만 가능
    • 많을수록 성능이 안좋았음
  • wait() - waiting pool에 넣는다. - 기다리기
  • notify() - waiting pool에서 하나의 스레드를 깨운다. - 통보
  • notifyAll() - waiting pool에서 대기중인 모든 스레드를 깨운다.
    • 공평하게 전부 깨워서 어차피 하나의 스레드만 들어가기 때문에 못들어가면 다시 waiting이 된다.
    • 이게 좋다고 한다.

후기

  • 양이 너무 많다보니 강의를 정리하기만 하는것으로 마무리가 되었다.

추가 내용

  • 일시정지 상태에서의 죽은스레드의 경위?
  • 단어
    • 스레드pool
    • busy-waiting
    • context-switching
  • os에 종속적인 스케줄러
    • 스레드의 한계, 갯수는?
    • os가 스케줄러를 관리를 안한다는 의견
    • 유저레벨, 커널레벨
  • start()
  • JAVA는 concurrent하게 작동
  • volatile
    • (c에서 사용)

참고

확인해볼곳