1. 개요

Java의 동기화 는 멀티 스레딩 문제를 제거하는 데 매우 유용합니다. 그러나 동기화 원칙은 신중하게 사용하지 않으면 많은 문제를 일으킬 수 있습니다.

이 자습서에서는 동기화와 관련된 몇 가지 나쁜 사례와 각 사용 사례에 대한 더 나은 접근 방식에 대해 설명합니다.

2. 동기화의 원리

일반적으로 외부 코드가 잠기지 않는다고 확신하는 객체에서만 동기화해야합니다 .

즉, 동기화를 위해 풀링되거나 재사용 가능한 객체를 사용하는 것은 나쁜 습관 입니다. 그 이유는 풀링 된 / 재사용 가능한 개체가 JVM의 다른 프로세스에 액세스 할 수 있고 외부 / 신뢰할 수없는 코드에 의해 이러한 개체를 수정하면 교착 상태 및 비 결정적 동작이 발생할 수 있기 때문입니다.

이제 String , Boolean , IntegerObject같은 특정 유형을 기반으로하는 동기화 원칙에 대해 설명하겠습니다 .

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. 부울 리터럴

두 값인 truefalse있는 부울 유형 은 잠금 용도로 적합하지 않습니다. 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);
    }
}

여기서 신뢰할 수없는 코드 예제는 무한 지연을 도입하여 setNamesetOwner 메서드 구현이 동일한 잠금을 획득 하지 못하도록합니다 .

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;
        }
    }
}

여기에서는 더 나은 동시성을 위해 setNamesetOwner 메서드 모두에 대한 동기화 문제를 분리하기 위해 여러 개인 최종 잠금 개체를 정의하여 잠금 체계를 세분화했습니다 .

또한 동기화 된 블록 을 구현하는 메서드가 정적 변수를 수정하는 경우 정적 개체를 잠가 동기화해야 합니다.

private static int staticCount = 0;
private static final Object staticObjLock = new Object();
public void staticVariableSolution() {
    synchronized (staticObjLock) {
        count++;
        // ...
    }
}

7. 결론

이 기사에서는 String , Boolean , IntegerObject같은 특정 유형에 대한 동기화와 관련된 몇 가지 나쁜 사례에 대해 논의했습니다 .

이 기사에서 가장 중요한 점은 동기화를 위해 풀링되거나 재사용 가능한 개체를 사용하지 않는 것이 좋습니다.

또한 Object 클래스 비공개 최종 인스턴스 에서 동기화하는 것이 좋습니다 . 이러한 객체는 공용 클래스 와 상호 작용할 수있는 외부 / 신뢰할 수없는 코드에 액세스 할 수 없으므로 이러한 상호 작용으로 인해 교착 상태가 발생할 가능성이 줄어 듭니다.

평소처럼 소스 코드는 GitHub에서 사용할 수  있습니다 .