스레드를 만들려면 Thread
를 상속하거나 Thread(Runnable)
생성자에 Runnable
을 넘기는 방법이 있지만, 스레드 풀을 사용하는 방법도 있다.
스레드 풀은 대기 상태의 스레드를 유지해서 스레드 종료/생성 오버헤드를 줄입으로써, 많은 개수의 비동기 작업을 실행할 때 퍼포먼스를 향상시킨다. 게다가 스레드 풀은 스레드를 포함한 리소스를 제한하고 관리하는 방법도 제공한다. 백그라운드에서 할 작업이 많다면 스레드 풀 사용을 고려해보자
자바에서는 스레드 풀이 ThreadPoolExecutor 클래스로 구현되어 있다. 이제는 Deprecated된 AsyncTask도 내부적으로 ThreadPoolExecutor
를 사용하고 있다.
어떤 앱에서 콜렉션에 Thread를 담아 직접 스레드 풀을 만든 것을 본 적이 있는데, 결국 메모리 누수가 발견됐다. 다시 발명하지 말고 잘 만들어진 ThreadPoolExecutor를 사용하자!
ThreadPoolExcutor
에는 4개의 생성자가 있다. 그 중에서 ThreadFactory 파라미터를 뺀 세 번째 생성자를 위주로 살펴보자 !
파라미터 가운데에 corePoolSize
와 maximumPoolSize
는 풀에서 스레드의 기본 개수와 최대 개수를 정한 것이다.
스레드 풀의 스레드 개수가 corePoolSize
보다 커진다면, 초과하는 개수만큼의 태스크는 끝나고 나서 스레드를 유지할 필요는 없다.
Q. 그럼 corePoolSize만큼 코어스레드를 미리 생성하는 가?
-> 그렇지 않다
prestartCoreThread()
라는 메서드가 별도로 있고 필요할 때 호출하면 된다.
ThreadPoolExecutor
는 기본적으로 execute()
나 submit()
을 호출하는 순간에 작업 중인 스레드 개수가 corePoolSize보다 적으면 스레드를 새로 추가하는 형태이다.
keepAliveTime과 unit은 태스크가 종료될 때 바로 제거하지 않고 대기하는 시간이다.
보통 unit에는 TimeUnit.SECONDS / TimeUnit.MINUTES를 사용한다.
workQueue도 중요하다. 스레드 풀에서는 스레드를 corePoolSize 개수만큼 유지하려고 하고 추가로 요청이 들어오면 이 워크큐에 쌓는다.
workQueue에 쓸 수 있는 것은 3가지이다.
maximumPoolSize
가 될 때까지 스레드를 하나씩 추가해서 사용maximumPoolsize
값은 의미 xmaximumPoolSize
까지는 스레드를 생성해서 처리ThreadPoolExecutor
가 정지(shutdown)되거나, maximumPoolSize + workQueue 개수를 초과할 때는 태스크가 거부된다.
이 때 거부되는 방식을 정하는 것이 ThreadPoolExecutor
생성자의 마지막 파라미터인 RejectedExecutionHandlerhandler
고, ThreadPoolExecutor
의 내부 클래스에 미리 정의된 4개가 있다.
ThreadPoolExecutor.AbortPolicy
디폴트 handler로 RejectedExecutionException
런타임 예외를 발생시킨다.
ThreadPoolExecutor.CallerRunsPolicy
스레드를 생성하지 않고 태스크를 호출하는 스레드에서 바로 실행된다.
ThreadPoolExecutor.DiscardPolicy
태스크가 조용히 제거된다.
ThreadPoolExecutor.DiscardOldestPolicy
workQueue에서 가장 오래된 태스크를 제거한다.
앱에서 ThreadPoolExecutor
를 사용할 때 가장 쓸모있는 리젝션핸들러는 DiscardOldestPolicy다.
리스트뷰, 스크롤뷰, 뷰플립버, 페이저 등에서 화면을 스크롤하면서 이동할 때, 이미 지나가버린 화면보다 새로 보이는 화면이 상대적으로 중요하다. 이 정책을 사용하면 오래된 것을 workQueue
에서 제거하고 최신 태스크를 workQueue
에 추가한다.
지연/반복 작업에 대해서는 이 실행 클래스를 사용할 수 있다. 앞에서 핸들러를 이용해도 지연/반복 작업을 할 수 있다고 했다. 화면 갱신이라면 Handler를 쓰는 게 적절하지만 백그라운드 스레드에서 네트워크 통신이나 DB 작업 등이 지연/반복 실행되는 경우는 ScheduledThreadPoolExecutor
를 고려하는게 좋다.
반복/지연 작업의 다른 옵션으로 타이머를 생각할 수도 있지만 Timer API 문서에서도 ScheduledThreadPoolExecutor를 사용하도록 권장한다. Timer는 실시간 태스크 스케줄링을 보장하지 않고, 여러 스레드가 동기화없이 하나의 타이머를 공유하기 때문이다.
이 클래스는 maximumPoolsize
, keepAliveTime
, unit
, workQueue
는 빠져 있다.
이 빠져 있는 4개의 파라미터는 ScheduledThreadPoolExecutor에서 고정되어 있다.
DelayWorkQueue
인스턴스가 전달DelayWorkQueue의 기본 사이즈는 16인데, 태스크가 많아지면 제한 없이 사이즈가 커진다.
ThreadPoolExecutor, ScheduledThreadPoolExecutor는 직접 생성하는 것보다는 Executors
의 팩토리 메서드로 생성하는 경우가 많다. 팩토리 메서드 가운데서 용도에 맞는 게 없다면 결국 ThreadPoolExecutor, ScheduledThreadPoolExecutor 생성자를 직접 사용해야만 한다.
Executor부터 시작되는 클래스 다이어그램은 아래와 같다.
Executor에서 자주 쓰이는 팩토리 메서드들 또한 아래와 같다.
newFixedThreadPool(int nThreads)
nThreas
개수까지 스레드를 생성
workQeue 크기 제한 x
newCachedThreadPool()
필요할 때 스레드 생성하는데, 스레드 개수에는 제한 x
keepAliveTime
은 60초로 길다. -> 이 때문에 Cached라는 수식어가 붙음
newSingleThreadExecutor()
단일 스레드 사용해서 순차적 처리
workQeue 크기 제한 x
newScheduledThreadPool(int corePoolSize)
corePoolSize
개수의 ScheduledThreadPoolExecutor를 만든다.
<Next Step>