본문 바로가기

issue & tip

스레드

반응형

스레드

- 스레드 사전적 "끈을 구성하는 실"


멀티 스레드

- 복수의 스레드로 하나의 프로그램을 실행하는 기술

멀티 스레드를 사용하는 이유로 처리를 빠르게 하기 위해.


프로그램에서는 주로 논리적인 조작을 하면서 동시에 외부와 데이터를 주고받는 처리를 하게 된다.

그런데 외부와의 연계에서 대기 시간이 발생하는 경우가 있다.

기다리는 동안 다른일을 해두면 전체적인 처리 시간이 짧아진다.

예) 집안일(세탁기가 돌아가는 동안 청소기로 방을 청소하는 것)


단, 멀티 스레드로 처리한다고 뭐든지 빨라지는 것은 아니다.

프로그램을 실행하는 컴퓨터의 CPU 코어 수가 적으면 병렬 처리 스레드를 그만큼 만들수 없기 때문에 생각보다 빨라지지 않는다.

또한 처리하는 데이터양이 적은 경우도 마찬가지로 속도 향상을 기대할 수 없다.

그러므로 이용 상황을 잘 고려해서 꼭 멀티 스레드로 만들어야 할지 검토할 필요가 있다.



package com.eeswk.thread;


public class MultiThreadSample implements Runnable {


private static final String MSG_TEMPLATE =" 출력 중입니다.[%s][%d회째]";

private final String threadName;

public MultiThreadSample(String threadName) {

this.threadName = threadName;

}

@Override

public void run() {

for(int i = 1; i<100; i++) {

System.out.println(String.format(MSG_TEMPLATE, threadName, i));

}

}

public static void main(String[] args) {

MultiThreadSample runnable1 = new MultiThreadSample("thread1");

MultiThreadSample runnable2 = new MultiThreadSample("thread2");

MultiThreadSample runnable3 = new MultiThreadSample("thread3");

Thread thread1 = new Thread(runnable1);

Thread thread2 = new Thread(runnable2);

Thread thread3 = new Thread(runnable3);

thread1.start();

thread2.start();

thread3.start();

}


}


출력 순서가 매번 달라진다.

처리가 동시에 실행되기 때문이다.


Runnable 구현 클래스의 인스턴스를 바탕으로 Thread 인스턴스를 생성함으로써 스레드를 생성할 수 있다.

그리고 start 메서드로 스레드를 시작한다.

단순히 Runnable 인터페이스를 구현하고 run메서드를 실행해서는 멀티 스레드로 실행되지 않으니 주의한다.


또한 멀티 스레드를 구현하는데는 Thread 클래스를 상속하는 방법도 있다.

하지만 클래스의 구조를 단수하게 유지할 수 있으므로 일밙거으로 Runnable 인터페이스를 구현하는 방법을 사용한다.


더 복잡한 멀티 스레드 제어방법

스레드가 몇 개 만들어질지 정해지지 않는 프로그램은 너무 많은 스레드가 한번에 실행될 가능성이 있다.

그 결과 동작 중인 컴퓨터의 메모리 자원을 다 써버려서 처리를 계속할 수 없게 된다.


이런 경우 스레드 풀을 사용한다.


스레드 풀

-자바에서 스레드 풀이라는 기능을 제공한다.

스레드 풀이란 사용할 스레드를 제한된 수마늠 만들어두고 일정한 규칙에 따라 실행하는 기능으로 java.util.concurrent 패키지에서 제공한다.


인터페이스        설명

ExecutorService - 스레드 수를 제한하는 등 일저한 제한 아래에서 멀티 스레드 처리를 실행하기 위한 인터페이스

ScheduledExectorService - 일정 시간 후에 시작하고 일정 제한 아래에서 멀티 스레드 처리를 실행하기 위한 인터페이스


Executor 클래스의 주요 메서드

인터페이스

newsingleThreadExecutor - 싱글 스레드로 동작하는 ExecutorService의 인스턴스를 반환한다.

