아이템 11 - equals 를 재정의하려거든 hashCode 도 재정의하라 - 완벽 공략
이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.
해시맵 내부의 연결 리스트
자바8 이전 까지는 해시 충돌 발생시 링크드 리스트 를 사용했다.
이때 링크드 리스트 는 해시맵 내부 에 구현되어 있는 링크드 리스트를 사용했다.
링크드 리스트에서 데이터를 추가 할때 성능은 O(1)
이다.
링크드 리스트에서 데이터를 조회 할때 성능은 O(n)
이다.
해시 충돌 이 자주 발생할수록 해당 버킷 에 들어있는 링크드 리스트 에서 값을 꺼낼때 성능이 저하된다.
자바8 버전부터는 성능 최적화를 위해 링크드 리스트 대신에 이진 트리 를 사용하도록 바뀌었다.
이진 트리 에서 값을 꺼낼 때의 성능은 O(logN)
이다.
사실 이렇게 해시 충돌 이 자주 발생하는 경우는 드물다.
Thread-safety
Thread-safety 란, 멀티 스레드 환경에서 안전한 코드 를 뜻한다.
멀티 스레드 환경이란 동시다발 적으로 여러 스레드가 해당 코드를 실행한다는 의미다.
이때 코드가 예측한 대로 동작하지 않는다면 그 코드는 멀티 스레드 환경에서 안전한 코드가 아니다.
// equals를 재정의하면 hashCode로 재정의해야 함을 보여준다. (70-71쪽)
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "area code");
this.prefix = rangeCheck(prefix, 999, "prefix");
this.lineNum = rangeCheck(lineNum, 9999, "line num");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
// 해시코드를 지연 초기화하는 hashCode 메서드 - 스레드 안정성까지 고려해야 한다. (71쪽)
private int hashCode; // 자동으로 0으로 초기화된다.
@Override public int hashCode() {
int result = hashCode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
this.hashCode = result;
return result;
}
}
}
위의 코드에서 hashCode 메서드 같은 경우 여러 스레드에서 동시 접근할 시 문제가 될 수 있다.
멀티 스레드 환경에서 안전한 코드를 만드는 방법 중 가장 원시적인 방법은 다음과 같다.
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "area code");
this.prefix = rangeCheck(prefix, 999, "prefix");
this.lineNum = rangeCheck(lineNum, 9999, "line num");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
// 해시코드를 지연 초기화하는 hashCode 메서드 - 스레드 안정성까지 고려해야 한다. (71쪽)
private int hashCode; // 자동으로 0으로 초기화된다.
@Override public synchronized int hashCode() {
int result = hashCode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
this.hashCode = result;
return result;
}
}
}
synchronized
키워드를 사용하여 동시에 공유되는 코드를 감싸는 방법이다.
이렇게하면 한번에 하나의 스레드 밖에 못사용한다.
대신에 메서드 단위로 synchronized
키워드를 사용하면 하나의 스레드 밖에 사용하지 못하기 때문에 성능에 문제가 생긴다.
이 문제를 해결하기 위해 double checked locking 기법이 있다.
public final class PhoneNumber {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "area code");
this.prefix = rangeCheck(prefix, 999, "prefix");
this.lineNum = rangeCheck(lineNum, 9999, "line num");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNum == lineNum && pn.prefix == prefix
&& pn.areaCode == areaCode;
}
// 해시코드를 지연 초기화하는 hashCode 메서드 - 스레드 안정성까지 고려해야 한다. (71쪽)
private int hashCode; // 자동으로 0으로 초기화된다.
@Override public synchronized int hashCode() {
if (this.hashCode != 0) {
return hashCode;
}
synchronized (this) {
int result = hashCode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
this.hashCode = result;
}
return result;
}
}
}
synchronized
키워드 자체가 클래스에 lock 을 사용하는 것이다.
해당 블럭을 최소화 해주는 방법이다.
최초에 필드 값을 검사해 null 이 아니면 return 을 해준다.
하지만 두개의 스레드가 동시에 들어왔을 때를 대비해
synchronized (this) {
int result = hashCode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
this.hashCode = result;
}
return result;
}
위처럼 synchronized
를 사용한다.
밖에서 한번, synchronized 블럭 안에서 한번 총 두번 체크를 하기 때문에 double checked locking 이라고한다.
private volatile int hashCode;
@Override public synchronized int hashCode() {
if (this.hashCode != 0) {
return hashCode;
}
synchronized (this) {
int result = hashCode;
if (result == 0) {
result = Short.hashCode(areaCode);
result = 31 * result + Short.hashCode(prefix);
result = 31 * result + Short.hashCode(lineNum);
this.hashCode = result;
}
return result;
}
}
여기에 추가로 hashCode 필드에 volatile 키워드를 추가한다.
volatile 키워드는 cpu 의 캐시에 데이터를 저장하게 되는데
캐시에 저장된 데이터를 읽어올 때는 값이 업데이트 되었지만 예전에 캐싱된 데이터를 불러올 수도 있다.
volatile 키워드를 사용하게되면 해당 값을 메인 메모리 에 저장하게 된다.
그래서 값을 읽어올 때 가장 최근의 데이터 를 읽어오게 된다.
다른 방법으로는 한 스레드 안에서만 유요한 local 변수를 사용하는 ThreadLocal 방법이 있다.
또한 불변 객체를 사용하면 Thread-safety 하다.
그리고 Synchronized 가 되어있는 컬렉션이 있는데 그걸 사용하는 방법이 있다.
만들어질때부터 Synchronized 가 적용되어 있는 클래스가 있다.
HashTable 과 같은 클래스가 그 예이다.
HashTable 은 Thread-safety 하지만, HashMap 은 Thread-safety 하지 않다.
공유하는 데이터를 HashTable 처럼 태생적으로 Thread-safety 한 데이터를 사용한다면 Thread-safety 한 코드가 된다.
다른 방법으로는 Concurrent 를 지원하는 List 나 Set 을 사용하는 방법이 있다.
Concurrent 는 동시에 접근이 가능하다.
값을 읽어오는 경우 굳이 한번에 하나의 스레드만 읽어가게 하는 대신에 동시에 여러 스레드를 허용하는 방법도 있을 것이다.
쓰기의 경우에도 동시성을 보장하는 방법이 있을 것이다.
Concurrent 가 없을 때에는 동시에 접근이 허용되지 않았다.
동시에 여러 스레드가 값을 읽고, 추가하고 삭제할 수 있도록 Concurrent 를 지원하는 List 나 Set 이 있다.
이러한 List 나 Set 을 사용하는 방법도 Thread-safety 한 방법이라고 할 수 있다.
'개발 공부 > Java' 카테고리의 다른 글
이펙티브 자바 아이템 13 - clone 재정의는 주의해서 진행하라 - 핵심 정리 (0) | 2022.10.27 |
---|---|
이펙티브 자바 아이템 12 - toString 을 항상 재정의하라 - 핵심 정리 (0) | 2022.10.26 |
이펙티브 자바 아이템 11 - equals 를 재정의하려거든 hashCode 도 재정의하라 - 핵심 정리 (0) | 2022.10.25 |
이펙티브 자바 아이템 10 - equals 는 일반 규약을 지켜 재정의하라 - 완벽 공략 (0) | 2022.10.24 |
이펙티브 자바 아이템 10 - equals 는 일반 규약을 지켜 재정의하라 - 핵심 정리 (0) | 2022.10.19 |
댓글