1. مقدمة متعددة الخيوط
في البرمجة ، لا يمكننا تجنب مشاكل البرمجة متعددة الخيوط ، لأن المعالجة المتزامنة مطلوبة في معظم أنظمة الأعمال. إذا كان في سيناريوهات متزامنة ، فإن متعدد الخيوط مهم للغاية. بالإضافة إلى ذلك ، خلال مقابلتنا ، عادة ما يسألنا القائم بإجراء المقابلة أسئلة حول متعدد الخيوط ، مثل: كيفية إنشاء موضوع؟ عادة ما نرد بهذه الطريقة ، هناك طريقتان رئيسيتان. الأول هو: ورث فئة الخيط وإعادة كتابة طريقة التشغيل ؛ والثاني هو: تنفيذ واجهة Runnable وإعادة كتابة طريقة التشغيل. ثم سيسأل القائم بإجراء المقابلة بالتأكيد عن مزايا وعيوب هاتين الطريقتين. بغض النظر عن ما ، سوف نتوصل إلى استنتاج ، أي الطريقة الثانية للاستخدام ، لأن المدافعين عن الكائنات أقل ميراثًا ويحاول استخدام مجموعات قدر الإمكان.
في هذا الوقت ، قد نفكر أيضًا في ما يجب القيام به إذا أردنا الحصول على قيمة الإرجاع من الررادية المتعددة؟ بناءً على المعرفة التي تعلمناها أكثر ، سوف نفكر في تنفيذ الواجهة القابلة للاتصال وإعادة كتابة طريقة الاتصال. كيف يتم استخدام العديد من المواضيع في المشاريع الفعلية؟ كم عدد الطرق لديهم؟
أولاً ، دعونا نلقي نظرة على مثال:
هذه طريقة بسيطة لإنشاء قواعد متعددة ، والتي يسهل فهمها. في المثال ، وفقًا لسيناريوهات الأعمال المختلفة ، يمكننا نقل معلمات مختلفة إلى Thread () لتنفيذ منطق أعمال مختلف. ومع ذلك ، فإن المشكلة التي تعرضها هذه الطريقة لإنشاء قواعد متعددة هي إنشاء مؤشرات ترابط بشكل متكرر ، ويجب تدميرها بعد إنشاء مؤشرات الترابط. إذا كانت متطلبات السيناريوهات المتزامنة منخفضة ، فيبدو أن هذه الطريقة على ما يرام ، ولكن في سيناريوهات التزامن العالية ، فإن هذه الطريقة غير ممكنة ، لأن إنشاء مؤشرات الترابط يستغرق موارد للغاية. لذلك وفقًا للتجربة ، فإن الطريقة الصحيحة هي استخدام تقنية تجمع مؤشرات الترابط. يوفر JDK مجموعة متنوعة من أنواع تجمع الخيوط لنا للاختيار من بينها. لطرق محددة ، يمكنك التحقق من وثائق JDK.
ما نحتاج إلى ملاحظته في هذا الرمز هو أن المعلمات التي تم تمريرها في عدد مؤشرات الترابط التي قمنا بتكوينها. هل كلما كان ذلك أفضل؟ بالتأكيد لا. لأنه عند تكوين عدد مؤشرات الترابط ، يجب أن ننظر تمامًا في أداء الخادم. إذا كان هناك المزيد من تكوينات مؤشرات الترابط ، فقد لا يكون أداء الخادم ممتازًا. عادة ، يتم تحديد الحسابات التي يكملها الجهاز من خلال عدد المواضيع. عندما يصل عدد المواضيع إلى الذروة ، لا يمكن تنفيذ الحساب. إذا كان منطق العمل هو الذي يستهلك وحدة المعالجة المركزية (المزيد من العمليات الحسابية) ، فسيصل عدد مؤشرات الترابط والنوى إلى ذروته. إذا كان منطق العمل هو الذي يستهلك I/O (قواعد بيانات التشغيل ، وتحميل الملفات ، والتنزيل ، وما إلى ذلك) ، والمزيد من مؤشرات الترابط ، والمزيد من مؤشرات الترابط ، وسوف يساعد على تحسين الأداء بمعنى معين.
صيغة أخرى لتعيين عدد المواضيع:
y = n*((a+b)/a) ، حيث n: عدد نوى وحدة المعالجة المركزية ، a: وقت حساب البرنامج عند تنفيذ الخيط ، b: وقت الحظر للبرنامج عند تنفيذ مؤشر الترابط. باستخدام هذه الصيغة ، سيتم تقييد تكوين عدد مؤشرات الترابط لمجموعة مؤشرات الترابط ، ويمكننا تكوينه بمرونة وفقًا للموقف الفعلي للجهاز.
2. التحسين متعدد مؤشرات الترقيم ومقارنة الأداء
تم استخدام تقنية الخيوط في المشاريع الحديثة ، وواجهت الكثير من المتاعب أثناء الاستخدام. الاستفادة من الشعبية ، سأقوم بفرز مقارنات الأداء للعديد من الأطر متعددة الخيوط. يتم تقسيم تلك التي أتقنتها تقريبًا إلى ثلاثة أنواع: النوع الأول: ThreadPool (Pool Pool) + CountdownLatch (عداد البرنامج) ، والنوع الثاني: Fork/Join Framework ، والنوع الثالث من دفق JDK8 الموازي. فيما يلي ملخص مقارن لأداء هذه الأساليب متعددة الخيوط.
أولاً ، افترض سيناريو العمل حيث يتم إنشاء كائنات ملفات متعددة في الذاكرة. هنا ، تم تصميم 30،000 Thread Sleep بشكل مبدئي على محاكاة منطق تجارية معالجة الأعمال لمقارنة الأداء متعدد الخيوط لهذه الأساليب.
1) خيوط واحدة
هذه الطريقة بسيطة للغاية ، لكن البرنامج يستغرق وقتًا طويلاً أثناء المعالجة وسيتم استخدامه لفترة طويلة ، لأن كل مؤشر ترابط ينتظر تنفيذ الخيط الحالي قبل تنفيذه. لا علاقة له بالتصورات المتعددة ، وبالتالي فإن الكفاءة منخفضة للغاية.
أولاً قم بإنشاء كائن الملف ، الرمز كما يلي:
الفئة العامة fileInfo {private string filename ؛ // اسم الملف السلسلة الخاصة fileType ؛ // نوع الملف السلسلة الخاصة fileize ؛ // حجم الملف سلسلة private filemd5 ؛ // md5 رمز الخاص } fileInfo العام (اسم ملف السلسلة ، سلسلة filetype ، سلسلة ملفات ، سلسلة fileMd5 ، string fileversionno) {super () ؛ this.filename = اسم الملف ؛ this.filetype = fileType ؛ this.filesize = filedize ؛ this.filemd5 = fileMd5 ؛ this.fileversionno = fileversionno ؛ } السلسلة العامة getFilename () {return filename ؛ } public void setFilename (اسم ملف السلسلة) {this.filename = filename ؛ } السلسلة العامة getFileType () {return fileType ؛ } public void setFileType (String fileType) {this.filetype = fileType ؛ } السلسلة العامة getFilesize () {return filedize ؛ } public void setFilesize (سلسلة ملفات) {this.filesize = filedize ؛ } السلسلة العامة getFileMd5 () {return fileMd5 ؛ } public void setFileMd5 (String fileMd5) {this.filemd5 = fileMd5 ؛ } السلسلة العامة getFileversionNo () {return fileversionno ؛ } public void setFileversionNo (string fileversionno) {this.fileversionno = fileversionno ؛ }بعد ذلك ، قم بمحاكاة معالجة الأعمال ، وإنشاء 30،000 كائن ملف ، وينام مؤشر ترابط لمدة 1 مللي ثانية ، ويحدد 1000 مللي ثانية من قبل ، ويجد أن الوقت طويل جدًا ، وأن الكسوف بأكمله عالق ، لذا قم بتغيير الوقت إلى 1 مللي ثانية.
اختبار الفئة العامة {القائمة الثابتة الخاصة <IbleInfo> fileList = new ArrayList <IbleInfo> () ؛ رميات الفراغ الثابتة العامة (سلسلة [] args) interruptedException {createFileInfo () ؛ وقت بدء طويل = system.currentTimeMillis () ؛ لـ (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 (fileInfo جديد ("الصورة الأمامية لبطاقة الهوية" ، "JPG" ، "101522" ، "md5"+i ، "1")) ؛ }}}نتائج الاختبار كما يلي:
يمكن ملاحظة أن إنشاء 30،000 كائن ملف يستغرق وقتًا طويلاً ، تقريبًا دقيقة واحدة ، والكفاءة منخفضة نسبيًا.
2) Threadpool (Thread Pool) +CountdownLatch (عداد البرنامج)
كما يوحي الاسم ، CountDownlatch هو عداد مؤشر ترابط. تكون عملية التنفيذ الخاصة بها كما يلي: أولاً ، يتم استدعاء طريقة AWAIT () في الخيط الرئيسي ، ويتم حظر الخيط الرئيسي ، ثم يتم تمرير عداد البرنامج إلى كائن مؤشر الترابط كمعلمة. أخيرًا ، بعد كل مؤشر ترابط ينتهي من تنفيذ المهمة ، يتم استدعاء طريقة العد التنازلي () للإشارة إلى الانتهاء من المهمة. بعد تنفيذ العد التنازلي () عدة مرات ، ستكون AWAIT () الخيط الرئيسي غير صالح. عملية التنفيذ هي كما يلي:
الفئة العامة Test2 {Private Static ExecutorService Executor = Executors.NewFixedThreadPool (100) ؛ خاص ثابت العد التنازلي cundDownLatch = New CountDownLatch (100) ؛ قائمة ثابتة خاصة <IbleInfo> fileList = new ArrayList <IbleInfo> () ؛ قائمة ثابتة خاصة <list <IbleInfo >> list = new ArrayList <> () ؛ رميات الفراغ الثابتة العامة (سلسلة [] args) interruptedException {createFileInfo () ؛ addList () ؛ وقت بدء طويل = system.currentTimeMillis () ؛ int i = 0 ؛ من أجل (قائمة <IbleInfo> fi: list) {executor.submit (new fileRunnable (countDownLatch ، fi ، i)) ؛ i ++ ؛ } countDownLatch.await () ؛ endtime long = system.currentTimeMillis () ؛ Executor.Shutdown () ؛ System.out.println (I+"Threads Take Time:"+(Endtime-StartTime)+"MS") ؛ } private static void createFileInfo () {for (int i = 0 ؛ i <30000 ؛ i ++) {fileList.add (fileInfo جديد ("صورة بطاقة الهوية الأمامية" ، "JPG" ، "101522" ، "MD5"+I ، "1") ؛ }} private static void addList () {for (int i = 0 ؛ i <100 ؛ i ++) {list.add (fileList) ؛ }}}فئة FileRunnable:
/** * معالجة multitheded * Author wangsj * * param <T> */public class 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 public void run () {for (t t: list) {try {thread.sleep (1) ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛ } countDownLatch.countDown () ؛ }}}نتائج الاختبار كما يلي:
3) إطار الشوكة/الانضمام
بدأت JDK بالإصدار 7 ، وظهر إطار الشوكة/Join. من منظور حرفي ، يتم تقسيم الشوكة والانضمام إلى الاندماج ، وبالتالي فإن فكرة هذا الإطار هي. اقسم المهمة عبر فورك ، ثم انضم إلى دمج النتائج بعد تنفيذ الأحرف المنقسمة وتلخيصها. على سبيل المثال ، نريد حساب العديد من الأرقام التي يتم إضافتها بشكل مستمر ، 2+4+5+7 =؟ ، كيف نستخدم إطار عمل الشوكة/الانضمام لإكماله؟ الفكرة هي تقسيم المهام الجزيئية. يمكننا تقسيم هذه العملية إلى مهامين فرعيين ، يحسب أحدهما 2+4 والآخر يحسب 5+7. هذه هي عملية الشوكة. بعد اكتمال الحساب ، يتم تلخيص نتائج حساب هاتين المهامين الفرعية ويتم الحصول على المبلغ. هذه هي عملية الانضمام.
فكرة تنفيذ Fork/Join Framework: أولاً ، قسّم المهام واستخدم فئة الشوكة لتقسيم المهام الكبيرة إلى عدة مهام فرعية. يجب تحديد عملية التجزئة هذه وفقًا للوضع الفعلي حتى تكون المهام المقسمة صغيرة بما يكفي. بعد ذلك ، تنفذ فئة Join المهمة ، والمهام الفرعية المقسمة موجودة في طوابير مختلفة. العديد من المواضيع تحصل على مهام من قائمة الانتظار وتنفيذها. يتم وضع نتائج التنفيذ في قائمة انتظار منفصلة. أخيرًا ، يتم تشغيل الخيط ، يتم الحصول على النتائج في قائمة الانتظار ويتم دمج النتائج.
يتم استخدام عدة فئات لاستخدام إطار الشوكة/الانضمام. لاستخدام الفصل ، يمكنك الرجوع إلى API JDK. باستخدام هذا الإطار ، تحتاج إلى ورث فئة ForkJointask. عادةً ما تحتاج فقط إلى ورث مقاسها الفرعي أو تكرار الفئة الفرعية. يتم استخدام Recursivetask للمشاهد مع نتائج الإرجاع ، ويتم استخدام RecursiveAction للمشاهد مع عدم وجود نتائج إرجاع. يتطلب تنفيذ ForkJointask تنفيذ ForkJoinPool ، والذي يستخدم للحفاظ على المهام الفرعية المقسمة المضافة إلى قوائم انتظار مختلفة.
هنا هو رمز التنفيذ:
Test Test3 {private static list <IbleInfo> fileList = new ArrayList <IbleInfo> () ؛ // private forkjoinpool forkjoinpool = new forkjoinpool (100) ؛ // private static job <ibiinfo> Job = new Job <> public static void main (string [] args) {createFileInfo () ؛ وقت بدء طويل = system.currentTimeMillis () ؛ forkjoinpool forkjoinpool = new forkjoinpool (100) ؛ // قسمة مهمة المهمة <IbleInfo> Job = new Job <> (FileList.size ()/100 ، fileList) ؛ // إرسال المهمة وأرجع النتيجة forkjointask <integer> fjtresult = forkjoinpool.submit (Job) ؛ // block بينما (! job.isdone ()) {system.out.println ("المهمة المكتملة!") ؛ } long endtime = system.currentTimeMillis () ؛ System.out.println ("Fork/Join Framework Careuming:"+(Endtime-StartTime)+"MS") ؛ } private static void createFileInfo () {for (int i = 0 ؛ i <30000 ؛ i ++) {fileList.add (fileInfo جديد ("صورة بطاقة الهوية الأمامية" ، "JPG" ، "101522" ، "MD5"+I ، "1") ؛ }}}/** * تنفيذ فئة المهام * Author wangsj * */وظيفة الفئة العامة <t> تمتد recursivetask <integer> {Private Static Final Long SerialVersionuid = 1L ؛ عدد int الخاص قائمة خاصة <T> العاطلين عن العمل ؛ الوظيفة العامة (عدد int ، قائمة <T> joblist) {super () ؛ this.count = count ؛ this.joblist = joblist ؛ } /*** قم بتنفيذ المهمة ، على غرار طريقة التشغيل التي تنفذ الواجهة القابلة للتشغيل* /Override certeger compute () {// تقسيم المهمة if (joblist.size () <= count) {executejob () ؛ إرجاع joblist.size () ؛ } آخر {// متابعة إنشاء المهمة حتى يمكن تحللها وتنفيذها <Recursivetask <Long >> fork = new LinkedList <Recursivetask <Brong >> () ؛ // تقسيم المهمة النووية ، هنا يتم استخدام طريقة الانقسام intjob = joblist.size ()/2 ؛ قائمة <T> LeftList = Joblist.sublist (0 ، countjob) ؛ قائمة <T> القائمة الجارية = joblist.sublist (countjob ، joblist.size ()) ؛ // تعيين المهام Job LeftJob = Job New <> (Count ، Leftlist) ؛ Job RightJob = Job New <> (Count ، Leightlist) ؛ // تنفيذ المهمة leftjob.fork () ؛ rightjob.fork () ؛ return integer.parseint (leftjob.join (). }} / *** تنفيذ طريقة المهمة* / private void executejob () {for (t job: joblist) {try {thread.sleep (1) ؛ } catch (interruptedException e) {E.PrintStackTrace () ؛ }}}نتائج الاختبار كما يلي:
4) تدفق متوازي JDK8
التدفق الموازي هو واحد من الميزات الجديدة لـ JDK8. تتمثل الفكرة في تحويل دفق تم تنفيذه بالتتابع إلى تدفق متزامن ، والذي يتم تنفيذه عن طريق استدعاء الطريقة الموازية (). يقسم التدفق الموازي دفقًا إلى كتل بيانات متعددة ، ويستخدم مؤشرات ترابط مختلفة لمعالجة تدفقات كتل البيانات المختلفة ، وأخيراً دمج نتائج المعالجة لكل كتلة من دفق البيانات ، على غرار إطار الشوكة/Join.
يستخدم الدفق الموازي تجمع الخيوط العامة ForkJoinPool افتراضيًا. عدد مؤشرات الترابط هو القيمة الافتراضية المستخدمة. وفقًا لعدد نوى الجهاز ، يمكننا ضبط حجم المواضيع بشكل مناسب. يتحقق ضبط عدد المواضيع بالطرق التالية.
System.SetProperty ("java.util.concurrent.forkjoinpool.common.paralleism" ، "100") ؛فيما يلي عملية تنفيذ الكود ، وهو أمر بسيط للغاية:
الفئة العامة test4 {قائمة ثابتة خاصة <IbleInfo> fileList = new ArrayList <IbleInfo> () ؛ public static void main (string [] args) {// system.setProperty ("java.util.concurrent.forkjoinpool.common.paralleism" ، "100") ؛ createFileInfo () ؛ وقت بدء طويل = system.currentTimeMillis () ؛ fileList.ParallelStream (). endtime long = system.currentTimeMillis () ؛ System.out.println ("jdk8 parallel time time:"+(Endtime-StartTime)+"MS") ؛} private static void createFileInfo () {for (int i = 0 ؛ i <30000 ؛ i ++) {fileList.add (fileInfo جديد ("الصورة الأمامية للمعرف" بطاقة "،" JPG "،" 101522 "،" MD5 "+I ،" 1 ")) ؛ }}}ما يلي هو الاختبار. لم يتم تعيين عدد برك الخيوط لأول مرة. يتم استخدام الافتراضي. نتائج الاختبار كما يلي:
رأينا أن النتيجة ليست مثالية للغاية وتستغرق وقتًا طويلاً. بعد ذلك ، قم بتعيين عدد تجمعات مؤشرات الترابط ، أي إضافة الكود التالي:
System.SetProperty ("java.util.concurrent.forkjoinpool.common.paralleism" ، "100") ؛ثم تم إجراء الاختبار ، وكانت النتائج على النحو التالي:
هذه المرة يستغرق وقتًا أقل ومثاليًا.
3. ملخص
لتلخيص المواقف المذكورة أعلاه ، باستخدام مؤشر ترابط واحد كمرجع ، فإن أطول وقت يستغرق وقتًا طويلاً هو إطار الشوكة/الوصلة الأصلية. على الرغم من تكوين عدد تجمعات الخيوط هنا ، فإن دفق JDK8 الموازي مع عدد برك مؤشرات الترابط أكثر فقراً. يقوم البث المتوازي بتنفيذ الكود بسيط وسهل الفهم ، ونحن لسنا بحاجة إلى الكتابة الإضافية للحلقات. يمكننا إكمال جميع أساليب ParallelStream ، ويتم تقليل كمية الكود بشكل كبير. في الواقع ، لا تزال الطبقة الأساسية للبث المتوازي هي إطار الشوكة/الانضمام ، مما يتطلب منا استخدام التقنيات المختلفة بمرونة أثناء عملية التطوير لتمييز مزايا وعيوب التقنيات المختلفة ، وذلك لخدمتنا بشكل أفضل.