Как написать семафор в Java, который определяет приоритеты предыдущих успешных претендентов?

У меня есть необходимость в одном разрешении семафорного объекта в моей программе Java, где есть дополнительный метод получения, который выглядит следующим образом:

boolean tryAcquire(int id)

и ведет себя следующим образом: если идентификатор ранее не встречался, помните об этом, а затем выполняйте все java.util.concurrent.Semaphore. Если идентификатор был встречен раньше и эта встреча привела к сдаче в аренду разрешения, то придать этому потоку приоритет над всеми другими потоками, которые могут ждать разрешения. Я также хочу использовать дополнительный метод выпуска, например:

void release(int id)

который делает то, что делает java.util.concurrent.Semaphore, а также "забывает" об идентификаторе.

Я действительно не знаю, как подойти к этому, но вот начало возможной реализации, но я боюсь, что это никуда не будет:

public final class SemaphoreWithMemory {

    private final Semaphore semaphore = new Semaphore(1, true);
    private final Set<Integer> favoured = new ConcurrentSkipListSet<Integer>();

    public boolean tryAcquire() {
        return semaphore.tryAcquire();
    }

    public synchronized boolean tryAcquire(int id) {
        if (!favoured.contains(id)) {
            boolean gotIt = tryAcquire();
            if (gotIt) {
                favoured.add(id);
                return true;
            }
            else {
                return false;
            }
        }
        else {
            // what do I do here???
        }
    }

    public void release() {
        semaphore.release();
    }

    public synchronized void release(int id) {
        favoured.remove(id);
        semaphore.release();
    }

}

Ответ 1

EDIT:
Сделал какой-то эксперимент. Для получения результатов см. этот ответ.

В принципе, у Семафор есть очередь потоков внутри, так что, как говорит Андрей, если вы сделаете эту очередь очередью приоритетов и опросом из этой очереди, чтобы выдавать разрешения, она, вероятно, ведет себя так, как вы хотите. Обратите внимание, что вы не можете сделать это с помощью tryAcquire, потому что таким образом потоки не помещаются в очередь. Из того, что я вижу, вы должны взломать класс AbstractQueuedSynchronizer, чтобы сделать это.

Я мог бы также подумать о вероятностном подходе, например:
(Я не говорю, что код ниже был бы хорошей идеей! Просто мозговой штурм здесь.)

public class SemaphoreWithMemory {

    private final Semaphore semaphore = new Semaphore(1);
    private final Set<Integer> favoured = new ConcurrentSkipListSet<Integer>();
    private final ThreadLocal<Random> rng = //some good rng

    public boolean tryAcquire() {
        for(int i=0; i<8; i++){
            Thread.yield();
            // Tend to waste more time than tryAcquire(int id)
            // would waste.
            if(rng.get().nextDouble() < 0.3){
                return semaphore.tryAcquire();
            }
        }
        return semaphore.tryAcquire();
    }

