여러 스레드가 어떤 객체에 접근할 때, 해당 스레드들이 어떻게 스케쥴링을 하든, 동시에 객체를 다루든, 순차적으로 객체를 다루든, 어떤 순서로 객체를 다루둔 항상 정확하게 동작하면 해당 클래스는 thread safe하다고 한다.
항상 정확하게 동작한다는 것은 개발자가 생각하는 클래스 명세에 항상 부합한다는 뜻이다.
클래스 명세에는
두가지가 존재한다. 이 두가지 명세에 따라 항상 특정 코드가 작동하고, 어떤 효과를 가질것이라고 개발자가 신뢰할 수 있다면 그 코드는 정확하게 동작한다고 볼 수 있다. 이 클래스 명세를 통해 개발자는 코드 신뢰도code confidence 얻을 수 있다.
개인적인 의견으로는 이 thread safe는 race condition과는 반대되는 개념이라고 생각한다.
race condition은 여러 스레드나 프로세스가 접근하여 조작할 때, 어떤 순서로 접근하냐에 따라 실행 결과가 달라지는 상황을 말한다.
Atomicity 를 확보해야 한다.
여러 스레드들이 동시에 접근할 수 있는 공유 영역을 임계영역이라고 하는데, 이 임계영역에 대해서 다른 스레드가 접근하지 못하도록 해야 한다. 해당 임계영역이 실행될 동안에는 다른 스레드가 접근하지 못해야한다. 그러기 위해선 이 임계영역 작업은 항상 원자적으로 실행되어 단일 연산처럼 수행되어야 한다. 그러면 다른 스레드에서 끼어들지 못하고 context switch가 일어나지 않는다. 더이상 interrupt 걸수 없는 연산이라고 보면 된다.
예를 들어 운영체제에서 대표적인 race condition 문제로 프로듀서 컨슈머 문제가 있다. 프로듀서는 아이템을 생성해 버퍼에 담고, 버퍼에서 아이템을 꺼내 컨슈머가 처리를 한다. 이 버퍼가 가득찼는지 비어있는지 확인하기 위해 버퍼에 담긴 아이템의 개수를 세는 count라는 공유 변수가 있다. 프로듀서는 아이템을 생성시 count++을 수행하고, 컨슈머는 아이템 소비시 count--를 수행한다.
프로듀서와 컨슈머가 동시에 수행할 경우 이 count++과 count--가 동시에 수행되면 race condition이 일어난다.
이유는 실제로 count ++같은 명령어는 실제로는 기계어로 보면 3개의 명령어로 이루어져있기 때문이다. 그 사이사이 명령어에 context switch가 일어날 수 있다. 그래서 순차적으로 명령어가 실행되지 않고 기계어 차원에서 count++과 count—명령어들이 임의 순서로 뒤섞어져 실행된다.
Atomicity를 확보하기 위해선
Atomic Variable 연산은 내부적으로 CAS를 기반으로 하여, 락이 아닌 cpu busy wait 방식으로 동작한다. 따라서 CPU를 계속쓰는 문제가 생길 수 있다. 하지만 여러 스레드가 context switch 하는 비용이 클 경우 context switch 비용보다는 CPU를 계속쓰는 비용이 더 적을 수 있으니 상황에 따라 사용해야 한다.
또한, 클래스에 상태 변수가 여러개인 경우에는 Atomic Variable을 사용해도 문제가 생길 수 있다.
예를 들어 자바에는 Sychronized가 있다.
Synchronized는 모니터락을 사용하여 락을 가진 객체를 지정할 수 있다. Synchronized 메소드나 Synchronized블록을 통해 락을 확보하며, 해당 블록을 벗어날 때 락이 해제된다.
하지만 synchornized는 아예 락을 걸어버리는 것이기 때문에 성능에 영향을 줄 수 있다.
어떤 메서드에 synchronized를 걸어버리면 해당 객체는 한번에 한 스레드만 실행하기 때문에 동시 처리가 불가하고,서블릿 프레임워크같이 요청별로 스레드를 생성해서 동시에 여러 요청을 처리해야할 경우 속도가 느려진다. 다른 클라이언트 요청은 현재 요청이 완료할 때까지 마냥 기다려야 한다.
따라서 synchronized 블록의 크기를 적정하게 유지하여야 한다.
Immutable이란 맨 처음 객체가 생성되는 시점을 제외하고는 객체 내부 상태가 전혀 바뀌지 않는 것을 말한다. 별다른 동기화를 하지 않아도 항상 thread safe하기 때문에 어느 스레드에서건 마음껏 안전하게 사용할 수 있다. 대표적인 불변 객체로는 String이 있다.
클래스를 불변으로 만들려면 다음 규칙을 따르면 된다.
//Game.class
public class Game {
private final List<String> list = new ArrayList<>();
...
public List<String> getList() {
return Collections.unmodifiableList(list);
}
public class Complex {
private final double re;
private final double im;
private Complex(double re, double im) {
this.re = re;this.im = im;
}
public static Complex valueOf(doulble re, double im) {
return new Complex(re, im);
}
}
장점은
public static final Complex ZERO = new Complex(0,0);
단점은
이펙티브 자바 아이템 17 변경 가능성을 최소화하라 글 참고
Collections.unmodifiableList()같은 메서드들은 Read-Only 용도로만 사용할 수 있으며 add(), addAll()과 같은 수정 메서드를 호출 시에 exception을 발생시킨다.
보통 클래스의 내부에 컬렉션인 가변 객체가 있을 때 참조에 의해 변경 가능성이 있기 때문에 그대로 가변객체를 주지 않고, unmodifiableList로 방어적 복사를 해서 전달한다. 복사된 리스트로는 수정이 되지 않는다. 하지만 원본 자체에 대한 수정을 막을 수는 없다.
public class Test {
public static void main(String []args) {
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
//수정을 못하도록 방어적 복사를 함.
List<String> unmodifiableList = Collections.unmodifiableList(list);
try {
unmodifiableList.add("d"); //수정을 못하기 때문에 exception 터짐
System.out.println("cannot be reached here");
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify unmodifiable list");//exception 터짐
}
list.add("d"); //원본자체에 대한 수정은 못막음!!!
System.out.println(unmodifiableList.get(3));//d로 수정된 값 출력
}
}
//출력 결과
Cannot modify unmodifiable list
d
Immutable이란 맨 처음 객체가 생성되는 시점을 제외하고는 객체 내부 상태가 전혀 바뀌지 않는 것을 말한다. unmodifiable을 써도 방어적 복사된 리스트에 대해서는 수정을 못하지만, 원본 자체에 대해서는 수정이 가능하기 때문에 불변성을 달성할 수 없다.
final은 값의 재할당을 하지 못한다. final 이란 영어 뜻 최종적, 마지막이라는 의미 그대로 초기에 한 번 할당을 하게 되면 최종적인 값이 된다. 즉, 재할당을 막는다는 것이지 불변이라는 것은 아니다.
final int num = 3; //할당 완료
num = 4;//불가
final 필드에는 primitive type, reference type이 있을 수 있다.
primitive type의 경우에는 객체가 아니라 리터럴 값이라서 불변하다.
하지만 reference type의 final 필드는 불변하지 않을 수 있다.
// reference type 필드
//Game.class
public class Game {
private final Car car = new Car();
...
}
//Main.class
public void makeCar() {
car.setPosition(3);//상태 변경 가능
}
//Game.class
public class Game {
private final List<String> playerNames = new ArrayList<>();
...
}
...
//Main.class
player.getPlayerNames().add("one");// 상태 변경 가능
player.getPlayerNames().add("two");// 상태 변경 가능
위 코드에서는 reference type인 car, playerNames 모두 내부 상태를 변경 시킬 수 있게 된다. 그러므로 final이 반드시 불변을 보장해주는 것은 아니다.
이펙티브 자바
자바 병렬 프로그래밍