newFixedThreadPool - 지정한 최대 동시 실행 스레드 수로, ExecutorService의 인스턴스를 반환한다.

newScheduledThreadPool - 지정한 최대 동시 실행 스레드 수로, ScheduledExecutor의 인스턴스를 반환한다.


package com.eeswk.thread;


import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.TimeUnit;


public class ThreadPoolSample implements Runnable {

private static final String MSG_TEMPLATE =" 출력 중입니다.[%s][%d회째]";

private final String threadName;

public ThreadPoolSample(String threadName) {

this.threadName = threadName;

}


@Override

public void run() {

for (int i = 1; i < 100; i++) {

System.out.println(String.format(MSG_TEMPLATE, threadName, i));

}

}

public static void main(String[] args) {

ThreadPoolSample runnable1 = new ThreadPoolSample("thread1");

ThreadPoolSample runnable2 = new ThreadPoolSample("thread2");

ThreadPoolSample runnable3 = new ThreadPoolSample("thread3");

ExecutorService executorService = Executors.newFixedThreadPool(3);

executorService.execute(runnable1);

executorService.execute(runnable2);

executorService.execute(runnable3);

executorService.shutdown();

try {

if(!executorService.awaitTermination(5, TimeUnit.MINUTES)) {

//타임아웃 후에도 아직 실행이 끝나지 않았다.

executorService.shutdownNow();

}

} catch (InterruptedException e) {

//종료 대기시에 뭔가 오류가 발생했다.

e.printStackTrace();

executorService.shutdownNow();

}

}


}


스레드 풀을 newFixedThreadPool 메서드로 생성하고 있다.

3개의 스레드를 동시에 실행하므로 인수에도 이값을 지정한다.

또한 newFixedThreadPool 메서드의 인수를 1로 지정하면 실행수가 1이 되어 최초의 스레드부터 차례로 실행된다.

이 경우는 getSingleThreadExecutor 메서드로 ExecutorService의 인스턴스를 가져오는 것과 같다.

스레드는 execute 메서드로 시작하고 shutdown 메서드로 처리를 종료한다.

단, 그자리에서 종료하는 것은 아니고, 실행중인 스레드가 끝나야 종료 상태가 된다.

그래서 awaitTermination 메서드로 모든 스레드가 종료될때까지 대기 상태로 둔다.

5분이 경과하면 타임아웃(시간 종료)된다.


타임아웃되면 awaitTermination 메서드가 false를 반환한다.

타임아웃시점에 shutdownNow 메서드를 호출해서 실행중인 메서드가 있어도 스레드 전체를 강제 종료한다.

단, 일반적인 애플리케이션에서는(단순히 강제 종교하는 것이 아니라) 어덯게 대처할지 검토한 다음에 이상 시 처리를 기술할 필요가 있다.


또한 InterruptedException이 발생했을 때의 처리가 예외 클래스의 printstacktrace 메서드 호출과 shutdownNow메서드 호출로 되어있다.

이는 오류 정보를 출력하고 스레드를 강제 종료한다.


스레드 세이프란

- 스레드 세이프(스레드 안전성)란 멀티 스레드로 동작하는 프로그램에서 복수의 스레드로부터 호출되더라도 기대대로 동작하는 것을 가리킨다.


예) 세탁과 청소는 사용하는 기계가 다르니 동시에 해도 지정없다.

티셔츠와 스웨터를 동시에 세탁하는 경우,

일반적으로 스웨터와 티셔츠를 따로 세탁해야한다. 스레드 세이프 하지 않는 프로그램의 경우 이를 무시하고 티셔츠를 세탁하는 도중에 스웨터를 넣는다.

이처럼 동시에 뭔가 하려고 했는데, 기대한 결과가 돌아오지 않는 프로그램은 스레드 세이프하지 않는 것이다.

멀티 스레드 프로그램에서는 복수의 스레드가 인스턴스를 공유할 가능성이 있으므로 인스턴스가 어떤 때라도 기대한 대로 처리해줄 필요가 있다.


예) 세탁 도중에 뭔가 넣었을 때 세탁조에 빨래가 떨어지기 전에 확보하고, 현재 처리중인 세탁이 끝나기를 기다렸다가 나중에 넣은 빨래를 세탁하는 기능이 있다면 그 세탁기는 스레드 세이프하다고 할 수 있다.


스레드 세이프하지 않는 경우



package com.example.demo.thread;


import java.text.DateFormat;

import java.text.SimpleDateFormat;

import java.util.Calendar;

import java.util.Date;


public class UnsafeSample {

public static void main(String[] args) {

DateFormat unsafeDateFormat = new SimpleDateFormat("yyyy/MM/dd");

Calendar cal1 = Calendar.getInstance();

cal1.set(1989, Calendar.MARCH, 10);

Date date1 = cal1.getTime();

Calendar cal2 = Calendar.getInstance();

cal2.set(2020, Calendar.JUNE, 20);

Date date2 = cal2.getTime();

Thread thread1 = new Thread( ()-> {

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

try {

String result = unsafeDateFormat.format(date1);

System.out.println("Thread1: "+ result);

} catch (Exception e) {

e.printStackTrace();

break;

}

}

});

Thread thread2 = new Thread( ()-> {

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

try {

String result = unsafeDateFormat.format(date2);

System.out.println("Thread2: "+ result);

} catch (Exception e) {

e.printStackTrace();

break;

}

}

});

System.out.println("스레드 세이프하지 않는 프로그램의 검증을 시작");

thread1.start();

thread2.start();

}

}


SimpleDateFormat 클래스를 2개의 스레드에서 동시에 사용한다.

예기치 않는 결과가 확인된다.


SimpleDateFormat 클래스를 동시에 사용하려고 해서 예측하지 못한 결과를 반환했기 때문이다.



스레드 세이프한 프로그램


1.스레드마다 인스턴스를 작성하는 방법

-사용할 클래스가 스레드 세이프하지 않은 경우 javadoc에서는 해당 클래스가 동기화되지 않는다고 알려준다.

이런경우에는 스레드마다 인스턴스를 준비하는 방법이 있다.


SimpleDateFormat 클래스의 인스턴스를 스레드마다 만들어 공유하지 않게 하면된다.


또한 StringBuilder 클래스도 동기화되지 않는다.

하지만 이 클래스는 같은 기능을 제공하는 StringBuffer라는 동기화된 클래스가 있으므로 사용하는 클래스 자체를 변경하는 식의 대처 방법도 클래스에 따라서는 가능하다.

다만 동기화되면 그 만큼 처리 대기 시간이 발생하므로 처리가 느려질 수 있다.


package com.example.demo.thread;


import java.text.DateFormat;

import java.text.SimpleDateFormat;

import java.util.Calendar;

import java.util.Date;


public class SafeSample {

public static void main(String[] args) {

Calendar cal1 = Calendar.getInstance();

cal1.set(1989, Calendar.MARCH, 10);

Date date1 = cal1.getTime();

Calendar cal2 = Calendar.getInstance();

cal2.set(2020, Calendar.JUNE, 20);

Date date2 = cal2.getTime();

Thread thread1 = new Thread( ()-> {

//스레드별로 포맷을 준비한다.

DateFormat unsafeDateFormat = new SimpleDateFormat("yyyy/MM/dd");

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

try {

String result = unsafeDateFormat.format(date1);

System.out.println("Thread1: "+ result);

} catch (Exception e) {

e.printStackTrace();

break;

}

}

});

Thread thread2 = new Thread( ()-> {

//스레드별로 포맷을 준비한다.

DateFormat unsafeDateFormat = new SimpleDateFormat("yyyy/MM/dd");

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

try {

String result = unsafeDateFormat.format(date2);

System.out.println("Thread2: "+ result);

} catch (Exception e) {

e.printStackTrace();

break;

}

}

});

System.out.println("스레드 세이프해진 프로그램의 검증을 시작");

thread1.start();

