CodeGym /مدونة جافا /Random-AR /أفضل معًا: Java وفئة Thread. الجزء الثالث – التفاعل
John Squirrels
مستوى
San Francisco

أفضل معًا: Java وفئة Thread. الجزء الثالث – التفاعل

نشرت في المجموعة
لمحة موجزة عن تفاصيل كيفية تفاعل المواضيع. لقد نظرنا سابقًا في كيفية مزامنة الخيوط مع بعضها البعض. هذه المرة سوف نتعمق في المشاكل التي قد تنشأ أثناء تفاعل المواضيع، وسنتحدث عن كيفية تجنبها. وسنقدم أيضًا بعض الروابط المفيدة لمزيد من الدراسة المتعمقة. أفضل معًا: Java وفئة Thread.  الجزء الثالث — التفاعل - 1

مقدمة

لذلك، نحن نعلم أن جافا لديها المواضيع. يمكنك أن تقرأ عن ذلك في المراجعة التي تحمل عنوان Better Together: Java and the Thread class. الجزء الأول – خيوط التنفيذ . وقد اكتشفنا حقيقة أن الخيوط يمكن أن تتزامن مع بعضها البعض في المراجعة التي تحمل عنوان Better Together: Java and the Thread class. الجزء الثاني – التزامن . حان الوقت للحديث عن كيفية تفاعل المواضيع مع بعضها البعض. كيف يتشاركون الموارد المشتركة؟ ما هي المشاكل التي قد تنشأ هنا؟ أفضل معًا: Java وفئة Thread.  الجزء الثالث — التفاعل - 2

طريق مسدود

المشكلة الأكثر رعبا على الإطلاق هي الجمود. الجمود هو عندما ينتظر خيطان أو أكثر الآخر إلى الأبد. سنأخذ مثالاً من صفحة ويب Oracle التي تصف حالة الجمود :
public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s bowed back to me!%n",
                    this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse = new Friend("Alphonse");
        final Friend gaston = new Friend("Gaston");
        new Thread(() -> alphonse.bow(gaston)).start();
        new Thread(() -> gaston.bow(alphonse)).start();
    }
}
قد لا يحدث حالة توقف تام هنا في المرة الأولى، ولكن إذا توقف برنامجك، فقد حان وقت التشغيل jvisualvm: أفضل معًا: Java وفئة Thread.  الجزء الثالث — التفاعل - 3مع تثبيت المكون الإضافي JVisualVM (عبر الأدوات -> المكونات الإضافية)، يمكننا معرفة مكان حدوث حالة توقف تام:
"Thread-1" - Thread t@12
   java.lang.Thread.State: BLOCKED
	at Deadlock$Friend.bowBack(Deadlock.java:16)
	- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
الخيط 1 ينتظر القفل من الخيط 0. لماذا يحدث ذلك؟ Thread-1يبدأ التشغيل وينفذ Friend#bowالطريقة. تم تمييزه بالكلمة synchronizedالأساسية، مما يعني أننا نحصل على الشاشة لـ this(الكائن الحالي). كان إدخال الطريقة بمثابة إشارة إلى Friendالكائن الآخر. الآن، Thread-1يريد تنفيذ الطريقة على الآخر Friend، ويجب عليه الحصول على القفل الخاص به للقيام بذلك. ولكن إذا تمكن الخيط الآخر (في هذه الحالة Thread-0) من الدخول إلى bow()الطريقة، فهذا يعني أن القفل قد تم الحصول عليه بالفعل وينتظر Thread-1، Thread-0والعكس صحيح. هذا الطريق المسدود غير قابل للحل، ونحن نسميه الطريق المسدود. مثل قبضة الموت التي لا يمكن تحريرها، الجمود هو انسداد متبادل لا يمكن كسره. للحصول على شرح آخر للجمود يمكنك مشاهدة هذا الفيديو: شرح Deadlock و Livelock .

لايفلوك

إذا كان هناك طريق مسدود، فهل هناك أيضًا طريق مسدود؟ نعم، هناك :) Livelock يحدث عندما تبدو الخيوط حية ظاهريًا، لكنها غير قادرة على فعل أي شيء، لأنه لا يمكن استيفاء الشرط (الشروط) المطلوبة لمواصلة عملها. في الأساس، Livelock يشبه حالة توقف تام، لكن الخيوط لا "تتعطل" في انتظار الشاشة. وبدلاً من ذلك، فإنهم يفعلون شيئًا ما إلى الأبد. على سبيل المثال:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class App {
    public static final String ANSI_BLUE = "\u001B[34m";
    public static final String ANSI_PURPLE = "\u001B[35m";

    public static void log(String text) {
        String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
        String color = ANSI_BLUE;
        int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
        if (val != 0) {
            color = ANSI_PURPLE;
        }
        System.out.println(color + name + ": " + text + color);
        try {
            System.out.println(color + name + ": wait for " + val + " sec" + color);
            Thread.currentThread().sleep(val * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Lock first = new ReentrantLock();
        Lock second = new ReentrantLock();

        Runnable locker = () -> {
            boolean firstLocked = false;
            boolean secondLocked = false;
            try {
                while (!firstLocked || !secondLocked) {
                    firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
                    log("First Locked: " + firstLocked);
                    secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
                    log("Second Locked: " + secondLocked);
                }
                first.unlock();
                second.unlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        new Thread(locker).start();
        new Thread(locker).start();
    }
}
يعتمد نجاح هذا الرمز على الترتيب الذي يبدأ به برنامج جدولة سلاسل المحادثات Java. إذا Thead-1بدأ أولاً، فسنحصل على قفل حي:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
كما ترون من المثال، يحاول كلا الخيطين الحصول على كلا القفلين بدورهما، لكنهما يفشلان. لكنهم ليسوا في طريق مسدود. ظاهريًا، كل شيء على ما يرام وهم يقومون بعملهم. أفضل معًا: Java وفئة Thread.  الجزء الثالث — التفاعل - 4وفقًا لـ JVisualVM، نرى فترات من السكون وفترة من التوقف (وهذا عندما يحاول الخيط الحصول على قفل - فهو يدخل في حالة الإيقاف، كما ناقشنا سابقًا عندما تحدثنا عن مزامنة الخيط ) . يمكنك مشاهدة مثال على القفل المباشر هنا: Java - Thread Livelock .

مجاعة

بالإضافة إلى الجمود والقفل المباشر، هناك مشكلة أخرى يمكن أن تحدث أثناء تعدد العمليات: المجاعة. تختلف هذه الظاهرة عن أشكال الحجب السابقة من حيث أن المواضيع غير محظورة - فهي ببساطة لا تملك الموارد الكافية. ونتيجة لذلك، في حين أن بعض المواضيع تأخذ كل وقت التنفيذ، فإن بعضها الآخر غير قادر على التشغيل: أفضل معًا: Java وفئة Thread.  الجزء الثالث — التفاعل - 5

https://www.logicbig.com/

يمكنك مشاهدة مثال رائع هنا: Java - Thread Starvation and Fairness . يوضح هذا المثال ما يحدث مع الخيوط أثناء المجاعة وكيف يتيح لك التغيير البسيط من Thread.sleep()إلى Thread.wait()توزيع الحمل بالتساوي. أفضل معًا: Java وفئة Thread.  الجزء الثالث — التفاعل - 6

شروط السباق

في تعدد مؤشرات الترابط، هناك شيء مثل "حالة السباق". تحدث هذه الظاهرة عندما تشارك سلاسل الرسائل موردًا ما، ولكن يتم كتابة الكود بطريقة لا تضمن المشاركة الصحيحة. ألق نظرة على مثال:
public class App {
    public static int value = 0;

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                int oldValue = value;
                int newValue = ++value;
                if (oldValue + 1 != newValue) {
                    throw new IllegalStateException(oldValue + " + 1 = " + newValue);
                }
            }
        };
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
    }
}
قد لا يؤدي هذا الرمز إلى إنشاء خطأ في المرة الأولى. عندما يحدث ذلك، قد يبدو مثل هذا:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
	at App.lambda$main$0(App.java:13)
	at java.lang.Thread.run(Thread.java:745)
كما ترون، حدث خطأ ما أثناء newValueتعيين قيمة. newValueهو كبير جدا. بسبب حالة السباق، تمكن أحد المواضيع من تغيير المتغير valueبين العبارتين. اتضح أن هناك سباق بين المواضيع. فكر الآن في مدى أهمية عدم ارتكاب أخطاء مماثلة في المعاملات النقدية... يمكن أيضًا رؤية الأمثلة والرسوم البيانية هنا: رمز لمحاكاة حالة السباق في موضوع Java .

متقلب

عند الحديث عن تفاعل المواضيع، فإن volatileالكلمة الرئيسية تستحق الذكر. دعونا نلقي نظرة على مثال بسيط:
public class App {
    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Runnable whileFlagFalse = () -> {
            while(!flag) {
            }
            System.out.println("Flag is now TRUE");
        };

        new Thread(whileFlagFalse).start();
        Thread.sleep(1000);
        flag = true;
    }
}
والأمر الأكثر إثارة للاهتمام هو أن هذا من المحتمل جدًا ألا ينجح. لن يرى الموضوع الجديد التغيير في flagالحقل. لإصلاح هذا الحقل flag، نحتاج إلى استخدام volatileالكلمة الأساسية. كيف ولماذا؟ يقوم المعالج بتنفيذ جميع الإجراءات. ولكن يجب تخزين نتائج الحسابات في مكان ما. لهذا، هناك الذاكرة الرئيسية وهناك ذاكرة التخزين المؤقت للمعالج. تشبه ذاكرة التخزين المؤقت للمعالج قطعة صغيرة من الذاكرة تُستخدم للوصول إلى البيانات بسرعة أكبر من الوصول إلى الذاكرة الرئيسية. ولكن لكل شيء جانب سلبي: قد لا تكون البيانات الموجودة في ذاكرة التخزين المؤقت محدثة (كما في المثال أعلاه، عندما لم يتم تحديث قيمة حقل العلامة). لذلك، الكلمة volatileالأساسية تخبر JVM أننا لا نريد تخزين المتغير الخاص بنا مؤقتًا. وهذا يسمح برؤية النتيجة المحدثة على كافة المواضيع. وهذا تفسير مبسط للغاية. أما بالنسبة للكلمة volatileالرئيسية، أوصي بشدة بقراءة هذا المقال . لمزيد من المعلومات، أنصحك أيضًا بقراءة Java Memory Model و Java Volatile Keyword . بالإضافة إلى ذلك، من المهم أن نتذكر أن الأمر volatileيتعلق بالرؤية وليس بذرية التغييرات. بالنظر إلى الكود في قسم "ظروف السباق"، سنرى تلميح أداة في IntelliJ IDEA: أفضل معًا: Java وفئة Thread.  الجزء الثالث — التفاعل - 7تمت إضافة هذا الفحص إلى IntelliJ IDEA كجزء من الإصدار IDEA-61117 ، والذي تم إدراجه في ملاحظات الإصدار في عام 2010.

الذرية

العمليات الذرية هي عمليات لا يمكن تقسيمها. على سبيل المثال، يجب أن تكون عملية إسناد قيمة إلى متغير ذرية. لسوء الحظ، عملية الزيادة ليست ذرية، لأن الزيادة تتطلب ما يصل إلى ثلاث عمليات لوحدة المعالجة المركزية: احصل على القيمة القديمة، وأضف واحدة إليها، ثم احفظ القيمة. لماذا تعتبر الذرية مهمة؟ مع عملية الزيادة، إذا كانت هناك حالة سباق، فقد يتغير المورد المشترك (أي القيمة المشتركة) فجأة في أي وقت. بالإضافة إلى ذلك، العمليات التي تتضمن بنيات 64 بت، على سبيل المثال longو double، ليست ذرية. يمكن قراءة المزيد من التفاصيل هنا: التأكد من الذرية عند قراءة وكتابة قيم 64 بت . يمكن رؤية المشاكل المتعلقة بالذرية في هذا المثال:
public class App {
    public static int value = 0;
    public static AtomicInteger atomic = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                value++;
                atomic.incrementAndGet();
            }
        };
        for (int i = 0; i < 3; i++) {
            new Thread(task).start();
        }
        Thread.sleep(300);
        System.out.println(value);
        System.out.println(atomic.get());
    }
}
AtomicIntegerستمنحنا الفئة الخاصة دائمًا 30 ألفًا، لكن هذا المبلغ valueسيتغير من وقت لآخر. هناك نظرة عامة قصيرة على هذا الموضوع: مقدمة للمتغيرات الذرية في جافا . تقع خوارزمية "المقارنة والمبادلة" في قلب الطبقات الذرية. يمكنك قراءة المزيد عنها هنا في مقارنة الخوارزميات الخالية من القفل - CAS وFAA في مثال JDK 7 و8 أو في مقالة المقارنة والمبادلة على ويكيبيديا. أفضل معًا: Java وفئة Thread.  الجزء الثالث — التفاعل - 9

http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-Java.html

يحدث-قبل

هناك مفهوم مثير للاهتمام وغامض يسمى "يحدث من قبل". كجزء من دراستك للخيوط، يجب أن تقرأ عنها. تُظهر العلاقة "يحدث قبل" الترتيب الذي سيتم به رؤية الإجراءات بين سلاسل الرسائل. هناك العديد من التفسيرات والتعليقات. فيما يلي أحد العروض التقديمية الأحدث حول هذا الموضوع: علاقات Java "يحدث من قبل" .

ملخص

في هذه المراجعة، استكشفنا بعض تفاصيل كيفية تفاعل سلاسل الرسائل. ناقشنا المشاكل التي قد تنشأ، وكذلك طرق التعرف عليها والقضاء عليها. قائمة المواد الإضافية حول الموضوع: أفضل معًا: Java وفئة Thread. الجزء الأول - خيوط التنفيذ أفضل معًا: Java وفئة Thread. الجزء الثاني – التزامن بشكل أفضل معًا: Java وفئة Thread. الجزء الرابع - الأشخاص القابلون للاستدعاء والمستقبل والأصدقاء أفضل معًا: Java وفئة Thread. الجزء الخامس — Executor، ThreadPool، Fork/Join Better Together: Java وفئة Thread. الجزء السادس – أطلق النار بعيدًا!
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION