규칙 73 - 스레드 그룹은 피하라.

JAVA/Effective Java|2018. 6. 15. 23:33

스레드 그룹(thread group)은 원래 applet을 격리시켜 보안문제 해결할 목적으로 만들어졌으나 성공하지 못했다.

그러면 이런 스레드 그룹은 왜 남아있는가? 아예 쓸곳이 없는가? 그렇지는 않다. 스레드 기본연산을 여러 스레드에 동시에 적용할 수 있도록 하는 기능을 가지고 있다. 하지만 대부분이 deprecated 되었다.

결론을 이야기하자면 이미 다 페기가 되어버린 기능이다. 그렇기 때문에 신경쓸 것 없이 사용하지 말아야한다.

 

 

출처 : 조슈아  블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1)

댓글()

규칙 72 - 스레드 스케줄러에 의존하지마라.

JAVA/Effective Java|2018. 6. 15. 23:26

실행해야 할 스레드가 많을 경우 어떠한 스레드를 얼마나 오랫동안 실행할지 결정은 스레드 스케줄러가 진행한다.

운영체제마다 스레드 스케줄러는 다르기 때문에 아무리 운영체제에서 효율적으로 진행한다고 하더라고 이에 의존하여 프로그램을 제작해서는 안된다.

정확하고 좋은 스레드 프로그램은 의존하는것이 아니라 실행가능한 스레드의 개수가 프로세수 개수보다 넘지 않도록 제작하는 것이다.

그렇게되면 스레드 스케줄러가 순차적으로 스레드를 실행시켜줄뿐 정책에 신경쓰지않는다.

그렇다면 실행중인 스레드의 개수를 최대한 줄일 수 있는 방법은 무엇일까?

바로 사용하지 않는 스레드는 실행하지 않고 정지하거나 종료해야한다. 그래서 바로 직전에 공부했던 스레드 풀을 사용하여 적절하게 스레드를 관리하면 좋은 프로그램을 만들 수 있다.

그렇다면 오래 반환하지 않고 잘못된 스레드 방식으로 개발하는 코드를 알아보자.

public class FailLatch {
	private int count;
	public FailLatch (int count) {
		if (count < 0) {
			throw new IllegalArgumentException(count + " < 0");
		}
		this.count = count;
	}
	
	public void await() {
		while (true) {
			synchronized (this) {
				if (count == 0) {
					System.out.println("the end");
					return;
				}
					
			}
		}
	}
	
	public synchronized void countDown() {
		if (count != 0) {
			count--;
		}
	}

}

 위의 코드를 보면 count가 0일 때까지 스레드가 놀고있어야 하는 아주 좋지 않은 프로그램이 된다. 만약 그럼 저상태에서 대기상태에서 Thread.yield를 사용한다면 조금 나아지려나?

그렇지 않다. 왜냐하면 이는 일부 JVM에서는 성능이 향상되는 것처럼 보일 수 있으나, 무조건 좋아지지 않는다. 그렇기 때문에 병렬적으로 실행 가능한 스레드 수를 애초에 줄이는것이 중요하다.

결론을 이야기하자면 프로그램의 정확성을 스레드 스케줄러에 의존하지말고 쓰레드 수를 제한하고 일부 쓰레드가 무조건 낭비되구 있는것을 방지하자. 단 Thread.yield 등과 같이 임시방편으로 무엇을 해결하려 하지 말고 근본적 문제를 해결하라.

 

출처 : 조슈아  블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1)

 

댓글()

규칙 71 - 초기화 지연은 신중하게 하라

JAVA/Effective Java|2018. 6. 14. 18:53

Lazy initialization(초기화 지연)은 필드 초기화를 실제로 그 값이 쓰일 때까지 미루는 것이다.


대부분 초기화 지연의 이유는 초기화의 비용이 증가하고 사용빈도가 특별한 경우에 사용하는 필드에 대해서 그렇게 적용한다.


만약 그렇지 않은 경우에도 초기화 지연을 사용하면 어떨까?

이럴 경우 클래스를 초기화하고 객체를 생성하는 비용은 줄이지만 필드 사용 비용은 증가시킨다.


그럼 동기화가 필요한 다중 스레드 환경에서는 초기화 지연은 어떻게 구현해야할까? 생각만 해도 어렵다. 


몇가지 방법을 살펴보자.

우선 초기화 지연을 사용하지 않고 진행하는 일반적인 초기화는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
 
