동시 프로그래밍은 Java 프로그래머에게 가장 중요한 기술 중 하나이며 마스터하기 가장 어려운 기술 중 하나입니다. 프로그래머는 컴퓨터의 가장 낮은 운영 원리를 깊이 이해해야하며 동시에 프로그래머는 명확한 논리와 세심한 사고를 가져야 효율적이고 안전하며 신뢰할 수있는 다중 스레드 동시 프로그램을 작성할 수 있어야합니다. 이 시리즈는 스레드 간 조정의 특성 (대기, 알림, Notifyall), 동기화 및 휘발성에서 시작하여 각 동시성 도구 및 JDK가 제공하는 기본 구현 메커니즘에 대해 자세히 설명합니다. 이를 바탕으로, 우리는 사용법, 소스 코드 구현 및 그 뒤의 원리를 포함하여 java.util.concurrent 패키지의 도구 클래스를 추가로 분석 할 것입니다. 이 기사는이 시리즈의 첫 번째 기사 이며이 시리즈의 가장 핵심 이론적 부분입니다. 후속 기사는 이에 따라 분석 및 설명됩니다.
1. 공유
데이터 공유는 스레드 안전의 주요 이유 중 하나입니다. 모든 데이터가 스레드에서만 유효한 경우 스레드 안전 문제가 없으므로 프로그래밍 할 때 스레드 안전을 고려할 필요가없는 주된 이유 중 하나입니다. 그러나 멀티 스레드 프로그래밍에서는 데이터 공유가 불가피합니다. 가장 일반적인 시나리오는 데이터베이스의 데이터입니다. 데이터의 일관성을 보장하기 위해서는 일반적으로 동일한 데이터베이스에서 데이터를 공유해야합니다. 마스터와 슬레이브의 경우에도 동일한 데이터에 액세스합니다. 마스터와 슬레이브는 액세스 및 데이터 보안의 효율성을 위해 동일한 데이터를 복사하고 있습니다. 이제 간단한 예를 통해 여러 스레드에서 데이터를 공유함으로써 발생하는 문제를 보여줍니다.
코드 스 니펫 1 :
package com.paddx.test.concurrent; public class sharedata {public static int count = 0; public static void main (String [] args) {최종 sharedata data = new sharedata (); 의 경우 (int i = 0; i <10; i ++) {new 스레드 (new Runnable () {@override public void run () {try {// 동시 문제 스레드의 가능성을 높이기 위해 입력 할 때 1 밀리 초에 대한 일시 중지 (1); data.addcount ();}; } try {// 메인 프로그램은 3 초 동안 일시 중지되어 위의 프로그램 실행이 완료되었는지 확인합니다. Sleep (3000); } catch (InterruptedException e) {e.printstacktrace (); } system.out.println ( "count =" + count); } public void addCount () {count ++; }}위의 코드의 목적은 1,000 회 계산하고 실행하기 위해 하나의 작업을 추가하는 것이지만 여기에는 10 개의 스레드를 통해 구현되며 각 스레드는 100 회 실행되며 정상적인 상황에서는 1,000이 출력해야합니다. 그러나 위 프로그램을 실행하면 결과가 발생하지 않음을 알 수 있습니다. 다음은 특정 시간의 실행 결과입니다 (각 실행 결과는 동일하지 않을 수 있으며 때로는 올바른 결과가 얻을 수 있습니다).
공유 변수 작업의 경우 다중 스레드 환경에서 다양한 예상치 못한 결과가 쉽게 볼 수 있음을 알 수 있습니다.
2. 상호 배제
자원 상호 배제는 한 명의 방문자만이 동시에 액세스 할 수 있음을 의미합니다. 이는 독특하고 독점적입니다. 우리는 일반적으로 여러 스레드가 동시에 데이터를 읽도록 허용하지만 하나의 스레드만이 동시에 데이터를 쓸 수 있습니다. 따라서 우리는 일반적으로 잠금 장치를 공유 잠금 장치와 독점 잠금 장치로 나눕니다. 자원이 상호 배타적이지 않은 경우 공유 리소스이더라도 스레드 안전에 대해 걱정할 필요가 없습니다. 예를 들어, 불변의 데이터 공유의 경우 모든 스레드가 읽을 수 있으므로 스레드 안전 문제가 필요하지 않습니다. 그러나 공유 데이터에 대한 작문은 일반적으로 상호 배제가 필요합니다. 위의 예에서, 데이터 수정 문제는 상호 배제가 없기 때문에 발생합니다. Java는 상호 배제를 보장하기 위해 여러 메커니즘을 제공합니다. 가장 쉬운 방법은 동기화 된 사용입니다. 이제 위의 프로그램에 동기화 된 추가 및 실행을 수행합니다.
코드 스 니펫 2 :
package com.paddx.test.concurrent; public class sharedata {public static int count = 0; public static void main (String [] args) {최종 sharedata data = new sharedata (); 의 경우 (int i = 0; i <10; i ++) {new 스레드 (new Runnable () {@override public void run () {try {// 동시 문제 스레드의 가능성을 높이기 위해 입력 할 때 1 밀리 초에 대한 일시 중지 (1); data.addcount ();}; } try {// 메인 프로그램은 3 초 동안 일시 중지되어 위의 프로그램 실행이 완료되었는지 확인합니다. Sleep (3000); } catch (InterruptedException e) {e.printstacktrace (); } system.out.println ( "count =" + count); } / *** 동기화 된 키워드 추가* / public synchronized void addCount () {count ++; }}위의 코드가 실행되었으므로 몇 번이나 실행하더라도 최종 결과는 1000이됩니다.
III. 원자력
원자력은 데이터의 작동을 독립적이고 불가분의 전체로 말합니다. 다시 말해, 그것은 연속적이고 무질서 수준의 작업입니다. 데이터 실행의 절반은 다른 스레드에 의해 수정되지 않습니다. 원자력을 보장하는 가장 쉬운 방법은 운영 체제 지침, 즉 한 번의 운영이 한 번에 하나의 운영 체제 명령에 해당하는 경우 원자력을 확실히 보장합니다. 그러나 하나의 명령으로 많은 작업을 완료 할 수 없습니다. 예를 들어, 긴 유형 작업의 경우 많은 시스템을 각각 고위 및 낮은 위치에서 작동하려면 여러 지시 사항으로 나누어야합니다. 예를 들어, 우리가 종종 사용하는 정수 I ++의 작동은 실제로 세 단계로 나뉘어져야합니다. (1) 정수 I의 값을 읽습니다. (2) I에 하나의 작업을 추가하십시오. (3) 결과를 메모리에 다시 작성하십시오. 이 프로세스는 멀티 스레딩에서 발생할 수 있습니다.
또한 코드 세그먼트 실행 결과가 잘못된 이유이기도합니다. 이 조합 작업의 경우 원자력을 유지하는 가장 일반적인 방법은 Java의 동기화 또는 잠금과 같은 잠금을 구현할 수 있으며 코드 세그먼트 2는 동기화 된 것을 통해 구현됩니다. 자물쇠 외에도 CAS (비교 및 스왑)를위한 또 다른 방법이 있습니다. 즉, 데이터를 수정하기 전에 이전 값이 일관성이 있는지 여부를 비교합니다. 일관성이 있다면 수정하면 일관성이 없으면 다시 실행됩니다. 이것은 또한 잠금 구현을 최적화하는 원칙입니다. 그러나 일부 시나리오에서는 CAS가 효과적이지 않을 수 있습니다. 예를 들어, 다른 스레드는 먼저 특정 값을 수정 한 다음 원래 값으로 다시 변경합니다. 이 경우 CAS는 판단 할 수 없습니다.
4. 가시성
가시성을 이해하려면 JVM의 메모리 모델에 대한 특정 이해가 필요합니다. JVM의 메모리 모델은 그림과 같이 운영 체제와 유사합니다.
이 그림에서 각 스레드에는 자체 작업 메모리가 있음을 알 수 있습니다 (CPU 고급 버퍼와 동일합니다.이를 목적으로는 스토리지 시스템과 CPU의 속도 차이를 더욱 좁히고 성능을 향상시키는 것입니다). 공유 변수의 경우, 스레드가 작업 메모리에서 공유 변수의 사본을 읽을 때마다. 글을 쓰면 작업 메모리에서 사본 값을 직접 수정 한 다음 작업 메모리를 특정 시점에서 기본 메모리의 값과 동기화합니다. 이 문제가 발생하는 문제는 스레드 1이 특정 변수를 수정하면 스레드 1이 공유 변수로 스레드 1에 의해 작성된 수정이 보이지 않을 수 있다는 것입니다. 다음 프로그램을 통해 보이지 않는 문제를 보여줄 수 있습니다.
package com.paddx.test.concurrent; public class VisibilityTest {private static boolean ready; 개인 정적 int 번호; 개인 정적 클래스 readerthread는 스레드 {public void run () {try {thread.sleep (10); } catch (InterruptedException e) {e.printstacktrace (); } if (! ready) {system.out.println (ready); } system.out.println (번호); }} private static class writerthread는 스레드 {public void run () {try {thread.sleep (10); } catch (InterruptedException e) {e.printstacktrace (); } 번호 = 100; ready = true; }} public static void main (String [] args) {new WriterThread (). start (); new readerthread (). start (); }}직관적 으로이 프로그램은 100 만 출력해야하며 준비 값은 인쇄되지 않습니다. 실제로 위의 코드를 여러 번 실행하면 많은 결과가있을 수 있습니다. 다음은 두 개의 실행 결과입니다.
물론,이 결과는 가시성으로 인해 가능하다고 말할 수 있습니다. Writ 두 번째 결과, 즉 IF (! Ready)를 실행할 때 쓰기 스레드의 결과를 읽지 않았지만 System.out.println (Ready)을 실행할 때 쓰기 실행 결과를 읽습니다. 그러나이 결과는 스레드의 대체 실행으로 인해 발생할 수 있습니다. Java의 동기화되거나 휘발성을 통해 가시성을 보장 할 수 있으며 특정 세부 사항은 후속 기사에서 분석됩니다.
5. 시퀀스
성능을 향상시키기 위해 컴파일러와 프로세서가 지침을 재정렬 할 수 있습니다. 재주문에는 세 가지 유형이 있습니다.
(1) 컴파일러 최적화 된 재주문. 컴파일러는 단일 스레드 프로그램의 의미를 변경하지 않고 진술의 실행 순서를 다시 예약 할 수 있습니다.
(2) 교육 수준 병렬 처리의 재정렬. 최신 프로세서는 지침 수준 병렬 기술 (ICP)을 사용하여 여러 지시 사항의 실행을 중복시킵니다. 데이터 종속성이 없으면 프로세서는 기계 지침에 해당하는 명령문의 실행 순서를 변경할 수 있습니다.
(3) 메모리 시스템의 재주문. 프로세서는 캐시 및 읽기/쓰기 버퍼를 사용하므로로드 및 스토리지 작업이 순서대로 실행되는 것처럼 보입니다.
JSR 133에서 재정렬 문제에 대한 설명을 직접 참조 할 수 있습니다.
(1) (2)
위의 그림에서 먼저 소스 코드 파트 (1)를 살펴 보겠습니다. 소스 코드에서 명령 1이 먼저 실행되거나 명령 3이 먼저 실행됩니다. 명령어 1이 먼저 실행되면 R2는 명령 4에 기록 된 값을 보지 않아야합니다. 명령어 3이 먼저 실행되면 R1은 명령 2에 의해 작성된 값을 보지 않아야합니다. 그러나 실행 결과는 R2 == 2 및 R1 == 1을 가질 수 있습니다. 위의 그림 (2)은 가능한 법적 편집 결과입니다. 컴파일 후, 명령 1 및 명령 2의 순서가 상호 교환 될 수 있습니다. 따라서 R2 == 2 및 R1 == 1의 결과가 나타납니다. 순서를 보장하기 위해 Java에서 동기화되거나 휘발성을 사용할 수도 있습니다.
6 개의 요약
이 기사는 Java 동시 프로그래밍의 이론적 기초를 설명하며, 일부는 가시성, 순서 등과 같은 후속 분석에서 더 자세히 논의 될 것입니다. 후속 기사는이 장의 내용에 따라 논의됩니다. 위의 내용을 잘 이해할 수 있다면, 다른 동시 프로그래밍 기사를 이해하는 것인지 또는 매일 동시 프로그래밍 작업에서 도움이 될지도 모릅니다.