1. 멀티 스레딩 소개
프로그래밍에서는 대부분의 비즈니스 시스템에서 동시 처리가 필요하기 때문에 다중 스레딩 프로그래밍 문제를 피할 수 없습니다. 동시 시나리오에 있다면 멀티 스레딩이 매우 중요합니다. 또한 인터뷰 중에 면접관은 일반적으로 다음과 같은 멀티 스레딩에 대한 질문을합니다. 스레드를 만드는 방법? 우리는 보통 이런 식으로 대답합니다. 두 가지 주요 방법이 있습니다. 첫 번째는 : 스레드 클래스를 상속하고 실행 메소드를 다시 작성합니다. 두 번째는 다음과 같습니다. 실행 가능한 인터페이스를 구현하고 실행 방법을 다시 작성하십시오. 그런 다음 면접관은이 두 방법의 장점과 단점이 무엇인지 확실히 묻습니다. 어쨌든, 우리는 객체 지향이 상속을 덜 옹호하고 가능한 한 많은 조합을 사용하려고 노력하기 때문에 결론, 즉 두 번째 사용 방법입니다.
현재 멀티 스레드의 반환 가치를 얻으려면 어떻게 해야하는지 생각할 수도 있습니까? 우리가 더 많이 배운 지식을 바탕으로, 우리는 호출 가능한 인터페이스를 구현하고 통화 방법을 다시 작성하는 것을 생각할 것입니다. 실제 프로젝트에서 많은 스레드를 어떻게 사용합니까? 그들은 얼마나 많은 방법을 가지고 있습니까?
먼저 예를 살펴 보겠습니다.
이것은 이해하기 쉬운 멀티 스레드를 만드는 간단한 방법입니다. 이 예에서는 다른 비즈니스 시나리오에 따르면 다른 매개 변수를 Thread ()로 전달하여 다른 비즈니스 로직을 구현할 수 있습니다. 그러나이 멀티 스레드를 생성하는이 방법으로 노출 된 문제는 스레드를 반복적으로 생성하는 것이며 스레드를 생성 한 후에는 파괴되어야합니다. 동시 시나리오에 대한 요구 사항이 낮 으면이 방법은 괜찮은 것처럼 보이지만 동시성 시나리오에서는 스레드를 만드는 것이 매우 소비하기 때문에이 방법은 불가능합니다. 경험에 따르면 올바른 방법은 스레드 풀 기술을 사용하는 것입니다. JDK는 선택할 수있는 다양한 스레드 풀 유형을 제공합니다. 특정 방법의 경우 JDK 문서를 확인할 수 있습니다.
이 코드에서 주목해야 할 것은 전달 된 매개 변수가 구성한 스레드 수를 나타냅니다. 더 좋을까요? 확실히 그렇지 않습니다. 스레드 수를 구성 할 때 서버의 성능을 완전히 고려해야합니다. 더 많은 스레드 구성이 있으면 서버의 성능이 우수하지 않을 수 있습니다. 일반적으로 기계가 완료 한 계산은 스레드 수에 의해 결정됩니다. 스레드 수가 피크에 도달하면 계산을 수행 할 수 없습니다. CPU를 소비하는 비즈니스 논리 (더 많은 계산) 인 경우 스레드와 코어 수가 최고점에 도달합니다. I/O (데이터베이스 운영, 파일 업로드, 다운로드 등)를 소비하는 비즈니스 로직 인 경우 스레드가 많을수록 스레드가 많을수록 특정 의미에서 성능을 향상시키는 데 도움이됩니다.
스레드 수를 설정하는 또 다른 공식 :
y = n*((a+b)/a), 여기서 n : cpu 코어의 수, a : 스레드가 실행될 때 프로그램의 계산 시간, b : 스레드가 실행될 때 프로그램의 차단 시간. 이 공식을 사용하면 스레드 풀의 스레드 수 구성이 제한되며 기계의 실제 상황에 따라 유연하게 구성 할 수 있습니다.
2. 멀티 스레드 최적화 및 성능 비교
스레딩 기술은 최근 프로젝트에서 사용되었으며 사용 중에 많은 문제가 발생했습니다. 인기를 활용하여 여러 멀티 스레드 프레임 워크의 성능 비교를 분류 할 것입니다. 우리가 마스터 한 것은 첫 번째 유형의 세 가지 유형으로 나뉘어져 있습니다 : ThreadPool (스레드 풀) + CountdownLatch (프로그램 카운터), 두 번째 유형 : 포크/조인 프레임 워크 및 JDK8 병렬 스트림의 세 번째 유형. 다음은 이러한 방법의 다중 스레딩 성능에 대한 비교 요약입니다.
먼저, 여러 파일 객체가 메모리에서 생성되는 비즈니스 시나리오를 가정하십시오. 여기서는 30,000 개의 스레드 수면이 비즈니스 처리 비즈니스 로직을 시뮬레이션하기 위해 잠정적으로 결정되어 이러한 방법의 다중 스레딩 성능을 비교합니다.
1) 단일 스레드
이 방법은 매우 간단하지만 프로그램은 처리 중에 시간이 많이 걸리며 오랫동안 사용됩니다. 각 스레드는 실행되기 전에 현재 스레드가 실행되기를 기다리고 있기 때문입니다. 멀티 스레드와는 거의 관련이 없으므로 효율은 매우 낮습니다.
먼저 파일 객체를 작성하고 코드는 다음과 같습니다.
public class fileInfo {private string filename; // 파일 이름 개인 문자열 filetype; // 파일 유형 private String filesize; // 파일 크기 개인 문자열 filemd5; // md5 코드 private String fileVersionNo; // 파일 버전 번호 public fileInfo () {super (); } public fileInfo (문자열 파일 이름, 문자열 파일 유형, 문자열 파일 크기, 문자열 filed5, String fileVersionNo) {super (); this.filename = filename; this.fileType = filetype; this.fileSize = 파일 크기; this.filemd5 = filemd5; this.fileversionno = FileVersionNo; } public String getFileName () {return filename; } public void setfilename (String filename) {this.filename = filename; } public String getFileType () {return filetype; } public void setFileType (String filetype) {this.fileType = filetype; } public String getFilesize () {return filesize; } public void setFilesize (String Filesize) {this.FileSize = Filesize; } public String getFileMd5 () {return filemd5; } public void setfilemd5 (String filemd5) {this.filemd5 = filemd5; } public String getFileVersionNo () {return fileVersionNo; } public void setFileVersionNo (String FileVersionNo) {this.FileVersionNo = FileVersionNo; }그런 다음 비즈니스 처리를 시뮬레이션하고, 30,000 파일 객체를 생성하고, 1ms에 대한 스레드가 잠을 자고, 1000ms 전에 설정하고 시간이 매우 길고 일식 전체가 붙어 있음을 알게되므로 시간을 1ms로 변경하십시오.
공개 클래스 테스트 {private static list <fileInfo> filleList = new ArrayList <fileInfo> (); public static void main (String [] args)은 InterruptedException {createFileInfo (); Long StartTime = System.CurrentTimeMillis (); for (fileInfo fi : filEList) {thread.sleep (1); } long endtime = System.CurrentTimeMillis (); System.out.println ( "단일 스레드 시간 시간 소모 :"+(endtime-starttime)+"ms"); } private static void createFileInfo () {for (int i = 0; i <30000; i ++) {filElist.add (new FileInfo ( "전면 사진", "JPG", "101522", "md5"+i, "1"); }}}테스트 결과는 다음과 같습니다.
30,000 개의 파일 객체를 생성하는 데 거의 1 분이 걸리며 효율이 상대적으로 낮다는 것을 알 수 있습니다.
2) ThreadPool (스레드 풀) +CountdownLatch (프로그램 카운터)
이름에서 알 수 있듯이 CountdownLatch는 스레드 카운터입니다. 실행 프로세스는 다음과 같습니다. 첫째, Await () 메소드가 기본 스레드에서 호출되고 기본 스레드가 차단 된 다음 프로그램 카운터가 스레드 개체로 전달됩니다. 마지막으로, 각 스레드가 작업을 실행하는 후에는 COUNTDOLD () 메소드가 호출되어 작업의 완료를 표시합니다. Countdown ()이 여러 번 실행되면 기본 스레드의 Await ()가 유효하지 않습니다. 구현 프로세스는 다음과 같습니다.
Public Class Test2 {private static executorService executor = executor.newfixedthreadpool (100); Private STATIC COUNTDOWNLATCH COUNTDOWNLATCH = New CountdownLatch (100); 개인 정적 목록 <fileInfo> filEList = new ArrayList <fileInfo> (); 개인 정적 목록 <목록 <fileInfo >> list = new ArrayList <> (); public static void main (String [] args)은 InterruptedException {createFileInfo (); addList (); Long StartTime = System.CurrentTimeMillis (); int i = 0; for (list <fileInfo> fi : list) {executor.submit (new FilerUnnable (CountdownLatch, fi, i)); i ++; } countdownlatch.await (); Long Endtime = System.CurrentTimeMillis (); executor.shutdown (); System.out.println (i+"스레드는 시간이 걸립니다 :"+(endtime-starttime)+"ms"); } private static void createFileInfo () {for (int i = 0; i <30000; i ++) {filElist.add (new FileInfo ( "Front ID 카드 사진", "JPG", "101522", "md5"+i, "1"); }} private static void addList () {for (int i = 0; i <100; i ++) {list.add (filleList); }}}FilerUnnable 클래스 :
/** * 멀티 스레드 처리 * @author wangsj * * @param <t> */public class filerunnable <t> implements runnable {private countdownlatch countdownlatch; 개인 목록 <T> 목록; 개인 INT I; public filerunnable (countdownLatch countdownlatch, list <t> 목록, int i) {super (); this.countdownlatch = CountdownLatch; this.list = list; this.i = i; } @override public void run () {for (t t : list) {try {thread.sleep (1); } catch (InterruptedException e) {e.printstacktrace (); } countdownlatch.countdown (); }}}테스트 결과는 다음과 같습니다.
3) 포크/조인 프레임 워크
JDK는 버전 7으로 시작했으며 포크/조인 프레임 워크가 나타났습니다. 문자 그대로의 관점에서, 포크는 갈라지고 결합 된 합병 이므로이 프레임 워크에 대한 아이디어는입니다. 포크를 통해 작업을 분할 한 다음 분할 문자를 실행하고 요약 한 후 결과를 병합하기 위해 가입하십시오. 예를 들어, 지속적으로 추가 된 여러 숫자를 계산하려고합니다. 2+4+5+7 =? , 포크/조인 프레임 워크를 어떻게 사용하여 완료합니까? 아이디어는 분자 작업을 분할하는 것입니다. 이 작업을 두 개의 하위 작업으로 나눌 수 있으며, 하나는 2+4를 계산하고 다른 하나는 5+7을 계산합니다. 이것은 포크의 과정입니다. 계산이 완료되면이 두 하위 작업의 계산 결과가 요약되고 합계가 얻어집니다. 이것은 가입 과정입니다.
포크/조인 프레임 워크 실행 아이디어 : 먼저 작업을 나누고 포크 클래스를 사용하여 큰 작업을 여러 하위 작업으로 나눕니다. 이 분할 프로세스는 분할 된 작업이 충분히 작을 때까지 실제 상황에 따라 결정해야합니다. 그런 다음 조인 클래스가 작업을 실행하고 분할 하위 작업은 다른 대기열에 있습니다. 여러 스레드가 큐에서 작업을 얻고 실행합니다. 실행 결과는 별도의 대기열에 배치됩니다. 마지막으로 스레드가 시작되고 결과는 큐에서 얻어지고 결과가 병합됩니다.
포크/조인 프레임 워크를 사용하는 데 여러 클래스가 사용됩니다. 클래스 사용을 위해 JDK API를 참조 할 수 있습니다. 이 프레임 워크를 사용하면 Forkjointask 클래스를 상속해야합니다. 일반적으로 서브 클래스 재귀 사전 또는 재귀 반응 만 상속하면됩니다. RecursiveTask는 반환 결과가있는 장면에 사용되며 재귀 반응은 반환 결과가없는 장면에 사용됩니다. Forkjointask의 실행에는 Forkjoinpool의 실행이 필요하며, 이는 다른 작업 대기열에 추가 된 분할 하위 태스크를 유지하는 데 사용됩니다.
구현 코드는 다음과 같습니다.
공개 클래스 테스트 3 {개인 정적 목록 <FileInfo> filleList = new ArrayList <fileInfo> (); // 개인 정적 포크 포울 포크 포울 (100); // 개인 정적 작업 <FileInfo> job = new job (filElist.size ()/100, filelelist); public static void main (String [] args) {createfileInfo (); Long StartTime = System.CurrentTimeMillis (); Forkjoinpool Forkjoinpool = 새로운 Forkjoinpool (100); // 작업 작업을 분할 <FileInfo> job = new job <> (fileList.size ()/100, filleList); // 작업을 제출하고 결과 Forkjointask <integer> fjtresult = forkjoinpool.submit (job); // block while (! job.isdone ()) {system.out.println ( "작업 완료!"); } long endtime = System.CurrentTimeMillis (); System.out.println ( "포크/조인 프레임 워크 시간 소모 :"+(endtime-starttime)+"ms"); } private static void createFileInfo () {for (int i = 0; i <30000; i ++) {filElist.add (new FileInfo ( "Front ID 카드 사진", "JPG", "101522", "md5"+i, "1"); }}}/** * 작업 클래스 실행 * @author wangsj */public class job <t> recursiveTask <integer> {private static final long serialversionuid = 1l; 개인 int 수; 개인 목록 <T> 실천; 공개 직업 (int count, list <t> joblist) {super (); this.count = count; this.joblist = joblist; } /*** runnable 인터페이스를 구현하는 실행 메소드와 유사하게 작업을 실행합니다* /@override protected integer compute () {// 작업을 분할 if (joblist.size () <= count) {executeJob (); return joblist.size (); } else {// 위조 및 실행 될 수있을 때까지 작업을 계속 만듭니다. 목록 <recursiveTask <long>> fork = new LinkedList <recursiveTask <long >> (); // 핵 성 작업을 분할하십시오. 여기서 이분법 방법이 사용됩니다 int countjob = joblist.size ()/2; List <T> LEFTLIST = JOBLIST.SUBLIST (0, COUNTJOB); List <T> RightList = Joblist.Sublist (CountJob, joblist.size ()); // 작업 할당 작업 job leftJob = 새 Job <> (count, leftList); job rightjob = new job <> (카운트, 오른쪽 목록); // leftjob.fork ()를 실행합니다. RightJob.fork (); return integer.parseint (leftjob.join (). tostring ()) +integer.parseint (rightjob.join (). toString ()); }} / *** 작업 메소드 실행* / private void executeJob () {for (t job : joblist) {try {thread.sleep (1); } catch (InterruptedException e) {e.printstacktrace (); }}}테스트 결과는 다음과 같습니다.
4) JDK8 병렬 스트리밍
병렬 흐름은 JDK8의 새로운 기능 중 하나입니다. 아이디어는 순차적으로 실행 된 스트림을 동시 흐름으로 바꾸는 것입니다. 병렬 흐름은 스트림을 여러 데이터 블록으로 나누고, 다른 스레드를 사용하여 다른 데이터 블록의 스트림을 처리하고, 마지막으로 포크/결합 프레임 워크와 유사한 각 데이터 스트림 블록의 처리 결과를 병합합니다.
병렬 스트림은 기본적으로 공개 스레드 풀 포크 조이 풀을 사용합니다. 스레드 수는 사용 된 기본값입니다. 기계의 코어 수에 따르면 스레드의 크기를 적절하게 조정할 수 있습니다. 스레드 수 조정은 다음과 같은 방식으로 달성됩니다.
System.setProperty ( "java.util.concurrent.forkjoinpool.common.parallelism", "100");
다음은 코드의 구현 프로세스이며 매우 간단합니다.
공개 클래스 test4 {private static list <fileInfo> filEList = new ArrayList <fileInfo> (); public static void main (String [] args) {// system.setProperty ( "java.util.concurrent.forkjoinpool.common.parallelism", "100"); createfileInfo (); Long StartTime = System.CurrentTimeMillis (); fileList.parallelStream (). foreach (e-> {try {thread.sleep (1);} catch (InterruptedException f) {f.printstacktrace ();}}); Long Endtime = System.CurrentTimeMillis (); System.out.println ( "jdk8 병렬 스트리밍 시간 :"+(endtime-starttime)+"ms");} private static void createFileInfo () {for (int i = 0; i <30000; i ++) {filelist.add (새 fileInfo ( "id의 전면 사진. 카드 ","JPG ","101522 ","MD5 "+I,"1 ")); }}}다음은 테스트입니다. 스레드 풀의 수는 처음으로 설정되지 않았습니다. 기본값이 사용됩니다. 테스트 결과는 다음과 같습니다.
우리는 결과가 그다지 이상적이지 않으며 오랜 시간이 걸린다는 것을 알았습니다. 다음으로 스레드 풀 수를 설정합니다. 즉, 다음 코드를 추가하십시오.
System.setProperty ( "java.util.concurrent.forkjoinpool.common.parallelism", "100");
그런 다음 테스트를 수행하고 결과는 다음과 같습니다.
이번에는 시간이 덜 걸리고 이상적입니다.
3. 요약
위의 상황을 요약하면 단일 스레드를 참조로 사용하면 가장 긴 시간이 소요되는 것은 기본 포크/조인 프레임 워크입니다. 스레드 풀의 수가 여기에서 구성되지만 스레드 풀 수를 갖는 JDK8 병렬 스트림은 더 나쁘다. 병렬 스트리밍은 코드가 간단하고 이해하기 쉽고 루프에 대한 추가 글을 쓸 필요가 없습니다. 모든 ParallelStream 방법을 완료 할 수 있으며 코드의 양이 크게 줄어 듭니다. 실제로, 병렬 스트리밍의 기본 계층은 여전히 포크/결합 프레임 워크이며, 개발 과정에서 다양한 기술을 유연하게 사용하여 다양한 기술의 장점과 단점을 구별하여 더 나은 서비스를 제공해야합니다.