모르지 않다는 것은 아는것과 다르다.

Java

Thread

채마스 2022. 2. 26. 00:16

Thread란?

  • 하드디스크에 있던 프로그램이 메모리에 적재되면 그것을 process라고 한다.
  • 그 process는 메모리를 점유한 상태고 실제로 실행되려면 CPU 를 점유해야한다.
  • 여기서 CPU를 점유하는 단위를 쓰레드라고 한다.
  • 하나의 프로그램이 돌아간다면 적어도 하나 이상의 쓰레드가 존재한다는 것을 말한다.

Muti-threading

  • 여러 thread가 동시에 수행되는 프로그래밍, 여러 작업이 동시에 실행되는 효과.
  • thread는 각각 자신만의 작업 공간을 가짐. ( context )
  • 각 thread 사이에서 공유하는 자원이 있을 수 있음. (자바에서는 static instance)
  • 여러 thread가 자원을 공유하여 작업이 수행되는 경우 서로 자원을 차지하려는 race condition이 발생할 수 있음.
  • 이렇게 여러 thread가 공유하는 자원중 경쟁이 발생하는 부분을 critical section 이라고 함.
  • critical section에 대한 동기화( 일종의 순차적 수행)를 구현하지 않으면 오류가 발생할 수 있음.

Thread 구현

- Thread 를 상속하여 구현

class MyThread extends Thread{

    public void run() {

        int i;
        for(i = 0; i<200; i++) {
            System.out.print(i + "\t");
        }
    }
}

public class ThreadTest {

    public static void main(String[] args) {

        System.out.println(Thread.currentThread());
        MyThread th1 = new MyThread();
        th1.start();

        MyThread th2 = new MyThread();
        th2.start();
    }

}
  • 자바는 다중 상속이 허용되지 않으므로 이미 다른 클래스를 상속한 경우 thread를 만들기 위해 Runnable interface를 구현한다.
  • 구햔은 아래와 같다.
class MyThread2 implements Runnable{

    public void run(){

        int i;
        for(i=0; i<200; i++){

            System.out.print(i + "\t");


        }
    }
}


public class ThreadTest2 {

    public static void main(String[] args) {

        System.out.println("main start");

        MyThread2 mth = new MyThread2();
        Thread th1 = new Thread(mth);
        th1.start();

        Thread th2 = new Thread(mth);
        th2.start();

        System.out.println("main end");
    }

}

Thread 상태 변화

  • Runnable -> Not Runnable : sleep(시간), wait(), join()
  • Not Runnable -> Runable : 시간지남, notify(), other thread exits
  • 리소스가 어배일러블 한 상태가 되면 notify()써서 thread를 깨운다.
  • 어떤 쓰레드가 진행하려면 다른 쓰레드의 결과가 필요하다면, 필요한 쓰레드에서 join() 한다.
  • join()을 건 Thread가 Not Runnable 상태가 되고, 결과가 필요한 thread가 exits가 되면 Runnable 상태가 된다.

Thread 우선순위

  • Thread.MIN_PRIORITY(=1) ~ Thread.MAX_PRIORITY(=10)
  • 디폴트 우선순위 : Thread.NORMAL_PRIORITY(=5)
  • 우선 순위가 높은 Thread가 CPU의 배분을 받을 확률이 높다
  • setPriority()/getPriority()
  • 예시는 아래와 같다.
class PriorityThread extends Thread{

    public void run(){

        int sum = 0;

        Thread t = Thread.currentThread();
        System.out.println( t + "start");

        for(int i =0; i<=1000000; i++){

            sum += i;
        }

        System.out.println( t.getPriority() + "end");
    }
}


public class PriorityTest {

    public static void main(String[] args) {

        PriorityThread pt1 = new PriorityThread();
        PriorityThread pt2 = new PriorityThread();
        PriorityThread pt3 = new PriorityThread();

        pt1.setPriorty(Thread.MIN_PRIORITY);
        pt1.setPriorty(Thread.NONE_PRIORITY);
        pt1.setPriorty(Thread.MAX_PRIORITY);

        pt1.start();
        pt2.start();
        pt3.start();
    }

}
  • 위에 결과는 pt1을 먼저 시작했지만 우선순위가 높은 pt3부터 시작된 것을 확인할 수 있었다.

Join()

  • 동시에 두 개 이상의 Thread가 실행 될 때 다른 Thread의 결과를 참조 하여 실행해야 하는 경우 join() 함수를 사용
  • join() 함수를 호출한 Thread가 not-runnable 상태가 됨 (Thread1)
  • 다른 Thread의 수행이 끝나면 runnable 상태로 돌아옴
public class JoinTest extends Thread{

    int start;
    int end;
    int total;

