요약
이 시리즈는 숫자를 금으로 정제하는 과정을 기반으로하며 더 잘 배우기 위해 일련의 레코드가 만들어졌습니다. 이 기사는 주로 다음을 소개합니다. 1. 잠금의 아이디어와 방법 최적화 2. 가상 머신의 잠금 최적화 3. 잠금의 잘못된 사용 4. 스레드 로컬 및 소스 코드 분석.
1. 잠금 최적화의 아이디어와 방법
동시성 수준은 [높은 동시성 Java 1]에 대한 소개에서 언급된다.
잠금 장치가 사용되면, 이것이 차단되므로 동시성은 일반적으로 잠금 상황보다 약간 낮습니다.
여기에 언급 된 잠금 최적화는 차단의 경우 성능이 너무 나빠지는 것을 방지하는 방법을 나타냅니다. 그러나 어떻게 최적화하더라도 성능은 일반적으로 잠금 상황보다 약간 나쁩니다.
[높은 동시성 Java V] JDK 동시성 패키지 1에 언급 된 Reintrantlock의 트리 록은 Trylock이 판단 할 때 자체적으로 매달리지 않기 때문에 잠금 방법이되는 경향이 있습니다.
잠금 최적화의 아이디어와 방법을 요약하기 위해 다음 유형이 있습니다.
1.1 잠금 상태 시간을 줄입니다
public synchronized void syncmethod () {OtherCode1 (); MUTEXTMETHOD (); 기타 코드 2 (); } 위의 코드와 마찬가지로 메소드를 입력하기 전에 잠금 장치를 가져와야하며 다른 스레드는 외부에서 기다려야합니다.
여기서 최적화 지점은 다른 스레드의 대기 시간을 줄이는 것이므로 스레드 안전 요구 사항이있는 프로그램에 잠금을 추가하는 데만 사용됩니다.
public void syncmethod () {OtherCode1 (); 동기화 (this) {mutextMethod (); } OtherCode2 (); }1.2 잠금 입자 크기를 줄입니다
큰 물체를 분할 (이 객체는 많은 스레드에 의해 액세스 할 수 있음)을 작은 물체로 액세스하여 병렬 처리를 크게 증가시키고 잠금 경쟁을 줄입니다. 자물쇠에 대한 경쟁을 줄이고 자물쇠에 대한 편견을 줄임으로써 경량 잠금의 성공률이 향상됩니다.
자물쇠 세분화를 줄이는 가장 일반적인 사례는 동의어입니다. 이것은 [High Concurrency Java V] JDK Concurrency 패키지 1에 언급되어 있습니다.
1.3 잠금 분리
가장 일반적인 잠금 분리는 읽기 쓰기 잠금 readwritelock이며, 이는 읽기 쓰기 잠금으로 분리되고 함수에 따라 잠금 장치로 분리됩니다. 이런 식으로 읽기와 독서는 상호 배타적이지 않으며 읽기와 쓰기는 상호 배타적이며 스레드 안전을 보장하고 성능을 향상시킵니다. 자세한 내용은 [High Concurrency Java V] JDK Concurrency 패키지 1을 확인하십시오.
읽기와 쓰기의 분리에 대한 아이디어는 확장 될 수 있으며, 운영이 서로 영향을 미치지 않는 한 잠금을 분리 할 수 있습니다.
예를 들어 LinkedBlockingqueue입니다
머리에서 꺼내어 데이터를 꼬리에서 넣으십시오. 물론 그것은 [High Concurrency Java VI] JDK 동시성 패키지 2에 언급 된 Forkjoinpool의 작업 도난과 유사합니다.
1.4 잠금 거칠게
일반적으로, 여러 스레드 간의 효과적인 동시성을 보장하기 위해, 각 스레드는 잠금을 가능한 한 짧게 고정해야합니다. 즉, 공공 자원을 사용한 후 즉시 잠금을 해제해야합니다. 이 방법으로만이 잠금을 기다리는 다른 스레드는 가능한 빨리 작업을 실행하기위한 리소스를 얻을 수 있습니다. 그러나 모든 것은 어느 정도입니다. 동일한 잠금 장치가 지속적으로 요청, 동기화 및 릴리스되면 시스템의 귀중한 리소스를 소비하여 성능 최적화에 도움이되지 않습니다.
예를 들어:
public void demomethod () {synchronized (lock) {// sth. } // 다른 원치 않는 동기화 작업을 수행하지만 빠르게 동기화 (잠금) {// sth. }} 이 경우, 록 러닝이라는 아이디어에 따르면 병합되어야합니다.
public void demomethod () {// 잠금 요청에 통합 (잠금) {// sth. // 다른 원치 않는 동기화 작업을 수행하지만 빠르게 실행할 수 있습니다}} 물론, 전제 조건이 있으며, 동기화가 필요하지 않은 중간에있는 작업이 빠르게 실행됩니다.
또 다른 극단적 인 예를 들어 드리겠습니다.
for (int i = 0; i <circle; i ++) {synchronized (lock) {}} 잠금 장치는 루프로 얻어야합니다. JDK는 내부적 으로이 코드를 최적화하지만 직접 작성하는 것이 좋습니다.
synchronized (lock) {for (int i = 0; i <circle; i ++) {}} 물론, 그러한 루프가 너무 길고 다른 스레드가 너무 오래 기다리지 않도록해야한다고 말할 필요가 있다면 위와 같이 쓸 수 있습니다. 그러한 유사한 요구 사항이 없다면 다음 요구 사항에 직접 작성하는 것이 좋습니다.
1.5 잠금 제거
잠금 제거는 컴파일러 레벨입니다.
인스턴트 컴파일러에서 공유 할 수없는 객체를 찾으면 이러한 객체의 잠금 작동을 제거 할 수 있습니다.
어쩌면 일부 객체는 여러 스레드로 액세스 할 수 없으므로 왜 잠금을 추가해야합니까? 코드를 작성할 때 잠금 장치를 추가하지 않는 것이 낫지 않을 것입니다.
그러나 때로는 이러한 자물쇠는 프로그래머가 작성하지 않습니다. 그들 중 일부는 벡터 및 Stringbuffer와 같은 클래스와 같은 JDK 구현에 잠금 장치가 있습니다. 그들의 방법 중 다수에는 자물쇠가 있습니다. 스레드 안전없이 이러한 클래스의 방법을 사용하면 특정 조건이 충족되면 컴파일러는 잠금을 제거하여 성능을 향상시킵니다.
예를 들어:
public static void main (String Args [])은 중단 된 결과를 던졌습니다. {long start = system.currenttimeMillis (); for (int i = 0; i <20000000; i ++) {CreateStringBuffer ( "JVM", "Diagnosis"); } long buffercost = System.CurrentTimeMillis () - 시작; System.out.println ( "CraetestringBuffer :" + buffercost + "ms"); } public static String createStringBuffer (String S1, String S2) {StringBuffer sb = new StringBuffer (); sb.append (S1); SB. Append (S2); 반환 sb.toString (); } 위의 Code의 StringBuffer.append는 동기화 작업이지만 StringBuffer는 로컬 변수이며 메소드는 StringBuffer를 반환하지 않으므로 여러 스레드가 액세스 할 수 없습니다.
그런 다음 StringBuffer의 동기화 작업은 현재 의미가 없습니다.
잠금 취소는 JVM 매개 변수에 설정되어 있습니다. 물론 서버 모드에 있어야합니다.
-server -xx :+doScapeAnalysis -xx :+Eminatelocks
탈출 분석을 켜십시오. 탈출 분석의 기능은 변수가 범위에서 벗어날 수 있는지 확인하는 것입니다.
예를 들어, 위의 StringBuffer에서 위의 코드에서 CraetestringBuffer의 반환은 문자열 이므로이 로컬 변수 StringBuffer는 다른 곳에서는 사용되지 않습니다. CraetestringBuffer를 변경하면
public static StringBuffer CraetestringBuffer (String S1, String S2) {StringBuffer SB = New StringBuffer (); sb.append (S1); SB. Append (S2); 반환 SB; } 그런 다음이 StringBuffer가 반환되면 다른 곳에서 사용될 수 있습니다 (예 : 기본 함수는 결과를 반환하여 맵에 넣습니다). 그런 다음 JVM 탈출 분석을 분석 할 수 있습니다.이 로컬 변수 StringBuffer가 그 범위를 벗어납니다.
따라서 탈출 분석을 기반으로 JVM은 로컬 변수 StringBuffer가 범위를 피하지 않으면 StringBuffer가 여러 스레드에 의해 액세스되지 않는다고 판단한 다음 이러한 추가 잠금 장치를 제거하여 성능을 향상시킬 수 있다고 판단 할 수 있습니다.
JVM 매개 변수가 다음과 같은 경우
-server -xx :+doScapeAnalysis -xx :+Eminatelocks
산출:
Craetestringbuffer : 302ms
JVM 매개 변수는 다음과 같습니다.
-server -xx :+docapeanalysis -xx : -eliminatelocks
산출:
Craetestringbuffer : 660ms
분명히, 잠금 제거 효과는 여전히 매우 분명합니다.
2. 가상 머신의 최적화 잠금
먼저 객체 헤더를 소개해야합니다. JVM에는 각 객체에는 객체 헤더가 있습니다.
마크 워드, 객체 헤더의 마커, 32 비트 (32 비트 시스템).
해시, 잠금 정보, 쓰레기 수집 태그, 나이를 설명하십시오
또한 잠금 레코드, 모니터에 대한 포인터, 바이어스 잠금 스레드 ID 등에 대한 포인터를 저장합니다.
간단히 말해서 객체 헤더는 일부 체계적인 정보를 저장하는 것입니다.
2.1 포지티브 잠금
소위 바이어스는 편심입니다. 즉, 잠금은 현재 잠금 장치를 소유 한 스레드를 향한 경향이 있습니다.
대부분의 경우 경쟁이 없습니다 (대부분의 경우 동기화 블록에는 경쟁 잠금 시간에 동시에 여러 스레드가 없음). 바이어스를 통해 성능을 향상시킬 수 있습니다. 즉, 경쟁이 없을 때, 이전에 잠금을 얻은 스레드가 잠금을 다시 얻을 때 잠금이 나에게 가리키는 지 여부를 결정하므로 스레드는 잠금을 다시 얻을 필요가 없으며 동기화 블록을 직접 입력 할 수 있습니다.
바이어스 잠금의 구현은 객체 헤더 마크의 마크를 바이어스 된 것으로 설정하고 스레드 ID를 객체 헤더 마크에 쓰는 것입니다.
다른 스레드가 동일한 잠금을 요청하면 바이어스 모드가 종료됩니다.
JVM은 기본적으로 -xx :+useBiasedLocking으로 바이어스 잠금을 활성화합니다
치열한 경쟁에서 편견 잠금 장치는 시스템 부담을 증가시킬 것입니다 (편견이 있는지에 대한 판단은 매번 추가됩니다)
바이어스 잠금의 예 :
패키지 테스트; import java.util.list; import java.util.vector; public class test {public static list <integer> numberlist = new vector <integer> (); public static void main (string [] args)은 중단 된 예를 던졌습니다. {long begin = system.currenttimeMillis (); int count = 0; int startnum = 0; while (count <100000000) {numberlist.add (startnum); startnum += 2; 카운트 ++; } long end = System.CurrentTimeMillis (); System.out.println (END- 시작); }} 벡터는 내부적으로 잠금 메커니즘을 사용하는 스레드 안전 클래스입니다. 추가 할 때마다 잠금 요청이 이루어집니다. 위의 코드에는 하나의 스레드 메인 만 있고 잠금 요청을 반복적으로 추가합니다.
다음 JVM 매개 변수를 사용하여 바이어스 잠금을 설정하십시오.
-xx :+useBiasedLocking -XX : BIISEDLOCKINGSTARTUPDELAY = 0
BIASEDLOCKINGSTARTUPDELAY는 시스템이 몇 초 동안 시작된 후 바이어스 잠금이 활성화됨을 의미합니다. 시스템이 시작될 때 일반 데이터 경쟁은 비교적 치열하기 때문에 기본값은 4 초입니다. 현재 활성화 된 바이어스 잠금으로 성능이 줄어 듭니다.
여기에서 바이어스 잠금의 성능을 테스트하기 위해 지연 바이어스 잠금 시간이 0으로 설정됩니다.
현재 출력은 9209입니다
아래 바이어스 잠금을 끄십시오.
-xx : -usebiasedlocking
출력은 9627입니다
일반적으로 경쟁이 없으면 바이어스 잠금을 가능하게하는 성능이 약 5%향상됩니다.
2.2 경량 잠금
Java의 다중 스레드 안전은 잠금 메커니즘을 기반으로 구현되며 잠금의 성능은 종종 만족스럽지 않습니다.
그 이유는 Moniterenter와 Monitorexit, Multithread Synchronization을 제어하는 2 개의 바이트 코드 프리미티브가 운영 체제의 Mutex에 대한 JVM에 의해 구현되기 때문입니다.
MUTEX는 비교적 자원 소비 작업으로 스레드가 매달려 있으며 짧은 시간 내에 원래 스레드로 다시 조정해야합니다.
Java의 잠금 메커니즘을 최적화하기 위해 Java6 이후 경량 잠금의 개념이 도입되었습니다.
경량 잠금은 MUTEX를 대체하지 않고 멀티 스레딩이 뮤텍스로 들어갈 가능성을 줄이기위한 것입니다.
CPU 원시 비교 및 스웨이 (CAS, 조립 명령 CMPXCHG)를 사용하고 MUTEX에 들어가기 전에 치료를 시도합니다.
바이어스 잠금이 실패하면 시스템은 경량 잠금 작동을 수행합니다. 그 존재의 목적은 성능이 상대적으로 열악하기 때문에 운영 체제 수준에서 MUTEX를 활용하는 것을 피하는 것입니다. JVM 자체는 응용 프로그램이므로 응용 프로그램 수준에서 스레드 동기화 문제를 해결하고자합니다.
요약하면, 경량 잠금은 빠른 잠금 방법입니다. MUTEX를 입력하기 전에 CAS 작업을 사용하여 잠금 장치를 추가하십시오. 운영 체제 수준에서 MUTEX를 사용하여 성능을 향상시키지 마십시오.
그런 다음 바이어스 잠금이 실패하면 가벼운 잠금의 단계가 다음과 같습니다.
1. 객체 헤더의 마크 포인터를 잠긴 객체에 저장합니다 (여기서 객체는 동기화 된 객체 (this) {}와 같은 잠긴 객체를 나타냅니다. 이것은 여기에서 객체입니다).
lock-> set_displaced_header (Mark);
2. 객체 헤더를 잠금 포인터 (스레드 스택 공간)로 설정합니다.
if (mark == (markoop) atomic :: cmpxchg_ptr (lock, obj ()-> mark_addr (), mark)) {tevent (slow_enter : 릴리스 스택 클락); 반품 ; } 잠금 장치는 스레드 스택에 있습니다. 따라서 스레드 가이 잠금을 고정하는지 여부를 결정하려면 객체 헤더가 가리키는 공간이 스레드 스택의 주소 공간에 있는지 확인하십시오.
경량 잠금 장치가 실패하면 경쟁이 있고 헤비급 잠금 장치 (일반 잠금)로 업그레이드되며, 이는 운영 체제 수준의 동기화 방법입니다. 잠금 경쟁이 없으면 경량 잠금 장치는 OS 뮤텍스를 사용한 기존 잠금으로 인한 성능 손실을 줄입니다. 경쟁이 매우 치열한 경우 (가벼운 잠금 장치가 항상 실패), 가벼운 잠금 장치는 많은 추가 작업을 수행하여 성능 저하를 초래합니다.
2.3 스핀 잠금
경쟁이 존재하면 경량 잠금 시도가 실패하기 때문에 경쟁이 실패하면 운영 체제 레벨 상호 배제를 사용하기 위해 헤비급 잠금으로 직접 업그레이드 될 수 있습니다. 스핀 잠금 장치를 다시 시도 할 수도 있습니다.
스레드가 잠금을 빨리 얻을 수 있다면 OS 레이어에 스레드를 걸지 않고 스레드가 여러 개의 빈 작업 (스핀)을 수행하고 끊임없이 잠금을 얻으려고 시도합니다 (Trylock과 유사). 물론 루프 수는 제한적입니다. 루프 수에 도달하면 여전히 헤비급 잠금으로 업그레이드됩니다. 따라서 각 스레드가 잠금을 고정 할 시간이 거의 없으면 스핀 잠금 장치는 OS 레이어에 스레드가 매달리지 않도록 시도 할 수 있습니다.
JDK1.6 -XX :+USESPINNING이 활성화되었습니다
JDK1.7 에서이 매개 변수를 제거하고 내장 구현으로 변경하십시오.
동기화 블록이 매우 길고 스핀이 실패하면 시스템 성능이 저하됩니다. 동기화 블록이 매우 짧고 스핀이 성공하면 스레드 서스펜션 스위칭 시간을 절약하고 시스템 성능을 향상시킵니다.
2.4 포지티브 잠금, 경량 잠금, 스핀 잠금 요약 요약
위의 잠금은 Java 언어 수준의 잠금 최적화 방법이 아니라 JVM에 내장되어 있습니다.
우선, 바이어스 잠금은 동일한 잠금 장치를 반복적으로 획득/릴리스 할 때 스레드의 성능 소비를 피하는 것입니다. 동일한 스레드가 여전히이 잠금 장치를 얻는 경우 잠금을 바이어스하려고 할 때 동기화 블록에 직접 입력되며 잠금을 다시 얻을 필요가 없습니다.
가벼운 잠금 장치와 스핀 잠금 장치는 모두 운영 체제 수준에서 뮤텍스 작업에 대한 직접적인 호출을 피하기위한 것입니다.
헤비급 잠금 장치 (운영 체제 수준에서의 뮤트) 사용을 피하기 위해 먼저 경량 잠금을 시도합니다. 경량 잠금은 CAS 작동을 사용하여 잠금을 얻으려고합니다. 경량 잠금이 얻지 못하면 경쟁이 있음을 의미합니다. 그러나 곧 잠금 장치를 얻을 수 있고 스핀 잠금 장치를 시도하고 스레드에 빈 루프를 몇 번 수행하고 루프 할 때마다 잠금을 얻으려고 노력할 것입니다. 스핀 잠금 장치도 실패하면 헤비급 잠금으로 만 업그레이드 할 수 있습니다.
바이어스 잠금 장치, 경량 잠금 및 스핀 잠금 장치는 모두 낙관적 인 잠금 장치임을 알 수 있습니다.
3. 자물쇠를 잘못 사용하는 경우
공개 클래스 integerlock {정적 정수 i = 0; public static class addthread는 스레드 {public void run () {for (int k = 0; k <100000; k ++) {synchronized (i) {i ++; }}}} public static void main (String [] args)은 InterruptedException {addThread t1 = new AddThread (); AddThread T2 = New AddThread (); t1.start (); t2.start (); t1.join (); t2.join (); System.out.println (i); }} 매우 기본적인 실수는 [High Concurrency Java VII] 동시성 설계 패턴에서 인터저기가 최종 변경되지 않으며, 각 ++ 후에는 새로운 인터저기가 생성되어 I에 할당 될 것이므로 두 스레드 사이에 경쟁하는 잠금 장치가 다르다는 것입니다. 따라서 스레드 안전이 아닙니다.
4. ThreadLocal 및 소스 코드 분석
여기에서 ThreadLocal을 언급하는 것은 약간 부적절 할 수 있지만 ThreadLocal은 잠금을 교체하는 방법입니다. 따라서 여전히 언급해야합니다.
기본 아이디어는 멀티 스레드에서 데이터 충돌이 잠겨 있어야한다는 것입니다. ThreadLocal을 사용하는 경우 각 스레드마다 객체 인스턴스가 제공됩니다. 다른 스레드는 다른 객체가 아닌 자신의 객체에만 액세스합니다. 이렇게하면 자물쇠가 존재할 필요가 없습니다.
패키지 테스트; import java.text.parseexception; import java.text.simpledateformat; import java.util.date; import java.util.concurrent.executorservice; import java.util.concurrent.executors; public static final sumpledateformat sdf = 새로운 Simpleateformat ( "new Simpleateformat ("himplecutors; import java.util.concurrent HH : MM : SS "); 공개 정적 클래스 구문 분석은 실행 가능한 {int i = 0; 대중 구문 분석 (int I) {this.i = i; } public void run () {try {날짜 t = sdf.parse ( "2016-02-16 17:00 :" + i % 60); System.out.println (i + ":" + t); } catch (parseException e) {e.printstacktrace (); }}} public static void main (string [] args) {executorService es = executors.newfixedthreadpool (10); for (int i = 0; i <1000; i ++) {es.execute (new parsedate (i)); }}} SimpledateFormat은 스레드 안전이 아니기 때문에 위의 코드는 잘못 사용됩니다. 가장 쉬운 방법은 클래스를 직접 정의하고 동기화 된 상태 (Collections.synchronizedMap과 유사)로 래핑하는 것입니다. 이것은 높은 동시성에서 문제를 일으킬 때 문제를 일으킬 것입니다. 동기화 된에 대한 경합은 한 번에 하나의 스레드 만 입력하고 동시성 부피는 매우 낮습니다.
이 문제는 shreadlocal을 사용하여 SimpledateFormat를 캡슐화하여 해결됩니다.
패키지 테스트; import java.text.parseexception; import java.text.simpledateformat; import java.util.date import java.util.concurrent.executorservice; import java.util.concurrent; public stresdlocal <simpledateformat> tl = new ThreadLocal <MimpledateFormat> (); 공개 정적 클래스 구문 분석은 실행 가능한 {int i = 0; 대중 구문 분석 (int I) {this.i = i; } public void run () {try {if (tl.get () == null) {tl.set (new simpledateformat ( "yyyy-mm-dd hh : mm : ss"); } 날짜 t = tl.get (). 구문 분석 ( "2016-02-16 17:00 :" + i % 60); System.out.println (i + ":" + t); } catch (parseException e) {e.printstacktrace (); }}} public static void main (string [] args) {executorService es = executors.newfixedthreadpool (10); for (int i = 0; i <1000; i ++) {es.execute (new parsedate (i)); }}}각 스레드가 실행될 때 현재 스레드에 단순한 객체가 있는지 여부가 결정됩니다.
if (tl.get () == null)
그렇지 않다면 새로운 simpledateformat가 현재 스레드에 바인딩됩니다.
tl.set (new simpledateformat ( "yyyy-mm-dd hh : mm : ss"));
그런 다음 현재 스레드의 SimpledateFormat를 사용하여 구문 분석하십시오.
tl.get (). Parse ( "2016-02-16 17:00 :" + i % 60);
초기 코드에는 SimpledateFormat이 하나만 있었는데, 이는 ThreadLocal을 사용했으며 SimpledateFormat은 각 스레드마다 새로운 것이 었습니다.
이는 쓸모가 없기 때문에 여기에서 각 스레드 로컬에 공개 단순화를 설정해서는 안된다는 점에 유의해야합니다. 각각은 단순한 형태에 새로운 것이 필요합니다.
최대 절전 모드에는 ThreadLocal에 대한 일반적인 응용 프로그램이 있습니다.
ThreadLocal의 소스 코드 구현을 살펴 보겠습니다.
우선, 스레드 클래스에는 회원 변수가 있습니다.
ThreadLocal.threadLocalMap ThreadLocals = NULL;
이지도는 ThreadLocal의 구현의 핵심입니다.
public void set (t value) {Thread T = Thread.CurrentThread (); ThreadLocalMap map = getMap (t); if (map! = null) map.set (this, value); else createmap (t, value); } ThreadLocal에 따르면 해당 값을 설정하고 얻을 수 있습니다.
여기에서 ThreadLocalMap 구현은 해시 맵과 유사하지만 해시 충돌을 처리하는 데 차이가 있습니다.
해시 충돌이 ThreadLocalMap에서 발생하는 경우 해시 맵과는 다릅니다. 연결된 목록을 사용하여 충돌을 해결하는 것이 아니라 다음 인덱스에 INDEX ++를 배치하여 충돌을 해결하는 것입니다.