티스토리 뷰
StringBuilder 와 StringBuffer 모두 AbstractStringBuilder 를 상속받아 메서드를 구현하고 있다.
두 클래스는 모두 String 을 만들기 위해 사용되지만 어떤 차이점으로 인해 구분되어있는지를 확인해본다.
append() 함수로 알아보는 두 클래스의 차이
StringBuilder.java 의 append() 함수
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuffer.java 의 append() 함수
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
StringBuilder 의 append() 와 StringBuffer 의 append() 모두 내부적으로 부모 클래스인 AbstractStringBuilder 의 append() 함수를 호출하는것을 알 수 있다. 하지만 두가지 다른점이 존재한다.
- synchronized
- toStringCache
StringBuffer 에는 StringBuilder 와는 다르게 append() 함수에 synchronized 가 달려있고 부모 클래스의 append() 함수를 호출하기 전에 toStringCache 를 null 로 초기화 하고 있다.
StringBuffer 에는 synchronzied 가 붙어 있는 이유
StringBuffer 의 append() 함수에 synchronized 가 붙어있다는 이유는 동기화를 위해 붙어있다는 것을 알 수 있다. 즉, 부모 클래스인 AbstractStringBuilder 의 append() 함수는 Thread Safe 하지 않다고 생각할 수 있다.
그렇다면 StringBuffer 는 synchronized 로 인해 Thread Safe 하고 StringBuilder 는 Thread Safe 하지 않으니 이 것을 테스트 코드로 확인해본다.
Main Thread 를 제외한 2개의 Thread 에서 각각 StringBuffer 와 StringBuilder 에 문자열 한가지를 10000번씩 append()한다.
public class Main {
public static void main(String[] args) throws InterruptedException {
StringBuffer stringBuffer = new StringBuffer();
StringBuilder stringBuilder = new StringBuilder();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
stringBuffer.append("A");
stringBuilder.append("A");
}
});
thread1.start();
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
stringBuffer.append("B");
stringBuilder.append("B");
}
});
thread2.start();
thread1.join();
thread2.join();
System.out.println("StringBuffer.length: "+ stringBuffer.length());
System.out.println("StringBuilder.length: "+ stringBuilder.length());
}
}
결과
StringBuffer 는 누락 없이 모든 문자열이 append 되어 20000 개의 문자를 포함하였지만 StringBuilder 는 일부 문자열이 누락되어 5개가 부족한 19995 개의 문자를 갖게 되었다.
이를 통해 StringBuffer 는 Multi Thread 환경에서 Thread Safe 하지만 StringBuilder 는 Thread Safe 하지 않음을 확인할 수 있었다.
그렇다면 부모 클래스의 어떤 코드로 인해서 Thread Safe 하지 않은지를 확인해보자
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
AbstarctStringBuilder 의 thread safe 하지 않은 의심스러운 메서드는 아래 두가지이다.
- ensureCapacityInternal() : AbstractStringBuilder 맴버 변수인 value 의 char[] 길이를 늘리는 역할을 하는 함수
- str.getChars() : 전달받은 char[] 로 해당 String 값을 복사
ensureCapacityInternal()
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
String.getChars()
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
StringBuffer 에서 append() 가 누락되는 시나리오
사나리오1
Thread1 - String.getChars() 를 통해 "A" 를 value 에 복사하는 중
Thread2 - ensureCapacityInternal() 를 통해 부족한 배열 길이를 늘리기 위해 value 복사 중
Thread1 - "A" 를 value 에 추가 완료
Thread2 - value 배열 길이 늘리기 위한 복사 완료
Thread1 이 "A" 를 value 에 추가했지만 Thread2 가 길이를 늘리기 위해 복사한 value 는 Thread1 이 "A" 를 추가하기 이전의 value 이다. 이렇게 되면 Thread1 이 추가한 "A" 가 유실된다
시나리오2
Thread1 - String.getChars() 를 통해 "A" 를 value 에 복사하는 중
Thread2 - String.getChars() 를 통해 "B" 를 value 에 복사하는 중
Thread1 - "A" 를 추가한 value 복사 완료
Thread2 - "B" 를 추가한 value 복사 와료
Thread1 이 "A" 를 value 에 추가했지만 Thread2 가 복사를 완료한 시점의 value 는 Thread1 이 "A" 를 추가하기 이전의 value 이다. 이렇게 되면 Thread1 이 추가한 "A" 가 유실된다.
toStringCache 의 역할
StringBuffer 는 맴버 변수로 toStringCache 를 갖는다. 이는 AbstractStringBuilder 와 StringBuilder 에는 없는 맴버 변수이다.
이 변수의 역할은 toString() 함수가 호출될때 value 배열의 값을 복사한 채로 가지고 있다가 또 다시 toString() 이 호출되었을때 value 배열에 변경이 없다면 toStringCache 를 사용하여 String 을 만들게 된다.
value 배열을 그대로 사용해서 String 을 만들지 않고 value 의 복사본인 toStringCache 를 사용하는 이유가 무엇일까?
'JAVA' 카테고리의 다른 글
TLS (SSL Certificate) 와 Java TrustStore KeyStore (0) | 2023.02.10 |
---|---|
Java wait(), notify(), notifyAll() 과 Thread 상태 (0) | 2022.06.29 |
Heap 가상 메모리 address 범위 확인 (1) | 2022.01.04 |
JVM GC 후 Heap 에게 할당된 물리 메모리 OS에게 회수 여부 (0) | 2022.01.03 |
-Xms 보다 Memory 사용량이 더 적은 이유 (0) | 2021.12.31 |
- Total
- Today
- Yesterday
- rate limit
- HashMap
- getBoolean
- Seperate Chaining
- ConcurrentHashMap
- DyanomoDB
- notify()
- aurora
- ResultSet
- N+1
- circurit breaker
- mariada-connector
- MariaDB
- custom config data convertion
- GlobalFilter
- dynamodb
- reative
- Lazy
- spring cloud gateway
- wait()
- reactor
- RouteDefinition
- referencedColumnName
- AbstractMethodError
- notifyAll()
- Flux
- mariadb-connector-j
- RoutePredication
- router
- msyql-connector-java
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |