본문 바로가기

기술 서적/자바의 정석

[자바의 정석] ch13. 쓰레드

1. 프로세스와 스레드

 

프로세스 : 실행중인 프로그램

-- 프로그램을 실행하면 OS로부터 실행에 필요한 자원을 할당받아 프로세스가 된다.

 

쓰레드 : 프로세스의 자원(데이터/메모리)를 사용해서 실제 작업을 수행하는 것

 

싱글쓰레드 프로세스 : 하나의 쓰레드를 가진 프로세스

멀티쓰레드 프로세스 : 둘 이상의 쓰레드를 가진 프로세스

>멀티 태스킹 vs 멀티 쓰레드

멀티 태스킹 : 동시에 여러 프로세스를 실행시키는 것

멀티 쓰레딩 : 하나의 프로세스에 여러 쓰레드를 실행시키는 것

- 프로세스 비용 > 쓰레드 생성 비용

- 같은 프로세스내 쓰레드들은 자원을 공유한다

 

>멀티 스레딩의 장단점

 

2. 쓰레드의 구현과 실행

 

방법 2가지

방법1. Thread 클래스를 상속
class MyThread extends Thread {
    public void run(){} //Thread 클래스의 run을 오버라이딩
}

 

방법2. Runnable 인터페이스 구현
class MyThread implements Runnable {
    public void run(){} //Runnalbe 인터페이스의 run()을 구현
}

 

- Thread의 경우 다른 클래스 상속이 힘듦

- Runnable 인터페이스를 구현하는 것이 재사용성 & 코드의 일관성이 높음

 

ex)

package ch13;

class MyThread_ex1 extends Thread {
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println(getName());
        }
    }
}

class MyThread_ex2 implements Runnable{
    public void run(){
        for (int i = 0; i < 5; i++) {
            //Runnable 구현을 했을 때는 쓰레드 반환 후 getName()사용
            System.out.println(Thread.currentThread().getName());
        }
    }
}

public class ThreadEx1 {
    public static void main(String[] args) {
        MyThread_ex1 t1= new MyThread_ex1();
        Thread t2 = new Thread(new MyThread_ex2());

        t1.start();
        t2.start();
    }
}


>> 결과
Thread-0
Thread-0
Thread-0
Thread-0
Thread-0
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1

 

=> Thread 클래스의 private 멤버변수로 Runnable 클래스가 존재

=> Runnable 구현을 통해 얻어낸 Thread라면 static currentThread()를 통해 현재 실행중인 쓰레드의 참조를 반환한 이후에 호출해야 함.

 

그래서 두 코드에 차이가 나게 됨

 

class MyThread_ex1 extends Thread {
    public void run(){
        for (int i = 0; i < 5; i++) {
            System.out.println(getName());
        }
    }
}

class MyThread_ex2 implements Runnable{
    public void run(){
        for (int i = 0; i < 5; i++) {
            //Runnable 구현을 했을 때는 쓰레드 반환 후 getName()사용
            System.out.println(Thread.currentThread().getName());
        }
    }
}

 

>쓰레드의 이름 지정

Thread(Runnable target, String name)
Thread(String name)
void setName(String name)

->이름 미지정시 Thread-번호로 이름이 정해짐

 

>쓰레드의 실행-start()

- start를 실행해야만 쓰레드가 실행됨

- 실행대기 상태에 있다가 자신의 차례가 오면 실행

- 한번 실행이 종료된 쓰레드는 다시 실행할 수 없음(하나의 쓰레드=한번의 실행)

public class ThreadEx1 {
    public static void main(String[] args) {
        MyThread_ex1 t1= new MyThread_ex1();
        Thread t2 = new Thread(new MyThread_ex2());

        t1.start();
        t1.start(); //IllegalThreadStateException 발생
    }
}

 

 

3. start()와 run()

start : 새로운 쓰레드에게 필요한 호출스택 생성 + run

run : 현재 쓰레드에서 run 메소드 호출

1) main 메서드에서 쓰레드의 start 호출
2) 새로운 쓰레드에서 사용할 callstack 생성
3) 호출스택에 run이 호출되어 독립된 공간에서 작업 수행
4) 스케줄러가 정한 순서에 의해 번갈아 가면서 수행

== 쓰레드가 둘 이상일 때는 호출스택 최상위 메서드여도 대기상태에 있을 수 있음

=> 스케줄러가 쓰레드 우선순위를 고려하여 실행순서/시간을 결정

 

ex1) 쓰레드(start) 호출시 => main 쓰레드는 종료됨

package ch13;

class ThreadEx2_1 extends Thread{
    public void run(){
        throwExceptin();
    }