public class TestClass {
    // 일반적인 초기화 기법
    // 클래스가 처음 로드될 때 바로 초기화
    private final int val = getValue();
    
    private int getValue() {
        return 0;
    }
}
 
cs


이 상태에서 동기화를 적용시키면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
 
public class TestClass {
    // 일반적인 초기화 기법
    // 클래스가 처음 로드될 때 바로 초기화
    private int val;
    
    synchronized int getValue() {
        return 0;
    }
}
 
cs


그렇다면 정적 필드의 초기화를 지연시키고 싶다면 어떻게 해야할까?

이럴때는 Lazy initialization holder class 숙어를 적용하면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestClass {
    
    private static class ValueHolder {
        static final int value = getValue(); 
    }
    
    static int getData() {
        return ValueHolder.value;
    }
    
    private static int getValue() {
        return 0;
    }
}
 
cs


처음 getData()가 호출되는 순간에 getValue()가 호출되면서 초기화가 진행된다. 이럴경우 synchronized를 붙혀주지 않아도된다. 왜냐하면 최신 VM은 클래스를 초기화하기위한 필드 접근은 동기화를 진행한다.


그리고 필드를 검사를 진행할 때 무조건 적으로 동기화 하는 것보다 다음과 같이 이중검사를 사용하는 것이 좋다.


이중검사란 무엇인가? 락을 먼저 걸고 확인하면 무조건 락을 걸어야해서 비용이 증가하지만, 먼저 락없이 한번 확인하고 진행하기 때문에 부담이 줄어든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
String getField() {
        String result = field;
        // 첫 번재 검사 (락 없이 진행)
        if (null == result) {
            // 두 번재 검사  (락을 사용하여 진행)
            synchronized(this) {
                result = field;
                if (result == null) {
                    field = result = getData();
                }
            }
        }
        return result;
    }
    
    String getData() {
        return "dbs";
    }
cs


위 코드에서 result를 사용하여 field값을 대입한 이유는 field 값을 한번만 읽게 하였기에 동기화 사용시에도 비용이 감소된다. 저수준 병렬 프로그래밍에서 25%가량 향상된것을 볼 수있다.


만약 여러번 초기화 되어도 크게 무리가 없는 필드에 경우에는 락을 거는 로직을 빼고 단일 검사만 진행해도 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
String getField() {
    String result = field;
    // 첫 번재 검사 (락 없이 진행)
    if (null == result) {
        field = result = getData();
    }
    return result;
}
    
String getData() {
    return "dbs";
}
cs



결론은 필요없는 필드는 초기화 지연을 사용하지 말자. 비용이 커진다. 만약 비용이 큰 초기화 방법을 적절하게 변경하고 싶은경우 위에 소개한 방식대로 초기화를 진행하라.

댓글()

규칙 70 - 스레드 안전성에 대해문서로 남겨라.

JAVA/Effective Java|2018. 6. 14. 08:39

클래스를 사용할 때 클래스의 객체와 정적 메서드가 병렬적으로 이용되었을 때, 어떠한 부작용이 있을 수 있는지 안전한지에 대한 정보가 없으면 추후에 큰 문제를 야기할 수있다.


JavaDoc에서 synchronized 키워드를 통해 병렬설 지원 여부를 확인할 수있다고 알고 있으나 실상 그렇지 않다.


왜냐하면 Javadoc이 만드는 문서에는 Javadoc이 들어가지 않는다. 왜냐하면 synchronized 키워드는 메서드의 구현 상세에 해당하는 정보이며, 공개 API의 일부가 아니기 때문이다.


그렇기 때문에 synchronized 키워드를 통해 판단해서는 안되고 병렬적으로 사용해도 되는지의 여부는 문서에 남겨져 있어야 한다.


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 

댓글()

규칙 69 - wait나 notify 대신 병행성 유틸리티를 이용하라.

JAVA/Effective Java|2018. 6. 13. 09:52

멀티 쓰레드 환경에서 wait와 notify를 사용할 경우에 주의가 필요하다. 하지만 그것을 효율적으로 사용하기에는 많은 어려움이 따른다.


그렇기 위해서 wait와 notify를 정확하게 사용하기 위해서는 high-level util(고수준 유틸리티)을 사용해야 한다.

이런 고수준 유틸리티들은 Executor, Concurrent Colloection, Synchronizer를 통해 사용할 수 있다.

그중 Conncurrent Collenction (병행 컬렉션)에 대해 알아보자. 대부분에 컬렉션 Map, List, Queue등은 병행 컬렉션을 제공한다. 



