Односторонняя синхронизация: как заблокировать один конкретный метод? - PullRequest
1 голос
/ 05 апреля 2011

Привет, коллеги, пользователи SO.

В настоящее время я пишу класс, экземпляры которого будут служить кэшем JavaBean PropertyDescriptors. Вы можете вызвать метод getPropertyDescriptor(Class clazz, String propertyName), который вернет соответствующий PropertyDescriptor. Если он не был извлечен ранее, экземпляр BeanInfo для класса получен и найден правильный дескриптор. Этот результат затем сохраняется для пары имя-класс, поэтому в следующий раз он может быть возвращен сразу же без поиска или без BeanInfo.

Первая проблема была, когда несколько вызовов для одного и того же класса перекрывались. Это было просто решено путем синхронизации по параметру clazz. Таким образом, несколько вызовов для одного и того же класса синхронизируются, но вызовы для другого класса могут продолжаться беспрепятственно. Это казалось достойным компромиссом между безопасностью потока и жизнеспособностью.

Теперь, возможно, что в какой-то момент некоторые классы, которые были подвергнуты самоанализу, возможно, потребуется выгрузить. Я не могу просто хранить ссылки на них, так как это может привести к утечке из загрузчика классов. Кроме того, класс Introspector API JavaBeans упоминает, что уничтожение загрузчика классов должно сочетаться со сбросом интроспектора: http://download.oracle.com/javase/6/docs/api/java/beans/Introspector.html

Итак, я добавил метод flushDirectory(ClassLoader cl), который удалит любой класс из кэша и очистит его от Introspector (с Introspector.flushFromCaches(Class clz)), если он был загружен этим загрузчиком классов.

Теперь у меня появилось новое беспокойство по поводу синхронизации. Никакие новые сопоставления не должны быть добавлены в кэш, пока идет эта очистка, тогда как очистка не должна начинаться, если доступ все еще продолжается. Другими словами, основная проблема:

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

Сначала я попробовал комбинацию java.util.concurrent.Lock и AtomicInteger, чтобы вести подсчет количества выполняемых вызовов, но заметил, что блокировка может быть получена только без проверки, если она в данный момент используется без блокировки. Теперь я использую простую синхронизацию Object по атомарному целому числу. Вот урезанная версия моего класса:

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

public class DescriptorDirectory {

    private final ClassPropertyDirectory classPropertyDirectory = new ClassPropertyDirectory();
    private final Object flushingLock = new Object();
    private final AtomicInteger accessors = new AtomicInteger(0);

    public DescriptorDirectory() {}

    public PropertyDescriptor getPropertyDescriptor(final Class<?> clazz, final String propertyName) throws Exception {

        //First incrementing the accessor count.
        synchronized(flushingLock) {
            accessors.incrementAndGet();
        }

        PropertyDescriptor result;

        //Synchronizing on the directory Class root
        //This is preferrable to a full method synchronization since two lookups for
        //different classes can never be on the same directory path and won't collide
        synchronized(clazz) {

            result = classPropertyDirectory.getPropertyDescriptor(clazz, propertyName);

            if(result == null) {
                //PropertyDescriptor wasn't loaded yet

                //First we need bean information regarding the parent class
                final BeanInfo beanInfo;
                try {
                    beanInfo = Introspector.getBeanInfo(clazz);
                } catch(final IntrospectionException e) {
                    accessors.decrementAndGet();
                    throw e;
                    //TODO: throw specific
                }

                //Now we must find the PropertyDescriptor of our target property
                final PropertyDescriptor[] propList = beanInfo.getPropertyDescriptors();
                for (int i = 0; (i < propList.length) && (result == null); i++) {
                    final PropertyDescriptor propDesc = propList[i];
                    if(propDesc.getName().equals(propertyName))
                        result = propDesc;
                }

                //If no descriptor was found, something's wrong with the name or access
                if(result == null) {
                    accessors.decrementAndGet();
                                            //TODO: throw specific
                    throw new Exception("No property with name \"" + propertyName + "\" could be found in class " + clazz.getName());
                }

                //Adding mapping
                classPropertyDirectory.addMapping(clazz, propertyName, result);

            }

        }

        accessors.decrementAndGet();

        return result;

    }

    public void flushDirectory(final ClassLoader cl) {

        //We wait until all getPropertyDescriptor() calls in progress have completed.
        synchronized(flushingLock) {

            while(accessors.intValue() > 0) {
                try {
                    Thread.sleep(100);
                } catch(final InterruptedException e) {
                    //No show stopper
                }
            }

            for(final Iterator<Class<?>> it =
                    classPropertyDirectory.classMap.keySet().iterator(); it.hasNext();) {
                final Class<?> clazz = it.next();
                if(clazz.getClassLoader().equals(cl)) {
                    it.remove();
                    Introspector.flushFromCaches(clazz);
                }
            }

        }

    }

      //The rest of the inner classes are omitted...

}

Я считаю, что это должно работать. Предположим, что поток 1 вызывает метод get ..., а поток 2 вызывает метод flush ... одновременно. Если поток 1 сначала получает блокировку на flushingLock, поток 2 будет ждать, пока счетчик доступа вернется к 0. Между тем, новые вызовы get ... не могут продолжаться, поскольку поток 2 теперь будет иметь flushingLock , Если поток 2 получил блокировку первым, он будет ждать, пока средства доступа уменьшатся до 0, а вызовы для получения ... будут ожидать завершения очистки.

Кто-нибудь может увидеть проблемы с этим подходом? Есть ли сценарии, которые я пропускаю? Или, возможно, я усложняю вещи. Более того, некоторые классы java.util.concurrent могут предоставить именно то, что я здесь делаю, или есть стандартный шаблон для решения этой проблемы, о котором я не знаю.

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

Спасибо всем, кто читает это и заранее за любые ответы.

1 Ответ

2 голосов
/ 05 апреля 2011

Насколько я понимаю, вы можете использовать ReadWriteLock здесь:

private ReadWriteLock lock = new ReentrantReadWriteLock();
private Lock readLock = lock.readLock();
private Lock writeLock = lock.writeLock();

public PropertyDescriptor getPropertyDescriptor(final Class<?> clazz, final String propertyName) throws Exception {
    readLock.lock();
    try {
        ...
    } finally {
        readLock.unlock();
    }
}

public void flushDirectory(final ClassLoader cl) {
    writeLock.lock();
    try {
        ...
    } finally {
        writeLock.unlock();
    }
}

Также синхронизация на экземпляре Class выглядит для меня подозрительно - он может мешать другимсинхронизации.Возможно, было бы лучше использовать потокобезопасный Map из Future<PropertyDescriptor> (см., Например, Синхронизация в кэше HashMap ).

...