    public void throwExceptin(){
        try{
            throw new Exception();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
public class ThreadEx2 {
    public static void main(String[] args) throws Exception{
        ThreadEx2_1 t1 = new ThreadEx2_1();
        t1.start();
    }
}


>>결과
java.lang.Exception
	at ch13.ThreadEx2_1.throwExceptin(ThreadEx2.java:10)
	at ch13.ThreadEx2_1.run(ThreadEx2.java:5)

 

ex2) 메소드(run) 호출시 => main 쓰레드는 종료x

public class ThreadEx2 {
    public static void main(String[] args) throws Exception{
        ThreadEx2_1 t1 = new ThreadEx2_1();
        t1.run();
    }
}

>>결과
"C:\Program Files\Java\jdk-17\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2023.2.2\lib\idea_rt.jar=9059:C:\Program Files\JetBrains\IntelliJ IDEA 2023.2.2\bin" -Dfile.encoding=UTF-8 -classpath C:\Users\김지운\demo3\out\production\classes;C:\Users\김지운\demo3\out\production\resources;C:\Users\김지운\.gradle\caches\modules-2\files-2.1\org.openjfx\javafx-fxml\17.0.6\5724aedc415683e62eeab3a3875550aa814c84fd\javafx-fxml-17.0.6-win.jar;C:\Users\김지운\.gradle\caches\modules-2\files-2.1\org.openjfx\javafx-controls\17.0.6\8a9b91d717ec8bcdb179b91ae8a4f1137756fe3a\javafx-controls-17.0.6.jar;C:\Users\김지운\.gradle\caches\modules-2\files-2.1\org.openjfx\javafx-controls\17.0.6\c95b460be3bc372060ff32d0c666c1233c3e8400\javafx-controls-17.0.6-win.jar;C:\Users\김지운\.gradle\caches\modules-2\files-2.1\org.openjfx\javafx-graphics\17.0.6\a71a20fc765c578c17e6bd6525c029b03286ff5e\javafx-graphics-17.0.6.jar;C:\Users\김지운\.gradle\caches\modules-2\files-2.1\org.openjfx\javafx-graphics\17.0.6\14326bf575b927d3c31a775f74bcf754ced35353\javafx-graphics-17.0.6-win.jar;C:\Users\김지운\.gradle\caches\modules-2\files-2.1\org.openjfx\javafx-base\17.0.6\808b77ecf79be87b304a78fa9fffad805bfdb9f5\javafx-base-17.0.6.jar;C:\Users\김지운\.gradle\caches\modules-2\files-2.1\org.openjfx\javafx-base\17.0.6\78ccb38688b70acc6b59718daca8bdb91f99db57\javafx-base-17.0.6-win.jar ch13.ThreadEx2
java.lang.Exception
	at ch13.ThreadEx2_1.throwExceptin(ThreadEx2.java:10)
	at ch13.ThreadEx2_1.run(ThreadEx2.java:5)
	at ch13.ThreadEx2.main(ThreadEx2.java:19)

=> 쓰레드가 생성되지 않아 아직 main 쓰레드 안임

=> run이 호출되었을 뿐

 

 

4. 싱글쓰레드 vs 멀티쓰레드

싱글쓰레드 : 하나의 작업을 마친 후, 다른 작업 수행

멀티쓰레드 : 짧은 시간동안 2개의 쓰레드가 번갈아 가면서 작업수행

 

ex1) 싱글쓰레드

public class ThreadEx4 {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        for (int i = 0; i <300 ; i++) {
            System.out.printf("%s", new String("-"));
        }
        System.out.print("소요시간1:"+ (System.currentTimeMillis()-startTime));

        for (int i = 0; i < 300; i++) {
            System.out.printf("%s", new String("|"));
        }
        System.out.print("소요시간2:"+ (System.currentTimeMillis()-startTime));

    }
}

>>결과
-------------------------------------------------------
-------------------------------------------------------
--------------------------------------------------------
--------------------------------------------------------
--------------------------------------------------------
----------------------소요시간1:14
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||소요시간2:33

 

ex2) 멀티쓰레드 => 번갈아가면서 사용

package ch13;

public class ThreadEx4 {
    static long startTime = 0;
    public static void main(String[] args) {
        myThread t1 = new myThread();
        t1.start();
        startTime= System.currentTimeMillis();

        for (int i = 0; i < 300; i++) {
            System.out.printf("%s", new String("|"));
        }
        System.out.print("소요시간1:"+ (System.currentTimeMillis()-startTime));

    }
}

class myThread extends Thread{
    public void run(){
        for (int i = 0; i <300 ; i++) {
            System.out.printf("%s", new String("-"));
        }
        System.out.print("소요시간2:"+ (System.currentTimeMillis()-ThreadEx4.startTime));
    }
}

>>결과
|||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||--------------------------------------||||||||||
||||||||||||||||||||||||||||||||||||---------------------------------
---------------------------------------------------------------------
----------------------------------------------------------------------
----------------------------------------------------------------------
--------------------소요시간1:13 소요시간2:19

 

>싱글코어/멀티코어

싱글코어 : 하나의 코어가 번갈아가면서 작업을 수행하므로 두 작업이 절대 겹치지 않음

멀티코어 : 동시에 두 쓰레드가 수행될 수 있음

=> console 자원을 두고 두 쓰레드가 경쟁하게 됨

 

- jvm의 쓰레드 스케줄러에 따라 실행결과가 달라질 수 있음

=> java의 OS 종속적인 면 중 하나 ==쓰레드

 

> 서로 다른 자원을 사용하는 작업의 경우, 멀티쓰레드 프로세스가 효율적임

 

ex1) 싱글스레드 => 입력 기다리는 동안 다른 것을 못함

public class ThreadEx5 {
    public static void main(String[] args) throws Exception{
        String input= JOptionPane.showInputDialog("아무 값이나 입력하세요.");
        System.out.println("입력하신 값은 "+input+"입니다.");

        for (int i = 10; i >0; i--) {
            System.out.println(i);
            try{
                Thread.sleep(1000);
            }catch(Exception e){
            }
        }
    }
}


>>결과
입력하신 값은 abcd입니다.
10
9
8
7
6
5
4
3
2
1

 

 

