synchronized 원리에 깊이 들어가다

참조 블로그
synchronized 키워드는 주로 다음과 같은 3가지 응용 방식이 있다.실례를 수식하는 방법은 현재 실례에 자물쇠를 채우는 데 작용하고 동기화 코드에 들어가기 전에 현재 실례의 자물쇠를 획득해야 한다.정적 방법을 수식하고 현재 클래스 대상에 자물쇠를 채우는 데 작용하며 동기화 코드에 들어가기 전에 현재 클래스 대상의 자물쇠를 획득해야 한다.수식 코드 블록, 잠금 대상을 지정하고 주어진 대상에 잠금을 가하며 동기화 코드 라이브러리에 들어가기 전에 주어진 대상의 잠금을 획득해야 한다.

Java 객체 헤드


Java 객체 헤드는 다음과 같은 두 부분으로 구성됩니다. 1.MarkWord: 개체의 hashCode, 잠금 정보 또는 세대별 연령 또는 GC 로고 등의 정보를 저장합니다.Class Metadata Address: 유형 포인터는 객체의 클래스 메타데이터를 가리키며 JVM은 이 포인터를 통해 객체가 어떤 클래스의 인스턴스인지 결정합니다.
여기서 Mark Word는 기본적으로 객체의 HashCode, 세대별 나이, 잠금 표시 위치를 저장합니다.객체 헤드의 정보는 객체 자체가 정의한 데이터와 관련이 없는 추가 저장 비용이기 때문에 JVM의 공간 효율을 고려하여 MarkWord는 더 많은 유효한 데이터를 저장하기 위해 고정되지 않은 데이터 구조로 설계되었고 대상 자체의 상태에 따라 자신의 저장 공간을 복용할 것이다.

Synchronized는 헤비급 자물쇠입니다.


자바 초기 버전에서synchronized는 중량급 자물쇠에 속하고 효율이 낮다. 모니터 자물쇠(monitor)는 하부의 운영체제Mutex Lock에 의존하여 이루어진 것이고 운영체제가 라인 간의 전환을 실현할 때 사용자 상태에서 핵심 상태로 전환해야 하기 때문에 이 상태 간의 전환은 비교적 긴 시간이 필요하고 시간 원가가 상대적으로 높다. 이것은 초기의synchronized 효율이 낮은 이유이다.
그러나 자바6 이후 자바는 JVM 차원에서 synchronized에 대한 최적화가 비교적 컸기 때문에 현재의 synchronized 자물쇠 효율도 잘 최적화되었다. 자바6 이후 자물쇠를 획득하고 방출하는 데 가져오는 성능 소모를 줄이기 위해 경량급 자물쇠와 편향 자물쇠를 도입하고 자전거 자물쇠를 기본적으로 열었다.

Synchronized 최적화


자물쇠의 상태는 모두 네 가지가 있는데 무자물쇠 상태, 편향 자물쇠, 경량급 자물쇠와 중량급 자물쇠가 있다.자물쇠의 경쟁에 따라 자물쇠는 편향 자물쇠에서 경량급 자물쇠로 승급할 수 있고 다시 승급할 수 있는 중량급 자물쇠이지만 자물쇠의 승급은 일방적이다. 즉, 낮은 자물쇠에서 높은 자물쇠로만 승급할 수 있고 자물쇠의 강등이 나타나지 않는다는 것이다.
  • 편향 잠금 편향 잠금은 자바6 이후에 추가된 것으로 잠금 조작을 위한 최적화 수단이다.대부분의 경우 자물쇠는 다중 스레드 경쟁이 존재하지 않을 뿐만 아니라 항상 같은 스레드에서 여러 번 얻을 수 있기 때문에 같은 스레드에서 자물쇠를 얻는 대가를 줄이기 위해 편향 자물쇠를 도입한다.편향 자물쇠의 핵심 사상은 만약에 한 라인이 자물쇠를 얻으면 자물쇠가 편향 모드에 들어간다는 것이다. 이때 마크 워드의 구조도 편향 자물쇠 구조로 바뀌었다. 이 라인이 다시 자물쇠를 요청할 때 동기화 작업을 하지 않아도 된다. 즉, 자물쇠를 얻는 과정을 절약하면 대량의 자물쇠 신청과 관련된 조작을 절약하고 프로그램의 성능을 제공할 수 있다.따라서 자물쇠가 없는 경쟁 장소에서 편향 자물쇠는 최적화 효과가 좋다. 왜냐하면 여러 번 같은 라인에서 같은 자물쇠를 신청할 가능성이 매우 높다.다른 라인이 이 자물쇠를 얻으려고 시도하면 편향 자물쇠가 효력을 상실합니다. 주의해야 할 것은 편향 자물쇠가 실패한 후에 바로 중량 자물쇠로 팽창하지 않고 경량 자물쇠로 승급됩니다.
  • 경량급 자물쇠가 편향 자물쇠에 실패하면 가상 기기는 즉각 중량급 자물쇠로 업그레이드되지 않고 경량급 자물쇠라고 불리는 최적화 수단(Java6 이후 추가)을 사용하려고 시도한다. 이때 마크 워드의 구조도 경량급 자물쇠의 구조로 바뀐다.경량급 자물쇠가 프로그램 성능을 향상시킬 수 있는 근거는'대부분의 자물쇠에 대해 전체 동기화 주기 내에 경쟁이 존재하지 않는다'는 것이다. 이것은 경험 데이터이다.알아야 할 것은 경량급 자물쇠가 적응하는 장면은 라인이 교체되어 동기 블록을 집행하는 장소이다. 일단 두 개 이상의 라인이 같은 자물쇠를 동시에 경쟁하면 경량급 자물쇠가 중량급 자물쇠로 팽창하게 된다.경량급 자물쇠 최적화의 본질은 CAS 조작을 사용하여 동기화가 사용하는 상호 배척량을 없애는 것이다. 즉, 사용자 상태에서 핵심 상태로 전환하는 것을 피하는 것이다.편향 자물쇠 최적화의 본질은 경쟁 없이 전체 동기화를 없애고 CAS도 하지 않는다는 것이다.
  • 자전거 자물쇠 경량급 자물쇠가 실패한 후 가상 기기는 라인이 실제적으로 운영체제 차원에서 걸리는 것을 피하기 위해 자전거 자물쇠라고 불리는 최적화 수단을 실시한다.이것은 대부분의 상황에서 라인이 자물쇠를 가지고 있는 시간이 길지 않기 때문에 만약에 운영체제 차원의 라인을 직접 걸면 얻는 것보다 잃는 것이 많을 수 있다. 왜냐하면 운영체제가 라인 간의 전환을 실현할 때 사용자 상태에서 핵심 상태로 전환해야 하기 때문에 이 상태 간의 전환은 비교적 긴 시간을 필요로 하고 시간 원가가 상대적으로 높다.따라서 자전거 자물쇠는 머지않아 현재의 라인에서 자물쇠를 얻을 수 있다고 가정할 수 있기 때문에 가상 기회는 현재 자물쇠를 얻으려는 라인에 몇 개의 빈 순환을 하게 한다(이것도 자전거의 원인이라고 부른다). 일반적으로 오래 걸리지 않고 기본적으로 10번의 순환으로 하고 몇 번의 순환을 거친 후에 자물쇠를 얻으면 임계 구역에 순조롭게 들어간다.만약에 자물쇠를 얻지 못하면 라인을 운영체제 차원에서 끊는다. 이것이 바로 자전거 자물쇠의 최적화 방식이다. 이런 방식도 확실히 효율을 높일 수 있다.결국 어쩔 수 없이 중량급 자물쇠로 승급할 수밖에 없었다.자전거 자물쇠의 성능을 한층 높이기 위해 JDK 1.6 이후 자체 적응 자전거 자물쇠를 도입했는데 그 자전거 횟수는 전번에 같은 자물쇠에 잠긴 자전거 시간과 자물쇠의 소유자의 상태에 따라 결정된다.만약 같은 자물쇠 대상에서 자전거가 자물쇠를 얻는 데 성공하고 자물쇠를 가진 라인이 실행 중이라면 가상 기회는 이번 자전거도 다시 성공할 가능성이 높다고 생각하고 상대적으로 더 많은 자전거 횟수를 허용할 것이다.반대로 어떤 자물쇠에 대해 자전거가 성공하지 못하면 나중에 이 자물쇠를 얻으려면 자전거 과정을 생략하고 CPU 자원의 낭비를 피할 수 있다.자전거 자물쇠 최적화의 본질도 CAS 조작을 사용하여 동기화를 없애는 데 사용되는 상호 배척량이다. 이것은 경량급 자물쇠와 달리 경량급 자물쇠는 순환이 없고 자전거 자물쇠는 여러 번 순환을 허용한다.
  • 잠금 제거 잠금 제거는 가상 기기의 또 다른 잠금 최적화이다. 이런 최적화는 더욱 철저하다. 자바 가상 기기가 JIT에서 컴파일할 때(어떤 코드가 처음 실행될 때 컴파일하는 것으로 간단하게 이해할 수 있고 실시간 컴파일이라고도 부른다) 상하문에 대한 스캔을 통해 공유 자원 경쟁이 존재하지 않는 잠금을 제거한다. 이런 방식으로 불필요한 잠금을 제거하면 무의미한 요청 잠금 시간을 절약할 수 있다.다음의 StringBuffer의 append는 동기화 방법이지만dd 방법에서 StringBuffer는 국부 변수에 속하고 다른 라인에 사용되지 않기 때문에 StringBuffer는 공유 자원 경쟁의 상황이 존재할 수 없기 때문에 JVM은 자동으로 자물쇠를 제거한다.
    public class StringBufferRemoveSync {
    
        public void add(String str1, String str2) {
            // StringBuffer      , append    synchronized      ,
            //   sb          ,J  VM         
            StringBuffer sb = new StringBuffer();
            sb.append(str1).append(str2);
        }
    
        public static void main(String[] args) {
            StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
            for (int i = 0; i < 10000000; i++) {
                rmsync.add("abc", "123");
            }
        }
    } 
  • 자물쇠 조화 원칙상 우리가 코드를 작성할 때 항상 동기화 코드 블록의 작용 범위를 최대한 적게 제한하는 것을 추천한다.그러나 만약에 일련의 조작이 같은 대상에 대해 반복적으로 자물쇠를 채우고 잠금을 풀고 심지어 자물쇠를 채우는 조작이 순환체 안에 나타나면 제때에 라인 경쟁이 없지만 빈번하게 서로 밀어붙이는 동기화 조작을 하면 많은 성능을 떨어뜨릴 수 있다.
    int i = 0;
    public void method1(){
        for(int i = 0;i<1000;i++){
            synchronized(this){ //1  synchronized      
                i++;
            }
        }
    }
    
    StringBuffer sb = new StringBuffer();
    public void method2(){
        for(int i = 0;i<1000;i++){
            sb.append(i+""); // 2       synchronized     
        }
    }
    상기 코드에서 1의 경우 우리는synchronized의 위치를 수동으로 조정하여 잦은 잠금, 잠금 해제를 피할 수 있다면 2의 경우 잠금 조화로 조정할 수 밖에 없다(당연히synchronized에stringBuilder를 넣어도 되지만 만stringBuffer를 사용해야 한다면?).

  • synchronized의 리셋 가능성


    상호 배척 자물쇠의 디자인에서 볼 때 한 라인이 다른 라인이 가지고 있는 대상 자물쇠의 임계 자원을 조작하려고 시도할 때 막힌 상태에 처하지만 한 라인이 자신이 가지고 있는 대상 자물쇠의 임계 자원을 다시 요청할 때 이런 상황은 재접속 자물쇠에 속한다. 요청은 성공할 것이다. 자바에서synchronized는 원자성을 바탕으로 하는 내부 자물쇠 메커니즘으로 재접속할 수 있다.따라서 한 라인에서synchronized 방법을 호출하는 동시에 그 방법체 내부에서 이 대상의 다른synchronized 방법을 호출한다. 즉, 한 라인이 하나의 대상 자물쇠를 받은 후에 다시 이 대상 자물쇠를 요청하는 것은 허용된다. 이것이synchronized의 중입성이다.
    ```
    int i = 0;
    public void method(){
        synchronized(this){//    this     
            synchronized(this){//     this     ,    monitor     ,      
                i++;
            }   
        }
    }
    ```
    

    다시 들어가면 모니터의 계수기의 값이 1보다 클 수 있음을 주의하십시오.

    좋은 웹페이지 즐겨찾기