thread2.start();

}

}


2. synchronized를 사용하는 방법

- synchronized로 지정한 곳을 배타 제어의 대상으로 할 수 있다.


package com.example.demo.thread;


import java.text.DateFormat;

import java.text.SimpleDateFormat;

import java.util.Calendar;

import java.util.Date;


public class SynchronizedSample {

public static void main(String[] args) {

DateFormat unsafeDateFormat = new SimpleDateFormat("yyyy/MM/dd");

Calendar cal1 = Calendar.getInstance();

cal1.set(1989, Calendar.MARCH, 10);

Date date1 = cal1.getTime();

Calendar cal2 = Calendar.getInstance();

cal2.set(2020, Calendar.JUNE, 20);

Date date2 = cal2.getTime();

Thread thread1 = new Thread( ()-> {

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

try {

String result;

synchronized (unsafeDateFormat) {

result = unsafeDateFormat.format(date1);

}

System.out.println("Thread1: "+ result);

} catch (Exception e) {

e.printStackTrace();

break;

}

}

});

Thread thread2 = new Thread( ()-> {

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

try {

String result;

synchronized (unsafeDateFormat) {

result = unsafeDateFormat.format(date2);

}

System.out.println("Thread2: "+ result);

} catch (Exception e) {

e.printStackTrace();

break;

}

}

});

System.out.println("스레드 세이프해진 프로그램의 검증을 시작");

thread1.start();

thread2.start();

}

}


synchronized의 인수에는 락을 걸 오브젝트를 지정한다.

복수의 스레드가 동작하지만 누군가 unsafeDateFormat 오브젝트를 사용하고 있을 때 다음 스레드는 먼저 사용하고 있는 스레드의 처리가 끝날 때까지 기다려야 한다.

그래서 결과적으로 처리 속도가 떨어지게 되는 것이다.


인스턴스를 스레드마다 두는 방식과 배타제어 방식중 어느쪽을 사용할지는 프로그램에 따라 다르다.

인스턴스를 스레드마다 가지면 그만큼 메모리를 많이 사용하게 된다.

그러므로 많은 스레드를 병행해서 동작시킬 경우 메모리가 부족해 버릴지도 모른다.

그런 경우에는 synchronized로 하나의 인스턴스를 잘 공유해서 스레드를 동작시키는 것도 좋다.

다소 느려지겠지만 제대로 처리할 수 없는 것보다 낫다.

synchronized를 메서드 수식자로 기술할 수도 있다.

이 경우 메서드 전체가 배타 제어의 대상이 된다.


public synchronized void methodName () {}


Atomic~ 클래스를 사용하는 방법

java.util.concurrent.atomic 패키지에 있는 AtomicInteger와 AtomicLong으로도 프로그램을 스레드 세이프한 구조로 만들수 있다.


예) 복수 스레드에서 하나의 변수에 1씩 더하는 프로그램

이런경우 늘 사용하는 int등의 기본형이 아니라 AtomicInteger와 같은 Atomic 계열 클래스로 올바른 값을 유지하며 변경할 수 있다.


package com.example.demo.thread;


import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.TimeUnit;


public class UnsafeIncrement implements Runnable {


public static int total = 0;

@Override

public void run() {

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

total++;

}

}

public static String getResult() {

return String.format("total = [%d]", total);

}

public static void main(String[] args) {

Runnable runnable1 = new UnsafeIncrement();

Runnable runnable2 = new UnsafeIncrement();

ExecutorService executorService = Executors.newFixedThreadPool(2);

executorService.execute(runnable1);

executorService.execute(runnable2);

executorService.shutdown();

try {

if(!executorService.awaitTermination(5, TimeUnit.MINUTES)){

executorService.shutdownNow();

}

} catch (Exception e) {

e.printStackTrace();

executorService.shutdownNow();

}

System.out.println(getResult());

}


}



결과

total = [17728]


멀티스레드로 동시에 하나의 수치를 변경하고자 했기  때문에 제대로 값을 증가하지 못한 경우가 몇번 있었다.


package com.example.demo.thread;


import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.atomic.AtomicInteger;


public class SafeIncrement implements Runnable {


public static AtomicInteger total = new AtomicInteger(0);

@Override

public void run() {

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

total.incrementAndGet();

}

}

public static String getResult() {

return String.format("total = [%d]", total.get());

}

public static void main(String[] args) {

Runnable runnable1 = new SafeIncrement();

Runnable runnable2 = new SafeIncrement();

ExecutorService executorService = Executors.newFixedThreadPool(2);

executorService.execute(runnable1);

executorService.execute(runnable2);

executorService.shutdown();

try {

if(!executorService.awaitTermination(5, TimeUnit.MINUTES)){

executorService.shutdownNow();

}

} catch (Exception e) {

e.printStackTrace();

executorService.shutdownNow();

}

System.out.println(getResult());

}


}


결과

total = [20000]


멀티 스레드로 하나의 변수를 변경하고자 할때 예상 밖의 값이 돌아오는 경우 Atomic 계열 클래스를 활용한다.


Stream API 병렬처리


자바8부터 추가된 함수형 인터페이스 기능

함수형 인터페이스는 자바 상에서 함수형 프로그래밍을 지원하는 기능이다.

함수형 프로그래밍은 프로그래밍 스타일의 하나로, 쉽게 말해 어떤 집합에 일정한 처리를 적용하는 사고방식을 바탕에 깐 프로그래밍 기법이다.

java.util.function 패키지로 제공한다.


함수형 프로그래밍

함수형 프로그래밍의 명확한 정의는 없지만 대략 선언형 프로그래밍의 일종으로 SQL에 가까운 부류의 프로그래밍 스타일을 가리킨다.

해법을 정의한 함수를 어떤 집합에 적용해 결과를 얻는 것이다.

자바는 명령형 프로그래밍 언어고 스타일은 다르지만, 상반된것도 아니므로 함께 사용할 수 있다.

함수평 프로그래밍의 도입은 오히려 서로의 장점을 살리는데 도움을 준다.



package com.example.demo.stream;


public class Item {


private String itemName;

private int price;

public Item(String itemName, int price) {

this.itemName = itemName;

this.price = price;

}


public String getItemName() {

return itemName;

}


public void setItemName(String itemName) {

this.itemName = itemName;

}


public int getPrice() {

return price;

}


public void setPrice(int price) {

this.price = price;

}

@Override

public String toString() {

return String.format("itemName=[%s] price=[%d]", this.itemName, this.price);

}

}



package com.example.demo.stream;


import java.util.ArrayList;

import java.util.Comparator;

import java.util.List;


public class FirstStreamSample {

public static void main(String[] args) {

List<Item> itemList = new ArrayList<>();

itemList.add(new Item("무",100));

itemList.add(new Item("두부",50));

itemList.add(new Item("달걀",210));

itemList.parallelStream()

//금액의 순서대로정렬

.sorted(Comparator.comparingInt(item-> item.getPrice()))

// 정렬한 결과를 출력

.forEachOrdered(item -> System.out.println(item));

}

}


List 오브젝트에서 parallelStream 메서드로 Stream을 가져왔다(Stream은 '흐름' 뜻으로 배열이나 리스트 등의 데이터 집합을 흐름으로 관리하는 오브젝트이다.) 그리고 sorted 메서드로 정렬한뒤 forEachOrdered로 하나씩 출력했다.


Stream API 처리는 Stream 생성에서 시작해 중간 조작을 거쳐 종단 조작으로 끝난다.


Stream  생성 -> 중간 조작 -> 종단 조작


Stream 생성 - Stream.of, Stream.generate, List.steam, List.parallelStream

중간 조작 - filter, map, sorted, skip 등

종단 조작 - forEach, forEachOrdered, toArray, min, max, count 등


Stream API와 람다식


stream과 parallelStream 메서드는 List를 바탕으로 Stream을 생성하는 메서드이다.