병행성 컬렉션

대표적으로 Map에서 제공하는 ConcurrentMap이다. 그중 putIfAbsent(key, value) 메서드가 대표적이다. 이 메서드는 키에 해당하는 값이 없을 경우에만 주어진 값을 넣고, 해당 키에 저장되어 있었던 기존의 값을 반환한다. 

속도도 일반 Map과 큰 차이가 없다. 그렇기 때문에 병행성 처리가 가능한 Map을 사용하도록 하자.



Wait와 notify, notifyAll 주의사항 및 사용법

wait를 통해 특정 스레드가 대기를 하고자 할 때는 다음과 같이 표준적 숙어를 사용해야 한다. (일반적으로 만들어진 규칙)

1
2
3
4
5
6
// wait 메서드를 사용하는 표준적 숙어
synchronized (obj) {
  while (<조건이 만족 되지 않을경우 순환문 실행>)
     obj.wait();
 
}
cs

왜냐하면 조건이 만족되지 않았는데 wait가 호출되어 스레드가 대기하게 되고 잘못된 코드로 인해 notify가 호출된 뒤에 wait가 호출된다거나 하는 경우에 영원히 깨어나지 못하는 생존오류가 발생할 수 있기 때문이다.


그래서 다른 스레드를 깨울때 notify 메서드를 사용하지만 notifyAll 메서드를 사용해서 일어나지 못한 다른 스레드를 깨워주는 것이 좋다. (notify 보다 무조건 notifyAll을 사용하라!)

하지만 그럴경우에 깨어날 필요가 없는 쓰레드를 깨우지만 프로그램의 정확성에는 영향을 끼치지 않는다. 왜냐면 위에 코드에서 while문에 보면 알겠지만 만약 조건이 만족이 되지 않으면 본인이 알아서 다시 wait 상태로 돌아가기 때문이다.


-> wait 사용시 숙어를 사용하고 notify대신 notifyAll을 사용하여 일어나지 못한 스레드를 깨워줘라.



시간 체크 메서드

마지막으로 병행성 처리에서 시간은 굉장히 예민하기 때문에 시간값이 필요로 할때는 System.currentTimeMillis 대신 System.nanoTime을 사용해야 한다.


두 개의 메소드를 비교한 내용을 보자.

  • currentTimeMillis()
    • 날짜와 관련한 시각 계산을 위해 사용(ms 단위)
    • 시스템 시간을 참조(외부데이터)
    • 일반적인 업데이트 간격은 인터럽트의 타이머(10microsec)에 따름
    • GetSystemTimeAsFileTime 메서드로 구현되어 있으며, 운영체제가 관리하는 시간값을 가져옴
  • nanoTime()
    • 기준 시점에서 경과 시간을 측정하는데 사용(ns 단위)
    • 시스템 시간과 무관
    • QueryPerformanceCounter/QueryPerformanceFrequency API로 구현되어 있음.

그렇기 때문에 정확한 시간 계산을 위해서 nanoTime()을 사용해야 한다.




정리하자면 병행 콜렉션과 같은 고수준 유틸리티를 사용하면 wait, notify를 직접적으로 사용할 이유가 많이 줄어들고 안정적으로 코딩할 수 있다. 


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙69

댓글()
  1. Favicon of https://wedul.site BlogIcon 위들 wedul 2018.06.13 11:08 신고 댓글주소  수정/삭제  댓글쓰기

    http://wedul.tistory.com/m/15 참고

규칙 68 - 스레드보다는 실행자와 태스크를 이용하라.

JAVA/Effective Java|2018. 6. 13. 09:35

여러 쓰레드를 실행해야할 때,  큐에 넣고 작업을 진행하거나 할 수 있으면 더욱 효율적으로 관리 할 수있다.


그래서 자바 1.5부터 자바 플랫폼에는 java.util.concurrent가 추가되었다. 이 패키지에는 Executor Framework가 들어 있는데 이는 인터페이스 기반 task 실행 프레임워크이다.


해당 Executor를 실행하기 위해서는 다음과 같이 입력하면 된다.

1
2
3
4
5
6
7
ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.    
    @Override
    public void run() {
        System.out.println("dbsafer");
    }
});
cs


그리고 만약 executor안에 요소가 모두 실행이 끝난뒤에는 실행자가 꺼지지는 않는다.

이를 명시적으로 꺼주어야 하는데 그때 shutdown() 메소드를 이용하면 된다.


