¿Cómo se determina si un objeto está bloqueado (sincronizado) para no bloquearlo en Java?

81

Tengo un proceso A que contiene una tabla en la memoria con un conjunto de registros (recordA, recordB, etc ...)

Ahora, este proceso puede lanzar muchos subprocesos que afectan los registros y, a veces, podemos tener 2 subprocesos intentando acceder al mismo registro; esta situación debe ser negada. Específicamente, si un registro está BLOQUEADO por un hilo, quiero que el otro hilo se anule (no quiero BLOQUEAR o ESPERAR).

Actualmente hago algo como esto:

synchronized(record)
{
performOperation(record);
}

Pero esto me está causando problemas ... porque mientras Process1 está realizando la operación, si Process2 entra, bloquea / espera en la declaración sincronizada y cuando Process1 finaliza, realiza la operación. En cambio, quiero algo como esto:

if (record is locked)
   return;

synchronized(record)
{
performOperation(record);
}

¿Alguna pista sobre cómo se puede lograr esto? Cualquier ayuda será muy apreciada. Gracias,

Shaitan00
fuente

Respuestas:

87

Una cosa a tener en cuenta es que en el instante en que recibe dicha información, está obsoleta. En otras palabras, se le podría decir que nadie tiene el candado, pero luego, cuando intenta adquirirlo, lo bloquea porque otro hilo eliminó el candado entre el cheque y usted tratando de adquirirlo.

Brian tiene razón al señalar Lock, pero creo que lo que realmente quieres es su tryLockmétodo:

Lock lock = new ReentrantLock();
......
if (lock.tryLock())
{
    // Got the lock
    try
    {
        // Process record
    }
    finally
    {
        // Make sure to unlock so that we don't cause a deadlock
        lock.unlock();
    }
}
else
{
    // Someone else had the lock, abort
}

También puede llamar tryLockcon una cantidad de tiempo de espera, por lo que podría intentar adquirirlo durante una décima de segundo y luego cancelar si no puede obtenerlo (por ejemplo).

(Creo que es una pena que la API de Java no proporcione, hasta donde yo sé, la misma funcionalidad para el bloqueo "integrado", como lo hace la Monitorclase en .NET. Por otra parte, hay muchas otras cosas que no me gustan en ambas plataformas cuando se trata de enhebrar: ¡cada objeto potencialmente tiene un monitor, por ejemplo!)

Jon Skeet
fuente
Si. Ese es un buen punto. Tomé el código de ejemplo literalmente, mientras que lo anterior es definitivamente una implementación más robusta
Brian Agnew
5
Pero, ¿cómo uso un candado por registro? Actualmente, los registros se almacenan en una tabla hash de registros ... ¿así que necesito una tabla hash de bloqueos que coincida? Estoy tratando de asegurarme de tener la mayor concurrencia posible, por lo que si un proceso quiere acceder a recordC, debería estar bien (si solo recordB está bloqueado), utilizo un LOCK global, entonces es esencialmente lo mismo que bloquear toda la tabla hash. ... eso tiene algún sentido?
Shaitan00
@ Shaitan00: La forma más fácil sería tener un candado dentro del registro. Básicamente, desea un candado asociado con cada registro, así que colóquelo en el objeto.
Jon Skeet
Obviamente :) ¿Asumo que no necesito administrar el desbloqueo? Lo que significa que cuando sale del {} de .tryLock () se desbloqueará automáticamente e inmediatamente, ¿correcto?
Shaitan00
1
Necesitas gestionar el desbloqueo. Vea el tutorial al que hice referencia en mi respuesta y el método unlock () en el bloque finalmente {}
Brian Agnew
24

Eche un vistazo a los objetos Lock introducidos en los paquetes de simultaneidad de Java 5.

p.ej

Lock lock = new ReentrantLock()
if (lock.tryLock()) {
   try {
      // do stuff using the lock...
   }
   finally {
      lock.unlock();
   }
}
   ...

El objeto ReentrantLock esencialmente hace lo mismo que el synchronizedmecanismo tradicional , pero con más funcionalidad.

EDITAR: Como ha señalado Jon, el isLocked()método le dice en ese instante y, a partir de entonces, esa información está desactualizada. El método tryLock () proporcionará una operación más confiable (tenga en cuenta que también puede usar esto con un tiempo de espera)

EDIT # 2: El ejemplo ahora incluye tryLock()/unlock()para mayor claridad.

Brian Agnew
fuente
11

Encontré esto, podemos usarlo Thread.holdsLock(Object obj)para verificar si un objeto está bloqueado:

Devuelve truesi y solo si el hilo actual mantiene el bloqueo del monitor en el objeto especificado.

Tenga en cuenta que Thread.holdsLock()regresa falsesi el bloqueo está retenido por algo y el hilo de llamada no es el hilo que mantiene el bloqueo.

Rahul Kulshreshtha
fuente
8

Si bien el enfoque anterior con un objeto de bloqueo es la mejor manera de hacerlo, si tiene que poder verificar el bloqueo con un monitor, se puede hacer. Sin embargo, viene con una advertencia de salud ya que la técnica no es portátil para máquinas virtuales Java que no sean de Oracle y puede fallar en futuras versiones de VM ya que no es una API pública compatible.