ex2) 멀티쓰레드 => 입력을 기다리는 동안 다른 쓰레드 수행

public class ThreadEx5 {
    public static void main(String[] args) throws Exception{

        myThread2 t1 = new myThread2();
        t1.start();
        String input= JOptionPane.showInputDialog("아무 값이나 입력하세요.");
        System.out.println("입력하신 값은 "+input+"입니다.");


    }
}

class myThread2  extends Thread{
    public void run(){
        for (int i = 10; i >0; i--) {
            System.out.println(i);
            try{
                Thread.sleep(1000);
            }catch(Exception e){
            }
        }
    }
}

>>결과
10
9
8
7
입력하신 값은 abcd입니다.
6
5
4
3
2
1

 

 

5. 쓰레드의 우선순위

- 우선순위에 따라 쓰레드가 얻는 실행시간이 달라짐

- 작업의 중요도에 따라 특정 쓰레드가 더 많은 작업시간을 가지게 할 수 있음

=>숫자가 높을 수록  우선순위가 높음

 

ex)

public class ThreadEx6 {
    public static void main(String[] args) {
        myThread3 th1 = new myThread3();
        myThread4 th2= new myThread4();

        th2.setPriority(7); //main 5보다 더 높은 우선순위 부여

        System.out.println("Priority f th1(-) : "+ th1.getPriority());
        System.out.println("Priority f th2(|) : "+ th2.getPriority());

        th1.start();
        th2.start();
    }
}

class myThread3 extends Thread{
    public void run(){
        for (int i = 0; i < 300; i++) {
            System.out.print("-");
            for (int j = 0; j < 10000000; j++) {}
        }
    }
}

class myThread4 extends Thread{
    public void run(){
        for (int i = 0; i < 300; i++) {
            System.out.print("|");
            for (int j = 0; j < 10000000; j++) {}
        }
    }
}


>>결과
Priority f th1(-) : 5
Priority f th2(|) : 7

-||----------------------------------------------------------------
------------|||||||||||||||||||||||||||||||||||||||||||||||||-------
------------------------||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---------------------------------------------------------------------
---------------------------------------------------------------------
------------------------------------------------------

=> 멀티코어에서는 쓰레드 우선순위에 따른 차이가 없었음

=> 그저 쓰레드에 높은 우선순위를 주면 더 많은 실행시간과 기회를 갖게 된다고 확정x

=> 차라리, 작업 우선순위를 바탕으로 PriorityQueue에 저장해서 처리하는 것도 방법임

 

 

6. 쓰레드 그룹

- 서로 관련된 쓰레드를 그룹으로 묶어 다루기 위한 것(보안상의 이유)

- 모든 쓰레드는 반드시 하나의 쓰레드 그룹에 포함되어 있어야 함

- 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 'main 쓰레드 그룹'에 속함

- 자신을 생성한 쓰레드의 그룹과 우선순위를 상속받음

 

>쓰레드를 쓰레드 그룹에 포함시키는 방법

Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)

- 모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 함

- 생성자를 사용하지 않은 쓰레드는 자신을 생성한 쓰레드와 같은 그룹에 속하게 됨

 

ThreadGroup getThreadGroup() : 쓰레드 자신이 속한 쓰레드 그룹을 반환
void uncaughtException(Thread t, Throwable e) //쓰레드 그룹의 쓰레드가 처리되지 않은 예외에 의해 실행 종료
package ch13;

public class ThreadEx7 {
    public static void main(String[] args) throws Exception{
        ThreadGroup main = Thread.currentThread().getThreadGroup();
        ThreadGroup grp1= new ThreadGroup("Group1");
        ThreadGroup grp2= new ThreadGroup("Group2");

        ThreadGroup subgrp1= new ThreadGroup(grp1, "SubGroup1");
        grp1.setMaxPriority(3);

        Runnable r= new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }
        };

        new Thread(grp1,r,"th1").start();
        new Thread(subgrp1, r, "th2").start();
        new Thread(grp2, r, "th3").start();

        System.out.println(">>List of ThreadGroup : "+main.getName()+", Active TrheadGroup : "+main.activeGroupCount()+", Active Trhead: "+ main.activeCount());
        main.list();
    }
}

>>결과
>>List of ThreadGroup : main, Active TrheadGroup : 3, Active Trhead: 5
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[main,5,main]
    Thread[Monitor Ctrl-Break,5,main]
    java.lang.ThreadGroup[name=Group1,maxpri=3]
        Thread[th1,3,Group1]
        java.lang.ThreadGroup[name=SubGroup1,maxpri=3]
            Thread[th2,3,SubGroup1]
    java.lang.ThreadGroup[name=Group2,maxpri=10]
        Thread[th3,5,Group2]

=> 쓰레드 그룹과 하위그룹이 들여쓰기를 통해 구분되어 있음

 

[쓰레드 그룹]

- main

    -grp1

        -subgrp1

    -grp2

 

=> grp1의 maxPriority를 3으로 설정해서 subgrp1도 3으로 상속됨

 

7.데몬 쓰레드

- 일반 쓰레드의 작업을 돕는 보조적 역할 수행

- 일반 쓰레드가 종료되면 자동종료 => "보조"라는 존재 의의가 없어지기에

- 가바지 컬렉터, 자동저장, 화면 자동 갱신 등에 사용

- 무한루프와 조건문을 이용해 대기하다가 특정조건 만족시 작업 수행하고 다시 대기

 

=> setDaemon은 start이전에 실행되어야 함

