비 최종 필드의 동기화


91

최종 클래스가 아닌 필드에서 동기화 할 때마다 경고가 표시됩니다. 다음은 코드입니다.

public class X  
{  
   private Object o;  

   public void setO(Object o)  
   {  
     this.o = o;  
   }  

   public void x()  
   {  
     synchronized (o) // synchronization on a non-final field  
     {  
     }  
   }  
 } 

그래서 다음과 같은 방식으로 코딩을 변경했습니다.

 public class X  
 {  

   private final Object o;       
   public X()
   {  
     o = new Object();  
   }  

   public void x()  
   {  
     synchronized (o)
     {  
     }  
   }  
 }  

위의 코드가 최종 클래스가 아닌 필드에서 동기화하는 적절한 방법인지 모르겠습니다. 최종 필드가 아닌 필드를 어떻게 동기화 할 수 있습니까?

답변:


127

우선, 더 높은 수준의 추상화에서 동시성 문제를 처리하기 위해 열심히 노력할 것을 권장합니다. 즉 ExecutorServices, Callables, Futures 등과 같은 java.util.concurrent의 클래스를 사용하여 해결하는 것입니다 .

즉, 최종 필드가 아닌 필드 자체에서 동기화하는 것은 잘못된 것이 아닙니다 . 객체 참조가 변경되면 동일한 코드 섹션이 병렬로 실행될 수 있음 을 명심해야 합니다 . 즉, 한 스레드가 동기화 된 블록의 코드를 실행하고 누군가를 호출 setO(...)하면 다른 스레드가 동일한 인스턴스 에서 동일한 동기화 된 블록을 동시에 실행할 수 있습니다 .

독점 액세스가 필요한 개체 (또는 보호 전용 개체)를 동기화합니다.


1
나는 그 말인지 경우 가 아닌 최종 필드에 동기화, 당신은 객체에 단독으로 액세스 할 수있는 코드 실행의 조각이 있다는 사실을 알고 있어야 o동기화 된 블록에 도달 한 시간에 언급했다. o참조 하는 객체 가 변경되면 다른 스레드가 따라와 동기화 된 코드 블록을 실행할 수 있습니다.
aioobe

42
나는 당신의 경험 법칙에 동의하지 않습니다. 나는 유일한 목적이 다른 상태 를 보호하는 것이 목적인 객체에 동기화하는 것을 선호합니다 . 객체를 잠그는 것 이외의 다른 작업을 수행하지 않는 경우 다른 코드가 객체를 잠글 수 없음을 확실히 알 수 있습니다. 메서드를 호출하는 "실제"개체를 잠그면 해당 개체도 자체적으로 동기화 될 수 있으므로 잠금에 대해 추론하기가 더 어려워집니다.
Jon Skeet

9
내 대답에서 말했듯이, 나는 그것을 매우 신중하게 정당화해야한다고 생각합니다. 왜 그런 일을 하고 싶어 하는지 . 그리고에서 동기화하는 this것도 권장하지 않습니다. 잠금 목적으로 만 클래스에 최종 변수를 생성하여 다른 사람이 동일한 객체를 잠그지 못하도록 하는 것이 좋습니다 .
Jon Skeet 2011 년

1
그것은 또 다른 좋은 점이며 동의합니다. 최종 변수가 아닌 변수를 잠 그려면 신중한 정당성이 필요합니다.
aioobe

동기화에 사용되는 개체 변경과 관련된 메모리 가시성 문제에 대해 잘 모르겠습니다. 나는 당신이 객체를 변경하는 데 큰 어려움을 겪을 것이라고 생각하고 "동일한 코드 섹션이 병렬로 실행될 수 있도록"올바르게 변경되는 코드에 의존 할 것이라고 생각합니다. 동기화 블록 내에서 액세스하는 변수와 달리 잠금에 사용되는 필드의 메모리 가시성까지 메모리 모델에 의해 확장되는 보증이 무엇인지 잘 모르겠습니다. 내 경험 법칙은 당신이 무언가를 동기화한다면 그것은 최종적이어야한다는 것입니다.
Mike Q

47

정말 좋은 생각이 아니다 - 동기화 된 블록을 더 이상하기 때문에 정말 일관성있는 방법으로 동기화하지 않습니다.

