CodeGym /وبلاگ جاوا /Random-FA /بهتر با هم: جاوا و کلاس Thread. بخش سوم - تعامل
John Squirrels
مرحله
San Francisco

بهتر با هم: جاوا و کلاس Thread. بخش سوم - تعامل

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

معرفی

بنابراین، ما می دانیم که جاوا دارای موضوعات است. شما می توانید در مورد آن در بررسی با عنوان Better together: Java and the Thread کلاس بخوانید. بخش اول - موضوعات اجرا . و ما این واقعیت را بررسی کردیم که رشته ها می توانند با یکدیگر همگام شوند در بررسی با عنوان Better together: Java and the Thread کلاس. بخش دوم - همگام سازی وقت آن است که در مورد نحوه تعامل رشته ها با یکدیگر صحبت کنیم. چگونه آنها منابع مشترک را به اشتراک می گذارند؟ چه مشکلاتی ممکن است در اینجا ایجاد شود؟ بهتر با هم: جاوا و کلاس Thread.  بخش سوم - تعامل - 2

بن بست

ترسناک ترین مشکل، بن بست است. بن بست زمانی است که دو یا چند رشته برای همیشه در انتظار دیگری هستند. مثالی از صفحه وب اوراکل می گیریم که بن بست را توضیح می دهد :
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: بهتر با هم: جاوا و کلاس Thread.  بخش سوم - تعامل - 3با نصب یک پلاگین JVisualVM (از طریق Tools -> Plugins)، می توانیم ببینیم که بن بست کجا رخ داده است:
"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 دیگر (در این مورد Thread-0) موفق شد وارد bow()روش شود، قفل قبلاً بدست آمده است و Thread-1منتظر است Thread-0و بالعکس. این بن بست حل نشدنی است و ما آن را بن بست می نامیم. مانند یک چنگال مرگ که نمی توان آن را رها کرد، بن بست نیز مانعی متقابل است که نمی توان آن را شکست. برای توضیح دیگری درباره بن بست، می توانید این ویدیو را مشاهده کنید: Deadlock و Livelock Explained .

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ابتدا شروع شود، پس ما زنده می شویم:
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
...
همانطور که از مثال می بینید، هر دو رشته به نوبه خود سعی می کنند هر دو قفل را بدست آورند، اما شکست می خورند. اما، آنها در بن بست نیستند. از نظر ظاهری همه چیز خوب است و آنها کار خود را انجام می دهند. طبق JVisualVM، ما دوره‌های خواب و یک دوره پارک را می‌بینیم (این زمانی است که یک نخ سعی می‌کند یک قفل به دست آورد - همانطور که قبلاً در مورد همگام‌سازی نخ بهتر با هم: جاوا و کلاس Thread.  بخش سوم - تعامل - 4 صحبت کردیم، وارد حالت پارک می‌شود) . شما می توانید یک مثال از livelock را در اینجا ببینید: Java - Thread Livelock .

گرسنگی

علاوه بر بن بست و زنده بودن، مشکل دیگری نیز وجود دارد که می تواند در طول چند رشته ای اتفاق بیفتد: گرسنگی. این پدیده با اشکال قبلی مسدود کردن تفاوت دارد زیرا رشته ها مسدود نمی شوند - آنها به سادگی منابع کافی ندارند. در نتیجه، در حالی که برخی از رشته ها تمام زمان اجرا را می گیرند، برخی دیگر قادر به اجرا نیستند: بهتر با هم: جاوا و کلاس Thread.  بخش سوم - تعامل - 5

https://www.logicbig.com/

می توانید یک مثال فوق العاده را در اینجا ببینید: جاوا - Thread Starvation and Fairness . این مثال نشان می دهد که در هنگام گرسنگی چه اتفاقی برای نخ ها می افتد و چگونه یک تغییر کوچک از Thread.sleep()به Thread.wait()به شما امکان می دهد بار را به طور مساوی توزیع کنید. بهتر با هم: جاوا و کلاس Thread.  بخش سوم - تعامل - 6

شرایط مسابقه

در multithreading چیزی به نام "شرط مسابقه" وجود دارد. این پدیده زمانی اتفاق می‌افتد که رشته‌ها یک منبع را به اشتراک می‌گذارند، اما کد به گونه‌ای نوشته می‌شود که از اشتراک‌گذاری صحیح اطمینان حاصل نمی‌کند. به یک مثال نگاه کنید:
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 بخوانید . برای اطلاعات بیشتر، به شما توصیه می کنم مدل حافظه جاوا و کلیدواژه فرار جاوا را نیز مطالعه کنید . علاوه بر این، مهم است که به یاد داشته باشید که در مورد دید است، و نه در مورد اتمی بودن تغییرات. با نگاهی به کد موجود در بخش "شرایط مسابقه"، یک راهنمای ابزار را در IntelliJ IDEA مشاهده خواهیم کرد: این بازرسی به عنوان بخشی از شماره IDEA-61117 به IntelliJ IDEA اضافه شد که در سال 2010 در یادداشت های انتشار فهرست شده بود. volatileبهتر با هم: جاوا و کلاس Thread.  بخش سوم - تعامل - 7

اتمی

عملیات اتمی عملیاتی است که قابل تقسیم نیست. به عنوان مثال، عملیات اختصاص دادن یک مقدار به یک متغیر باید اتمی باشد. متأسفانه، عملیات افزایشی اتمی نیست، زیرا افزایش به سه عملیات CPU نیاز دارد: مقدار قدیمی را دریافت کنید، یکی به آن اضافه کنید، سپس مقدار را ذخیره کنید. چرا اتمی مهم است؟ با عملیات افزایش، اگر شرایط مسابقه وجود داشته باشد، منبع مشترک (یعنی مقدار مشترک) ممکن است در هر زمانی ناگهان تغییر کند. علاوه بر این، عملیاتی که شامل ساختارهای 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همیشه 30000 به ما می دهد، اما valueهر از گاهی تغییر می کند. مروری کوتاه بر این موضوع وجود دارد: مقدمه ای بر متغیرهای اتمی در جاوا . الگوریتم "مقایسه و مبادله" در قلب کلاس های اتمی قرار دارد. می‌توانید در اینجا در مقایسه الگوریتم‌های بدون قفل - CAS و FAA در مثال JDK 7 و 8 یا در مقاله مقایسه و تعویض در ویکی‌پدیا، درباره آن بیشتر بخوانید. بهتر با هم: جاوا و کلاس Thread.  بخش سوم - تعامل - 9

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

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

یک مفهوم جالب و مرموز به نام "پیش از این اتفاق می افتد" وجود دارد. به عنوان بخشی از مطالعه خود در مورد موضوعات، باید در مورد آن مطالعه کنید. رابطه اتفاق می افتد-پیش از ترتیبی که اعمال بین رشته ها دیده می شود را نشان می دهد. تفاسیر و تفاسیر فراوان است. در اینجا یکی از جدیدترین ارائه ها در مورد این موضوع است: Java "Happens-Before" Relationships .

خلاصه

در این بررسی، برخی از ویژگی‌های نحوه تعامل رشته‌ها را بررسی کرده‌ایم. ما مشکلاتی را که ممکن است پیش بیاید و همچنین راه های شناسایی و حذف آنها را مورد بحث قرار دادیم. فهرست مطالب اضافی در مورد موضوع: بهتر با هم: جاوا و کلاس Thread. قسمت اول - موضوعات اجرا بهتر با هم: جاوا و کلاس Thread. بخش دوم - همگام سازی بهتر با هم: کلاس جاوا و Thread. بخش چهارم - Callable، Future، و دوستان بهتر با هم: Java and the Thread کلاس. قسمت پنجم - Executor، ThreadPool، Fork/Join Better together: Java و کلاس Thread. قسمت ششم - آتش دور شوید!
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION