Java 심층 학습(1): 다중 스레드

39742 단어
다중 루틴 목적: 같은 시간에 여러 개의 다른 경로로 프로그램을 실행하여 프로그램 운영 효율을 높인다.
다중 루틴 응용: 데이터베이스 연결 탱크, 다중 루틴 파일 다운로드 등
 
참고: 파일 다운로드에 멀티스레드를 사용하면 속도를 높일 수 없습니다.
하나의 프로세스 중에는 반드시 주선이 있을 것이다
 
기본부터 멀티스레드 사용 방법:
1. Thread 클래스 상속: (권장하지 않음)
public class ThreadDemo extends Thread {
    @Override
    public void run() {
        //         
    }

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        threadDemo.start();
    }
}

주의:threadDemo는 start 방법을 호출합니다.만약run 방법을 호출했다면 본질적으로 단선정이다
2. Runnable 인터페이스 구현:
public class ThreadDemo implements Runnable {
    @Override
    public void run() {
        //         
        System.out.println("demo");
    }

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        new Thread(threadDemo).start();
    }
}

 
3. 익명 내부 클래스
public class ThreadDemo {
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                //         
            }
        }.start();
    }
}

자바 8 은 이렇게 간략하게 쓸 수 있어요.
public class ThreadDemo {
    public static void main(String[] args) {
        new Thread(() -> {
            //         
        }).start();
    }
}

 
다중 스레드 상태:
1. 새 상태: start 방법을 사용하기 전에
2. 준비 상태: start 메서드를 호출하여 CPU 할당 실행 대기
3. 실행 상태:run 방법의 코드를 실행합니다
4. 사망 상태:run 방법 실행 완료
5. 막힌 상태:wait나sleep방법을 호출하여 라인이 막힌 상태로 변하고, 막힌 상태는 즉시 준비된 상태로 변할 수 있다
 
데몬 스레드:
자바 프로그램에서 메인 스레드와 GC 스레드(쓰레기 회수용)가 있으면 메인 스레드가 사망하면 GC 스레드도 사망하고 폐기합니다
주 스레드와 함께 폐기된 이런 스레드가 바로 수호 스레드입니다.
비수호 스레드: 스레드의 상태는 메인 스레드와 무관합니다
사용자 스레드: 상기 세 가지 방식으로 만들어진 것은 모두 사용자 현장이고 주 스레드로 만들어졌으며 비수호 스레드이기도 하다.
 
예:
public class ThreadDemo {
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 30; i++) {
                try {
                    Thread.sleep(300);
                    System.out.println("   i:" + i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        for (int i = 0; i < 5; i++) {
            System.out.println("   i:" + i);
        }
        System.out.println("       ");
    }
}

출력을 관찰한 결과 프린터 메인 라인이 실행된 후에도 하위 라인의 실행 정보를 계속 인쇄하고 있음을 알 수 있습니다
 
하위 스레드만 설정하면 수호 스레드가 됩니다.
public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println("   i:" + i);
            }
        });
        thread.setDaemon(true);
        thread.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("   i:" + i);
        }
        System.out.println("       ");
    }
}

출력을 관찰한 결과 하위 라인이 999까지 인쇄되지 않았고 프로그램이 끝났습니다
 
join 메서드:
A라인에서 B라인의join 방법을 호출하면 A라인은 B라인이 실행된 후에 실행합니다 (A는 CPU 실행권을 방출합니다)
 
예: 주 스레드는 하위 스레드가 실행된 후에 실행합니다
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 60; i++) {
                System.out.println("   i:" + i);
            }
        });
        thread.start();
        thread.join();
        for (int i = 0; i < 10; i++) {
            System.out.println("   i:" + i);
        }
        System.out.println("       ");
    }
}

출력 관찰 결과: 하위 라인이 59를 인쇄하고 나서야 주 라인의 인쇄를 시작합니다
 
스레드 보안 문제:
여러 개의 스레드가 같은 전역 변수를 공유하고 쓸 때 스레드 안전 문제가 발생할 수 있다
 
아날로그 라인 안전 문제: 역 매표 경전 사례
public class ThreadDemo implements Runnable {
    //       
    private int count = 100;