이른 기능을 제외하고도 임의에 task의 작업을 기다릴 수도 있고 task가 끝날때마다 정보를 가져오도록 할수도 있다. 


여러 작업을 관리해야하는 경우에는 ThreadPool을 만들어서 작업할 수 있다. 이런 작업을 제공하는 ThreadPool은 newCachedThreadPool과 newFixedThreadPool이 있다.


먼저 newCachedThreadPool은 부하가 크지않고 작은 프로그램에서 사용하기에 적합하다. 설정도 필요없고 보통 많은일을 알아서 처리한다. 하지만 작업량이 많은곳에서는 적합하지 않다. 

왜냐하면 해당 쓰레드풀은 작업이 큐에 들어가는 것이 아니라 실행을 담당하는 스레드에 바로 넘겨진다. 그렇기 때문에 task가 많이 지정될 경우에는 상당히 많은 양의 쓰레드가 생성이 되어 CPU의 사용량이 증가된다.


그렇기 때문에 이를 보안하는 ThreadPool이 있는데 newFixedThreadPool이다.


newFixedThreadPool은 스레드 개수가 고정된 풀을 만들어서 제어가 손쉽다.


이런 실행자들을 이용하여 쓰레드를 관리하면 별도의 개별쓰레드를 만들어서 관리하는 것보다 훨씬 편하고 안정적이다.


  출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙68



댓글()

규칙 67 - 과도한 동기화는 피하라

JAVA/Effective Java|2018. 6. 10. 21:17

동기화 시에 너무 많은 동기화 블록을 사용할 경우에 데드락이 걸리거나 성능저하 등등 문제를 일으킬 수 있는 소지들이 몇 가지 있다.


특히 동기화 영역안에서 수행되는 작업의 양을 가능한 줄여야 한다.


자바에서는 동기화에 대한 비용처리가 그나마 잘되어있지만 잘 사용해야 하는 이유는 잘못된 동기화 사용은 각 쓰레드들의 메인 메모리 접근에 대한 지연시간을 늘릴 수 있기 때문에 비용이 증가할 수 있다.


또한 클래스 내에서 동기화를 수행하는 것이 외부에서 객체 호출 시 사용하는것 보다 높은 병행성을 달성 할 수 있을 때문 진행해야한다.


다시말하자면 필요할 때 해당 메서드등을 호출하여 동기화를 실행해야지 해당 메서드 자체를 동기화 하는것은 좋지 않다. 예를 들면 기존에는 StringBuffer를 사용하여 내부적으로 Thread-safe하게 동작하여 많이 사용하였지만 굳이 병렬성을 보장하지 않아도 되는경우에도 자주 사용하여 문제가 자주 발생했다.


그래서 StringBuilder이 생겼고, 필요시에 동기화 블록을 만들어서 Thread safe하게 처리를 하였다.


그리고 


static 필드를 변경하는 메서드가 있을 때는 해당 필드에 대한 접근을 반드시 동기화 해야한다. 왜냐하면 해당 필드에 대해서 다른 쓰레드가 접근하고 있는지에 대해 알 수 있는 방법이 없기 때문이다. 


요약하자면 주의해서 동기화 블록을 사용하고 해당 내용에 대해서 문서에 정확히 적어주어야 한다.

문서에 해당내용을 잘 적지 않는 개발자들이 많다. 잘 적지 않고 알아서 코드를 보고 분석해서 그때그때 사용하기를 원하는 사람들을 일을 하면서 많이 보았다.


물론 같은 개발자로서 코드로 이야기 한다는 명목안에서 그럴 수 있지만, 일에 대한 처리가 순조롭지 않다. 그렇다면 본인들은 개발을 진행할 때 필요한 라이브러리가 있을 때 Document를 보지 않고, 코드를 모두 분석 후 사용하는지 묻고 싶다.. 


협력하는 시대에 협력하지 않고 구태적 마인드를 가지고 있다면 그는 효울적인 자바코드를 사용하는 근본이 잘못되었다고 생각한다.


 출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙67

댓글()

규칙 66 - 변경 가능 공유 데이터에 대한 접근은 동기화하라.

JAVA/Effective Java|2018. 6. 10. 12:48

우리는 동시에 사용이 가능한 객체에 대해서 synchronized키워드를 사용하여 락을 걸어 코드 블록을 한번에 하나의 스레드만 접근할 수 있도록 한다.


이런 동기화를 적절하게 사용하면 모든 메서드가 항상 객체의 일관된 상태만 바라보도록 할 수 있다.


하지만 동기화 없이는 한 스레드가 만든 변화를 다른 스레드가 확인할 수 없다. 

동기화는 스레드가 일관성이 깨진 객체를 관측할 수 없도록 할 뿐 아니라, 동기화 메서드나 동기화 블록에 진입한 스레드가 동일한 락의 보호 아래 이루어진 변경의 영향을 관측할 수 있도록 보장한다.


그렇기 때문에 특정 객체의 값을 다른 쓰레드가 읽기를 원한다면 동기화를 무조건 진행해야한다.

즉, 변경 가능한 공유 데이터에 대한 접근을 동기화 해야한다.


다음 예를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.concurrent.TimeUnit;
 
public class Main {
    private static boolean stopRequested;
    
    public static void main(String args[]) throws InterruptedException {
        
        Thread backgroundThreat = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    System.out.println(i);
                    i++;
                }
            }
        });
        
        backgroundThreat.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
    
}
 
cs


 예상을 한다면 1초 후에 프로그램의 반복이 종료될 것 같으나, 종료되지 않는다. 왜그럴까? 이는 메인 Thread에서 stopRequest의 값을 변경하였어도 backgroundThreat가 언제 그 변경된 값을 인지 할지는 알 수 없기 때문이다. 그래서 정확하게 1초에 종료가 되지 않는다.

-> main Thread에서 stopRequest에 값을 변경하여 메모리에 값을 변경하려고 하는 순간에 backgroundThreat 스레드에서는 그 전에 상태를 읽어가기 때문이다.

-> 이는 JVM에서 호이스팅을 하는 작업때문이다. (자세히는 나도 모르겠다.)

-> 이를 위해서는 동기화작업이 필요하다.


 그래서 이렇게 변경되는 객체에 대해서 정확하게 공유하고 싶을 때는 변경되는 객체에 대해서도 동기화 처리를 해주어야 한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.concurrent.TimeUnit;
 
public class Main {
    private static boolean stopRequested;
    
    public static synchronized void requestStop() {
        stopRequested = true;
    }
    
    public static synchronized boolean checkRequestStop() {
        return stopRequested;
    }
    
    public static void main(String args[]) throws InterruptedException {
        
        Thread backgroundThreat = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!checkRequestStop()) {
                    System.out.println(i);
                    i++;
                }
            }
        });
        
        backgroundThreat.start();
        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
    
}
 
cs

읽기 그리고 쓰기 메서드인 requestStop(), checkRequestStop() 메서드를 모두 동기화 처리 해주었다. 쓰는 작업과 읽는 작업이 모두 동기화가 되지 않으면 사실 동기화 작업은 의미가 없다.


이렇게 해결하는 방법은 비용이 크지 않지만 그래도 비용을 조금이라도 더 줄일 수 있는 방법이 있다.

바로 stopRequested를 volatile로 선언하는 것이다. 그러면 락없이도 사용할 수 있다. volatile은 상호 배제성은 해소되지 않지만 어떤 스레드에서 값에 접근할 때 가장 최근에 기록된 값을 읽도록 할 수있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.concurrent.TimeUnit;
 
public class Main {
    private volatile static boolean stopRequested;
    
    public static void main(String args[]) throws InterruptedException {
        
        Thread backgroundThreat = new Thread(new Runnable() {
            @Override
            public void run() {
                int i = 0;
                while (!stopRequested) {
                    System.out.println(i);
                    i++;
                }
            }
        });
        
        backgroundThreat.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
    
}
 
cs


volatile에 대한 설명은 이 블로그에 자세히 나와있다.

(각 쓰레드에서 데이터를 각 cpu의 캐시에서 데이터를 읽어가기 때문에 한쪽 쓰레드에서 변경한 값이 메인메모리에 아직 적용되기 전에는 다른 쓰레드에서는 이값을 알지 못하기 때문에 원자성이 깨진다. 그래서 volatile을 사용하여 원자성을 지켜준다.)

http://thswave.github.io/java/2015/03/08/java-volatile.html


결론은 변경 가능한 데이터를 공유할 때는 해당 데이터를 읽거나 쓰는 모든 스레드는 동기화를 수행해야 한다는 것이다.

상호 배제와 원자성을 확보할 때는 synchronize를 사용하고 원자성만 필요할 때는 volatile을 사용하면 되지만 사용이 까다롭다. 잘 사용하시기를..



출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙66

댓글()