1. 개요
Java의 동기화 는 멀티 스레딩 문제를 제거하는 데 매우 유용합니다. 그러나 동기화 원칙은 신중하게 사용하지 않으면 많은 문제를 일으킬 수 있습니다.
이 자습서에서는 동기화와 관련된 몇 가지 나쁜 사례와 각 사용 사례에 대한 더 나은 접근 방식에 대해 설명합니다.
2. 동기화의 원리
일반적으로 외부 코드가 잠기지 않는다고 확신하는 객체에서만 동기화해야합니다 .
즉, 동기화를 위해 풀링되거나 재사용 가능한 객체를 사용하는 것은 나쁜 습관 입니다. 그 이유는 풀링 된 / 재사용 가능한 개체가 JVM의 다른 프로세스에 액세스 할 수 있고 외부 / 신뢰할 수없는 코드에 의해 이러한 개체를 수정하면 교착 상태 및 비 결정적 동작이 발생할 수 있기 때문입니다.
이제 String , Boolean , Integer 및 Object 와 같은 특정 유형을 기반으로하는 동기화 원칙에 대해 설명하겠습니다 .
3. 문자열 리터럴
3.1. 나쁜 관행
문자열 리터럴은 풀링 되어 Java에서 자주 재사용됩니다. 따라서 동기화를 위해 동기화 된 키워드 와 함께 문자열 유형 을 사용하지 않는 것이 좋습니다 .
public void stringBadPractice1() {
String stringLock = "LOCK_STRING";
synchronized (stringLock) {
// ...
}
}
마찬가지로 비공개 최종 문자열 리터럴을 사용하면 상수 풀에서 계속 참조됩니다.
private final String stringLock = "LOCK_STRING";
public void stringBadPractice2() {
synchronized (stringLock) {
// ...
}
}
또한 동기화를 위해 문자열 을 인턴 하는 것은 나쁜 습관으로 간주됩니다 .
private final String internedStringLock = new String("LOCK_STRING").intern();
public void stringBadPractice3() {
synchronized (internedStringLock) {
// ...
}
}
Javadocs 에 따라 intern 메소드는 String 객체에 대한 표준 표현을 가져옵니다 . 즉, intern 메서드는 풀에서 String 을 반환 하고이 String 과 동일한 내용을 가진 풀이 없으면이를 풀에 명시 적으로 추가합니다 .
따라서 재사용 가능한 개체에 대한 동기화 문제는 인턴 된 문자열 개체에 대해서도 지속 됩니다.
참고 : 모든 문자열 리터럴 및 문자열 값 상수 식은 자동으로 인턴 됩니다.
3.2. 해결책
문자열 리터럴 에 대한 동기화와 관련된 잘못된 관행을 피하기위한 권장 사항 은 new 키워드를 사용하여 String 의 새 인스턴스 를 만드는 것 입니다.
이미 논의한 코드에서 문제를 수정 해 보겠습니다. 먼저, 고유 한 참조 (재사용 방지)와 동기화에 도움이되는 고유 한 잠금을 갖는 새 String 개체를 만듭니다 .
그런 다음 외부 / 신뢰할 수없는 코드가 액세스하지 못하도록 개체를 비공개 및 최종 상태로 유지 합니다.
private final String stringLock = new String("LOCK_STRING");
public void stringSolution() {
synchronized (stringLock) {
// ...
}
}
4. 부울 리터럴
두 값인 true 및 false 가 있는 부울 유형 은 잠금 용도로 적합하지 않습니다. JVM의 문자열 리터럴과 마찬가지로 부울 리터럴 값도 부울 클래스 의 고유 인스턴스를 공유합니다 .
Boolean 잠금 개체 에서 동기화하는 잘못된 코드 예제를 살펴 보겠습니다 .
private final Boolean booleanLock = Boolean.FALSE;
public void booleanBadPractice() {
synchronized (booleanLock) {
// ...
}
}
여기서 외부 코드 가 동일한 값을 가진 부울 리터럴 에서 동기화되는 경우 시스템이 응답하지 않거나 교착 상태가 될 수 있습니다 .
따라서 Boolean 개체를 동기화 잠금으로 사용하지 않는 것이 좋습니다 .
5. 박스형 프리미티브
5.1. 나쁜 습관
부울 리터럴과 유사하게 boxed 유형은 일부 값에 대해 인스턴스를 재사용 할 수 있습니다. 그 이유는 JVM이 바이트로 표시 할 수있는 값을 캐시하고 공유하기 때문입니다.
예를 들어, 박스형 Integer 에서 동기화하는 잘못된 코드 예제를 작성해 보겠습니다 .
private int count = 0;
private final Integer intLock = count;
public void boxedPrimitiveBadPractice() {
synchronized (intLock) {
count++;
// ...
}
}
5.2. 해결책
그러나 부울 리터럴 과 달리 boxed primitive에 대한 동기화 솔루션은 새 인스턴스를 만드는 것입니다.
String 객체 와 마찬가지로 new 키워드를 사용하여 고유 한 고유 잠금 이있는 Integer 객체 의 고유 인스턴스를 만들고 비공개 및 최종 상태로 유지해야합니다 .
private int count = 0;
private final Integer intLock = new Integer(count);
public void boxedPrimitiveSolution() {
synchronized (intLock) {
count++;
// ...
}
}
6. 클래스 동기화
JVM은 클래스가 this 키워드를 사용하여 메서드 동기화 또는 블록 동기화를 구현할 때 개체 자체를 모니터 (내재 잠금)로 사용 합니다.
신뢰할 수없는 코드는 액세스 가능한 클래스의 고유 잠금을 획득하고 무기한 보유 할 수 있습니다. 결과적으로 교착 상태가 발생할 수 있습니다.
6.1. 나쁜 습관
예를 들어 동기화 된 메서드 setName 과 동기화 된 블록 이있는 setOwner 메서드를 사용하여 Animal 클래스를 생성 해 보겠습니다 .
public class Animal {
private String name;
private String owner;
// getters and constructors
public synchronized void setName(String name) {
this.name = name;
}
public void setOwner(String owner) {
synchronized (this) {
this.owner = owner;
}
}
}
이제 Animal 클래스 의 인스턴스를 만들고 동기화 하는 몇 가지 잘못된 코드를 작성해 보겠습니다 .
Animal animalObj = new Animal("Tommy", "John");
synchronized (animalObj) {
while(true) {
Thread.sleep(Integer.MAX_VALUE);
}
}
여기서 신뢰할 수없는 코드 예제는 무한 지연을 도입하여 setName 및 setOwner 메서드 구현이 동일한 잠금을 획득 하지 못하도록합니다 .
6.2. 해결책
이 취약점을 방지하는 솔루션 은 개인 잠금 개체 입니다.
아이디어는 객체 자체 의 고유 잠금 대신 클래스 내에 정의 된 Object 클래스 의 비공개 최종 인스턴스 와 관련된 고유 잠금을 사용하는 것입니다.
또한 메서드 동기화 대신 블록 동기화를 사용하여 동기화되지 않은 코드를 블록 밖으로 유지하는 유연성을 추가해야합니다.
따라서 Animal 클래스에 필요한 사항을 변경해 보겠습니다 .
public class Animal {
// ...
private final Object objLock1 = new Object();
private final Object objLock2 = new Object();
public void setName(String name) {
synchronized (objLock1) {
this.name = name;
}
}
public void setOwner(String owner) {
synchronized (objLock2) {
this.owner = owner;
}
}
}
여기에서는 더 나은 동시성을 위해 setName 및 setOwner 메서드 모두에 대한 동기화 문제를 분리하기 위해 여러 개인 최종 잠금 개체를 정의하여 잠금 체계를 세분화했습니다 .
또한 동기화 된 블록 을 구현하는 메서드가 정적 변수를 수정하는 경우 정적 개체를 잠가 동기화해야 합니다.
private static int staticCount = 0;
private static final Object staticObjLock = new Object();
public void staticVariableSolution() {
synchronized (staticObjLock) {
count++;
// ...
}
}
7. 결론
이 기사에서는 String , Boolean , Integer 및 Object 와 같은 특정 유형에 대한 동기화와 관련된 몇 가지 나쁜 사례에 대해 논의했습니다 .
이 기사에서 가장 중요한 점은 동기화를 위해 풀링되거나 재사용 가능한 개체를 사용하지 않는 것이 좋습니다.
또한 Object 클래스 의 비공개 최종 인스턴스 에서 동기화하는 것이 좋습니다 . 이러한 객체는 공용 클래스 와 상호 작용할 수있는 외부 / 신뢰할 수없는 코드에 액세스 할 수 없으므로 이러한 상호 작용으로 인해 교착 상태가 발생할 가능성이 줄어 듭니다.