    @Override
    public void run() {
        while (count > 0) {
            try {
                Thread.sleep(100);
                sale();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void sale() {
        if (count > 0) {
            System.out.println(Thread.currentThread().getName() + "   " + (100 - count + 1) + "  ");
            count--;
        }
    }

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        Thread t1 = new Thread(threadDemo, "  1");
        Thread t2 = new Thread(threadDemo, "  2");
        t1.start();
        t2.start();
    }
}

수출 관찰 결과: 많은 표 중복 판매
 
스레드 보안 문제 해결:
1.sale 방법에서synchronized 키워드 사용하기
원리: 스레드가 이 방법에 들어갈 때 자동으로 자물쇠를 얻는다. 어떤 스레드가 자물쇠를 얻으면 다른 스레드는 이 스레드 코드를 실행하고 자물쇠를 풀 때까지 기다린다.
단점: 프로그램 효율을 낮추고 매번 이 방법을 실행할 때마다 판단해야 한다
    private synchronized void sale() {
        if (count > 0) {
            System.out.println(Thread.currentThread().getName() + "   " + (100 - count + 1) + "  ");
            count--;
        }
    }

 
2. 동기화 코드 블록 사용
public class ThreadDemo implements Runnable {
    //       
    private int count = 100;

    private final Object object = new Object();

    @Override
    public void run() {
        while (count > 0) {
            try {
                Thread.sleep(100);
                sale();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void sale() {
        synchronized (object) {
            if (count > 0) {
                System.out.println(Thread.currentThread().getName() + "   " + (100 - count + 1) + "  ");
                count--;
            }
        }
    }

    public static void main(String[] args) {
        ThreadDemo threadDemo = new ThreadDemo();
        Thread t1 = new Thread(threadDemo, "  1");
        Thread t2 = new Thread(threadDemo, "  2");
        t1.start();
        t2.start();
    }
}

관찰 출력: 문제 해결
 
주의: 이렇게 썼는데도 문제가 있습니다
    public static void main(String[] args) {
        ThreadDemo threadDemo1 = new ThreadDemo();
        ThreadDemo threadDemo2 = new ThreadDemo();
        Thread t1 = new Thread(threadDemo1, "  1");
        Thread t2 = new Thread(threadDemo2, "  2");
        t1.start();
        t2.start();
    }

 
이 때 전역 변수에 static 키워드를 추가해야 합니다: 같은 자물쇠를 공유합니다
    private static int count = 100;

    private static final Object object = new Object();

관찰 출력: 문제 해결
 
다중 스레드 잠금 해제 문제:
장면 생성: 초보자는 모든 곳에synchronized를 넣는 것을 좋아하기 때문에synchronized에 synchronized를 끼워 넣으면 자물쇠가 생기기 쉽다.
발생 원인: A 라인이 자물쇠 2를 받았습니다. 지금 자물쇠 1을 가져가야 합니다.B라인이 자물쇠 1을 가져왔는데 지금 자물쇠 2를 가져가야 합니다.A라인은 자물쇠 1을 가지지 못하면 자물쇠 2를 풀지 않는다.B 라인은 자물쇠 2를 못 잡으면 자물쇠 1이 풀리지 않아요.
 
ThreadLocal 클래스:
ThreadLocal이란 무엇입니까? 각 스레드에 로컬 변수를 제공합니다.
원리: 밑바닥은 맵 집합으로 현재 스레드를 가져오고 맵의 put과 get 방법을 호출하여 실현한다.
초기화:
    public static ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 0);

가져오기:
        threadLocal.get();

설정:
        threadLocal.set(count);

 
다중 스레드 기능:
1. 원자성
2. 가시성
3. 질서성
 
JMM(Java 메모리 모델):
JMM은 공유 변수에 대한 한 스레드의 쓰기를 결정할 때 다른 스레드를 볼 수 있는지 여부를 결정합니다
주 메모리:공유 저장소의 변수
로컬 메모리:공유 변수의 사본
 
루틴 안전 문제의 근본 원리: 공유 변수는 메인 메모리에 저장되고 모든 루틴은 로컬 메모리가 있다.예를 들어 나는 메인 메모리에count=100을 저장하면 두 로컬 메모리에count=100 복사본을 저장한다.이 때 두 라인은 공유 변수count-1을 동시에 조작한다. 우선 두 라인은 현재 로컬 메모리에서count-1 조작을 한 다음에 메인 메모리로 리셋해야 한다.그래서 라인 안전 문제가 생겼어요!
 
Volatile 키워드:
예:
class ThreadTest extends Thread {
    public boolean flag = true;

    @Override
    public void run() {
        System.out.println("    ");
        while (flag) {

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

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

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
        Thread.sleep(3000);
        threadTest.setRunning(false);
        System.out.println("flag  false");
        Thread.sleep(3000);
        System.out.println("flag:" + threadTest.flag);
    }
}

인쇄는 다음과 같습니다.
    
flag  false
flag:false

그리고 프로그램이 끊겼어요.
 
왜 이미flag을false로 바꿨는지, 서브라인은while 순환에 들어갔는지
왜냐하면, 주 루틴이 flag를 바꿨기 때문에, 아직 주 메모리에 들어가지 않았습니다. 하위 루틴은 로컬 메모리의 변수를 계속 읽고 있습니다
 
해결:volatile 키워드만 추가하면
역할: 수정된 값을 메인 메모리로 즉시 업데이트하여 다른 라인이 변수에 대한 가시성을 확보합니다
    public volatile boolean flag = true;

인쇄는 다음과 같습니다.
    
flag  false
    
flag:false 

주의:volatile는 가시성만 보장할 수 있고 라인 안전은 보장할 수 없습니다
 
사용 장면: 주류 프레임워크를 관찰하면 전역적으로 공유하는 변수만volatile 키워드를 추가한 것을 발견할 수 있다.
 
Synchronized와 Volatile 키워드의 차이점:
Volatile는 가시성을 보장하고 원자성을 보장하지 못합니다. 즉, 라인의 안전을 보장하지 못하며 재배열을 금지합니다.
Synchronized는 원자성 및 스레드 보안을 보장하며 순서 변경을 방지합니다.
 
순서재정리:
개념: CPU는 코드를 최적화하고 의존 관계성을 다시 정렬하지 않는다
의존 관계란:
            int a = 1;
            int b = 2;
            int c = a + b;

c 의존 a, b.c와 a, b는 모두 관계가 있다.c는 반드시 a, b 다음에 집행하고 a, b는 반드시 집행하는 순서가 아니다
그래서 코드를 실행할 때 int b = 2를 먼저 실행할 수도 있고 int a = 1이 아니라 int b = 2를 먼저 실행할 수도 있다
그런데 여기서 수행한 결과는 달라지지 않을 거예요.
 
주의: 일반적으로 다중 라인에서만 정렬 문제에 부딪힐 수 있습니다
정렬 문제 해결:volatile 키워드 추가
 
스레드 간 통신:
여러 개의 스레드가 같은 자원을 처리하지만 스레드의 임무는 같지 않다. 일정한 수단을 통해 각 스레드가 자원을 효과적으로 이용할 수 있도록 한다.
이런 수단은 바로 깨우기 메커니즘을 기다리는 것으로 라인 간의 통신이라고도 부른다
관련된 방법:wait (), notify ()
 
예:
두 라인, 한 입력, 한 출력
package demo;

public class Resource {
    public String name;
    public String sex;
}

입력 스레드:
package demo;

public class Input implements Runnable {
    private Resource r = new Resource();

    public void run() {
        int i = 0;
        while (true) {
            if (i % 2 == 0) {
                r.name = "  ";
                r.sex = " ";
            } else {
                r.name = "  ";
                r.sex = " ";
            }
            i++;
        }
    }

}

출력 스레드:
package demo;

public class Output implements Runnable {
    private Resource r = new Resource();
    public void run(){
        while (true) {
            System.out.println(r.name+"..."+r.sex);
        }
    }
}

테스트 클래스:
package demo;

public class ThreadDemo {
    public static void main(String[] args) {
        Input in = new Input();
        Output out = new Output();
        Thread tin = new Thread(in);
        Thread tout = new Thread(out);
        
        tin.start();
        tout.start();
    }
}

 
실행 후 출력이 모두null인 것을 발견했습니다.null
입력 스레드와 출력 스레드에서 만든 Resource 객체가 서로 다르기 때문에
 
null 문제 해결:
package demo;

public class Input implements Runnable {
    private Resource r;
    
    public Input(Resource r){
        this.r = r;
    }

    public void run() {
        int i = 0;
        while (true) {
            if (i % 2 == 0) {
                r.name = "  ";
                r.sex = " ";
            } else {
                r.name = "  ";
                r.sex = " ";
            }
            i++;
        }
    }

}
package demo;

public class Output implements Runnable {
    private Resource r;
    
    public Output(Resource r){
        this.r = r;
    }
    
    public void run(){
        while (true) {
            System.out.println(r.name+"..."+r.sex);
        }
    }
}
package demo;

public class ThreadDemo {
    public static void main(String[] args) {
        
        Resource r = new Resource();
        
        Input in = new Input(r);
        Output out = new Output(r);
        Thread tin = new Thread(in);
        Thread tout = new Thread(out);
        
        tin.start();
        tout.start();
    }
}

 
실행 후 또 다른 문제가 발견되었습니다.
출력: 장삼...여자나 이사...남자, 성별 오류
발생 원인:
부치가 장삼화남을 끝낸 후에 계속 부치가 이사화녀를 부치는데 이때 부치녀를 얻지 못하면 출력 라인에 들어간다. 이때 이사를 출력한다...남자
 
그래서 동기화를 추가하는 것을 생각했다.
    public void run() {
        int i = 0;
        while (true) {
            synchronized (this) {
                if (i % 2 == 0) {
                    r.name = "  ";
                    r.sex = " ";
                } else {
                    r.name = "  ";
                    r.sex = " ";
                }
                i++;
            }
        }
    }
    public void run() {
        while (true) {
            synchronized (this) {
                System.out.println(r.name + "..." + r.sex);
            }
        }
    }

 
그러나 문제는 해결되지 않았다.
이유:
이곳의 동기화는 작용을 잃어 자물쇠가 아니다
 
해결 방법:
하나의 공통 자물쇠를 사용하면 된다
public void run() {
        int i = 0;
        while (true) {
            synchronized (r) {
                if (i % 2 == 0) {
                    r.name = "  ";
                    r.sex = " ";
                } else {
                    r.name = "  ";
                    r.sex = " ";
                }
                i++;
            }
        }
    }
    public void run() {
        while (true) {
            synchronized (r) {
                System.out.println(r.name + "..." + r.sex);
            }
        }
    }

이때는 정상적인 출력이죠.
그러나 아직도 문제가 존재한다. 우리가 바라는 것은 장삼과 이사가 엇갈려 나타나는 것이다. 하나는 장삼일 개, 이사는 지금도 무작위로 나타난다. 큰 영화의 장삼이나 이사는
 
 
해결 방법:
먼저 input 라인에 값을 부여한 다음에output 라인을 출력하고 입력 라인을 기다리게 합니다. 다시 값을 부여하는 것은 허용되지 않습니다. 출력 장삼이 끝난 후에 다시 값을 부여하는 것은 허용되지 않습니다. 순서대로 내려가세요.
입력 라인도 같은 방식이 필요합니다. 출력이 끝난 후에 기다려야 합니다.
이때 깨우기를 기다리는 메커니즘이 필요하다.
입력: 값을 부여한 후, 실행 방법wait () 를 영원히 기다립니다
출력: 인쇄 후, 다시 출력하기 전에 입력 notify () 를 깨우고,wait () 를 영원히 기다립니다
입력: 깨어난 후 값을 다시 부여하고 notify () 출력의 라인을 깨우고 자신이wait () 기다려야 합니다
차례차례 순환해 가다
 
코드 구현:
package demo;

public class Resource {
    public String name;
    public String sex;
    public boolean flag = false;
}
package demo;

public class Input implements Runnable {
    private Resource r;

    public Input(Resource r) {
        this.r = r;
    }

    public void run() {
        int i = 0;
        while (true) {
            synchronized (r) {
                if (r.flag) {
                    try {
                        r.wait();
                    } catch (Exception e) {
                    }
                }
                if (i % 2 == 0) {
                    r.name = "  ";
                    r.sex = " ";
                } else {
                    r.name = "  ";
                    r.sex = " ";
                }
                r.flag = true;
                r.notify();
            }
            i++;
        }
    }
}
package demo;

public class Output implements Runnable {
    private Resource r;

    public Output(Resource r) {
        this.r = r;
    }

    public void run() {
        while (true) {
            synchronized (r) {
                if (!r.flag) {
                    try {
                        r.wait();
                    } catch (Exception e) {
                    }
                }
                System.out.println(r.name + "..." + r.sex);
                r.flag = false;
                r.notify();
            }
        }
    }
}
package demo;

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

        Resource r = new Resource();

        Input in = new Input(r);
        Output out = new Output(r);
        Thread tin = new Thread(in);
        Thread tout = new Thread(out);

        tin.start();
        tout.start();
    }
}

이때 바로 장삼이사 가 엇갈려 출력하였다
완성

좋은 웹페이지 즐겨찾기