Stream은 한번 종단 조작까지 하면 재사용할 수 없다.

무명클래스로 해서 처음부터 인스턴스를 재사용할수 없게 기술해두는 편이 좋다.


sorted 메서드는 Stream의 내용을 정렬한다.

어떻게 정렬할찌는 인수의 Comparator 인터페이스에 달려있다.

Comparator의 정적 메서드 comparingInt의 인수에 상품 금액을 넘겨줌으로써 금액순으로 정렬을 한다.

sorted 메서드로 정렬된 순서를 유지하면서 forEachOrdered 메서드로 상품 클래스의 문자열 표현을 콘솔에 출력했다.


sorted와 forEachOrdered 메서드에서 -> 기호 앞에 item이라는 선언도 하지 않는 변수가 기술되어있다.

Stream에 있는 요소를 메서드 내에서 사용할 때 하는 선언방법으로, 사실은 형이 생략되어있다.

Stream안에 있는 요소의 형은 그때까지의 프로그램에서 유추할 수 있으므로 생략할 수 있다.



메서드참조

Stream API에서 람다식을 다른방법으로 기술할 수 있다. 메서드참조라는 기술방법이다.

sorted(Comparator.comparingInt(Item::getPrice))

.forEachOrdered(System.out::println);


람다식으로 하면 이해하기 쉽지만, 메서드 참조로 기술하면 장황한 기술이 사라지고 간결하게 쓸수 있다.

메서드 참조를 사용할 수 있는 경우

static 메서드

Stream을 실행하는 인스턴스의 메서드

함수형 인터페이스의 메서드


Stream API 주의할점

Stream API를 이용하면 특별히 의식하지 않고도 멀티 스레드가 처리된다는 특징이 있다.

따라서 예전의 스레드 처리보다 손쉽게 프로그램에 병렬 처리를 도입할 수 있따.

하지만 주의할 점이 있다.


실행순서

Stream API는 어떤 집합체에 일정한 처리를 적용할 때 사용할수 있다.

그중에 순서에 의미가 있는것도 있다.

처음부터 순서를 고려하여 forEachOrdered 메서드를 사용했다.

그럼 순서를 고려하지 않고 순수하게 병렬 실행하고 싶을경우

forEach(item -> System.out.println(item));


출력순서가 실행할때마다 바뀐다. 그리고 처리 속도가 빠르다.

순서를 유지할 필요가 없는 만큼 프로그램이 제어하는 수고가 줄어든다.

메서드를 조금 변경한 것만으로도 프로그램의 동작이 크게 바뀌므로 forEach 메서드와 forEachOrdered 메서드 어느쪽을 사용할지 주의할 필요가 있다.


고속화

Stream API인지 일반 멀티 스레드 처리인지와 관계없이 동작시킬 머신의 구성과 취급할 데이터양 등에 따라 성능 향상 정도가 달라진다.

예) 데이터 수가 너무 적으면 고속화의 덕을 봤는지 알수 없다.

그리고 Stream API를 사용해서 오히려 처리속도가 느려려지는 일도 있다.

Stream API에서는 기능을 이용하기 위해 내부적으로 클래스를 생성하는 등의 준비가 필요하다.

그러므로 for문 등으로 만드는 단순한 루프 처리보다 처리속도가 떨어지는 일도 있다.(오버헤드가 커서 오히려 느려진다.)

성능 문제는 애초 실행해보지 않으면 알수 없는 경우도 있다.

그러므로 프로그램을 작성하고 생각한 만틈 빠르지 않다고 생각햇을때는 실제로 처리 시간을 측정하고 구현을 재검토하는 작업이 필요하다.



출처-실무에서 바로 통하는 자바

반응형

'issue & tip' 카테고리의 다른 글

래퍼클래스와 제네릭  (0) 2018.04.24
부동소수점  (0) 2018.04.24
문자열과 날짜  (0) 2018.04.24
백세코딩#소프트웨어 개발의 기본  (0) 2018.04.16
백세코딩#코드리뷰  (0) 2018.04.16