JVM 가상 머신 노트 <팔> 스레드 안전 잠금 및 최적화

thread 세이프

1.1 스레드 안전은 무엇인가?

안전하게 개체에 대한 여러 스레드를 사용할 수 있다면, 그것은 스레드 안전합니다.

 

스레드 안전 1.2 Java 언어

다음은 토론 게시글 안전, 그것은 여러 스레드 사이의 전제 공유 데이터 액세스의 존재로 제한됩니다.

자바 언어 공유 데이터의 다양한 작업은 다섯 개 가지 범주로 나누어 져 있습니다 :

(1) 불변

(개인이 발생할 스레드, 탈출의 경우이에 대한 참조 다른 스레드를 작동하지 않습니다) 불변의 객체는 스레드 안전해야합니다.

공유 데이터는 한 최종 키워드로는 불변임을 확인하기 위해 수정 될 수있다 정의하는 기본적인 데이터 유형 인 경우.

공유 데이터가 객체 인 경우에, 당신은 개체의 행동이 자신의 상태에 어떤 영향을 미칠하지 않도록해야합니다. (이러한 String 클래스로,),) (교체) CONCAT를 (그 문자열을 (전화 이러한 방법은 원래 값에 영향을 미치지 않습니다)

가장 쉬운 방법은 개체 변수에있는 객체의 불변의 상태를 확인하는 상태 마지막으로 선언됩니다.

(2) 절대 스레드 안전

많은 자바 API는 대부분이 결국 다음 메소드 호출 (예 : 동기화 등) 추가 동기화 측정을하지 않을 경우, 다중 스레드 상황에서 절대적인 보안 스레드하지 아직도 안전하지, 그는 스레드 안전 클래스 인 것을 지적했다.

"에 관계없이 운영 환경의 호출자가 추가 동기화 조치를 필요로하지 않는다"고, 절대 보안 스레드를 달성하기 위해 일반적으로 오버 헤드 심지어 비현실적 많이 걸립니다.

(3)의 상대적인 보안 쓰레드

자바 언어에서, 스레드 안전 클래스의 대부분이 유형입니다. 이러한 벡터, 해시 등

(4) 쓰레드 대응

자체 스레드 안전하지 않고, 안전하게 동시에 환경에서 사용될 수있는 동기화 객체를 보장하는 수단의 적절한 사용을 호출하여 종료 될 대상물을 지칭한다. 의 HashMap, ArrayList를한다.

(5) 라인 적대적

상관없이 호출 동기 조치를 취할 것인지의 코드는 다중 스레드 환경에서 동시에 사용할 수 없다 끝을 의미한다.

 

1.3 스레드 안전 구현

1.3.1 배타적 동기 (차단 동기)

동기화 : 공유 데이터에 액세스하는 복수의 동시 스레드 지칭 공유 데이터 개수 만 스레드되도록 (일부 또는 세마포어를 사용하는 경우)를 동시에 사용한다.

동기화 열매 때문에 뮤텍스는 동기화를 달성하는 수단이며, 상호 배제이다.

(1) synchronized 키워드

자바에서 상호 배제 동기화 수단의 가장 기본적인 동기화 된 키워드의 사용이다.

원리 : 키를 통해 동기화 편집 전후 및 동기 블록 후 두 개의 명령어 바이트 코드 monitorenter 및 monitorexit을 형성하고,이 두 바이트 코드 잠그고 해제되는 파라미터의 유형을 지정하는 객체에 대한 참조를 필요로한다.

잠긴 개체 : 이 개체를 참조 명시 적으로 지정된 객체 인수를 동기화하는 경우, 당신은 동기화 된 인스턴스 메소드는 메소드 나 클래스의 수정입니다 기반으로 지정해야 잠금 또는 객체에 대한 클래스 객체로 해당 개체 인스턴스를 가져옵니다.

처리 절차 : monitorenter 명령, 우리는 먼저 락 객체를 얻기 위해 시도해야합니다. 오브젝트가 로크되어 있지 않거나 현재 스레드가 이미 객체가 로크 카운트는 카운터가 0이면, 로크는 해제된다 (1)에 의해 카운터를 잠근다 monitorexit 명령의 구현에서, 적절한 증가되는 로크를 소유하면 . 잠금은 다른 스레드 객체 잠금이 지금까지 해제 될 때까지 현재 스레드를 차단하는 오브젝트를 얻기 위해 실패합니다.

헤비급 잠금 : Java 스레드는 기본 운영 체제 스레드에 매핑되는, 또는 당신이 차단 된 스레드를 깨워하려는 경우, 우리는 핵심 상태로 사용자 상태의 전환을 필요로 도움 완전한 운영 체제에 필요한 상태 전이 필요하므로 프로세서 많은 시간을 소비한다. 간단한 동기 블록의 경우, 상태 전이 시간이 더 오래 사용자의 코드가 실행되는 것보다이있을 수 있습니다 소비, 그것은 자바 언어 동기화 헤비급 작업입니다. 필요한 경우 가상 머신 자체가 핵심 상태로 자주 삭감을 피하기 위해 같은 과정을 차단하기위한 운영 체제 스레드 대기를 통지하기 전에 약간의 스핀을 추가하는 등, 몇 가지 최적화를 할 것입니다 동안 따라서 만이 작업을 사용합니다.

(2) ReentrantLock와

java.util.concurrent의 패키지는 동기화 잠금 (잠금 () 마지막으로 사용 된 시도 /와 잠금 해제 () 메소드)를 달성하기 위해 재진입 ReentrantLock와 사용할 수있다.

차이 : 동기 비교가, ReentrantLock와 주로 다음 세 가지로, 일부 고급 기능을 추가 : 인터럽트 대기, 공정 잠금 달성 할 수있다 (부울 값 생성자를 통해를, 기본이 아닌 자본이다), 잠금은 바인딩 더 할 수있다 조건

PS :  공정 잠금비 잠금 박람회

공정 잠금 : 잠금을 기다리는 동시에 여러 스레드를 의미, 잠금 시간 순서 응용 프로그램 잠금에 순차적으로 획득해야합니다.

불공정 잠금 : 잠금이 해제되면, 어떤 스레드가 락을 대기, 잠금에 액세스 할 수 있습니다.

비 동기화 잠금 박람회, ReentrantLock와 케이스 불공평하지만 공정한 잠금 기본이 될 수 있습니다.

 

1.3.2 동기 비 차단

뮤텍스 동기화 큰 문제는 여파로 인한 스레드 차단 및 성능 문제입니다. 이러한 동기화는 동기화를 차단 알려져있다 그래서.

(1) 낙관적 동시성 전략 기반 충돌 감지

상호 배제 동기화 문제 : 동기화 뮤텍스는 데이터가 존재 여부 경쟁이 증가 동시성으로, 잠겨, 잠금 시간이 오래 경우, 성능 오버 헤드하는 관계없이 명백한 단점이있다, 비관적 잠금에 속하는 그것은 매우 커지게된다.

해결책 : 낙관적 낙관적 동시성 전략 기반의 충돌 감지. 이 모드에서, 소위 잠금의 개념이 없습니다, 각 스레드는 계산이 완료된 후 공유 데이터의 경쟁이있을 경우 다음이 작업의 성공을 못하게하는 경우, 다른 스레드와 공유 데이터에 대한 경쟁의 존재를 감지, 직접 이동 작업을 수행합니다 이 동시성 전략의 성공까지, 운영 및 테스트를 다시 실행을 계속하고이 작업이 동기 비 차단 동기화 호출되기 때문에 많은 스레드를 일시 중단 할 필요를 인식하지 않습니다.

하드웨어 보장 : 하드웨어 명령어 세트의 개발, 및 두 동작 원자있는 충돌 검출 동작을 보장 할 수있다, 이러한 설명은 같은 CAS (비교 및 교환), LL / SC있다.

CAS 명령 : 즉 메모리 위치 (V) 세 피연산자가 있는데, 이전 값 (A)의 새로운 값 (B). B. 만약 의해 갱신 후 값 V == V의 값

달성 : CAS에 작업을 사용할 수있는 Java 프로그램이 패키지는 클래스 compareAndSwapInt 내부 sun.misc.Unsafek ()와 compareAndSwapLong () 및 여러 가지 방법에 의해 제공됩니다. 이러한 방법과 특별한 거래를 할 수있는 가상 머신 내부, CAS는 즉각적인 결과가 관련 플랫폼, 아니 과정 메소드 호출의 번역입니다 명령했다.

CAS 논리의 허점 : 변수 V 때 초기 읽기 A 값이며,이 과제의 준비의 시간에서의 값을 확인하기 위해 남아있는 경우에, 우리는 그것의 쓰레드의 값 교 아니라고 말할 수 있습니까? 그 값이 B로 변경이 기간 동안 상황이있을 수 있습니다 다음, 다시로가 변경 한 후 CAS 작업은 실수가 변경 적이 있다고 생각합니다. 이 취약점은 CAS 작업 "ABA"문제라고합니다. ABA의 문제는 대부분의 경우에 동시 프로그램의 유효성에 영향을주지 않습니다.

PS :   비관적 잠금낙관적 잠금

비관적 잠금 : 데이터를 보유 할 때는 항상, 그래서 상관없이 경쟁 데이터가 있는지, 잠겨해야 문제가 항상있다, 조치를 동기화하지 않을 경우 데이터가 다른 스레드에 의해 수정 될 것이라고 생각 사용자 모드 및 정신 변환, 유지 보수 잠금 장치를 시계 반대 차단 된 스레드에 대한 검사 및 기타 작업을 깨울 필요가있다.

낙관적 잠금 : 다른 사람이 데이터를 선택할 때마다이 잠겨되지 않도록 수정되지 않지만 마지막 업데이트시 업데이트가 실패 할 경우 반복적으로 데이터를 업데이트하지 않은이 시간 동안 다른 사람들이 결정한다 및 동작 검출을 수행.

 

1.3.3 없음 동기화 방식

방법은 동기화의 정확성을 보장하기 위해 어떤 조치를하지 않고, 자연, 데이터의 공유를 포함하지 않아야하는 경우, 코드의 일부는 본질적으로 스레드 안전합니다.

 

두 잠금 최적화

그리고 스핀 - 스핀 잠금은 2.1 어댑티브

2.1 잠금 제거

2.2 锁粗 화

2.3 경량 잠금

2.4 편향된 로킹

바이어스 잠금, 경량 잠금은 낙관적 잠금, 비관적 잠금 잠금 헤비급이다.

어떤 스레드가 시간에 액세스하지 할 때 개체의 시작은 인스턴스화됩니다. 처음 그렇게 할 때, 지금 액세스 그것을 하나의 스레드 만이있을 수 있습니다 믿는 것을 의미 바이어스 개체가 잠금을 바이어스 보유하고, 접근을하는 시간 스레드, 그것은,이 글에 이번에는 경향이있다. 첫 번째 스레드를 향해, 스레드가 CAS 작업, 때 머리를 고정 바이어스 할 객체를 수정하는 데 사용 객체 헤더는 자신의 ID로 ThreadID, 시간이 객체를 다시 방문한 후, 유일한 ID를 비교해야, 사용할 필요가 없습니다 작업하는 동안 CAS.

두 번째 스레드가 개체를 볼 수 있습니다 때 바이어스 잠금, 해제에 이렇게 바이어스 상태가 주도권을 쥐고하지 않기 때문에 두 번째 스레드가이 개체에 액세스 할 수 있습니다하면 다음 검사 대상 잠금의 원래 소유자, 그 경쟁은 이미이 객체에 존재 보여 스레드가 아직 살아 있는지 여부를 끊었 경우, 당신은 객체 잠금 상태가 될 다음 다시 새 스레드를 향해 원래의 스레드가 아직 살아 있다면, 즉시 스레드 스택 것으로 작업을 수행, 객체의 사용을 검사 할 수 없습니다, 당신은 여전히 유지 바이어스 잠금해야하는 경우 (잠금 경량 잠금이 때 업그레이드 바이어스) 경량 잠금으로 업그레이드 록킹 바이어스된다. 이 사용되는 경우, 객체의 비 잠금 상태로 돌아갈 수 있습니다, 다음 바이어스 다시.
이 경쟁하지만, 경쟁의 정도가 매우 가벼운 것을 경량 잠금 같은 잠금 작업을 위해 일반적으로 두 개의 스레드가 비틀, 또는 조금 (스핀), 다른 스레드 출시 잠금을 기다립니다. 그러나 스레드 외에, 또 다른 세 번째 방문 동안, 경량 잠금 인플레이션 헤비급 잠금, 잠금 헤비급를 일정 횟수 또는 잠금, 스핀을 들고 스레드보다 더 락 스핀을 소유하는 경우 스레드는 CPU의 유휴 방지, 차단됩니다.

헤비급 잠금

  마지막 문서 동기화 사용 및 구현의 원칙을 소개합니다. 이제 우리는 알아야 할, 동기화를 달성하기 위해 내부 대상이 잠금 모니터 (모니터)이라고합니다. 그러나 모니터 락의 본질은 달성하기 위해 기본 운영 체제 뮤텍스 잠금에 따라 달라집니다. 커널 모드로 사용자 모드로 변환 할 필요가 스레드 사이를 전환 할 운영 체제에 대한 이유는,이 비용은 상태 사이의 전환은 낮은 효율성을 동기화 된 이유는 시간의 비교적 긴 기간을 필요로 매우 높다. 따라서,이 뮤텍스 잠금 잠금 우리가 전화를 실현 운영 체제에 따라 다릅니다 "헤비급 잠금을." 최적화의 모든 종류의 작업을 수행 할 JDK에 동기화, 핵심은 헤비급 잠금 장치의 사용을 줄이는 것입니다. JDK1.6 후, 순서대로 획득하고 해제 잠금 장치는 성능, "경량 잠금"을 도입 향상, 성능 가져 소비를 줄이기 "바이어스 잠금을."

둘째, 경량 잠금 

  어떤 잠금 상태, 가볍고 헤비급 잠금 자물쇠를 잠금하는 경향이 : 네 가지 잠금 상태입니다. 경쟁의 잠금 장치를 사용하여, 잠금 경량 잠금을 바이어스에 다음, 잠금 업그레이드 헤비급 락 (잠금을 업그레이드하지만 업그레이드는 말을 오름차순으로 업그레이드하는 것입니다, 단방향 수, 잠금이 표시되지 않습니다 다운 그레이드). JDK 1.6 우리는 또한 -XX 수, 경량 바이어스 잠금 잠금에서 기본적으로 활성화되어 비활성화 -UseBiasedLocking 잠금 바이어스. 잠금 상태는 JDK 예 (32)에 파일 헤더 객체에 저장됩니다

잠금 상태

25 비트

4 비트

1 비트

2 비트

23bit

2 비트

이 잠금을 바이어스 여부

잠금 플래그

경량 잠금

스택 포인터 레코드 잠금 포인팅

00

헤비급 잠금

뮤텍스 포인터 (헤비급 잠금)의

(10)

GC 마크

(11)

바이어스 잠금

스레드 ID

시대

객체 세대 시대

1

01

없음 잠금 없다

해시 코드 객체

객체 세대 시대

0

01

  "경량"는 기존의 잠금을 달성하기 위해 운영 체제 뮤텍스의 사용에 상대적입니다. 그러나이 잠금 가벼운 헤비급 잠금을 대체하기위한 것이 아닙니다 것을 스트레스에 대한 최초의 필요성이없는 멀티 스레딩 경쟁의 전제 의도, 소비의 성능을 기존의 헤비급 잠금 장치의 사용을 줄일 수 있습니다. 구현하기 전에하는 것이 먼저 경량 잠금 적응 시나리오의 경우 스레드가 교대로 동기화 블록을 수행하는 것을 이해, 경량 잠금을 설명, 같은 시간에 같은 잠금 장치에 액세스 할 수있는 경우 상황은, 그것은 경량 잠금 인플레이션으로 이어질 것입니다 헤비급 잠금.

1, 경량 잠금 잠금 절차

  코드 동기 블록을 입력 할 때, 동기 대상 잠금 상태에는 잠금 상태가없는 경우 (1), 현재의 스레드 가상 머신 제 스택 프레임 (이 "0"으로 고정 바이어스 여부 로크 플래그는 "01"상태) 공식적으로 난민 마크 워드로 알려진 마크 워드의 현재 락 객체의 복사본을 저장하기 위해 잠금 (잠금 기록) 공간이라는 기록을 수립. 이때 쓰레드와 오브젝트 헤더의 상태도 2.1에 도시 된 스택.

  (2) 복사 대상 헤더 마크 말씀은 잠금 기록에 복사.

  (3) 사본에 성공, 가상 머신은 객체 마크 단어 소유자 포인터의 마크 말씀이 잠금 레코드 포인터를 가리 키도록 업데이트 CAS 작업 대상 및 잠금 레코드를 사용하려고합니다. 업데이트가 완료되면 그렇지 않은 경우 (4) 단계, 단계 (3)를 진행합니다.

  업데이트 작업이 성공하면 (4), 다음 스레드는이 시간을,이 객체가 가벼운 잠긴 상태가 있다는 것을 의미, 스레드 스택을 개체의 잠금을 소유, 마크 워드 잠금 플래그의 객체는 "00"로 설정 대상물의 상태도 2.2에 도시 향한다.

  이 업데이트가 실패 할 경우 현재의 thread가이 객체의 잠금을 소유하는 것을 의미하는 경우 (5), 가상 머신은 먼저 동기화 블록이 계속가 직접 입력 할 수 있습니다, 현재의 thread의 스택 프레임에 마크 워드 포인트의 대상 여부를 확인합니다 . 그렇지 않으면, 복수의 스레드가 잠금 경합, 경량 확장 헤비급 잠금 잠 가야합니다, 잠금 플래그 상태 값이된다 "10"을 설명, 마크 워드는 헤비급 잠금 (뮤텍스) 포인터 등을 가리키는 저장 잠금을 기다리는 스레드가 차단 된 상태를 입력해야합니다. 현재 스레드가 잠금이 스핀 스레드가 차단되지 않도록하는 것입니다 취득 스핀을 사용하려고하며, 공정 사이클의 사용은 잠금을 획득 할 수 있습니다.

 

                     도 2.1는 스택 상태 객체 전에 경량 잠금 동작 CAS

   

                      객체의 스택 상태도 경량 잠금의 작동이 2.2 CAS 후

2, 경량 잠금 해제 과정 :

  현재의 thread 마크 말씀 난민 마크 워드 복사 객체를 대체 할 수있는 CAS 작업을 시도 (1).

  교체가 성공하면 (2), 전체 동기화 프로세스가 완료됩니다.

  (3) 교체가 실패하면 다른 스레드가 스레드를 중단 웨이크 (잠금이 시간을 확대하고있다), 그것은 동시에 잠금을 해제해야합니다 잠금을 얻기 위해 노력했다.

셋째, 바이어스 잠금

  로크 획득 및 여러 CAS 따라 가벼운 원자 명령어를 방출하기 때문에, 불필요한 경량 잠금 실행 경로를 최소화하기 위해 다중 스레드의 경우에 경쟁없는 잠금 바이어스를 소개하지만 ThreadID의 여분의 잠금 편향 CAS의 종속 원자 지시 할 때 (멀티 스레드 경쟁 철회해야 일단 바이어스 잠금이 발생하기 때문에, 바이어스 잠금 취소 작업의 성능 손실이 성능 비용 절감 미만이어야 원자 지시 CAS). 스레드 동기화 블록이 교대로 수행 될 때 로크가 하나의 스레드 만이 동기 블록을 실행할 때 성능을 더욱 향상 가압하면서 상술 한 로크는 성능을 향상시키기 위해서 경량이다.

1 바이어스 잠금 획득 과정 :

  편향된 로킹 있는지 여부를 설정 한 (1) 액세스 마크 단어 식별은 로크 플래그가 확인 된 경우는 01-- 상태로 바이어스된다.

  (2) 상태로 바이어스 될 경우, 현재 스레드 테스트 가리키면 스레드 ID, 그리고, 스텝 (5)으로 진행하고, 그렇지 않으면 단계 (3)으로 이동하는 경우.

  (3) 스레드 ID가 현재 스레드 경쟁하여 CAS 동작 잠금 가리키고 있지 않은 경우. 경쟁이 성공하면, 마크 워드 스레드 ID가 현재 스레드의 ID로 설정 한 후 (5)을 실행, 경쟁이 수행하는 데 실패 할 경우 (4).

  CAS에 로크가 실패 바이어스 얻으면 (4)은, 그 경쟁이있는 것을 의미한다. 경량 잠금으로 업그레이드 록킹 바이어 싱, 일시 중단되는 글로벌 보안 점 (safepoint)에 도달 할 때 바이어스 잠금 스레드가 도착, 다음 스레드 안전 동기 코드의 시점에서 차단은 계속됩니다.

  (5) 동기 부호를 수행한다.

2, 잠금을 해제하는 경향이있다 :

  네 번째 단계에서 설명한 바이어스 잠금 해지 . 바이어스 잠금을 경쟁하는 다른 스레드 시도가 발생하는 경우에만 고정 바이어스 때, 잠금 잠금 경향이 스레드 출시, 스레드가 잠금 바이어스를 해제 주도권을 쥐고하지 않습니다십시오. 취소 잠금 바이어스, 필요 (바이트 코드가이 시점에서 실행하지 않고), 처음 바이어스 잠금 스레드를 중단해야한다 글로벌 보안 점을 기다리는 개체가 잠금 상태가 잠겨 있는지 여부를 확인하는 것입니다, 취소에 복귀 편향되지 않은 잠금 로크 (플래그 '01') 또는 경량 로크 (플래그 '00') 상태.

3 중량 잠금 간의 변환 잠금 경량 편향된 로킹

 

                                        도도 세 2.3 변환

  더 나은 위의 이해가있는 경우 그림은 주로 위를 요약 한 것입니다,이 그림은 이해하기 쉬워야한다.

넷째, 다른 최적화 

1、适应性自旋(Adaptive Spinning):从轻量级锁获取的流程中我们知道当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

2、锁粗化(Lock Coarsening):锁粗化的概念应该比较好理解,就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成一个范围更大的锁。举个例子:

코드를 복사
 1 package com.paddx.test.string;
 2 
 3 public class StringBufferTest {
 4     StringBuffer stringBuffer = new StringBuffer();
 5 
 6     public void append(){
 7         stringBuffer.append("a");
 8         stringBuffer.append("b");
 9         stringBuffer.append("c");
10     }
11 }
코드를 복사

  这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

3、锁消除(Lock Elimination):锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:

코드를 복사
 1 package com.paddx.test.concurrent;
 2 
 3 public class SynchronizedTest02 {
 4 
 5     public static void main(String[] args) {
 6         SynchronizedTest02 test02 = new SynchronizedTest02();
 7         //启动预热
 8         for (int i = 0; i < 10000; i++) {
 9             i++;
10         }
11         long start = System.currentTimeMillis();
12         for (int i = 0; i < 100000000; i++) {
13             test02.append("abc", "def");
14         }
15         System.out.println("Time=" + (System.currentTimeMillis() - start));
16     }
17 
18     public void append(String str1, String str2) {
19         StringBuffer sb = new StringBuffer();
20         sb.append(str1).append(str2);
21     }
22}
코드를 복사

StringBuffer에의 로컬 변수에 속하는 동기화 방법,하지만이 프로그램의 StringBuffer를 추가, 그래서 사실, 공정 밖으로 탈출하지는 않지만,이 프로세스는 스레드 안전 잠금이 제거 될 수있다. 여기에 로컬로 수행 된 결과는 다음과 같습니다

  (: -UseBiasedLocking -XX) 다른 요인 곳 장애인 편견 잠금의 영향을 최소화하기 위해. 성능 로크의 제거는 여전히 비교적 큰 개선 후 상기 과정을 통해 알 수있다.

  참고 : 결과는 JDK 버전 사이에 수행 될 수있다 내가 여기에 사용 된 1.6이었다 같은, JDK 버전이 아니다.

추천

출처www.cnblogs.com/lvoooop/p/12142467.html