    public JoinTest(int start, int end){
        this.start = start;
        this.end = end;
    }

    public void run(){

        int i;
        for(i = start; i <= end; i++){
            total += i;
        }
    }


    public static void main(String[] args) {

        JoinTest jt1 = new JoinTest(1, 50);
        JoinTest jt2 = new JoinTest(51, 100);


        jt1.start();
        jt2.start();

        try{
            jt1.join();
            jt2.join();

        }catch (InterruptedException e) {
            System.out.println(e);
        }


        int lastTotal = jt1.total + jt2.total;

        System.out.println("jt1.total = " + jt1.total);
        System.out.println("jt2.total = " + jt2.total);

        System.out.println("lastTotal = " + lastTotal);

    }

}
  • join()을 걸지 않으면 lastTotal 값이 0이 나온다. -> 그 이유는 lastTotal에 값이 대입되는 순간에는 아직 thread들이 작업중이기 때문에 0을 뱉어냈기 때문이다.
  • join()을 사용하게 되면 5050이 나온다.

interrupt()

  • 다른 Thread에 예외를 발생시키는 interrupt를 보낸다.
  • Thread가 join(), sleep(), wait() 함수에의해 not-runnable 상태일 때 interrupt() 메서드를 호출하면 다시 runnable 상태가 될 수 있음

Thread 종료하기

  • Thread를 종료할 때 사용함
  • 무한 반복의 경우 while(flag)의 flag 변수값을 false로 바꾸어 종료를 시킴
  • Thread 종료하기 예제

public class TerminateThread extends Thread{

    private boolean flag = false;
    int i;

    public TerminateThread(String name){
        super(name);
    }

