Kotlin 스 레 드 동기 화 실현 방법

면접 을 볼 때 다 중 스 레 드 동기 화 에 대한 질문 을 자주 받는다.예 를 들 어:
이 어"현재 Task 1,Task 2 등 여러 병행 작업 을 수행 하고 있 는데,어떻게 모든 수행 이 완료 되면 Task 3 를 수행 할 수 있 을 까"라 고 덧 붙 였 다.
Kotlin 에서 우 리 는 여러 가지 실현 방식 이 있 습 니 다.본 고 는 이 모든 방식 을 정리 하고 소장 하 는 것 을 권장 합 니 다.
1. Thread.join
2. Synchronized
3. ReentrantLock
4. BlockingQueue
5. CountDownLatch
6. CyclicBarrier
7. CAS
8. Future
9. CompletableFuture
10. Rxjava
11. Coroutine
12. Flow
우 리 는 먼저 세 개의 Task 를 정의 하고 상기 장면 을 모 의 합 니 다.Task 3 는 Task 1,Task 2 를 바탕 으로 돌아 오 는 결과 연결 문자열 을 기반 으로 합 니 다.모든 Task 는 sleep 를 통 해 모 의 시간 을 소모 합 니 다.

val task1: () -> String = {
    sleep(2000)
    "Hello".also { println("task1 finished: $it") }
}

val task2: () -> String = {
    sleep(2000)
    "World".also { println("task2 finished: $it") }
}

val task3: (String, String) -> String = { p1, p2 ->
    sleep(2000)
    "$p1 $p2".also { println("task3 finished: $it") }
}

1. Thread.join()
Kotlin 호 환 자바,자바 의 모든 스 레 드 도 구 는 기본적으로 사용 할 수 있 습 니 다.그 중에서 가장 간단 한 스 레 드 동기 화 방식 은 Thread 의 join()을 사용 하 는 것 입 니 다.

@Test
fun test_join() {
    lateinit var s1: String
    lateinit var s2: String

    val t1 = Thread { s1 = task1() }
    val t2 = Thread { s2 = task2() }
    t1.start()
    t2.start()

    t1.join()
    t2.join()
    
    task3(s1, s2)

}

2. Synchronized
synchronized 자 물 쇠 를 사용 하여 동기 화

 @Test
    fun test_synchrnoized() {
        lateinit var s1: String
        lateinit var s2: String

        Thread {
            synchronized(Unit) {
                s1 = task1()
            }
        }.start()
        s2 = task2()

        synchronized(Unit) {
            task3(s1, s2)
        }
    }
그러나 세 개의 작업 이 넘 으 면 synchrnoized 라 는 쓰기 가 어색 합 니 다.여러 병렬 작업 의 결 과 를 동기 화하 기 위해 서 는 n 개의 자 물 쇠 를 설명 하고 n 개의 synchronized 를 끼 워 넣 어야 합 니 다.
3. ReentrantLock
ReentrantLock 은 JUC 가 제공 하 는 스 레 드 잠 금 으로 synchronized 를 교체 하여 사용 할 수 있 습 니 다.

 @Test
    fun test_ReentrantLock() {

        lateinit var s1: String
        lateinit var s2: String

        val lock = ReentrantLock()
        Thread {
            lock.lock()
            s1 = task1()
            lock.unlock()
        }.start()
        s2 = task2()

        lock.lock()
        task3(s1, s2)
        lock.unlock()
    }
ReentrantLock 의 장점 은 여러 개의 병렬 작업 이 있 을 때 synchrnoized 가 내장 되 어 있 는 문제 가 발생 하지 않 지만 여러 개의 lock 관리 가 다른 작업 을 만들어 야 한 다 는 것 입 니 다.
4. BlockingQueue
차단 대기 열 내부 도 Lock 을 통 해 이 루어 지기 때문에 동기 화 잠 금 효과 도 얻 을 수 있 습 니 다.

 @Test
    fun test_blockingQueue() {

        lateinit var s1: String
        lateinit var s2: String

        val queue = SynchronousQueue<Unit>()

        Thread {
            s1 = task1()
            queue.put(Unit)
        }.start()

        s2 = task2()

        queue.take()
        task3(s1, s2)
    }
물론 차단 대기 열 은 생산/소비 장면 에서 의 동기 화 에 더 많이 사용 된다.
5. CountDownLatch
JUC 의 자 물 쇠 는 대부분 AQS 를 기반 으로 이 루어 지 며 독점 자물쇠 와 공유 자물쇠 로 나 눌 수 있다.ReentrantLock 은 일종 의 독점 자물쇠 다.이에 비해 공유 자 물 쇠 는 이 장면 에 더욱 적합 하 다.예 를 들 어 Countdown Latch 는 다른 스 레 드 가 모두 실 행 될 때 까지 스 레 드 를 차단 할 수 있 습 니 다.

 @Test
    fun test_countdownlatch() {

        lateinit var s1: String
        lateinit var s2: String
        val cd = CountDownLatch(2)
        Thread() {
            s1 = task1()
            cd.countDown()
        }.start()

        Thread() {
            s2 = task2()
            cd.countDown()
        }.start()

        cd.await()
        task3(s1, s2)
    }
공유 자물쇠 의 장점 은 모든 작업 을 위해 별도의 자 물 쇠 를 만 들 필요 가 없 으 며,아무리 많은 병행 작업 을 해도 쉽게 쓸 수 있다 는 것 이다.
6. CyclicBarrier
Cyclic Barrier 는 JUC 가 제공 하 는 또 다른 공유 잠 금 장치 로 한 스 레 드 가 동기 점 에 도착 한 후에 다시 함께 실행 할 수 있 습 니 다.그 중에서 임의의 스 레 드 가 동기 점 에 이 르 지 못 하면 다른 스 레 드 가 모두 막 힐 수 있 습 니 다.
CountDownlatch 와 차이 점 은 CountDownlatch 가 일회 성 이 며,Cyclic Barrier 는 리 셋 후 중복 사용 할 수 있 습 니 다.이것 이 바로 Cyclic 의 명명 유래 로 순환 적 으로 사용 할 수 있 습 니 다.

 @Test
    fun test_CyclicBarrier() {

        lateinit var s1: String
        lateinit var s2: String
        val cb = CyclicBarrier(3)

        Thread {
            s1 = task1()
            cb.await()
        }.start()

        Thread() {
            s2 = task1()
            cb.await()
        }.start()

        cb.await()
        task3(s1, s2)
    }
7. CAS
AQS 내 부 는 자 회전 자 물 쇠 를 통 해 동기 화 되 고 자 회전 자물쇠 의 본질은 CompareAndSwap 을 이용 하여 스 레 드 가 막 히 는 비용 을 피 하 는 것 이다.
따라서 우 리 는 CAS 에 기반 한 원자 계 수 를 사용 하여 잠 금 없 는 조작 을 실현 하 는 목적 을 달성 할 수 있다.

  @Test
    fun test_cas() {

        lateinit var s1: String
        lateinit var s2: String

        val cas = AtomicInteger(2)

        Thread {
            s1 = task1()
            cas.getAndDecrement()
        }.start()

        Thread {
            s2 = task2()
            cas.getAndDecrement()
        }.start()

        while (cas.get() != 0) {}

        task3(s1, s2)
    }
while 순환 공전 은 자원 을 낭비 하 는 것 처럼 보이 지만 자 회전 자물쇠 의 본질은 이 렇 기 때문에 CAS 는 일부 cpu 밀집 형의 짧 은 작업 동기 화 에 만 적용 된다.
volatile
CAS 의 잠 금 없 는 실현 을 보면 많은 사람들 이 volatile 을 생각 할 것 이다.잠 금 없 는 스 레 드 안전 도 실현 할 수 있 을 까?

  @Test
    fun test_Volatile() {
        lateinit var s1: String
        lateinit var s2: String

        Thread {
            s1 = task1()
            cnt--
        }.start()

        Thread {
            s2 = task2()
            cnt--
        }.start()

        while (cnt != 0) {
        }

        task3(s1, s2)
    }
주의 하 세 요.이런 문법 은 잘못된 것 입 니 다.
volatile 은 가시 성 을 보장 할 수 있 으 나 원자 성 을 보장 할 수 없습니다.cnct-스 레 드 안전 이 아니 므 로 잠 금 작업 이 필요 합 니 다.
8. Future
위 에 자물쇠 조작 이 있 든 없 든 두 변 수 를 정의 해 야 합 니 다.s1,s2 기록 결 과 는 매우 불편 합 니 다.
자바 1.5 를 시작 으로 Callable 과 Future 를 제공 하여 작업 수행 이 끝 날 때 결 과 를 되 돌 릴 수 있 습 니 다.

@Test
fun test_future() {

    val future1 = FutureTask(Callable(task1))
    val future2 = FutureTask(Callable(task2))

    Executors.newCachedThreadPool().execute(future1)
    Executors.newCachedThreadPool().execute(future2)

    task3(future1.get(), future2.get())

}
future.get()을 통 해 결과 가 돌아 오 기 를 동시에 기다 릴 수 있어 쓰기 에 매우 편리 합 니 다.
9. CompletableFuture
future.get()은 편리 하지만 스 레 드 를 막 을 수 있 습 니 다.Java 8 에 CompletableFuture 가 도입 되 었 습 니 다.  ,퓨 처 인 터 페 이 스 를 구현 하면 서 컴 플 리 케 이 션 스테이지 인 터 페 이 스 를 구현 했다.Complete bleFuture 는 여러 개의 Complete Stage 를 논리 적 으로 조합 하여 복잡 한 비동기 프로 그래 밍 을 실현 할 수 있다.이러한 논리 적 조합 방법 은 리 셋 형식 으로 라인 의 막힘 을 피 했다.

@Test
fun test_CompletableFuture() {
    CompletableFuture.supplyAsync(task1)
        .thenCombine(CompletableFuture.supplyAsync(task2)) { p1, p2 ->
             task3(p1, p2)
        }.join()
}
10. RxJava
RxJava 가 제공 하 는 각종 연산 자 와 스 레 드 전환 능력 역시 우리 가 수 요 를 실현 하 는 데 도움 을 줄 수 있다.
zip 연산 자 는 두 개의 Observable 결 과 를 조합 할 수 있 습 니 다.subscribeOn 은 비동기 작업 을 시작 하 는 데 사 용 됩 니 다.

@Test
fun test_Rxjava() {

    Observable.zip(
        Observable.fromCallable(Callable(task1))
            .subscribeOn(Schedulers.newThread()),
        Observable.fromCallable(Callable(task2))
            .subscribeOn(Schedulers.newThread()),
        BiFunction(task3)
    ).test().awaitTerminalEvent()

}

11. Coroutine
앞에서 그렇게 많이 말 했 는데 사실은 모두 자바 의 도구 이다.Coroutine 은 마침내 Kotlin 특유 의 도구 라 고 할 수 있 습 니 다.

@Test
fun test_coroutine() {

    runBlocking {
        val c1 = async(Dispatchers.IO) {
            task1()
        }

        val c2 = async(Dispatchers.IO) {
            task2()
        }

        task3(c1.await(), c2.await())
    }
}

쓰기 에 매우 편 해서 앞의 각종 공구 의 장점 을 한 몸 에 모 았 다 고 할 수 있다.
12. Flow
Flow 는 바로 Corotine 버 전의 RxJava 로 RxJava 의 연산 자 를 많이 가지 고 있 습 니 다.예 를 들 어 zip:

@Test
fun test_flow() {

    val flow1 = flow<String> { emit(task1()) }
    val flow2 = flow<String> { emit(task2()) }
        
    runBlocking {
         flow1.zip(flow2) { t1, t2 ->
             task3(t1, t2)
        }.flowOn(Dispatchers.IO)
        .collect()
    }
}

flow on 은 Task 를 비동기 로 계산 하고 결 과 를 발사 합 니 다.
총결산
위의 이렇게 많은 방식 은 회향 두 의'희'자의 네 가지 표기 법 처럼 모두 파악 할 필요 가 없다.결론 적 으로 Kotlin 에서 가장 좋 은 스 레 드 동기 화 방안 의 첫 번 째 추진 과정!
여기 서 Kotlin 스 레 드 동기 화 에 관 한 몇 가지 실현 방법 에 관 한 글 은 여기까지 소개 되 었 습 니 다.더 많은 Kotlin 스 레 드 동기 화 내용 은 예전 의 글 을 검색 하거나 아래 의 관련 글 을 계속 찾 아 보 세 요.앞으로 저 희 를 많이 사랑 해 주세요!

좋은 웹페이지 즐겨찾기