Java에서 StringBuilder와 StringBuffer 중 사용 선택 목적은 보통 단일 Thread인지 Multi Thread인지에 따라 갈린다.
그런데 본인이 생각한 Multi Thread환경에서의 기대 값과 실제 값이 달라서 어떤식으로 Thread safe를 구현하고 있는지 예시코드를 작성하며 개념 좀 정리해보고자 한다.
Multi-Thread Test
StringBuilder
public class SbTest implements Runnable {
StringBuilder sb;
public SbTest() {
sb = new StringBuilder();
}
public void run() {
addChar();
}
public void addChar() {
for (inti = 0; i < 10000; i++) {
sb.append("1");
sb.append("0");
}
}
public static void main(String[]args){
SbTest sbTest = new SbTest();
Thread threadOne = new Thread(sbTest, "ThreadOne");
Thread threadTwo = new Thread(sbTest, "ThreadTwo");
threadOne.start();
threadTwo.start();
try {
threadOne.join();
threadTwo.join();
} catch (Exception e) {
System.out.println(e.getMessage());
}
System.out.println("length:" + sbTest.sb.length());
}
}
기대 값이 40000 이지만 당연 Thread safe한 객체가 아니기 때문에 Multi-Thread환경에서 값이 올바르게 더해지지 못한 모습이다.
StringBuffer
public class SbTest implements Runnable {
StringBuffer sb;
public SbTest() {
sb = new StringBuffer();
}
public void run() {
addChar();
}
public void addChar() {
for (inti = 0; i < 10000; i++) {
sb.append("1");
sb.append("0");
}
}
public static void main(String[]args){
SbTest sbTest = new SbTest();
Thread threadOne = new Thread(sbTest, "ThreadOne");
Thread threadTwo = new Thread(sbTest, "ThreadTwo");
threadOne.start();
threadTwo.start();
try {
threadOne.join();
threadTwo.join();
} catch (Exception e) {
System.out.println(e.getMessage());
}
System.out.println("length:" + sbTest.sb.length());
}
}
이와 반대로 StringBuffer에서는 Thread safe한 객체이기 때문에 기대 값인 40000이 올바르게 출력되었다.
두 객체의 append에는 어떤 비밀이 숨어있는 것일까?
바로 syncronized 이다.
append
StringBuilder
@Override
@HotSpotIntrinsicCandidate
public StringBuilder append(String str) {
super.append(str);
return this;
}
StringBuffer
@Override
@HotSpotIntrinsicCandidate
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
Synchronized는 java에서 Multi Thread 환경에서 동기화를 제어 해야할 경우 사용된다.
Synchronized 메서드를 호출하였을 때, 자신이 포함된 객체에 lock을 걸게 된다. 따라서 확실하게 동기화를 확실히 보장시켜주지만, 객체에 포함된 다른 모든 synchronized의 접근까지 lock을 걸게 된다. 이를 해결하기위해 synchronized block 있으며 이에 관해 자세한 내용은 아래의 블로그를 참조하면 좋을 것 같다.
투덜이의 리얼 블로그 :: Java의 동기화 Synchronized 개념 정리#1 (tistory.com)
그렇다면 Multi-Thread환경에서 StringBuffer를 사용하면 Thread 순서대로 값이 들어갈까?
일단 현재 output을 확인해보자.
StringBuilder
StringBuilder는 당연 Thread non-safe 환경이니 밑줄 친 것처럼 Multi-Thread에서 동시에 값이 들어가 중복되는 데이터가 발생 하는걸 예상 했었다.
StringBuffer
그런데 StringBuffer 에서도 총 삽입되어야 하는 문자의 길이는 맞지만, 11 이나 00 같은 연속되는 문자열의 확인을 통해, Thread 순서대로 값이 들어가는게 아닌 걸 확인했다.
왜 이럴까?
아까 위에서 말한것 처럼 syncronized가 해당 객체를 lock을 건다.
즉, StringBuffer 자체에 lock을 걸고 Thread one, Thread two에서는 각각 계속 append를 하기 때문에 순서를 보장하지 않는 것이다. 결국 StringBuffer의 Thread Safe라는게 Thread의 순서를 보장하는 것은 아닌 것이다.
그럼 순서를 보장하기 위해서는 어떻게 해야할까?
StringBuffer를 메소드 내부에 선언하여 인스턴스를 생성하거나,
위의 소스의 경우 addChar 메서드 자체에 syncronized를 사용하여 해당 SbTest에 lock을 걸어 순서를 보장할 수 있다.
즉 , 이렇게 사용할 경우 addcar에서 SbTest 클래스에 lock을 걸어 StringBuilder를 사용해도 값이 순서대로 들어가는 걸 보장할 수 있다.
따라서, Thread one에서 addcar를 사용하고 있기 때문에 Thread Two가 SbTest클래스의 lock이 풀릴 때 까지 대기한 후, StringBuilder를 사용하게 된다.
public class SbTest implements Runnable {
StringBuilder sb;
public SbTest() {
sb = new StringBuilder();
}
public void run() {
addChar();
}
public synchronized void addChar() {
for (int i = 0; i < 10000; i++) {
sb.append("1");
sb.append("0");
}
}
public static void main(String[] args) {
SbTest sbTest = new SbTest();
Thread threadOne = new Thread(sbTest, "Thread One");
Thread threadTwo = new Thread(sbTest, "Thread Two");
threadOne.start();
threadTwo.start();
try {
threadOne.join();
threadTwo.join();
} catch (Exception e) {
System.out.println(e.getMessage());
}
System.out.println("length : " + sbTest.sb.length());
System.out.println(sbTest.sb.toString());
}
}
물론 해당 코드는 Sample코드이기 때문에, 실제 Multi Thread 환경에서 더 고려할 사항도 있을 것이며 지금처럼 무식하게 해결하는 방법 말고 다양한 방법이 있을 것이다. 일단 오늘은 StringBuffer와 StringBuilder의 Thread safe의 원리와 보장범위를 살펴 보았고 값에 대한 순서를 보장해야할 경우에 대해 간단하게 해결방안을 살펴보았다.
그럼 이만 끗!
'Language > ☕️Java' 카테고리의 다른 글
EnumMap을 써야하는 이유 (0) | 2022.11.20 |
---|---|
JVM 아키텍처 2탄 - 런타임 데이터 영역(Run-time Data Area) (0) | 2022.08.31 |
JAVA로 카카오 메시지 API연동 (4) | 2022.02.25 |
JVM 아키텍처 1탄 - Class Loader SubSystem (0) | 2021.12.08 |
Java Collections 시간 복잡도 (0) | 2021.12.03 |
댓글