동기화 된 블록이 한 번에 하나의 스레드 만 일부 공유 데이터에 액세스하도록 보장하기위한 것이라고 가정하면 다음을 고려하십시오.

  • 스레드 1이 동기화 된 블록에 들어갑니다. 예-공유 데이터에 독점적으로 액세스 할 수 있습니다 ...
  • 스레드 2는 setO ()를 호출합니다.
  • 스레드 3 (또는 여전히 2 ...)이 동기화 된 블록에 들어갑니다. Eek! 공유 데이터에 대한 배타적 액세스 권한이 있다고 생각하지만 스레드 1은 여전히 ​​이동 중입니다 ...

왜 것입니다 원하는 이 일이? 아마도 거기에 몇 가지 가 의미가 매우 전문 상황 ...하지만 당신은 내가 만족하실 것입니다 전에 (내가 위에서 준 시나리오의 종류를 완화하는 방법과 함께) 특정 사용 케이스에 저를 제시해야 할 것 그것.


2
@aioobe : 그러나 1가 여전히 목록을 돌연변이 (자주 참조되는 코드 실행 될 수있는 스레드 o) - 그리고 그 실행 과정에서 부분적으로 다른 목록을 돌연변이 시작합니다. 그게 어떻게 좋은 생각일까요? 나는 우리가 근본적으로 다른 방식으로 만지는 물체를 잠그는 것이 좋은 생각인지에 대해 동의하지 않는다고 생각합니다. 다른 코드가 잠금과 관련하여 무엇을하는지 알지 못해도 내 코드에 대해 추론 할 수 있습니다.
Jon Skeet 2011 년

2
@Felype : 좀 더 자세한 질문을 별도의 질문으로해야하는 것 같지만, 그렇습니다. 저는 종종 자물쇠처럼 별도의 객체를 생성합니다.
Jon Skeet

3
@VitBernatik : 아니요. 스레드 X가 구성 수정을 시작하면 스레드 Y가 동기화되는 변수의 값을 변경 한 다음 스레드 Z가 구성 수정을 시작하면 X와 Z가 동시에 구성을 수정하게됩니다. .
Jon Skeet 2015 년

1
요컨대, 이러한 잠금 객체를 항상 최종적으로 선언 하는 것이 더 안전 합니까?
St. Antario 2015

2
@LinkTheProgrammer : "동기화 된 메서드는 인스턴스의 모든 단일 개체를 동기화합니다."-그렇지 않습니다. 이것은 사실이 아니며 동기화에 대한 이해를 다시 검토해야합니다.
Jon Skeet

12

나는 John의 의견 중 하나에 동의합니다 . 변수의 참조가 변경되는 경우 불일치를 방지하기 위해 비 최종 변수에 액세스하는 동안 항상 최종 잠금 더미를 사용해야합니다. 따라서 어떤 경우에도 첫 번째 경험 법칙 :

규칙 # 1 : 필드가 최종이 아닌 경우 항상 (개인) 최종 잠금 더미를 사용하십시오.

이유 # 1 : 잠금을 유지하고 직접 변수의 참조를 변경합니다. 동기화 된 잠금 외부에서 대기중인 다른 스레드는 보호 된 블록에 들어갈 수 있습니다.

이유 # 2 : 잠금을 유지하고 다른 스레드가 변수의 참조를 변경합니다. 결과는 동일합니다. 다른 스레드가 보호 된 블록에 들어갈 수 있습니다.

그러나 최종 잠금 더미를 사용할 때 또 다른 문제 가 있습니다. 동기화 (object)를 호출 할 때 최종이 아닌 개체는 RAM 과만 동기화되기 때문에 잘못된 데이터를 얻을 수 있습니다. 따라서 두 번째 경험 법칙으로 :

규칙 # 2 : 최종 잠금이 아닌 개체를 잠글 때 항상 두 가지를 모두 수행해야합니다. RAM 동기화를 위해 최종 잠금 더미와 최종 잠금이 아닌 개체의 잠금을 사용합니다. (유일한 대안은 객체의 모든 필드를 휘발성으로 선언하는 것입니다!)

이러한 잠금을 "중첩 잠금"이라고도합니다. 항상 동일한 순서로 호출해야합니다. 그렇지 않으면 데드락이 발생합니다 .

public class X {
    private final LOCK;
    private Object o;

    public void setO(Object o){
        this.o = o;  
    }  

    public void x() {
        synchronized (LOCK) {
        synchronized(o){
            //do something with o...
        }
        }  
    }  
} 

보시다시피 두 자물쇠는 항상 함께 속하기 때문에 동일한 줄에 직접 작성합니다. 이와 같이 10 개의 중첩 잠금을 수행 할 수도 있습니다.

synchronized (LOCK1) {
synchronized (LOCK2) {
synchronized (LOCK3) {
synchronized (LOCK4) {
    //entering the locked space
}
}
}
}

이 코드는 synchronized (LOCK3)다른 스레드에서 와 같이 내부 잠금을 획득하는 경우 중단되지 않습니다 . 그러나 다음과 같은 다른 스레드를 호출하면 중단됩니다.

synchronized (LOCK4) {
synchronized (LOCK1) {  //dead lock!
synchronized (LOCK3) {
synchronized (LOCK2) {
    //will never enter here...
}
}
}
}

최종이 아닌 필드를 처리하는 동안 이러한 중첩 된 잠금에 대한 해결 방법은 하나뿐입니다.

규칙 # 2-대안 : 개체의 모든 필드를 휘발성으로 선언합니다. (여기서는이 작업의 단점에 대해 설명하지 않을 것입니다. 예를 들어 읽기를위한 x 레벨 캐시의 스토리지 방지, aso.)

따라서 aioobe가 옳습니다. java.util.concurrent를 사용하십시오. 또는 동기화에 대한 모든 것을 이해하고 중첩 된 잠금을 사용하여 직접 수행하십시오. ;)

최종 필드가 아닌 필드의 동기화가 중단되는 이유에 대한 자세한 내용은 내 테스트 사례를 살펴보십시오. https://stackoverflow.com/a/21460055/2012947

RAM 및 캐시로 인해 동기화가 필요한 이유에 대한 자세한 내용은 https://stackoverflow.com/a/21409975/2012947을 참조 하십시오.


1
setter를 o동기화 된 (LOCK) 으로 감싸서 설정과 읽기 객체 사이의 "이전 발생"관계를 설정 해야한다고 생각 합니다 o. 나는 비슷한 질문에서 이것을 논의하고있다 : stackoverflow.com/questions/32852464/…
Petrakeas

dataObject를 사용하여 dataObject 멤버에 대한 액세스를 동기화합니다. 그게 어떻게 잘못 되었나요? dataObject가 다른 곳을 가리 키기 시작하면 동시 스레드가 수정하는 것을 방지하기 위해 새 데이터에서 동기화되기를 원합니다. 그것에 문제가 있습니까?
Harmen 2015

2

나는 여기서 정답을 실제로 보지 못하고 있습니다. 즉, 그것을 하는 것은 완벽하게 괜찮습니다.

왜 경고인지 모르겠지만 아무 문제가 없습니다. JVM은 당신이 얻을 있는지 확인합니다 일부 는 값을 읽을 때 유효한 오브젝트 다시 (또는 null), 당신은에 동기화 할 수 있는 객체입니다.

잠금을 사용하는 동안 실제로 변경하려는 경우 (예 : 사용을 시작하기 전에 init 메서드에서 변경하는 것과 반대) 변경할 계획 인 변수를 만들어야합니다 volatile. 그런 다음 이전 개체와 새 개체 를 모두 동기화하기 만하면 값을 안전하게 변경할 수 있습니다.

public volatile Object lock;

...

synchronized (lock) {
    synchronized (newObject) {
        lock = newObject;
    }
}

그곳에. 복잡하지 않고 잠금 (뮤텍스)을 사용하여 코드를 작성하는 것은 실제로 매우 쉽습니다. 그것들없이 코드를 작성하는 것은 어려운 일입니다.


작동하지 않을 수 있습니다. o가 O1에 대한 참조로 시작된 다음 스레드 T1이 o (= O1)와 O2를 잠그고 o를 O2로 설정한다고 가정합니다. 동시에 스레드 T2는 O1을 잠그고 T1이 잠금을 해제 할 때까지 기다립니다. 잠금 O1을 받으면 o를 O3으로 설정합니다. 이 시나리오에서는 T1이 O1을 해제하고 T2가 O1을 잠그는 사이에 O1이 o를 통한 잠금에 대해 유효하지 않게되었습니다. 이때 다른 스레드는 잠금을 위해 o (= O2)를 사용하고 T2와의 레이스에서 중단없이 진행할 수 있습니다.
GPS

2

편집 : 따라서이 솔루션 (Jon Skeet이 제안한대로)은 개체 참조가 변경되는 동안 "synchronized (object) {}"구현 원자성에 문제가있을 수 있습니다. 개별적으로 물었고 erickson 씨에 따르면 스레드로부터 안전하지 않습니다. 참조 : 동기화 된 블록 원자로 들어가는가? . 그래서 그것을하지 않는 방법을 예로 들어보십시오-왜 링크와 함께;)

synchronised ()가 원자 적이면 어떻게 작동하는지 코드를 참조하십시오.

public class Main {
    static class Config{
        char a='0';
        char b='0';
        public void log(){
            synchronized(this){
                System.out.println(""+a+","+b);
            }
        }
    }

    static Config cfg = new Config();

    static class Doer extends Thread {
        char id;

        Doer(char id) {
            this.id = id;
        }

        public void mySleep(long ms){
            try{Thread.sleep(ms);}catch(Exception ex){ex.printStackTrace();}
        }

        public void run() {
            System.out.println("Doer "+id+" beg");
            if(id == 'X'){
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(1000);
                    // do not forget to put synchronize(cfg) over setting new cfg - otherwise following will happend
                    // here it would be modifying different cfg (cos Y will change it).
                    // Another problem would be that new cfg would be in parallel modified by Z cos synchronized is applied on new object
                    cfg.b=id;
                }
            }
            if(id == 'Y'){
                mySleep(333);
                synchronized(cfg) // comment this and you will see inconsistency in log - if you keep it I think all is ok
                {
                    cfg = new Config();  // introduce new configuration
                    // be aware - don't expect here to be synchronized on new cfg!
                    // Z might already get a lock
                }
            }
            if(id == 'Z'){
                mySleep(666);
                synchronized (cfg){
                    cfg.a=id;
                    mySleep(100);
                    cfg.b=id;
                }
            }
            System.out.println("Doer "+id+" end");
            cfg.log();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Doer X = new Doer('X');
        Doer Y = new Doer('Y');
        Doer Z = new Doer('Z');
        X.start();
        Y.start();
        Z.start();
    }

}

1
이것은 괜찮을 있습니다. 그러나 동기화 한 값이 가장 최근에 작성된 값이라는 메모리 모델에 대한 보장이 있는지 여부는 알 수 없습니다. 원자 적으로 "읽고 동기화"한다는 보장이 없다고 생각합니다. 개인적으로 저는 단순성을 위해 어쨌든 다른 용도로 사용되는 모니터에서 동기화를 피하려고합니다. (별도의 필드를 가짐으로써, 코드가된다 명확하게 그것에 대해 신중 이유로하는 대신 올바른.)
존 소총

@Jon. 대답을위한 Thx! 당신의 걱정을 들었습니다. 이 경우 외부 잠금 장치가 "동기화 된 원 자성"에 대한 질문을 피할 것이라는 데 동의합니다. 따라서 바람직합니다. 런타임에 더 많은 구성을 도입하고 다른 스레드 그룹에 대해 다른 구성을 공유하려는 경우가있을 수 있습니다 (내 경우는 아님). 그러면이 솔루션이 흥미로워 질 수 있습니다. 나는 질문을 게시 stackoverflow.com/questions/29217266/... 우리가 사용하는 (누군가의 회신입니다) 할 수 있는지 볼 수 있도록 - 동기화 () 원 자성을
비타 Bernatik

2

AtomicReference 는 귀하의 요구 사항에 적합합니다.

원자 패키지 에 대한 Java 문서에서 :

단일 변수에 대한 잠금없는 스레드 안전 프로그래밍을 지원하는 작은 클래스 툴킷입니다. 본질적으로이 패키지의 클래스는 휘발성 값, 필드 및 배열 요소의 개념을 다음 형식의 원자 조건부 업데이트 작업도 제공하는 것으로 확장합니다.

boolean compareAndSet(expectedValue, updateValue);

샘플 코드 :

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

위의 예에서 String자신의Object

관련 SE 질문 :

Java에서 AtomicReference를 언제 사용합니까?


1

o의 인스턴스 수명 동안 변경되지 않는 경우 X동기화 관련 여부에 관계없이 두 번째 버전이 더 나은 스타일입니다.

이제 첫 번째 버전에 문제가 있는지 여부는 해당 클래스에서 다른 일이 벌어지고 있는지 모른 채 대답 할 수 없습니다. 나는 컴파일러가 오류가 발생하기 쉽다는 것에 동의하는 경향이 있습니다 (다른 사람들이 말한 것을 반복하지 않을 것입니다).


1

2 센트 만 더하면 : 디자이너를 통해 인스턴스화 된 구성 요소를 사용할 때이 경고가 표시되었으므로 생성자가 매개 변수를 사용할 수 없기 때문에 필드가 실제로 최종일 수 없습니다. 즉 final 키워드없이 준결승전을 가졌다.

나는 그것이 단지 경고하는 이유라고 생각합니다. 당신은 아마도 뭔가 잘못하고 있지만 그것이 옳을 수도 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.