1. บทนำมัลติเธรด
ในการเขียนโปรแกรมเราไม่สามารถหลีกเลี่ยงปัญหาการเขียนโปรแกรมแบบมัลติเธรดได้เนื่องจากจำเป็นต้องมีการประมวลผลพร้อมกันในระบบธุรกิจส่วนใหญ่ หากอยู่ในสถานการณ์ที่เกิดขึ้นพร้อมกันการทำมัลติเธรดมีความสำคัญมาก นอกจากนี้ในระหว่างการสัมภาษณ์ของเราผู้สัมภาษณ์มักจะถามคำถามเกี่ยวกับมัลติเธรดเช่น: วิธีสร้างเธรด เรามักจะตอบด้วยวิธีนี้มีสองวิธีหลัก อย่างแรกคือ: สืบทอดคลาสเธรดและเขียนวิธีการเรียกใช้ใหม่; ประการที่สองคือ: ใช้อินเทอร์เฟซ Runnable และเขียนวิธีการเรียกใช้ใหม่ จากนั้นผู้สัมภาษณ์จะถามว่าข้อดีและข้อเสียของวิธีการทั้งสองนี้คืออะไร ไม่ว่าจะเกิดอะไรขึ้นเราจะได้ข้อสรุปนั่นคือวิธีการใช้งานที่สองเพราะผู้สนับสนุนเชิงวัตถุที่มุ่งเน้นการสืบทอดน้อยลงและพยายามใช้ชุดค่าผสมให้มากที่สุด
ในเวลานี้เราอาจคิดว่าจะทำอย่างไรถ้าเราต้องการได้รับค่าตอบแทนของมัลติเธรด? จากความรู้ที่เราได้เรียนรู้มากขึ้นเราจะนึกถึงการใช้อินเทอร์เฟซที่เรียกได้และเขียนวิธีการโทรใหม่ มีการใช้เธรดจำนวนมากในโครงการจริงอย่างไร? พวกเขามีกี่วิธี?
ก่อนอื่นมาดูตัวอย่าง:
นี่เป็นวิธีง่ายๆในการสร้างมัลติเธรดซึ่งเข้าใจง่าย ในตัวอย่างตามสถานการณ์ทางธุรกิจที่แตกต่างกันเราสามารถส่งพารามิเตอร์ที่แตกต่างกันไปในเธรด () เพื่อใช้ตรรกะทางธุรกิจที่แตกต่างกัน อย่างไรก็ตามปัญหาที่เปิดเผยโดยวิธีการสร้างมัลติเธรดนี้คือการสร้างเธรดซ้ำ ๆ และจะต้องถูกทำลายหลังจากสร้างเธรด หากข้อกำหนดสำหรับสถานการณ์พร้อมกันอยู่ในระดับต่ำวิธีนี้ดูเหมือนจะโอเค แต่ในสถานการณ์ที่เกิดขึ้นพร้อมกันสูงวิธีนี้เป็นไปไม่ได้เพราะการสร้างเธรดนั้นใช้ทรัพยากรมาก ดังนั้นตามประสบการณ์วิธีที่ถูกต้องคือการใช้เทคโนโลยีพูลเธรด JDK มีประเภทพูลเธรดที่หลากหลายเพื่อให้เราเลือก สำหรับวิธีการเฉพาะคุณสามารถตรวจสอบเอกสาร JDK
สิ่งที่เราต้องทราบในรหัสนี้คือพารามิเตอร์ที่ส่งผ่านเป็นตัวแทนจำนวนเธรดที่เรากำหนดค่า ยิ่งดีขึ้น? ไม่แน่นอน เนื่องจากเมื่อกำหนดค่าจำนวนเธรดเราต้องพิจารณาประสิทธิภาพของเซิร์ฟเวอร์อย่างเต็มที่ หากมีการกำหนดค่าเธรดเพิ่มเติมประสิทธิภาพของเซิร์ฟเวอร์อาจไม่ยอดเยี่ยม โดยปกติแล้วการคำนวณที่เสร็จสมบูรณ์โดยเครื่องจะถูกกำหนดโดยจำนวนเธรด เมื่อจำนวนเธรดมาถึงจุดสูงสุดการคำนวณจะไม่สามารถทำได้ หากเป็นตรรกะทางธุรกิจที่ใช้ CPU (การคำนวณเพิ่มเติม) จำนวนเธรดและแกนจะไปถึงจุดสูงสุด หากเป็นตรรกะทางธุรกิจที่ใช้ I/O (ฐานข้อมูลการดำเนินงานการอัปโหลดไฟล์การดาวน์โหลด ฯลฯ ) ยิ่งเธรดมากขึ้นเธรดมากขึ้นมันจะช่วยปรับปรุงประสิทธิภาพในแง่หนึ่ง
สูตรอื่นในการตั้งค่าจำนวนเธรด:
y = n*((a+b)/a) โดยที่ n: จำนวนคอร์ CPU, A: เวลาการคำนวณของโปรแกรมเมื่อมีการดำเนินการเธรด, B: เวลาปิดกั้นของโปรแกรมเมื่อมีการดำเนินการเธรด ด้วยสูตรนี้การกำหนดค่าจำนวนเธรดของพูลเธรดจะถูก จำกัด และเราสามารถกำหนดค่าได้อย่างยืดหยุ่นตามสถานการณ์จริงของเครื่อง
2. การเพิ่มประสิทธิภาพแบบมัลติเธรดและการเปรียบเทียบประสิทธิภาพ
เทคโนโลยีเกลียวถูกใช้ในโครงการล่าสุดและฉันพบปัญหามากมายระหว่างการใช้งาน การใช้ประโยชน์จากความนิยมฉันจะแยกแยะการเปรียบเทียบประสิทธิภาพของกรอบหลายเธรด สิ่งที่เรามีความเชี่ยวชาญนั้นแบ่งออกเป็นสามประเภท: ประเภทแรก: ThreadPool (เธรดพูล) + Countdownlatch (ตัวนับโปรแกรม), ประเภทที่สอง: Fork/เข้าร่วมเฟรมเวิร์กและสตรีมขนาน JDK8 ประเภทที่สาม นี่คือบทสรุปเปรียบเทียบของประสิทธิภาพมัลติเธรดของวิธีการเหล่านี้
ขั้นแรกให้สมมติสถานการณ์ทางธุรกิจที่มีการสร้างวัตถุไฟล์หลายรายการในหน่วยความจำ ที่นี่การนอนหลับของเธรด 30,000 ครั้งนั้นถูกกำหนดอย่างไม่แน่นอนในการจำลองการประมวลผลทางธุรกิจตรรกะทางธุรกิจเพื่อเปรียบเทียบประสิทธิภาพการทำมัลติเธรดของวิธีการเหล่านี้
1) เกลียวเดี่ยว
วิธีนี้ง่ายมาก แต่โปรแกรมใช้เวลานานมากในระหว่างการประมวลผลและจะใช้เป็นเวลานานเพราะแต่ละเธรดกำลังรอให้เธรดปัจจุบันถูกดำเนินการก่อนที่จะดำเนินการ มันมีส่วนเกี่ยวข้องกับหลายเธรดดังนั้นประสิทธิภาพจึงต่ำมาก
ก่อนอื่นสร้างวัตถุไฟล์รหัสมีดังนี้:
คลาสสาธารณะ FileInfo {private String filename; // ชื่อไฟล์ Private String fileType; // ประเภทไฟล์สตริงส่วนตัวไฟล์ไฟล์; // ขนาดไฟล์สตริงส่วนตัว filemd5; // md5 รหัสส่วนตัวสตริง private string fileversionno; // หมายเลขไฟล์หมายเลขไฟล์ public fileinfo () {super (); } public fileInfo (ชื่อไฟล์สตริง, สตริง fileType, สตริงไฟล์, สตริง filemd5, สตริง fileVersionNo) {super (); this.filename = ชื่อไฟล์; this.fileType = fileType; this.filesize = filesize; this.filemd5 = filemd5; this.fileversionno = fileversionno; } สตริงสาธารณะ getFilename () {return filename; } โมฆะสาธารณะ setFileName (ชื่อไฟล์สตริง) {this.filename = filename; } สตริงสาธารณะ getFileType () {return fileType; } โมฆะสาธารณะ setFileType (สตริง fileType) {this.fileType = fileType; } สตริงสาธารณะ getFilesize () {return filesize; } โมฆะสาธารณะ setFilesize (String filesize) {this.filesize = filesize; } สตริงสาธารณะ getFilemd5 () {return filemd5; } โมฆะสาธารณะ setFilemd5 (String filemd5) {this.filemd5 = filemd5; } สตริงสาธารณะ getFileVersionNo () {return fileVersionNo; } โมฆะสาธารณะ setFileVersionNo (String fileVersionNo) {this.fileversionNo = FileVersionNo; -จากนั้นจำลองการประมวลผลทางธุรกิจสร้างวัตถุไฟล์ 30,000 ไฟล์เธรดนอนหลับเป็นเวลา 1ms และตั้งค่า 1,000ms ก่อนหน้านี้และพบว่าเวลานั้นยาวมากและ Eclipse ทั้งหมดติดอยู่ดังนั้นเปลี่ยนเวลาเป็น 1ms
การทดสอบคลาสสาธารณะ {รายการคงที่ส่วนตัว <FileInfo> fileList = new ArrayList <FileInfo> (); โมฆะคงที่สาธารณะหลัก (สตริง [] args) พ่น InterruptedException {createFileInfo (); Long StartTime = System.currentTimeMillis (); สำหรับ (fileinfo fi: fileList) {thread.sleep (1); } endtime long = system.currentTimeMillis (); System.out.println ("เธรดเดี่ยวใช้เวลานาน:"+(endtime-starttime)+"MS"); } โมฆะคงที่ส่วนตัว createFileInfo () {สำหรับ (int i = 0; i <30000; i ++) {fileList.add (FileInfo ใหม่ ("รูปถ่ายด้านหน้าของบัตรประจำตัว", "jpg", "101522", "Md5"+i, "1")); -ผลการทดสอบมีดังนี้:
จะเห็นได้ว่าการสร้างวัตถุไฟล์ 30,000 รายการใช้เวลานานเกือบ 1 นาทีและประสิทธิภาพค่อนข้างต่ำ
2) ThreadPool (เธรดพูล) +Countdownlatch (ตัวนับโปรแกรม)
ตามชื่อแนะนำ Countdownlatch เป็นตัวนับเธรด กระบวนการดำเนินการของมันมีดังนี้: ประการแรกวิธีการรอคอย () เรียกว่าในเธรดหลักและเธรดหลักจะถูกบล็อกจากนั้นตัวนับโปรแกรมจะถูกส่งผ่านไปยังวัตถุเธรดเป็นพารามิเตอร์ ในที่สุดหลังจากแต่ละเธรดเสร็จสิ้นการทำงานของงานวิธีการนับถอยหลัง () จะถูกเรียกว่าเพื่อระบุความสำเร็จของงาน หลังจาก Countdown () ดำเนินการหลายครั้งหลายครั้งการรอคอยของเธรดหลัก () จะไม่ถูกต้อง กระบวนการดำเนินการมีดังนี้:
คลาสสาธารณะ test2 {ส่วนตัว ExecutorService Executor = Executors.NewFixedThreadPool (100); เอกชน countdownlatch countdownlatch = new countdownlatch (100); รายการคงที่ส่วนตัว <FileInfo> fileList = new ArrayList <FileInfo> (); รายการคงที่ส่วนตัว <list <fileInfo>> list = new ArrayList <> (); โมฆะคงที่สาธารณะหลัก (สตริง [] args) พ่น InterruptedException {createFileInfo (); addlist (); Long StartTime = System.currentTimeMillis (); int i = 0; สำหรับ (list <fileinfo> fi: list) {executor.submit (filerunnable ใหม่ (Countdownlatch, fi, i)); i ++; } countdownlatch.await (); endtime long = system.currentTimeMillis (); Executor.shutdown (); System.out.println (i+"เธรดใช้เวลา:"+(endtime-starttime)+"MS"); } โมฆะคงที่ส่วนตัว createFileInfo () {สำหรับ (int i = 0; i <30000; i ++) {fileList.add (FileInfo ใหม่ ("รูปถ่ายบัตรไอทีหน้า", "JPG", "101522", "MD5"+I, "1")); }} โมฆะคงที่ส่วนตัว addList () {สำหรับ (int i = 0; i <100; i ++) {list.add (fileList); -ชั้นเรียน filerunnable:
/** * การประมวลผลแบบมัลติเธรด * @author wangsj * * @param <t> */คลาสสาธารณะ Filerunnable <t> ใช้งาน Runnable {Private Countdownlatch Countdownlatch; รายการส่วนตัว <T> รายการ; ส่วนตัว int i; public filerunnable (countdownlatch countdownlatch, รายการ <t> รายการ, int i) {super (); this.countdownlatch = countdownlatch; this.list = list; this.i = i; } @Override โมฆะสาธารณะเรียกใช้ () {สำหรับ (t t: list) {ลอง {thread.sleep (1); } catch (interruptedException e) {e.printStackTrace (); } countdownlatch.countdown (); -ผลการทดสอบมีดังนี้:
3) Fork/เข้าร่วมเฟรมเวิร์ก
JDK เริ่มต้นด้วยเวอร์ชัน 7 และเฟรมเวิร์ก Fork/เข้าร่วมปรากฏขึ้น จากมุมมองที่แท้จริงส้อมจะแยกและเข้าร่วมคือการควบรวมกิจการดังนั้นแนวคิดของกรอบนี้คือ แยกงานผ่านส้อมแล้วเข้าร่วมเพื่อรวมผลลัพธ์หลังจากที่อักขระแยกถูกดำเนินการและสรุป ตัวอย่างเช่นเราต้องการคำนวณตัวเลขหลายตัวที่เพิ่มอย่างต่อเนื่อง 2+4+5+7 =? เราจะใช้เฟรมเวิร์กส้อม/เข้าร่วมเพื่อให้เสร็จได้อย่างไร? ความคิดคือการแยกงานโมเลกุล เราสามารถแยกการดำเนินการนี้ออกเป็นสองงานย่อยหนึ่งคำนวณ 2+4 และอื่น ๆ คำนวณ 5+7 นี่คือกระบวนการของส้อม หลังจากการคำนวณเสร็จสิ้นผลลัพธ์ของการคำนวณงานย่อยทั้งสองนี้จะสรุปและได้รับผลรวม นี่คือกระบวนการเข้าร่วม
Fork/เข้าร่วม Framework Idea Idea: ก่อนอื่นแบ่งงานและใช้คลาสส้อมเพื่อแบ่งงานขนาดใหญ่เป็นงานย่อยหลายแบบ กระบวนการแบ่งส่วนนี้จะต้องได้รับการพิจารณาตามสถานการณ์จริงจนกว่างานที่ถูกแบ่งจะมีขนาดเล็กพอ จากนั้นคลาสเข้าร่วมจะดำเนินงานและงานย่อยที่แบ่งออกเป็นคิวที่แตกต่างกัน หลายเธรดได้รับงานจากคิวและดำเนินการ ผลการดำเนินการถูกวางไว้ในคิวแยกต่างหาก ในที่สุดเธรดจะเริ่มต้นผลลัพธ์จะได้รับในคิวและผลลัพธ์จะถูกรวมเข้าด้วยกัน
หลายคลาสใช้เพื่อใช้เฟรมเวิร์กส้อม/เข้าร่วม สำหรับการใช้คลาสคุณสามารถอ้างถึง JDK API การใช้เฟรมเวิร์กนี้คุณต้องสืบทอดคลาส Forkjointask โดยปกติแล้วคุณจะต้องสืบทอดการเรียกคืน subclass ของมันหรือการเรียกคืน Recursivetask ใช้สำหรับฉากที่มีผลลัพธ์การส่งคืนและการเรียกใช้ซ้ำจะใช้สำหรับฉากที่ไม่มีผลลัพธ์การส่งคืน การดำเนินการของ Forkjointask ต้องใช้การดำเนินการของ ForkJoinpool ซึ่งใช้เพื่อรักษางานย่อยที่แบ่งออกเป็นคิวงานที่แตกต่างกัน
นี่คือรหัสการใช้งาน:
การทดสอบระดับสาธารณะ 3 {รายการคงที่ส่วนตัว <FileInfo> fileList = new ArrayList <FileInfo> (); // ส่วนตัวคงที่ ForkJoinPool ForkJoinPool = New ForkJoinPool (100); // งานคงที่ โมฆะคงที่สาธารณะหลัก (สตริง [] args) {createFileInfo (); Long StartTime = System.currentTimeMillis (); ForkJoinPool ForkJoinPool = New ForkJoinPool (100); // แยกงานงาน <fileinfo> job = งานใหม่ <> (filelist.size ()/100, fileList); // ส่งงานและส่งคืนผลลัพธ์ forkjointask <integer> fjtresult = forkjoinpool.submit (Job); // block ในขณะที่ (! job.isdone ()) {system.out.println ("งานเสร็จ!"); } endtime long = system.currentTimeMillis (); System.out.println ("Fork/เข้าร่วม Framework ใช้เวลานาน:"+(endtime-starttime)+"MS"); } โมฆะคงที่ส่วนตัว createFileInfo () {สำหรับ (int i = 0; i <30000; i ++) {fileList.add (FileInfo ใหม่ ("รูปถ่ายบัตรไอทีหน้า", "JPG", "101522", "MD5"+I, "1")); }}}/** * ดำเนินการคลาสงาน * @author wangsj * */งานระดับสาธารณะ <t> ขยาย RecursiVetask <integer> {ส่วนตัวคงที่สุดท้าย Long SerialVersionUid = 1L; จำนวน int ส่วนตัว; รายการส่วนตัว <T> JOBLIST; งานสาธารณะ (จำนวน int, รายการ <t> jOblist) {super (); this.count = นับ; this.joblist = joblist; } /*** ดำเนินการงานคล้ายกับวิธีการเรียกใช้ที่ใช้อินเตอร์เฟส runnable* /@Override ที่ได้รับการป้องกันการคำนวณจำนวนเต็ม () {// แยกงานถ้า (joBlist.size () <= นับ) {executeJob (); return joblist.size (); } else {// ดำเนินการต่อเพื่อสร้างงานจนกว่าจะสามารถย่อยสลายและดำเนินการรายการ <recursivetask <long>> fork = new LinkedList <Recursivetask <long>> (); // แยกงานนิวเคลียสที่นี่วิธีการแบ่งขั้วถูกใช้ int countJob = joBlist.size ()/2; รายการ <t> leftlist = joBlist.Sublist (0, CountJob); รายการ <t> rightList = joBlist.Sublist (CountJob, jOblist.size ()); // กำหนดงานงาน leftJob = งานใหม่ <> (นับ, รายการซ้าย); Job RightJob = งานใหม่ <> (count, rightlist); // ดำเนินการงาน leftjob.fork (); rightjob.fork (); return integer.parseint (leftjob.join (). toString ()) +integer.parseint (rightjob.join (). toString ()); }} / *** ดำเนินการวิธีงาน* / โมฆะส่วนตัว ExecuteJob () {สำหรับ (t Job: joBlist) {ลอง {thread.sleep (1); } catch (interruptedException e) {e.printStackTrace (); -ผลการทดสอบมีดังนี้:
4) การสตรีมแบบขนาน JDK8
การไหลแบบขนานเป็นหนึ่งในคุณสมบัติใหม่ของ JDK8 แนวคิดคือการเปลี่ยนสตรีมที่ดำเนินการตามลำดับเป็นโฟลว์พร้อมกันซึ่งดำเนินการโดยเรียกใช้วิธีการขนาน () การไหลแบบขนานแบ่งสตรีมออกเป็นหลายบล็อกข้อมูลใช้เธรดที่แตกต่างกันเพื่อประมวลผลสตรีมของบล็อกข้อมูลที่แตกต่างกันและในที่สุดก็รวมผลลัพธ์การประมวลผลของแต่ละบล็อกของสตรีมข้อมูลคล้ายกับเฟรมเวิร์กส้อม/เข้าร่วม
สตรีมแบบขนานใช้พูลพูลสาธารณะ ForkJoinpool โดยค่าเริ่มต้น จำนวนเธรดคือค่าเริ่มต้นที่ใช้ ตามจำนวนแกนของเครื่องเราสามารถปรับขนาดของเธรดได้อย่างเหมาะสม การปรับจำนวนเธรดทำได้ในวิธีต่อไปนี้
System.SetProperty ("java.util.concurrent.forkjoinpool.common.parallelism", "100");ต่อไปนี้เป็นกระบวนการใช้งานของรหัสซึ่งง่ายมาก:
การทดสอบคลาสสาธารณะ 4 {รายการคงที่ส่วนตัว <FileInfo> fileList = new ArrayList <FileInfo> (); โมฆะคงที่สาธารณะหลัก (สตริง [] args) {// system.setProperty ("java.util.concurrent.forkjoinpool.common.parallelism", "100"); createFileinfo (); Long StartTime = System.currentTimeMillis (); filelist.parallelsstream (). foreach (e -> {ลอง {thread.sleep (1);} catch (interruptedexception f) {f.printstacktrace ();}}); endtime long = system.currentTimeMillis (); System.out.println ("JDK8 แบบขนานเวลาสตรีม:"+(endtime-starttime)+"ms");} โมฆะคงที่ส่วนตัว createFileInfo () {สำหรับ (int i = 0; i <30000; i ++) การ์ด "," JPG "," 101522 "," MD5 "+I," 1 ")); -ต่อไปนี้คือการทดสอบ จำนวนพูลเธรดไม่ได้ถูกตั้งค่าเป็นครั้งแรก ใช้ค่าเริ่มต้น ผลการทดสอบมีดังนี้:
เราเห็นว่าผลลัพธ์นั้นไม่เหมาะและใช้เวลานาน จากนั้นตั้งค่าจำนวนพูลเธรดนั่นคือเพิ่มรหัสต่อไปนี้:
System.SetProperty ("java.util.concurrent.forkjoinpool.common.parallelism", "100");จากนั้นทำการทดสอบและผลลัพธ์มีดังนี้:
เวลานี้ใช้เวลาน้อยลงและเหมาะ
3. สรุป
ในการสรุปสถานการณ์ข้างต้นโดยใช้เธรดเดียวเป็นข้อมูลอ้างอิงการใช้เวลานานที่สุดคือ Framework Native Fork/เข้าร่วม แม้ว่าจำนวนพูลเธรดจะถูกกำหนดค่าที่นี่สตรีมขนาน JDK8 ที่มีจำนวนพูลเธรดไม่ดี การสตรีมแบบขนานใช้รหัสนั้นง่ายและเข้าใจง่ายและเราไม่จำเป็นต้องเขียนเพิ่มเติมสำหรับลูป เราสามารถทำวิธี ParallelSream ทั้งหมดให้สมบูรณ์และปริมาณของรหัสจะลดลงอย่างมาก ในความเป็นจริงเลเยอร์พื้นฐานของการสตรีมแบบขนานยังคงเป็นเฟรมเวิร์กส้อม/เข้าร่วมซึ่งต้องการให้เราใช้เทคโนโลยีต่าง ๆ อย่างยืดหยุ่นในระหว่างกระบวนการพัฒนาเพื่อแยกแยะข้อดีและข้อเสียของเทคโนโลยีต่าง ๆ เพื่อให้บริการเราดีขึ้น