    public void run(){


        while(!flag){
            try {
                sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        System.out.println( getName() + " end" );

    }

    public void setFlag(boolean flag){
        this.flag = flag;
    }


    public static void main(String[] args) throws IOException {

        TerminateThread threadA = new TerminateThread("A"); //thread 이름 설정 "A"
        TerminateThread threadB = new TerminateThread("B");
        TerminateThread threadC = new TerminateThread("C");

        threadA.start();
        threadB.start();
        threadC.start();

        int in;
        while(true){
            in = System.in.read();
            if ( in == 'A'){
                threadA.setFlag(true);
            }else if(in == 'B'){
                threadB.setFlag(true);
            }else if( in == 'C'){
                threadC.setFlag(true);
            }else if( in == 'M'){
                threadA.setFlag(true);
                threadB.setFlag(true);
                threadC.setFlag(true);
                break;
            }else{
                System.out.println("type again");
            }
        }

        System.out.println("main end");

    }
}
  세 개의 thread를 만든다.
  각각 무한 루프를 수행하게 한다.     
  작업 내용 this.sleep(100);

  ‘A’ 를 입력 받으면 첫 번째 thread를
  ‘B’ 를 입력 받으면 두 번째 thread를
  ‘C’ 를 입력 받으면 세 번째 thread를 
  ‘M’을 입력 받으면 모든 thread와 main() 함수를 종료한다.

멀티 Thread 프로그래밍에서의 동기화

- critical section 은 두개 이상의 thread가 동시에 접근 하는 경우 문제가 생길 수 있기 때문에 동시에 접근할 수 없는 영역이다. - semaphore 는 특별한 형태의 시스템 객체이며 get/release 두 개의 기능이 있다. - 한 순간 오직 하나의 thread 만이 semaphore를 얻을 수 있고, 나머지 thread들은 대기(blocking) 상태가 된다. - semaphore를 얻은 thread 만이 critical section에 들어갈 수 있다. - 예시는 아래와 같다.

class Bank{

    private int money = 10000;

    public synchronized  void saveMoney(int save){

        int m = this.getMoney();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        setMoney( m + save);
    }

    public synchronized  void minusMoney(int minus){


            int m = this.getMoney();

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            setMoney( m - minus);


    }

    public int getMoney(){
        return money;
    }

    public void setMoney(int money){
        this.money = money;
    }
}

class Park extends Thread{

    public  void run(){
        System.out.println("start save");
        SyncMain.myBank.saveMoney(3000);
        System.out.println("saveMoney(3000): " + SyncMain.myBank.getMoney() );    
    }
}

class ParkWife extends Thread{

    public void run(){
        System.out.println("start minus");
        SyncMain.myBank.minusMoney(1000);
        System.out.println("minusMoney(1000): " + SyncMain.myBank.getMoney() );

    }

}

public class SyncMain {

    public static Bank myBank = new Bank();

    public static void main(String[] args) throws InterruptedException {

        Park p = new Park();
        p.start();

        Thread.sleep(200);

        ParkWife pw = new ParkWife();
        pw.start();
    }

}
  • synchronized 가 걸려있으면 그 함수가 실행되는 동안에는 그 함수가 포함된 인스턴스(shared resource)에 락이 걸려서 다른 함수에서 참조할 수 없다 -> 동기화문제를 해결할 수 있다.
public void saveMoney(int save){

        synchronized (this){
            int m = this.getMoney();

            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            setMoney( m + save);
        }
    }
  • 위의 방식이 synchronized block 이다. -> 원하는 리소스에 블락을 거든 거는건데, 이 방법으로도 위의 결과를 만들어 낼 수 있다.
  • 자바에서는 deadlock을 방지하는 기술이 제공되지 않으므로 되도록이면 synchronized 메서드에서 다른 synchronized 메소드를 걸지 않는 것이 바람직하다.

 

wait()/notify() 메서드를 활용한 동기화 프로그래밍

  • wait()는 현재 메서드를 실행하고 있는 스레드를 not runnable 상태로 바꾼다.
  • wait() 상태가 된 Thread는 notify()가 호출 될 때까지 대기한다.
  • 유효한 자원이 생기면 notify()가 호출되고 wait() 하고 있는 Thread 중 무작위로 하나의 Thread를 재시작 하도록 한다.
  • notifyAll()이 호출되는 경우 wait() 하고 있는 모든 Thread가 재시작 된다.
  • 이 경우 유효한 리소스만큼의 Thread만이 수행될 수 있고 자원을 갖지 못한 Thread의 경우는 다시 wait() 상태로 만든다.
  • 자바에서는 notifyAll() 메서드의 사용을 권장한다.
  • 예시는 아래와 같다.
class FastLibrary{

    public ArrayList<String> shelf = new ArrayList<String>();

    public FastLibrary(){

        shelf.add("태백산맥 1");
        shelf.add("태백산맥 2");
        shelf.add("태백산맥 3");
    }

    public synchronized String lendBook() throws InterruptedException{

        Thread t = Thread.currentThread();

        if(shelf.size() == 0 ) {
            System.out.println(t.getName() + " waiting start");
            wait();
            System.out.println(t.getName() + " waiting end");
        }
        String book = shelf.remove(0);
        System.out.println(t.getName() + ": " + book + " lend");

        return book;
    }

    public synchronized void returnBook(String book){
        Thread t = Thread.currentThread();

        shelf.add(book);
        notify();
        System.out.println(t.getName() + ": " + book + " return");
    }

}

class Student extends Thread{

    public void run(){

        try{


            String title = LibraryMain.library.lendBook();
            if( title == null ) return;
            sleep(5000);
            LibraryMain.library.returnBook(title);

        }catch (InterruptedException e) {
            System.out.println(e);
        }
    }

}

public class LibraryMain {

    public static FastLibrary library = new FastLibrary(); 
    public static void main(String[] args) {

        Student std1 = new Student();
        Student std2 = new Student();
        Student std3 = new Student();
        Student std4 = new Student();
        Student std5 = new Student();
        Student std6 = new Student();

        std1.start();
        std2.start();
        std3.start();
        std4.start();
        std5.start();
        std6.start();
    }

}
  • 위의 코드를 보면 책은 3권인데 책을 빌리려는 학생은 6명인 것을 확인할 수 있다.
  • 만약 wait()를 걸어주지 않으면 자원이 부족해서 오류가 뜬다.
  • notify()가 실행되면 wait()상태인 쓰레드를 깨운다.
  • 또한, notifyAll()을 쓰는게 더 좋은 아래와 같이 바꿔줘야한다.
public synchronized String lendBook() throws InterruptedException{

        Thread t = Thread.currentThread();

        while( shelf.size() == 0 ){
            System.out.println(t.getName() + " waiting start");
            wait();
            System.out.println(t.getName() + " waiting end");
        }
        String book = shelf.remove(0);
        System.out.println(t.getName() + ": " + book + " lend");

        return book;
    }
}

public synchronized void returnBook(String book){

        Thread t = Thread.currentThread();

        shelf.add(book);
        notifyAll();
        System.out.println(t.getName() + ": " + book + " return");
}
  • notifyAll()은 wait()상태인 모든 쓰레드를 깨우고 깨워진 쓰레드중 무작위로 선택되기 때문에 결국 사용되지 못하는 쓰레드가 생길 수 있다. -> 그렇기 때문에 while로 사용될때까지 반복되도록 바꿔줘야한다.

'Java' 카테고리의 다른 글

람다 표현식  (0) 2022.02.26
내부 클래스  (0) 2022.02.26
String,StringBuilder,StringBuffer  (0) 2022.02.26
Stream  (0) 2022.02.26
Static 변수와 Static 메서드  (0) 2022.02.26