Antes de Java 5.0, los únicos mecanismos que podrían usarse para coordinar el acceso a objetos compartidos fueron sincronizados y volátiles. Sabemos que la palabra clave sincronizada implementa los bloqueos incorporados, mientras que la palabra clave volátil garantiza la visibilidad de la memoria para los múltiples hilos. En la mayoría de los casos, estos mecanismos pueden hacer bien el trabajo, pero no pueden implementar algunas funciones más avanzadas, como no poder interrumpir un hilo que espera para adquirir un bloqueo, al no poder implementar un mecanismo de bloqueo de tiempo limitado, no poder implementar reglas de bloqueo para estructuras no bloqueadas, etc. y estos mecanismos de locking más flexibles generalmente proporcionan una mejor actividad o desempeño. Por lo tanto, se ha agregado un nuevo mecanismo en Java 5.0: Reentrantlock. La clase ReentrantLock implementa la interfaz de bloqueo y proporciona la misma visibilidad de Mutex y memoria que sincronizada. Su capa subyacente es lograr la sincronización de múltiples hilos a través de AQS. En comparación con las cerraduras incorporadas, ReentrantLock no solo proporciona un mecanismo de bloqueo más rico, sino que tampoco es inferior a las cerraduras incorporadas en el rendimiento (incluso mejor que las cerraduras incorporadas en versiones anteriores). Habiendo hablado de tantas ventajas de Reentrantlock, descubramos su código fuente y veamos su implementación específica.
1. Introducción a las palabras clave sincronizadas
Java proporciona cerraduras incorporadas para admitir la sincronización de múltiples hilos. El JVM identifica el bloque de código sincronizado de acuerdo con la palabra clave sincronizada. Cuando un hilo ingresa al bloque de código sincronizado, adquirirá automáticamente el bloqueo. Al salir del bloque de código sincronizado, el bloqueo se lanzará automáticamente. Después de que un hilo adquiere el bloqueo, se bloquearán otros hilos. Cada objeto Java se puede usar como un bloqueo que implementa la sincronización. La palabra clave sincronizada se puede utilizar para modificar los métodos de objetos, los métodos estáticos y los bloques de código. Al modificar los métodos del objeto y los métodos estáticos, el bloqueo es el objeto donde se encuentra el método y el objeto de clase. Al modificar el bloque de código, se deben proporcionar objetos adicionales como bloqueos. La razón por la cual cada objeto Java se puede usar como un bloqueo es que un objeto de monitor (manipulación) se asocia en el encabezado del objeto. Cuando el hilo ingresa al bloque de código sincrónico, mantendrá automáticamente el objeto del monitor, y cuando salga, liberará automáticamente el objeto Monitor. Cuando se sostiene el objeto del monitor, se bloquearán otros subprocesos. Por supuesto, estas operaciones de sincronización son implementadas por la capa subyacente de JVM, pero todavía hay algunas diferencias en la implementación subyacente del método de modificación de palabras clave sincronizadas y el bloque de código. El método de modificación de palabras clave sincronizado está implícitamente sincronizado, es decir, no es necesario controlarlo a través de las instrucciones de Bytecode. El JVM puede distinguir si un método es un método sincronizado basado en el indicador de acceso SynChronizado en la tabla de métodos; Mientras que los bloques de código modificados por palabra clave sincronizada se sincronizan explícitamente, que controlan la retención y liberación de la tubería del hilo a través del monitor y las instrucciones de bytecode monitorexit. El objeto Monitor contiene el campo _Count internamente. _Count igual a 0 significa que la tubería no se mantiene, y la cuenta mayor que 0 significa que se ha mantenido la tubería. Cada vez que el hilo de retención se reintense, se agregará 1, y cada vez que el hilo de retención salga, _Count se reducirá en 1. Este es el principio de implementación de la reingreso de bloqueo incorporado. Además, hay dos colas dentro del objeto Monitor _Entrylist y _WaitSet, que corresponden a la cola de sincronización y la cola condicional de AQS. Cuando el hilo no logra adquirir el bloqueo, se bloqueará en _Entrylist. Cuando se llama al método de espera del objeto de bloqueo, el hilo ingresará al _waitset para esperar. Este es el principio de implementación de la sincronización de hilos y la espera condicional de bloqueos incorporados.
2. Comparación entre reentrantlock y sincronizado
La palabra clave sincronizada es un mecanismo de bloqueo incorporado proporcionado por Java. Sus operaciones de sincronización son implementadas por el JVM subyacente. Reentrantlock es un bloqueo explícito proporcionado por el paquete java.util.concurrent, y sus operaciones de sincronización están alimentadas por el sincronizador AQS. Reentrantlock proporciona la misma semántica en el bloqueo y la memoria que las cerraduras incorporadas, además proporciona algunas otras características que incluyen la espera de bloqueo cronometrado, la espera de bloqueo interrumpible, el bloqueo justo e implementación de bloqueo estructurado no bloqueado. Además, Reentrantlock también tuvo ciertas ventajas de rendimiento en las primeras versiones de JDK. Dado que Reentrantlock tiene tantas ventajas, ¿por qué deberíamos usar la palabra clave sincronizada? De hecho, muchas personas usan Reentrantlock para reemplazar la operación de bloqueo de las palabras clave sincronizadas. Sin embargo, las cerraduras incorporadas todavía tienen sus propias ventajas. Las cerraduras incorporadas son familiares para muchos desarrolladores y son más simples y compactos en uso. Debido a que los bloqueos explícitos deben llamarse manualmente desbloqueado en el bloque Finalmente, es relativamente más seguro usar cerraduras incorporadas. Al mismo tiempo, es más probable que mejore el rendimiento de sincronizado en lugar de reentrante en el futuro. Debido a que sincronizado es una propiedad incorporada del JVM, puede realizar algunas optimizaciones, como la optimización de eliminación de bloqueos para los objetos de bloqueo con hilo, eliminando la sincronización de los bloqueos incorporados al aumentar la granularidad del bloqueo, y si estas funciones se implementan a través de bloqueos basados en la biblioteca de clase, es poco probable. Entonces, cuando se necesitan algunas características avanzadas, se debe utilizar el reentrantlock, que incluye: operaciones de adquisición de bloqueos de bloqueo, optimables, encuestables e interrumpibles, colas justas y cerraduras de estructura sin bloque. De lo contrario, sincronizado debe usarse primero.
3. Operaciones para adquirir y lanzar cerraduras
Primero veamos el código de muestra usando ReentrantLock para agregar bloqueos.
public void dosomething () {// El valor predeterminado es obtener un bloqueo de bloqueo de bloqueo no de fair bloqueo = new ReentrantLock (); intente {// bloquear bloque.lock () antes de la ejecución; // Ejecutar la operación ...} Finalmente {// el bloqueo de bloqueo de bloqueo () finalmente se libera; }}La siguiente es la API para adquirir y liberar cerraduras.
// La operación de obtener bloqueo de bloqueo public void bloqueo () {sync.lock ();} // La operación de liberar bloqueo public void desbloock () {sync.release (1);}Puede ver que las operaciones de adquirir el bloqueo y la liberación del bloqueo se delegan al método de bloqueo y el método de liberación del objeto Sync respectivamente.
Public Class Reentrantlock implementa Lock, java.io.serializable {Private Final Sync Sync; La sincronización de clase estática abstracta extiende AbstractQueueedSynChronizer {abstract void Lock (); } // Syncronizer que implementa la clase final de bloqueo no de fair NonfairSync extiende Sync {final void bloqueo () {...}} // Syncronizer que implementa el bloqueo de la clase final estática FairSync extiende Sync {Final Void Lock () {...}}}Cada objeto Reentrantlock contiene una referencia de Sync de tipo. Esta clase de sincronización es una clase interna abstracta. Hereda de StraceCheedynchronizer. El método de bloqueo dentro es un método abstracto. La sincronización variable miembro de Reentrantlock se asigna el valor durante la construcción. Echemos un vistazo a lo que hacen los dos métodos de constructor de Reentrantlock.
// El constructor predeterminado sin parámetros pública reentrantlock () {sync = new Nonfairsync ();} // El constructor parametrizado publicor Reentrantlock (boolean fair) {sync = jair? New FairSync (): New NonfairSync ();}Llamar al constructor sin parámetros predeterminado asignará la instancia NonfairSync a Sync, y el bloqueo es un bloqueo no fair en este momento. El constructor de parámetros permite que los parámetros especifiquen si se debe asignar una instancia de FairSync o una instancia no FAIRSYNC para sincronizar. NonfairSync y Fairsync heredan de la clase de sincronización y reescribieron el método Lock (), por lo que hay algunas diferencias entre los bloqueos justos y los bloqueos no faires en el camino de obtener cerraduras, de las que hablaremos a continuación. Echemos un vistazo a la operación de liberar la cerradura. Cada vez que llame al método desbloqueo (), simplemente ejecuta la operación Sync.Release (1). Esta operación llamará al método Release () de la clase de SynChronizer abstractQueueed. Vamos a revisarlo de nuevo.
// Libere la operación de bloqueo (modo exclusivo) Public Final Boolean Release (int arg) {// Gire el bloqueo de contraseña para ver si puede desbloquear if (tryRelease (arg)) {// Obtener el nodo del nodo Head h = head; // Si el nodo de la cabeza no está vacío y el estado de espera no es igual a 0, despierta el nodo sucesor if (h! = Null && h.waitstatus! = 0) {// despertar el nodo sucesor sinksuccessor (h); } return verdadero; } return false;}Este método de lanzamiento es la API para lanzar operaciones de bloqueo proporcionadas por AQS. Primero llama al método TryRelease para intentar adquirir el bloqueo. El método de TryRelease es un método abstracto, y su lógica de implementación está en la sincronización de subclase.
// Intenta liberar el tryrelase de booleano final protegido de bloqueo (inteleates) {int c = getState () - libras; // Si el hilo que contiene el bloqueo no es el hilo actual, se lanzará una excepción si (thread.currentThread ()! = GetExClusiveOnderThread ()) {Throw New IlegalMonitorStateException (); } boolean free = false; // Si el estado de sincronización es 0, significa que el bloqueo se libera si (c == 0) {// establece el indicador del bloqueo que se lanza como true Free = True; // Establecer el hilo ocupado en setExClusiveSownerThread vacío (nulo); } setState (c); regresar gratis;}Este método de tryrelease adquirirá primero el estado de sincronización actual, restará el estado de sincronización actual de los parámetros aprobados al nuevo estado de sincronización y luego determinará si el nuevo estado de sincronización es igual a 0. Si es igual a 0, significa que el bloqueo actual se libera. Luego establezca el estado de lanzamiento del bloqueo en verdadero, luego borre el hilo que actualmente ocupa el bloqueo y finalmente llame al método SetState para establecer el nuevo estado de sincronización y devolver el estado de liberación del bloqueo.
4. Lock justo y bloqueo injusto
Sabemos qué instancia específica apunta el reentrantlock en función de la sincronización. Durante la construcción, se asignará la sincronización de la variable miembro. Si el valor se asigna a la instancia de NonfairSync, significa que es un bloqueo no fair, y si el valor se asigna a la instancia de FairSync, significa que es un bloqueo justo. Si es un bloqueo justo, los hilos obtendrán el bloqueo en el orden en que realiza las solicitudes, pero en el bloqueo injusto, el comportamiento de corte está permitido: cuando un hilo solicite un bloqueo injusto, si el estado del bloqueo está disponible al mismo tiempo que se emite la solicitud, el hilo saltará todos los subprocesos en la cola para obtener el bloqueo directamente. Echemos un vistazo a cómo obtener cerraduras injustas.
// Synchronizer injusto la clase final estática NonfairSync extiende Sync {// Implementar el método abstracto de la clase principal para adquirir el bloqueo final de bloqueo de bloqueo () {// use el método CAS para establecer el estado de sincronización if (compareSetState (0, 1)) {// Si la configuración es exitosa, significa que el bloqueo no está ocupado setExClusewnerwnerSeRead (hilo de hilo (hilo de hilo (hilo de hilo)); } else {// De lo contrario, significa que el bloqueo ha sido ocupado, llame a adquirir y deje que la cola de hilo para sincronizar la cola para obtener adquirir (1); }} // El método para tratar de adquirir el bloqueo de bloqueo Boolean Final TryAcquire (int adquirir) {return NonfairryAcquire (adquirir); }} // adquirir bloqueos en modo no interrogante (modo exclusivo) público final void adquirir (int arg) {if (! TreyAcquire (arg) && adquireueed (addWaIter (node.exclusive), arg)) {autointerrupt (); }}Se puede ver que en el método de bloqueo de bloqueo injusto, el hilo cambiará el valor del estado de sincronización de 0 a 1 en el primer paso en CAS. De hecho, esta operación es equivalente a tratar de adquirir el bloqueo. Si el cambio es exitoso, significa que el hilo ha adquirido el bloqueo en este momento, y ya no hay necesidad de hacer cola en la cola de sincronización. Si el cambio falla, significa que el bloqueo no se ha lanzado cuando el hilo llega por primera vez, por lo que el método de adquisición se llama Siguiente. Sabemos que este método de adquisición se hereda del método de SynChronizer abstractqueed. Revisemos este método. Después de que el hilo ingresa al método de adquisición, la primera llamada del método Tryacquire para intentar adquirir el bloqueo. Dado que NonfairSync sobrescribe el método TryAcquire y llama al método NonfairryAcquire de la sincronización de clase principal en el método, el método no FairryAcquire se llamará aquí para intentar adquirir el bloqueo. Veamos qué hace específicamente este método.
// Adquisición injusta de Lock Final Boolean NonfairryAcquire (int adquirir) {// Obtenga el subproceso actual de subproceso actual = thread.currentThread (); // Obtener el estado de sincronización actual int c = getState (); // Si el estado de sincronización es 0, significa que el bloqueo no está ocupado si (c == 0) {// use CAS para actualizar el estado de sincronización if (compareSetState (0, adquirir)) {// Establecer el hilo actualmente ocupando el bloqueo setExCluseSownThreadThread (actual); devolver verdadero; } // de lo contrario, se determina si el bloqueo es el hilo actual} else if (current == getExClusiveOwnerThread ()) {// Si el bloqueo se mantiene con el hilo actual, modifique directamente el estado de sincronización actual int nextc = c + adquirir; if (nextc <0) {tirar un nuevo error ("conteo de bloqueo máximo excedido"); } setState (nextC); devolver verdadero; } // Si el bloqueo no es el hilo actual, devuelva el indicador de falla return False;}El método no FairryAcquire es un método de sincronización. Podemos ver que después de que el hilo ingrese este método, primero adquiere el estado de sincronización. Si el estado de sincronización es 0, use la operación CAS para cambiar el estado de sincronización. De hecho, esto es para adquirir el bloqueo nuevamente. Si el estado de sincronización no es 0, significa que el bloqueo está ocupado. En este momento, primero determinaremos si el hilo que sostiene el bloqueo es el hilo actual. Si es así, el estado de sincronización se incrementará en 1. De lo contrario, la operación de tratar de adquirir el bloqueo fallará. Por lo tanto, se llamará al método AddWaiter para agregar el hilo a la cola de sincronización. Para resumir, en el modo de bloqueo injusto, un hilo intentará adquirir dos cerraduras antes de ingresar la cola de sincronización. Si la adquisición es exitosa, no ingresará a la cola de cola de cola de sincronización, de lo contrario ingresará a la cola de cola de la cola de sincronización. A continuación, echemos un vistazo a cómo obtener cerraduras justas.
// Syncronizer que implementa la clase final de bloqueo de bloqueo FairSync extiende Sync {// Implementar el método abstracto de la clase principal para adquirir bloqueo final de bloqueo final () {// llamar adquirir adquirir y dejar que las colas de subprocesos sincronizaran la cola para obtener adquirir (1); } // Intenta adquirir el bloqueo Booleano final protegido tryacquire (int adquirir) {// Obtener el subproceso actual de subproceso Finalin Current = Thread.CurrentThread (); // Obtener el estado de sincronización actual int c = getState (); // Si el estado de sincronización 0 significa que el bloqueo no está ocupado si (c == 0) {// defiende si hay un nodo a la altura en la cola de sincronización if (! HasqueedPredEcessors () && compareSetState (0, adquirir)) {// si no hay un nodo avanzado y el estado de sincronización de sincronización se establece con éxito, significa que el bloqueo es adquirido succionado. setExClusiveSownerThread (actual); devolver verdadero; } // de lo contrario, determine si el subproceso actual contiene el bloqueo} else if (current == getExClusiveSownerThread ()) {// Si el hilo actual contiene el bloqueo, modifique directamente el estado de sincronización int nextc = c + adquirir; if (nextc <0) {tirar un nuevo error ("conteo de bloqueo máximo excedido"); } setState (nextC); devolver verdadero; } // Si el hilo actual no contiene el bloqueo, la adquisición falla return false; }} Al llamar al método de bloqueo de bloqueo justo, el método de adquisición se llamará directamente. Del mismo modo, el método de adquisición primero llama al método de reescritura de Fairsync Tryacquire para intentar adquirir el bloqueo. En este método, primero se obtiene el valor del estado de sincronización. Si el estado de sincronización es 0, significa que el bloqueo se lanza en este momento. La diferencia de la cerradura injusta es que primero llamará al método de Hiscedecesss de HasqueedPedess para verificar si alguien está haciendo cola en la cola de sincronización. Si nadie está haciendo cola, se modificará el valor del estado de sincronización. Puede ver que el bloqueo justo adopta un método de cortesía aquí en lugar de adquirir el bloqueo de inmediato. Excepto por este paso que es diferente del bloqueo injusto, las otras operaciones son las mismas. En resumen, podemos ver que el bloqueo justo solo verifica el estado del bloqueo una vez antes de ingresar la cola de sincronización. Incluso si encuentra que el bloqueo está abierto, no lo adquirirá de inmediato. En cambio, dejará que los hilos en la cola de sincronización lo adquieran primero. Por lo tanto, se puede asegurar que el orden en el que todos los hilos adquieren las cerraduras debajo del bloqueo justo es primero y luego llegando, lo que también garantiza la justicia de obtener las cerraduras.
Entonces, ¿por qué no queremos que todas las cerraduras sean justas? Después de todo, la justicia es un buen comportamiento, y la injusticia es un mal comportamiento. Debido a que las operaciones de suspensión y atención del hilo tienen una gran sobrecarga, afecta el rendimiento del sistema, especialmente en el caso de una feroz competencia, las cerraduras justas conducirán a operaciones frecuentes de suspensión y despertar de hilos, mientras que los bloqueos no ferios pueden reducir tales operaciones, por lo que serán mejores que los bloqueos justos en el rendimiento. Además, dado que la mayoría de los hilos usan cerraduras durante un tiempo muy corto, y la operación de atención del hilo tendrá un retraso, es posible que el subproceso B adquiera el bloqueo inmediatamente y libere el bloqueo después de usarlo. Esto lleva a una situación de ganar-ganar. El momento en que el hilo A adquiere el bloqueo no se retrasa, pero el hilo B usa el bloqueo por adelantado y su rendimiento también se ha mejorado.
5. El mecanismo de implementación de las colas condicionales
Hay algunos defectos en la cola de condición incorporada. Cada bloqueo incorporado solo puede tener una cola de condición asociada, lo que hace que múltiples hilos esperen diferentes predicados de condición en la misma cola de condición. Luego, cada vez que se llama a todos, todos los hilos de espera se despertarán. Cuando el hilo se despierta, encuentra que no es el predicado de condición que está esperando, y se suspenderá. Esto lleva a muchas operaciones inútiles de activación y suspensión de hilos, lo que desperdiciará muchos recursos del sistema y reducirá el rendimiento del sistema. Si desea escribir un objeto concurrente con múltiples predicados condicionales, o si desea obtener más control que la visibilidad de la cola condicional, debe usar bloqueo y condición explícitos en lugar de cerraduras y colas condicionales incorporadas. Una condición y una cerradura están asociadas, al igual que una cola de condición y un bloqueo incorporado. Para crear una condición, puede llamar al método de bloqueo. Newcondition en el bloqueo asociado. Primero veamos un ejemplo usando la condición.
public class BoundedBuffer {final Lock Lock = New ReentrantLock (); condición final notfull = lock.newcondition (); // Condición predicada: Notfull Condición final Notempty = Lock.NewCondition (); // Condición predicada: Objeto final notiento [] elementos = nuevo objeto [100]; int Putptr, Takeptr, Count; // Método de producción public void put (objeto x) arroja interruptedException {Lock.lock (); intente {while (count == items.length) notfull.await (); // La cola está llena, y el hilo está esperando los elementos [PUTPTR] en la cola nota. elementos [putptr] = x; if (++ putptr == items.length) putptr = 0; ++ recuento; Notempty.signal (); // La producción es exitosa, despierta el nodo de la cola notable} finalmente {Lock.unlock (); }} // Consumir el método Public Object Take () lanza interruptedException {Lock.lock (); intente {while (count == 0) noTempty.Await (); // La cola está vacía, el hilo espera el objeto x = items [TAKPTR] en la cola notimada; if (++ teakptr == items.length) TakePtr = 0; --contar; notflido.signal (); // El consumo es exitoso, despierta el nodo del notado retorno de la cola x; } Finalmente {Lock.unlock (); }}}Un objeto de bloqueo puede generar múltiples colas de condición, y dos colas de condición se generan aquí notas y notables. Cuando el contenedor está lleno, el hilo que llama al método PUT debe bloquearse. Espere hasta que el predicado de condición sea verdadero (el contenedor no está satisfecho) se despierta y continúa ejecutándose; Cuando el contenedor está vacío, el hilo que llama al método de toma debe bloquearse. Espere hasta que el predicado de condición sea verdadero (el contenedor no está vacío) se despierta y continúa ejecutándose. Estos dos tipos de hilos esperan de acuerdo con diferentes predicados de condición, por lo que ingresarán dos colas de condición diferentes para bloquear, y esperarán hasta el momento adecuado antes de despertar llamando a la API en el objeto de condición. El siguiente es el código de implementación del método de trituración.
// Crear una condición de la condición de condición pública newCondition () {return sync.newcondition ();} sync de clase estática abstracta extiende abstractqueedsynChronizer {// crear un nuevo objeto de condición condición final de niewcondition () {return new condicionOnding (); }}La implementación de la cola de condición en Reentrantlock se basa en StraceCheedynchronizer. El objeto de condición que obtenemos al llamar al método de trituración es una instancia de la clase interna ConditionObject de AQS. Todas las operaciones en las colas de condición se realizan llamando a la API proporcionada por ConditionObject. Para la implementación específica de ConditionObject, puede consultar mi artículo "Serie de concurrencia de Java [4] ----- AbrazedSynChronizer Code Fuente Análisis Conditional Queue" y no lo repetiré aquí. En este punto, nuestro análisis del código fuente de Reentrantlock ha llegado a su fin. Espero que leer este artículo ayude a los lectores a comprender y dominar el reentrado.
Lo anterior es todo el contenido de este artículo. Espero que sea útil para el aprendizaje de todos y espero que todos apoyen más a Wulin.com.