He aquí cómo hacerlo:

private static sun.misc.Unsafe getUnsafe() {
    try {
        Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(null);
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

public void doSomething() {
  Object record = new Object();
  sun.misc.Unsafe unsafe = getUnsafe(); 
  if (unsafe.tryMonitorEnter(record)) {
    try {
      // record is locked - perform operations on it
    } finally {
      unsafe.monitorExit(record);
    }
  } else {
      // could not lock record
  }
}

Mi consejo sería usar este enfoque solo si no puede refactorizar su código para usar objetos de bloqueo java.util.concurrent para esto y si está ejecutando en una VM de Oracle.

Chris Wraith
fuente
12
@alestanis, no estoy de acuerdo. Aquí estoy leyendo estas respuestas hoy y feliz por todas las respuestas / comentarios, sin importar cuándo se den.
Tom
@Tom Estas respuestas están marcadas por el sistema SO, por eso dejé un mensaje. Verá cuando comience a revisar :)
alestanis
2
@alestanis, creo que es desafortunado: a menudo veo preguntas antiguas que tienen respuestas aceptadas, pero que también tienen respuestas más nuevas y mejores, ya sea porque los cambios tecnológicos o porque la respuesta aceptada no fue completamente correcta.
Tom
1
Además de que este hilo es antiguo y tiene una buena respuesta, esta respuesta aquí no es buena. Si realmente desea bloquear usando un monitor, puede usar Thread.holdsLock (objeto).
Tobias
3

Si bien las respuestas de Lock son muy buenas, pensé en publicar una alternativa usando una estructura de datos diferente. Esencialmente, sus diversos hilos quieren saber qué registros están bloqueados y cuáles no. Una forma de hacerlo es realizar un seguimiento de los registros bloqueados y asegurarse de que la estructura de datos tenga las operaciones atómicas correctas para agregar registros al conjunto bloqueado.

Usaré CopyOnWriteArrayList como ejemplo porque es menos "mágico" para la ilustración. CopyOnWriteArraySet es una estructura más apropiada. Si tiene muchos registros bloqueados al mismo tiempo en promedio, puede haber implicaciones de rendimiento con estas implementaciones. Un HashSet correctamente sincronizado también funcionaría y los bloqueos son breves.

Básicamente, el código de uso se vería así:

CopyOnWriteArrayList<Record> lockedRecords = ....
...
if (!lockedRecords.addIfAbsent(record))
    return; // didn't get the lock, record is already locked

try {
    // Do the record stuff
}        
finally {
    lockedRecords.remove(record);
}

Le evita tener que administrar un bloqueo por registro y proporciona un solo lugar en caso de que sea necesario borrar todos los bloqueos por alguna razón. Por otro lado, si alguna vez tiene más de un puñado de registros, un HashSet real con sincronización puede funcionar mejor, ya que las búsquedas de agregar / eliminar serán O (1) en lugar de lineales.

Solo una forma diferente de ver las cosas. Solo depende de cuáles sean sus requisitos de subprocesamiento reales. Personalmente, usaría un Collections.synchronizedSet (new HashSet ()) porque será muy rápido ... la única implicación es que los subprocesos pueden ceder cuando de otra manera no lo harían.

PSpeed
fuente
Si quieres tener el control total, Locksupongo que podrías poner esto en tu propia clase.
bvdb
1

Otra solución es (en caso de que no haya tenido la oportunidad con las respuestas dadas aquí) está usando tiempos de espera. es decir, debajo de uno devolverá nulo después de 1 segundo colgado:

ExecutorService executor = Executors.newSingleThreadExecutor();
        //create a callable for the thread
        Future<String> futureTask = executor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return myObject.getSomething();
            }
        });

        try {
            return futureTask.get(1000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            //object is already locked check exception type
            return null;
        }
HRgiger
fuente
-2

Gracias por esto, me ayudó a resolver una condición de carrera. Lo cambié un poco para usar cinturón y tirantes.

Así que aquí está mi sugerencia para UNA MEJORA de la respuesta aceptada:

Puede asegurarse de obtener acceso seguro al tryLock()método haciendo algo como esto:

  Lock localLock = new ReentrantLock();

  private void threadSafeCall() {
    boolean isUnlocked = false;

    synchronized(localLock) {
      isUnlocked = localLock.tryLock();
    }

    if (isUnlocked) {
      try {
        rawCall();
      }
      finally {
        localLock.unlock();
      }
    } else {
      LOGGER.log(Level.INFO, "THANKS! - SAVED FROM DOUBLE CALL!");
    }
  }

Esto evitaría la situación en la que podría recibir dos llamadas tryLock()al mismo tiempo, haciendo que la devolución sea potencialmente dudosa. Me gustaría ahora, si me equivoco, podría haber sido demasiado cauteloso. ¡Pero hey! Mi concierto es estable ahora :-) ..

Lea más sobre mis problemas de desarrollo en mi Blog .

El Schwartz
fuente
1
el uso de sincronizado en un objeto de bloqueo es muy, muy malo. El objetivo de una cerradura es evitar la sincronización, por lo que puede desconectarse o regresar rápidamente cuando no puede obtener la cerradura.
Ajax