    public boolean tryAcquire(int id) {
        if (!favoured.contains(id)) {
            boolean gotIt = semaphore.tryAcquire();
            if (gotIt) {
                favoured.add(id);
                return true;
            } else {
                return false;
            }
        } else {
            return tryAquire();
    }
}

Или у вас есть "любимые" потоки, которые немного больше похожи на это:
EDIT:. Оказалось, что это была очень плохая идея (как с честным, так и с неясным семафором) (подробнее см. мой .

    public boolean tryAcquire(int id) {
        if (!favoured.contains(id)) {
            boolean gotIt = semaphore.tryAcquire(5,TimeUnit.MILLISECONDS);
            if (gotIt) {
                favoured.add(id);
                return true;
            } else {
                return false;
            }
        } else {
            return tryAquire();
    }

Я предполагаю, что таким образом вы можете отклонить способ выдачи разрешений, в то время как это будет нечестно. Хотя с помощью этого кода вы, вероятно, будете тратить много времени на производительность...

Ответ 2

Для блокировки модели сбора данных, как насчет этого:

public class SemWithPreferred {
    int max;
    int avail;
    int preferredThreads;

    public SemWithPreferred(int max, int avail) {
        this.max = max;
        this.avail = avail;
    }

    synchronized public void get(int id) throws InterruptedException {
        boolean thisThreadIsPreferred = idHasBeenServedSuccessfullyBefore(id);
        if (thisThreadIsPreferred) {
            preferredThreads++;
        }
        while (! (avail > 0 && (preferredThreads == 0 || thisThreadIsPreferred))) {
            wait();
        }
        System.out.println(String.format("granted, id = %d, preferredThreads = %d", id, preferredThreads));
        avail -= 1;
        if (thisThreadIsPreferred) {
            preferredThreads--;
            notifyAll(); // removal of preferred thread could affect other threads' wait predicate
        }
    }

    synchronized public void put() {
        if (avail < max) {
            avail += 1;
            notifyAll();
        }
    }

    boolean idHasBeenServedSuccessfullyBefore(int id) {
        // stubbed out, this just treats any id that is a 
        // multiple of 5 as having been served successfully before 
        return id % 5 == 0;
    }
}

Ответ 3

Предполагая, что вы хотите, чтобы потоки подождали, я взломал решение, которое не идеально, но должно сделать.

Идея состоит в том, чтобы иметь два семафора и флаг "любимый".

Каждый поток, который пытается приобрести SemaphoreWithMemory, сначала пытается получить "favouredSemaphore". Поток "благоприятствования" удерживает Семафор и немедленные релизы немедленно. Таким образом, предпочтительная нить блокирует все другие входящие потоки, как только он приобрел этот Семафор.

Затем нужно получить второе "normalSemaphore" для завершения. Но неподдерживаемый поток затем снова проверяет, что нет ожидаемого потока, ожидающего использования изменчивой переменной). Если никто не ждет, он просто продолжает; если кто-то ждет, он освобождает normalSemaphore, а рекурсивные вызовы снова получают.

Я не уверен, что никаких условий гонки не скрывается. Если вы хотите быть уверенным, вы, возможно, должны переформатировать свой код в руки "рабочих элементов" в очередь приоритетов, где другой поток принимает рабочий элемент с наивысшим приоритетом и выполняет этот код.

public final class SemaphoreWithMemory {

private volatile boolean favouredAquired = false;
private final Semaphore favouredSemaphore = new Semaphore(1, true);
private final Semaphore normalSemaphore = new Semaphore(1, true);
private final Set<Integer> favoured = new ConcurrentSkipListSet<Integer>();

public void acquire() throws InterruptedException {
    normalSemaphore.acquire();
}

public void acquire(int id) throws InterruptedException {
    boolean idIsFavoured = favoured.contains(id);
    favouredSemaphore.acquire();
    if (!idIsFavoured) {
        favouredSemaphore.release();
    } else {
        favouredAquired = true;
    }
    normalSemaphore.acquire();

    // check again that there is no favoured thread waiting
    if (!idIsFavoured) {
        if (favouredAquired) {
            normalSemaphore.release();
            acquire(); // starving probability!
        } else {
            favoured.add(id);
        }
    }

}

public void release() {
    normalSemaphore.release();
    if (favouredAquired) {
        favouredAquired = false;
        favouredSemaphore.release();
    }
}

public void release(int id) {
    favoured.remove(id);
    release();
}

}

Ответ 4

Я прочитал эту статью Ceki и был заинтересован в том, как может быть предвзятое получение семафора (так как я чувствовал, что поведение "смещенной блокировки" иметь смысл и в семафорах..). На моем оборудовании с двумя процессорами и Sun JVM 1.6 это фактически приводит к довольно равномерной аренде.

В любом случае, я также пытался "уклониться" от аренды семафора со стратегией, которую я написал в своем другом ответе. Оказывается, простое дополнение yield само по себе приводит к значительному смещению. Ваша проблема сложнее, но, возможно, вы можете сделать аналогичные тесты с вашей идеей и посмотреть, что вы получаете:)

ПРИМЕЧАНИЕ Код ниже основан на коде Ceki здесь

код:

import java.util.concurrent.*;

public class BiasedSemaphore implements Runnable {
    static ThreadLocal<Boolean> favored = new ThreadLocal<Boolean>(){
        private boolean gaveOut = false;
        public synchronized Boolean initialValue(){
            if(!gaveOut){
                System.out.println("Favored " + Thread.currentThread().getName());
                gaveOut = true;
                return true;
            }
            return false;
        }
    };

    static int THREAD_COUNT = Runtime.getRuntime().availableProcessors();
    static Semaphore SEM = new Semaphore(1);
    static Runnable[] RUNNABLE_ARRAY = new Runnable[THREAD_COUNT];
    static Thread[] THREAD_ARRAY = new Thread[THREAD_COUNT];

    private int counter = 0;

    public static void main(String args[]) throws InterruptedException {
        printEnvironmentInfo();
        execute();
        printResults();
    }

    public static void printEnvironmentInfo() {
        System.out.println("java.runtime.version = "
                + System.getProperty("java.runtime.version"));
        System.out.println("java.vendor          = "
                + System.getProperty("java.vendor"));
        System.out.println("java.version         = "
                + System.getProperty("java.version"));
        System.out.println("os.name              = "
                + System.getProperty("os.name"));
        System.out.println("os.version           = "
                + System.getProperty("os.version"));
    }

    public static void execute() throws InterruptedException {
        for (int i = 0; i < THREAD_COUNT; i++) {
            RUNNABLE_ARRAY[i] = new BiasedSemaphore();
            THREAD_ARRAY[i] = new Thread(RUNNABLE_ARRAY[i]);
            System.out.println("Runnable at "+i + " operated with "+THREAD_ARRAY[i]);
        }

        for (Thread t : THREAD_ARRAY) {
            t.start();
        }
        // let the threads run for a while
        Thread.sleep(10000);

        for (int i = 0; i< THREAD_COUNT; i++) {
            THREAD_ARRAY[i].interrupt();
        }

        for (Thread t : THREAD_ARRAY) {
            t.join();
        }
    }

    public static void printResults() {
        System.out.println("Ran with " + THREAD_COUNT + " threads");
        for (int i = 0; i < RUNNABLE_ARRAY.length; i++) {
            System.out.println("runnable[" + i + "]: " + RUNNABLE_ARRAY[i]);
        }
    }


    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            if (favored.get()) {
                stuff();
            } else {
                Thread.yield();
//                try {
//                    Thread.sleep(1);
//                } catch (InterruptedException e) {
//                    Thread.currentThread().interrupt();
//                }
                stuff();
            }
        }
    }

    private void stuff() {
        if (SEM.tryAcquire()) {
            //favored.set(true);
            counter++;
            try {
                Thread.sleep(10);
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
            SEM.release();
        } else {
            //favored.set(false);
        }
    }

    public String toString() {
        return "counter=" + counter;
    }
}

Результаты:

java.runtime.version = 1.6.0_21-b07
java.vendor          = Sun Microsystems Inc.
java.version         = 1.6.0_21
os.name              = Windows Vista
os.version           = 6.0
Runnable at 0 operated with Thread[Thread-0,5,main]
Runnable at 1 operated with Thread[Thread-1,5,main]
Favored Thread-0
Ran with 2 threads
runnable[0]: counter=503
runnable[1]: counter=425

Пробовал 30 секунд вместо 10:

java.runtime.version = 1.6.0_21-b07
java.vendor          = Sun Microsystems Inc.
java.version         = 1.6.0_21
os.name              = Windows Vista
os.version           = 6.0
Runnable at 0 operated with Thread[Thread-0,5,main]
Runnable at 1 operated with Thread[Thread-1,5,main]
Favored Thread-1
Ran with 2 threads
runnable[0]: counter=1274
runnable[1]: counter=1496

P.S.: Похоже, что "болтаться" было очень плохой идеей. Когда я пробовал называть SEM.tryAcquire(1,TimeUnit.MILLISECONDS); для избранных потоков и SEM.tryAcquire() для непривилегированных потоков, непривилегированные потоки получили разрешение почти в 5 раз больше, чем одобренный поток!

Кроме того, я хотел бы добавить, что эти результаты измеряются только в одной конкретной ситуации, поэтому неясно, как эти меры ведут себя в других ситуациях.

Ответ 5

Мне кажется, что самый простой способ сделать это - не пытаться объединить Семафоры, а строить его с нуля поверх мониторов. Обычно это рискованно, но в этом случае, поскольку в java.util.concurrent нет хороших строительных блоков, это самый ясный способ сделать это.

Вот что я придумал:

public class SemaphoreWithMemory {

    private final Set<Integer> favouredIDs = new HashSet<Integer>();
    private final Object favouredLock = new Object();
    private final Object ordinaryLock = new Object();
    private boolean available = true;
    private int favouredWaiting = 0;

    /**
    Acquires the permit. Blocks until the permit is acquired.
    */
    public void acquire(int id) throws InterruptedException {
        Object lock;
        boolean favoured = false;

        synchronized (this) {
            // fast exit for uncontended lock
            if (available) {
                doAcquire(favoured, id);
                return;
            }
            favoured = favouredIDs.contains(id);
            if (favoured) {
                lock = favouredLock;
                ++favouredWaiting;
            }
            else {
                lock = ordinaryLock;
            }
        }

        while (true) {
            synchronized (this) {
                if (available) {
                    doAcquire(favoured, id);
                    return;
                }
            }
            synchronized (lock) {
                lock.wait();
            }
        }
    }

    private void doAcquire(boolean favoured, int id) {
        available = false;
        if (favoured) --favouredWaiting;
        else favouredIDs.add(id);
    }

    /**
    Releases the permit.
    */
    public synchronized void release() {
        available = true;
        Object lock = (favouredWaiting > 0) ? favouredLock : ordinaryLock;
        synchronized (lock) {
            lock.notify();
        }
    }

}