=> 그렇지 않으면 IllegalThreadStateException 발생

 

ex) 데몬쓰레드 => 자동저장 기능

 

public class ThreadEx8 implements Runnable{
    static boolean autoSave=false;

    public static void main(String[] args) {
        Thread t= new Thread(new ThreadEx8());
        t.setDaemon(true);
        t.start();

        for (int i = 1; i <= 10; i++) {
            try{
                Thread.sleep(1000);
            }catch(InterruptedException e){}
            System.out.println(i);
            if(i==5){
                autoSave=true;
            }
        }
    }

    public void run(){
        while(true){
            try{
                Thread.sleep(3*1000);
            } catch(InterruptedException e){

            }
            if(autoSave){
                autoSave();
            }
        }
    }
    public void autoSave(){
        System.out.println("작업파일이 자동저장되었습니다.");
    }

}

>>결과
1
2
3
4
5
작업파일이 자동저장되었습니다.
6
7
8
작업파일이 자동저장되었습니다.
9
10

 

8. 쓰레드의 실행제어

- 효율적인 멀티 쓰레드 프로그램을 위해서는 보다 정교한 스케줄링을 통해 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비없이 잘 사용하도록 프로그래밍해야 함.

 

- 쓰레드의 실행을 제어할 수 있는 메서드가 제공됨

 

> 쓰레드의 상태

1) 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행대기열에 저장됨

2) 실행대기 상태에 있다가 자신의 차례가 되면 실행됨

3) yield() 혹은/ time-out이 되면 다시 실행 대기 상태가 되고 다음 차례 쓰레드를 실행함

4) 실행 중에 suspend() / sleep() . wait(), join() / I/O block 을 만나면 일시정지 상태가 될 수 있음. I/O block은 입출력 작업에서 발생하는 지연상태로, 일시정지 상태에 있다가 사용자가 입력을 마치만 다시 실행대기 상태가 됨

5) 지정된 일시정지 시간이 다 되거나, resume() / notify() / interrupt() 가 호출되면 일시정지상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 됨.

6) 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸(Terminated)됨

 

> sleep() : 쓰레드를 지정된 시간동안 멈추게 함

: 시간이 다 되거나, interrupt이 발생하면 실행 대기 상태가 됨

=>static 이라 현재 실행중인 쓰레드에 대해 적용됨(특정 쓰레드를 지정해서 멈추게 하는 것 불가)

 

=> interruptException이 발생하면 깨어나므로 예외처리를 해야함

 

ex)

package ch13;

public class ThreadEx9 {
    public static void main(String[] args) {
        ThreadEx9_1 th1 = new ThreadEx9_1();
        ThreadEx9_2 th2= new ThreadEx9_2();

        th1.start();
        th2.start();

        try{
            th1.sleep(2000);
        }catch(InterruptedException e){}

        System.out.println("<<main 종료>>");
    }
}

class ThreadEx9_1 extends Thread{

    public void run(){
        for (int i = 0; i < 300; i++) {
            System.out.printf("%s", new String("-"));
        }
        System.out.println("<<th1 종료>>");
    }
}

class ThreadEx9_2 extends Thread{

    public void run(){
        for (int i = 0; i < 300; i++) {
            System.out.printf("%s", new String("|"));
        }

        System.out.println("<<th2 종료>>");
    }
}


>>실행결과
---------------|||||||||||||||||---||||||-----------------||||||||
-----------------------------------------------|||||||||||||------
----------------||||||||||||||||||||------------------------|||||-
----------------||||||||||||||||||||||||||||||||||||||||||||||||||
||||||||||||------------------------------------------------------
--------------||||||||||||||||||----------||||||||||-------||||||||
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|||||||||||||||||||||||||||||||||||||||-------||||||||||||||-------
-----------------------------|||||||||||||-------------------------
--<<th1 종료>> <<th2 종료>> <<main 종료>>

 

=> th1.sleep()을 했어도  현재 실행중인 main 쓰레드에 영향을 받음

=> 그래서, sleep()은 Thread.sleep(2000)과 같이 호출해야 함.

 

 

>>interrupt() : 대기상태(WAITING)인 쓰레드를 실행대기(RUNNALBE)로 만듦

-isinterrupted : 단순 interrupted 만 반환

-interrupted : interrupted 반환 + false초기화

 

package ch13;
import javax.swing.JOptionPane;
public class ThreadEx5_1 {
    public static void main(String[] args) throws Exception{

        myThread5 t1 = new myThread5();
        t1.start();
        String input= JOptionPane.showInputDialog("아무 값이나 입력하세요.");
        System.out.println("입력하신 값은 "+input+"입니다.");
        t1.interrupt();
        System.out.println("isInterruptd() : "+ t1.isInterrupted());

    }
}

class myThread5  extends Thread{
    public void run(){
        int i=10;
        while(i!=0 && !isInterrupted()){
            System.out.println(i--);
            try{
                Thread.sleep(1000);
            }catch(InterruptedException e){ //sleep일때 interrupt 호출시
                interrupt();
            }
        }
        System.out.println("카운트가 종료되었습니다.");
    }
}

>>실행결과
10
9
8
7
6
입력하신 값은 abcd입니다.
카운트가 종료되었습니다.
isInterruptd() : true

 

 

> suspend(), resume(), stop() : 쓰레드 실행을 일시정지, 재개, 완전 정지 시킴

=> 교착상태에 빠지기 쉬워 Deprecated 됨

 

 

ex) suspend() / resume() / stop()은 deprecated되었으므로 직접 구현해야 함.

package ch13;

public class ThreadEx17 {
    public static void main(String[] args) {
        myThread7 th1= new myThread7("*");
        myThread7 th2= new myThread7("**");
        myThread7 th3= new myThread7("***");
        th1.start();
        th2.start();
        th3.start();
        try{
            Thread.sleep(2000);
            th1.suspend();
            Thread.sleep(2000);
            th2.suspend();
            Thread.sleep(3000);
            th1.resume();
            Thread.sleep(3000);
            th1.stop();
            th2.stop();
            Thread.sleep(2000);
            th3.stop();
        }catch(InterruptedException e){}

    }
}

class myThread7 implements Runnable{

    Thread th;
    volatile boolean suspended=false;
    volatile boolean stopped = false;
    myThread7(String name){
        th= new Thread(this,name);
    }

    public void run(){
        while(!stopped){
            if(!suspended){
                System.out.println(Thread.currentThread().getName());
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                }
            }
        }
        System.out.println(Thread.currentThread().getName()+" - stopped");
    }
    public void suspend(){suspended=true;}
    public void resume(){suspended=false;}
    public void stop(){stopped=true;}

    public void start(){
        th.start();
    }
    
}

 

 

>yield() : 남은 시간을 다음 쓰레드에 양보

=> yield()와 interrupt()를 적절히 사용하면 응답성과 효율을 높일 수 있음

 

ex) 1초의 실행시간을 할당받은 쓰레드가 0.5시간 동안 작업한 상태에서 yield가 호출되면

나머지 0.5초는 다음 쓰레드에게 양보함

 

ex)

public class ThreadEx18 {
    public static void main(String[] args) {
        myThread8 th1= new myThread8("*");
        myThread8 th2= new myThread8("**");
        myThread8 th3= new myThread8("***");
        th1.start();
        th2.start();
        th3.start();
        try{
            Thread.sleep(2000);
            th1.suspend();
            Thread.sleep(2000);
            th2.suspend();
            Thread.sleep(3000);
            th1.resume();
            Thread.sleep(3000);
            th1.stop();
            th2.stop();
            Thread.sleep(2000);
            th3.stop();
        }catch(InterruptedException e){}

    }
}

class myThread8 implements Runnable{

    Thread th;
    volatile boolean suspended=false;
    volatile boolean stopped = false;
    myThread8(String name){
        th= new Thread(this,name);
    }

    public void run(){
        while(!stopped){
            if(!suspended){
                System.out.println(Thread.currentThread().getName());
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    System.out.println(Thread.currentThread().getName()+"- interrupted");
                }
            }
            else{
                Thread.yield(); //멈춰 있으면 양보하기
            }
        }
        System.out.println(Thread.currentThread().getName()+" - stopped");
    }
    public void suspend(){
        suspended=true;
        th.interrupt();
        System.out.println(th.getName()+ " - interrupt by suspend()");
    }
    public void resume(){suspended=false;}
    public void stop(){
        stopped=true;
        th.interrupt();
        System.out.println(th.getName()+ " - interrupt by stop()");}

    public void start(){
        th.start();
    }
    
}

>>실행결과
**
*
***
**
*
***
**
*
***
* - interrupt by suspend()
*- interrupted
**
***
**
***
** - interrupt by suspend()
**- interrupted
***
***
*
***
*
***
*
***
*- interrupted
* - interrupt by stop()
** - interrupt by stop()
** - stopped
* - stopped
***
***
*** - interrupt by stop()
***- interrupted
*** - stopped

 

 

포인트1 : suspended가 되어있을 때는 시간 양보

=> busy-waiting 상태에 있지 않고 바로 시간 양보하도록 while문을 구성

 

if(!suspended){
                System.out.println(Thread.currentThread().getName());
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException e){
                    System.out.println(Thread.currentThread().getName()+"- interrupted");
                }
            }
            else{
                Thread.yield(); //멈춰 있으면 양보하기
            }

 

 

포인트2 : stop/suspended에서 interrupt 발생

-> sleep에 있으면 최소 1초의 대기시간이 필요했던 것에 반해 바로 interrupexception 발생시킴 => 응답성 향상

 

 public void suspend(){
        suspended=true;
        th.interrupt();
        System.out.println(th.getName()+ " - interrupt by suspend()");
    }

 

    public void stop(){
        stopped=true;
        th.interrupt();
        System.out.println(th.getName()+ " - interrupt by stop()");}

 

 public void run(){
        while(!stopped){
            if(!suspended){
                System.out.println(Thread.currentThread().getName());
                try{
                    Thread.sleep(1000); // interrupt이 발생하면 바로 예외 발생되어 일시정지 상태에서 벗어남
                }catch(InterruptedException e){
                    System.out.println(Thread.currentThread().getName()+"- interrupted");
                }
            }

 

 

 

 

> join() : 특정 쓰레드 작업을 기다림

 

- 작업 중에 특정 쓰레드 작업이 먼저 수행될 필요가 있을 때

- join은 특정 쓰레드의 대기를 지정할 수 있음 <=> sleep은 현재 쓰레드만 가능

 

 

ex) main 쓰레드가 t1,t2가 끝날때까지 기다림

package ch13;

public class ThreadEx19 {
    static long startTime = 0;
    public static void main(String[] args) {
        myThread19 t1 = new myThread19();
        myThread19_2 t2 = new myThread19_2();

        t1.start(); 
        t2.start();
        startTime= System.currentTimeMillis();

        try{
            t1.join(); //main 쓰레드가 t1이 끝날때까지 기다린다.
            t2.join();  //main 쓰레드가 t2가 끝날때까지 기다린다.
        }catch(InterruptedException e){
        }
        System.out.println();
        System.out.print("소요시간:"+ (System.currentTimeMillis()-ThreadEx19.startTime));

    }
}

class myThread19 extends Thread{
    public void run(){
        for (int i = 0; i <300 ; i++) {
            System.out.printf("%s", new String("-"));
        }
    }
}

class myThread19_2 extends Thread{
    public void run(){
        for (int i = 0; i <300 ; i++) {
            System.out.printf("%s", new String("|"));
        }
    }
}

>>실행결과
-------------------------------------------------------------------------------|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||---------------------------|||||||||||||||||||||||||||||||||||||||||||||||-------|||||||||||||||||||||||||||||||---------------------||||||||||||||-----------||||||||||||||||||||||||||||||||||||---------------------------------------------------------------------------|||||||||||||------------------------------------------||||||||--------------------------------------||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
소요시간:37

 


9. 쓰레드 동기화(Synchronized)

 

멀티 쓰레드 작업의 문제상황 : Data Hazard

: 쓰레드 A가 작업하는 도중에 사용하는 자원을 쓰레드가 B가 변경 => 의도했던 것과 다른 결과

 

쓰레드 동기화 : 특정 작업을 마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것

: 한번에 하나의 쓰레드만 객체에 접근할 수 있도록 객체에 락을 걸어 일관성 유지

 

ex) 동기화가 필요한 상황 : 조건식을 통과한 이후 출금하는 상황

package ch13;

public class ThreadEx21 {
    public static void main(String[] args) {
        Runnable r = new RunnableEx21();
        new Thread(r).start();
        new Thread(r).start();
    }

}

class Account{
    private int balance = 1000;

    public int getBalance(){
        return balance;
    }

    public void withdraw(int money){
        if(balance>=money){
            try{Thread.sleep(1000);
            }catch(InterruptedException e){}
            balance-=money;
        }
    }
}

class RunnableEx21 implements Runnable {
    Account acc = new Account();
    public void run(){
        while(acc.getBalance()>0){
            int money = (int)(Math.random()*3+1)*100;
            acc.withdraw(money);
            System.out.println("balance: "+ acc.getBalance());
        }
    }
}

 

>>실행결과

balance: 900
balance: 700
balance: 600
balance: 600
balance: 300
balance: 200
balance: 0
balance: -200

 

=> 조건을 통과하고 출금을 수행하려는 동안 다른 쓰레드에게 제어권이 넘어가 출금(음수 결과가 나옴)

=> 잔고를 확인하는 문장은 하나의 임계영역으로 묶여져야 함

 

public synchronized void withdraw(int money){
    if(balance>=money){
        try{Thread.sleep(1000);
        }catch(InterruptedException e){}
        balance-=money;
    }
}

 

또한, balance가 private인 것도 중요함

=> private이 아니면 동기화가 되었더라도 외부에 의해 변할 위험이 있음


 

9.2) wait과 notify

- wait() : 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣음

- notify() - waiting pool에서 대기중인 쓰레드 중 하나를 깨운다.

- notifyAll() - waiting pool에서 대기중인 모든 쓰레드를 깨움

 

ex) 빵을 사려고 빵집 앞에 줄을 서있는데, 자신의 차례에도 원하는 빵이 나오지 않았다면 다음 사람에게 순서를 양보하고 기다리다가 자신이 원하는 빵이 나오면 통보를 받고 빵을 사감

 

wait(), notify(), notifyAll()
- Object에 정의되어 있다.
- 동기화 블록(synchronized 블록) 내에서만 사용할 수 있다.
- 보다 효율적인 동기화를 가능하게 한다.

 

package ch13;

import java.util.ArrayList;

public class ThreadWaitEx2 {
    public static void main(String[] args) throws Exception {
        Table_ table = new Table_();

        new Thread(new Cook_(table), "COOK1").start();
        new Thread(new Customer_(table, "donut"), "CUST1").start();
        new Thread(new Customer_(table, "burger"), "CUST2").start();

        Thread.sleep(100);
        System.exit(0);
    }
}

class Customer_ implements Runnable{
    private Table_ table;
    private String food;

    Customer_(Table_ table, String food){
        this.table = table;
        this.food= food;
    }
    public void run(){
        while(true){
            try{Thread.sleep(10);}catch(InterruptedException e){}
            String name= Thread.currentThread().getName();

            if(eatFood()){
                System.out.println(name+" ate a "+ food);
            }else{
                System.out.println(name+" failed to eat. :(");
            }
        }//while
    }

    boolean eatFood(){return table.remove(food);}
}

class Cook_ implements Runnable{
    private Table_ table;
    Cook_(Table_ table){this.table = table;}

    public void run(){
        while(true){
            //임의의 요리를 하나 선택해서 table에 추가
            int idx= (int)(Math.random()*table.dishNum());
            table.add(table.dishNames[idx]);
            try{Thread.sleep(1);}catch(InterruptedException e){}
        }
    }
}

class Table_{
    String [] dishNames= {"donut", "donut", "burger"}; //donut이 더 자주나온다.
    final int MAX_FOOD = 6;

    private ArrayList<String> dishes = new ArrayList<>();

    public void add(String dish){
        //테이블에 음식이 가득찼으면, 테이블에 음식을 추가하지 않음
        if(dishes.size()>=MAX_FOOD)
            return;
        dishes.add(dish);
        System.out.println("Dishes: "+ dishes.toString());
    }

    public boolean remove(String dishName){
        for (int i = 0; i < dishes.size(); i++) {
            if(dishName.equals(dishes.get(i))){
                dishes.remove(i);
                return true;
            }
        }
        return false;
    }

    public int dishNum(){ return dishNames.length;}

}

 

=> 실행결과에서 예외 발생상황

1) 요리사가 Table에 요리를 추가하는 과정에서 손님이 요리를 먹어버림

2) 하나 남은 요리를 손님2가 먹으려는데 손님1이 먹음

 

 

=> 동기화 수정

 

수정1 : 만약 음식이 없다면 0.5초마다 기다리면서 추가되었는지 확인

=> 문제는 Table 락 권한을 Cook에게 안넘김

 

public boolean remove(String dishName){
    synchronized(this) {
        while (true) {
            if (dishes.size() == 0) {
                String name = Thread.currentThread().getName();
                System.out.println(name + "is waiting");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                }
            }

            for (int i = 0; i < dishes.size(); i++) {
                if (dishName.equals(dishes.get(i))) {
                    dishes.remove(i);
                    return true;
                }
            }
        }
    }

 

수정2. add에 동기화

public synchronized void add(String dish){
    //테이블에 음식이 가득찼으면, 테이블에 음식을 추가하지 않음
    if(dishes.size()>=MAX_FOOD)
        return;
    dishes.add(dish);
    System.out.println("Dishes: "+ dishes.toString());
}

 

>>실행결과

Dishes: [donut]
CUST2 failed to eat. :(
CUST2is waiting
CUST1 ate a donut
CUST2is waiting
CUST2is waiting
CUST2is waiting

.... 이하 동일

 

문제 : Table을 여러 쓰레드가 공유하기 때문에 손님이 락을 쥐고 이 접근권한을 Cook에게 넘기지 않음

 

=> wait()과 notify()를 통해 쓰레드 일시정지/계속 여부를 판단

 

ex1) remove - 손님이 음식을 먹으려는데 없을 때 => 손님 wait() 

ex2) remove- 손님이 음식을 먹었을 때 => 요리사에게 notify()

public void remove(String dishName) {
    synchronized (this) {
        String name = Thread.currentThread().getName();

        while (dishes.size() == 0) { //음식이 없을 때
            System.out.println(name + "is waiting");
            try {
                wait();
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }

        while (true) {
            for (int i = 0; i < dishes.size(); i++) {
                if (dishName.equals(dishes.get(i))) {
                    dishes.remove(i);
                    notify(); //요리사에게 먹었다고 알림
                    return;
                }
            }
            try {
                System.out.println(name + " is waiting");
                wait(); //원하는 음식이 없으면 손님을 기다리게 함.
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }
    }
}

 

ex3) add -  요리사가 음식추가하려는데 꽉차있을 때 => 요리사 wait() 

ex4) add - 요리사가 음식을 추가할 때 => 손님에게 notfiy()

public synchronized void add(String dish){
    //테이블에 음식이 가득찼으면, 테이블에 음식을 추가하지 않음
    if(dishes.size()>=MAX_FOOD){
        String name= Thread.currentThread().getName();
        System.out.println(name+"is waiting");;
        try{wait();} catch(InterruptedException e){}
    }

    dishes.add(dish); // 추가하고 손님한테 알려줌
    notify();
    System.out.println("Dishes: "+ dishes.toString());
}

 

- notify()는 요리사 쓰레드와 손님 쓰레드 중에서 누가 통지를 받을지 알 수 없음

=> notify()는 waiting pool에서 대기 중인 스레드 중에서 하나를 임의로 선택해서 통지할 뿐

 

>>기아현상과 경쟁상태

기아현상 : 통지를 계속 받지 못하고 오랫동안 기다리게 되는 상황 => notifyAll()을 통해 극복

경쟁상태 : lock을 얻기 위해 경쟁하게 되는 상태

=> 요리사 쓰레드와 손님 쓰레드를 구별해서 통지하는 것이 필요 by Lock&Condition


9.3. Lock & Condition

ReentrantLock

: 특정조건에서 lock을 풀고 다시 lock을 얻어 임계영역으로 돌아와 작업을 수행할 수 있음

 

ReentrantReadWriteLock

: 읽기 lock이 걸려있으면, 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행할 수 있음, 그러나 읽기 lock이 걸린 상태에서 쓰기 lock을 거는 것은 허용되지 않음

 

StampledLock 

: 낙관적 읽기로 우선 읽다가 쓰기 lock에 의해 바로 풀림

=> 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 이후에 읽기 lock을 검

 

ex)

 

> ReentrantLock 생성자

=> 임계영역에서 예외가 발생하거나 return으로 빠져나갈 수 있으니 finally를 통해 unlock() 해줌

 

 

>tryLock() : 다른 쓰레드에 의해 lock이 걸려있으면 기다리지 않음

=> lock을 얻으면 true를 반환하고 얻지 못하면 false를 반환함

boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
=> lock을 얻으면 true를 반환하고 얻지 못하면 false를 반환함

 

>Condition : 각 쓰레드를 구분할 수 있게 해주는 것

요리사를 위한 Condition(forCook)과 손님을 위한 Condition(forCust)이 다름

 

wait() & notify()  <=> await() & signal()

Object Condition
void wait() void await()
void awaitUninterruptibly()
void wait(long timeout) boolean await(long time, TimeUnit unit)
long awaitNanos(long nanosTimeout)
boolean awaitUntil(DAte deadline)
void notify() void signal()
void notifyAll() void signalAll()

 

 

 

ex)

class Table {
    String[] dishNames = {"donut", "donut", "burger"};
    final int MAX_FOOD = 6; //테이블에 놓을 수 있는 최대 음식;

    private ArrayList<String> dishes = new ArrayList<>();
    private ReentrantLock lock = new ReentrantLock();
    private Condition forCook = lock.newCondition();
    private Condition forCust = lock.newCondition();

    public void add(String dish) {
        lock.lock();
        try {
            while (dishes.size() >= MAX_FOOD) {
                String name = Thread.currentThread().getName();
                System.out.println(name + " is waiting");
                try {
                    forCook.await(); //COOK 스레드를 기다리게 함
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                }
            }
            dishes.add(dish);
            forCust.signal();//추가되었다고 CUST에게 알림
            System.out.println("Dishes:" + dishes.toString());
        } finally {
            lock.unlock();
        }
    }

 


>9.4 volatile : cache-메모리간의 불일치 해소

- 성능 향상을 위해 변수의 값을 core의 cache에 저장해놓고 작업

=> 메모리에 저장된 값이 변경되었는데도 캐시에 갱신되지 않는 경우가 발생

=>  해결방안 : 여러 쓰레드가 공유하는 변수에는 volatile을 붙여야 항상 메모리에서 읽어옴


>9.5 : join & fork 프레임웍

 

- 하나의 작업을 나누어 여러 쓰레드가 동시에 처리하도록 도와줌

- RecursiveAction이나 RecursiveTask를 상속받아서 구현

=> 두 클래스 모두 compute()라는 추상 메서드가 있는데, 이 추상메서드를 구현하기만 하면 됨

=> 이후 쓰레드 풀과 수행할 작업을 생성하고, invoke()로 작업을 시작하면 됨

 

 

ex)

ForkJoinPool pool = new ForkJoinPool(); //쓰레드 풀을 생성

SumTask task= new SumTask(from, to); //수행할 작업을 생성

 

Long result= pool.invoke(task); //invoke를 호출해서 작업을 시작

 

 

>compute()의 구현

- 수행할 작업과 작업을 어떻게 나눌 것인지 정해주어야 함

- fork()로 나눈 작업을 큐에 넣고, compute()를 재귀호출

>다른 쓰레드의 작업 훔쳐오기

- 1-8까지숫자를 더한다고 가정하면 size가 2가 될 때까지 나눔

- compute()가 처음 호출되면, 더할 숫자의 범위를 반으로 나눔

-> 한쪽에는 fork()를 호출해서 작업 큐에 저장

-> 나머지 쓰레드는 compute를 재귀호출하면서 작업을 계속 반으로 나눔

=> 다른 쓰레드는 fork()에 의해 작업큐에 추가된 작업을 수행

 

-자신의 작업 큐가 비어있는 쓰레드는 다른 쓰레드 작업 큐에서 작업을 가져와 수행

== 작업 훔쳐오기

 

 

>fork와 join

-compute는 작업을 나누고, fork는 작업을 큐에 넣는다.

- join은 작업의 결과를 합친다

 

> fork와 join의 주요차이 :  동기메서드 vs 비동기메서드

fork : 지시만 하고 결과를 기다리지 않음(비동기 메서드)

join : 결과를 기다렸다가 더해서 결과를 반환함(동기메서드)

 

ackage ch13;

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

public class ThreadPool {
    static final ForkJoinPool pool = new ForkJoinPool();

    public static void main(String[] args) {
        long from =1L;
        long to = 100_000_000L;

        SumTask task= new SumTask(from, to);
        long start= System.currentTimeMillis();
        Long result = pool.invoke(task);
        System.out.println("Elapsed time(4core) : "+ (System.currentTimeMillis()-start));
        System.out.printf("sum of %d~%d=%d%n", from, to, result);
        System.out.println();

        result=0L;
        start= System.currentTimeMillis();
        for(long i=from; i<=to; i++){
            result+=i;
        }
        System.out.println("Elapsed time(1core) : "+ (System.currentTimeMillis()-start));
        System.out.printf("sum of %d~%d=%d%n", from, to, result);
        System.out.println();
    }
}

class SumTask extends RecursiveTask<Long> {
    long from, to;

    SumTask(long from, long to){
        this.from = from;
        this.to= to;
    }

    public Long compute(){
        long size= to-from+1;
        if(size<=5){
            return sum();
        }
        long half=(from+to)/2;
        SumTask leftSum = new SumTask(from, half);
        SumTask rightSum= new SumTask(half+1, to);
        leftSum.fork();

        return rightSum.compute()+leftSum.join();
    }

    long sum(){
        long tmp=0L;
        for (long  i = from; i <= to; i++) {
            tmp+=i;
        }
        return tmp;
    }
}

=> 막상 작업을 나누고 합치는데 걸리는 시간이 있어서 멀티쓰레드가 더 시간이 오래걸림.

=> 멀티 쓰레드가 무조건 좋은 것이 아니라 반드시 테스트해보고 더 이득이 있을 때만 처리할 것