Как на самом деле работает Java-несправедливый ReentrantReadWriteLock?

#java #multithreading

#java #многопоточность

Вопрос:

Я написал следующий тестовый код на Java с использованием ReentrantReadWriteLock, чтобы понять разницу между справедливым и несправедливым режимом. Однако я вижу, что в обоих режимах результат и вывод всегда одинаковы. Кажется, он всегда работает в честном режиме. Кто-нибудь может объяснить, в каком случае справедливый и несправедливый режимы приведут к различному поведению?

 package lockTest;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MyLockTest {
    static private ReadWriteLock myLock = new ReentrantReadWriteLock(false);

    public class Reader extends Thread {
        int val_;
        public Reader(int val) {
            val_ = val;
        }
        public void run() {
            if (val_ > 0) {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }

            myLock.readLock().lock();
            System.out.println(Thread.currentThread().getName()   ": Reader inside critical section - val: "   val_   "-----");
            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            myLock.readLock().unlock();
        }
    }

    public class Writer extends Thread {
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            myLock.writeLock().lock();
            System.out.println(Thread.currentThread().getName()   ": Writer inside critical section *****");
            try {
                Thread.sleep(6000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            myLock.writeLock().unlock();
        }
    }

    public static void main(String[] args) {
        MyLockTest test1 = new MyLockTest();
        MyLockTest.Reader reader1 = test1.new Reader(0);
        MyLockTest.Writer writer1 = test1.new Writer();
        MyLockTest.Reader reader2 = test1.new Reader(1);

        reader2.start();
        writer1.start();
        reader1.start();
    }
}
 

Вывод всегда:

 Thread-0: Reader inside critical section - val: 0-----
Thread-1: Writer inside critical section *****
Thread-2: Reader inside critical section - val: 1-----
 

Приведенный выше результат — это то, что я ожидаю увидеть, когда я переключу создание блокировки в режим fair:

 static private ReadWriteLock myLock = new ReentrantReadWriteLock(true);
 

Для несправедливого режима я ожидал бы увидеть следующий вывод:

 Thread-0: Reader inside critical section - val: 0-----
Thread-2: Reader inside critical section - val: 1-----
Thread-1: Writer inside critical section *****
 

Комментарии:

1. Я не уверен в каких-либо деталях, но я думаю, что это будет сильно зависеть от базовой платформы. Поэтому, если вам требуется «справедливое» поведение, укажите его; не полагайтесь на результаты тестирования в одной конкретной среде.

2. Неясно, что вы подразумеваете под «действительно работой» здесь. Вы спрашиваете о поведенческих различиях или деталях реализации?

3. «несправедливого» режима не существует. Два режима — «честный» и «по умолчанию». API библиотеки требует, чтобы режим fair вел себя определенным образом, но API не требует, чтобы режим по умолчанию вел себя каким-либо особым образом. В частности, он не требует, чтобы режим по умолчанию отличался от режима fair.

4. @SolomonSlow: Я не уверен, что вы подразумеваете под «нечестным» режимом». Документация для ReentrantReadWriteLock ( docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks /… ) явно имеет заголовок в разделе «Заказ на приобретение» с надписью «Несправедливый режим (по умолчанию)», который начинается с «При построении как несправедливый …» Несправедливость — это действительно распространенный термин во взаимном исключении, который означает неопределенный порядок получения.

5. Наверное, я плохо это сказал. Да, в документации используется фраза «несправедливо», но в документации нет ничего, что требовало бы, чтобы «несправедливый» режим не был справедливым. Реализация, которая просто игнорирует аргумент fair конструктора и всегда создает справедливую блокировку, была бы совершенно законной.

Ответ №1:

Использование режима «справедливый» или «несправедливый» влияет на то, как блокировка назначается потокам в случае конфликта.

Из Javadoc для ReentrantReadWriteLock: при использовании «несправедливого» режима порядок входа в блокировку чтения и записи не указан, при использовании «справедливого» режима потоки конкурируют за вход, используя приблизительно политику порядка поступления.


Мы можем видеть, как использование fair / non-fair влияет на выполнение программы, поскольку некоторые потоки конкурируют за одну и ту же блокировку; смотрите Программу ниже.

Запустив пример кода, a ReentrantWriteLock оспаривается разными потоками; после 1000 операций блокировки мы сбрасываем, сколько раз каждый поток получал блокировку.

В случае USE_FAIR=false , если используется, подсчеты являются случайными, и возможный результат:

 Thread thread-B finished, count=920
Thread thread-A finished, count=79
Thread thread-D finished, count=0
Thread thread-C finished, count=0
 

в случае USE_FAIR=true , если используется, вывод всегда имеет вид

 Thread thread-D finished, count=249
Thread thread-A finished, count=250
Thread thread-C finished, count=250
Thread thread-B finished, count=250
 

Пример кода

 package sample1;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class UseLock {

  public static void main(String[] args) {
    UseLock o = new UseLock();
    o.go();
  }

  private void go() {
    TotalPermits tp = new TotalPermits();

    tp.lock.writeLock().lock();

    Contender a = new Contender(tp, "thread-A");
    Contender b = new Contender(tp, "thread-B");
    Contender c = new Contender(tp, "thread-C");
    Contender d = new Contender(tp, "thread-D");

    a.start();
    b.start();
    c.start();
    d.start();

    tp.lock.writeLock().unlock();
  }
}

class TotalPermits {
  private static final boolean USE_FAIR = true;

  private int count = 1_000;
  ReentrantReadWriteLock lock = new ReentrantReadWriteLock(USE_FAIR);

  public boolean get() {
    try {
      lock.writeLock().lock();
      try {
        Thread.sleep(1);
      } catch (InterruptedException e) { }
      return --count>0;
    } finally {
      lock.writeLock().unlock();
    }
  }
}

class Contender extends Thread {
  private int count = 0;
  final String name;
  final TotalPermits tp;

  Contender(TotalPermits tp, String name) {
    this.tp = tp;
    this.name = name;
  }

  @Override
  public void run() {
    while ( tp.get() ) {
      count  ;
    }
    System.out.printf("Thread %s finished, count=%d%n", name, count);
  }
}
 

Примечание:

В приведенном выше примере кода используется блокировка «запись», которая может удерживаться только одним потоком одновременно. Таким образом, мы можем использовать это для разделения N разрешений между претендентами. С другой стороны, блокировка «чтения» может удерживаться несколькими потоками, если ни один из них не удерживает блокировку записи.

Комментарии:

1. Спасибо. Ваш ответ правильный. Что-то, что нужно добавить, это то, что я ориентировал вопрос на справедливость между блокировками записи и чтения, опасаясь, что автор будет голодать сотнями читателей. Однако концепция справедливости шире и относится к любому типу конфликтов писатель-писатель, писатель-читатель, читатель-читатель.