JavaRush /مدونة جافا /Random-AR /لا يمكنك إفساد جافا بخيط: الجزء الثالث - التفاعل
Viacheslav
مستوى

لا يمكنك إفساد جافا بخيط: الجزء الثالث - التفاعل

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

مقدمة

لذلك، نحن نعلم أن هناك خيوط في Java، يمكنك أن تقرأ عنها في المراجعة " Thread Can't Spoil Java: Part I - Threads " وأنه يمكن مزامنة الخيوط مع بعضها البعض، وهو ما تناولناه في المراجعة " الموضوع لا يمكن أن يفسد Java ” Spoil: الجزء الثاني - المزامنة ." حان الوقت للحديث عن كيفية تفاعل المواضيع مع بعضها البعض. كيف يتقاسمون الموارد المشتركة؟ ما هي المشاكل التي يمكن أن تكون هناك مع هذا؟

طريق مسدود

أسوأ مشكلة هي الجمود. عندما ينتظر خيطان أو أكثر لبعضهما البعض إلى الأبد، يُسمى هذا باسم Deadlock. لنأخذ مثالاً من موقع Oracle على الويب من وصف مفهوم " Deadlock ":
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 has bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s has 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: لا يمكنك تدمير جافا بخيط: الجزء الثالث - التفاعل - 2إذا تم تثبيت مكون إضافي في JVisualVM (عبر الأدوات -> المكونات الإضافية)، فيمكننا معرفة مكان حدوث الجمود:
"Thread-1" - Thread t@12
   java.lang.Thread.State: BLOCKED
    at Deadlock$Friend.bowBack(Deadlock.java:16)
    - waiting to lock &lt33a78231> (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 - Concurrency #1 - Advanced Java ".

لايفلوك

إذا كان هناك طريق مسدود، فهل هناك 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، فسنحصل على Livelock:
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
...
كما يتبين من المثال، يحاول كلا الخيطين التقاط كلا القفلين بالتناوب، لكنهما يفشلان. علاوة على ذلك، فإنهم ليسوا في طريق مسدود، أي بصريا كل شيء على ما يرام معهم ويقومون بعملهم. لا يمكنك تدمير جافا بخيط: الجزء الثالث - التفاعل - 3وفقًا لـ JVisualVM، نرى فترات السكون وفترة الإيقاف (وهذا عندما يحاول خيط ما احتلال قفل، فإنه ينتقل إلى حالة الإيقاف، كما ناقشنا سابقًا عند الحديث عن مزامنة الخيط ). فيما يتعلق بموضوع القفل المباشر، يمكنك رؤية مثال: " Java - Thread Livelock ".

مجاعة

بالإضافة إلى الحظر (طريق مسدود وLivelock)، هناك مشكلة أخرى عند العمل مع تعدد مؤشرات الترابط - الجوع، أو "المجاعة". تختلف هذه الظاهرة عن الحظر في أن المواضيع غير محظورة، لكنها ببساطة لا تحتوي على موارد كافية للجميع. لذلك، في حين أن بعض الخيوط تتولى كل وقت التنفيذ، لا يمكن تنفيذ بعضها الآخر: لا يمكنك تدمير جافا بخيط: الجزء الثالث - التفاعل - 4

https://www.logicbig.com/

يمكن العثور على مثال رائع هنا: " Java - Thread Starvation and Fairness ". يوضح هذا المثال كيفية عمل الخيوط في Starvation وكيف يمكن لتغيير واحد صغير من Thread.sleep إلى Thread.wait توزيع الحمل بالتساوي. لا يمكنك تدمير جافا بخيط: الجزء الثالث - التفاعل - 5

حالة السباق

عند العمل مع مؤشرات الترابط المتعددة، هناك شيء مثل "حالة السباق". تكمن هذه الظاهرة في حقيقة أن الخيوط تشترك في مورد معين فيما بينها وأن الكود مكتوب بطريقة لا توفر التشغيل الصحيح في هذه الحالة. لنلقي نظرة على مثال:
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يوصى بشدة بقراءة ترجمة " الأسئلة الشائعة حول JSR 133 (نموذج ذاكرة Java) ". أنصحك أيضًا بقراءة المزيد عن المواد " Java Memory Model " و" Java Volatile Keyword ". بالإضافة إلى ذلك، من المهم أن نتذكر أن volatileالأمر يتعلق بالرؤية، وليس بذرية التغييرات. إذا أخذنا الكود من "حالة السباق"، فسنرى تلميحًا في IntelliJ Idea: لا يمكنك تدمير جافا بخيط: الجزء الثالث - التفاعل - 6تمت إضافة هذا الفحص (الفحص) إلى 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());
    }
}
Integerستظهر لنا دائمًا فئة خاصة للعمل مع الذرة 30000، لكنها valueستتغير من وقت لآخر. هناك نظرة عامة قصيرة حول هذا الموضوع " مقدمة للمتغيرات الذرية في جافا ". يعتمد Atomic على خوارزمية المقارنة والمبادلة. يمكنك قراءة المزيد عنها في مقالة حبري " مقارنة الخوارزميات الخالية من القفل - CAS وFAA باستخدام مثال JDK 7 و8 " أو على ويكيبيديا في المقالة حول " المقارنة مع التبادل ". لا يمكنك تدمير جافا بخيط: الجزء الثالث - التفاعل - 8

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

يحدث من قبل

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

نتائج

في هذه المراجعة، نظرنا إلى ميزات تفاعل الخيط. ناقشنا المشاكل التي قد تنشأ وطرق اكتشافها والقضاء عليها. قائمة المواد الإضافية حول الموضوع: # فياتشيسلاف
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION