JavaRush /وبلاگ جاوا /Random-FA /شما نمی توانید جاوا را با یک موضوع خراب کنید: قسمت سوم - ...
Viacheslav
مرحله

شما نمی توانید جاوا را با یک موضوع خراب کنید: قسمت سوم - تعامل

در گروه منتشر شد
مروری کوتاه بر ویژگی های تعامل نخ. قبلاً به نحوه همگام سازی نخ ها با یکدیگر نگاه کردیم. این بار به مشکلاتی می پردازیم که ممکن است هنگام تعامل نخ ها ایجاد شوند و در مورد اینکه چگونه می توان از آنها اجتناب کرد صحبت خواهیم کرد. همچنین چند لینک مفید برای مطالعه عمیق تر ارائه خواهیم داد. شما نمی توانید جاوا را با یک موضوع خراب کنید: قسمت سوم - تعامل - 1

معرفی

بنابراین، ما می دانیم که موضوعاتی در جاوا وجود دارد که می توانید در بررسی " Thread Can't Spoil Java: Part I - Threads " در مورد آنها بخوانید و این موضوعات را می توان با یکدیگر همگام کرد که در بررسی به آن پرداختیم. Thread Can't Spoil Java " Spoil: Part II - Synchronization ." وقت آن است که در مورد نحوه تعامل نخ ها با یکدیگر صحبت کنیم. چگونه آنها منابع مشترک را به اشتراک می گذارند؟ این چه مشکلاتی می تواند داشته باشد؟

بن بست

بدترین مشکل بن بست است. هنگامی که دو یا چند رشته برای همیشه در انتظار یکدیگر می مانند، به این حالت 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 (از طریق Tools -> Plugins) نصب شده باشد، می توانیم ببینیم که بن بست کجا رخ داده است:
"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و بالعکس. انسداد غیر قابل حل است، پس مرده است، یعنی مرده است. هم یک چنگال مرگ (که نمی توان آن را رها کرد) و هم یک بلوک مرده که نمی توان از آن فرار کرد. در مورد بن بست، می توانید ویدیو را تماشا کنید: " بن بست - همزمانی شماره 1 - جاوا پیشرفته ".

Livelock

اگر یک بن بست وجود دارد، پس آیا یک 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();
    }
}
موفقیت این کد بستگی به ترتیبی دارد که زمانبندی رشته جاوا رشته ها را شروع می کند. اگر ابتدا شروع شود 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 ".

گرسنگی

علاوه بر مسدود کردن (بن بست و زنده)، هنگام کار با چند رشته ای مشکل دیگری وجود دارد - گرسنگی یا "گرسنگی". تفاوت این پدیده با بلاک کردن در این است که رشته ها مسدود نمی شوند، اما آنها به سادگی منابع کافی برای همه ندارند. بنابراین، در حالی که برخی از رشته ها تمام زمان اجرا را در اختیار می گیرند، برخی دیگر را نمی توان اجرا کرد: شما نمی توانید جاوا را با یک موضوع خراب کنید: قسمت سوم - تعامل - 4

https://www.logicbig.com/

یک مثال فوق العاده را می توان در اینجا یافت: " جاوا - گرسنگی و انصاف نخ ". این مثال نشان می دهد که نخ ها در Starvation چگونه کار می کنند و چگونه یک تغییر کوچک از Thread.sleep به Thread.wait می تواند بار را به طور مساوی توزیع کند. شما نمی توانید جاوا را با یک موضوع خراب کنید: قسمت سوم - تعامل - 5

شرایط مسابقه

هنگام کار با multithreading، چیزی به نام "شرایط مسابقه" وجود دارد. این پدیده در این واقعیت نهفته است که thread ها منبع خاصی را بین خود به اشتراک می گذارند و کد به گونه ای نوشته شده است که عملکرد صحیح را در این مورد ارائه نمی دهد. بیایید به یک مثال نگاه کنیم:
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بین این دو تیم تغییر کنند. همانطور که می بینیم، یک مسابقه بین رشته ها ظاهر شده است. حالا تصور کنید که چقدر مهم است که اشتباهات مشابهی در تراکنش‌های پولی مرتکب نشوید... مثال‌ها و نمودارها را نیز می‌توانید در اینجا پیدا کنید: “ کد شبیه‌سازی شرایط مسابقه در رشته جاوا ”.

فرار

در مورد تعامل نخ ها، به ویژه باید به کلمه کلیدی توجه کرد 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 Memory Model) FAQ بسیار توصیه می شود . من همچنین به شما توصیه می کنم در مورد مواد " مدل حافظه جاوا " و " کلید کلیدی فرار جاوا " بیشتر بخوانید. علاوه بر این، مهم است که به یاد داشته باشید که volatileاین در مورد دید است، و نه در مورد اتمی بودن تغییرات. اگر کد را از "شرایط مسابقه" بگیریم، در IntelliJ Idea اشاره ای خواهیم داشت: این بازرسی (بازرسی) به عنوان بخشی از شماره IDEA-61117شما نمی توانید جاوا را با یک موضوع خراب کنید: قسمت سوم - تعامل - 6 به IntelliJ Idea اضافه شد که در سال 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());
    }
}
یک کلاس ویژه برای کار با atomic Integerهمیشه 30000 را به ما نشان می دهد، اما valueهر از گاهی تغییر می کند. یک مرور کوتاه در مورد این موضوع " مقدمه ای بر متغیرهای اتمی در جاوا " وجود دارد . Atomic بر اساس الگوریتم Compare-and-Swap است. می‌توانید در مقاله Habré " مقایسه الگوریتم‌های بدون قفل - CAS و FAA با استفاده از مثال JDK 7 و 8 " یا در مقاله " مقایسه با تبادل " در ویکی‌پدیا بیشتر بخوانید. شما نمی توانید جاوا را با یک موضوع خراب کنید: قسمت سوم - تعامل - 8

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

قبلا اتفاق می افتد

یک چیز جالب و مرموز وجود دارد - قبلا اتفاق می افتد. وقتی در مورد جریان صحبت می کنیم، خواندن در مورد آن نیز ارزش دارد. رابطه Happens Before نشان‌دهنده ترتیبی است که در آن اعمال بین رشته‌ها دیده می‌شود. تفاسیر و تفاسیر بسیار است. یکی از جدیدترین گزارش ها در این زمینه این گزارش است:
احتمالاً بهتر است که این ویدیو چیزی در مورد آن نگوید. بنابراین من فقط یک لینک به ویدیو می گذارم. شما می توانید " جاوا - درک اتفاقات قبل از روابط " را بخوانید.

نتایج

در این بررسی، ویژگی‌های تعامل نخ را بررسی کردیم. ما در مورد مشکلاتی که ممکن است به وجود بیاید و راه های شناسایی و حذف آنها صحبت کردیم. فهرست مطالب اضافی در مورد موضوع: #ویاچسلاو
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION