Locking and deadlocks

Transaction locks are taken in the following cases on a Managed Object:

Read locks are promoted to write locks if object fields are first read and then modified. Lock promotion can only occur if a serializable isolation level is being used in a transaction.

Transaction locks are held until the current transaction commits or aborts.

[Note]

No locks are taken when a method is invoked. It is possible to execute a method on an object without taking any transaction locks if that method does not access any fields in the object.

Example 4.8. Object locking

//     $Revision: 1.1.2.1 $
package com.kabira.snippets.transactions;

import com.kabira.platform.Transaction;
import com.kabira.platform.ManagedObject;
import com.kabira.platform.annotation.Managed;

/**
 * Snippet showing transaction locking
 * <p>
 * <h2> Target Nodes</h2>
 * <ul>
 * <li> <b>domainnode</b> = A
 * </ul>
 */
public class Locking extends Transaction
{
    @Managed
    public static class MyObject
    {
        private MyObject() { };

        /**
         * Create MyObject
         *
         * @param name  Initialize name field
         */
        public MyObject(String name)
        {
            this.name = name;
        }
        /**
         * Name
         */
        public String name;
        /**
         * Lock field
         */
        public boolean lock;
    }

    /**
     *    Transaction to create an instance of MyObject
     */
    public static class CreateTransaction extends Transaction
    {
        private MyObject m_a;

        /**
         * Transaction run method
         */
        @Override
        protected void run()
        {
            m_a = new MyObject("existing");
        }
    }

    /**
     *    Transaction to delete an instance of MyObject
     */
    public static class DeleteTransaction extends Transaction
    {
        private MyObject m_a;

        /**
         * Transaction run method
         */
        @Override
        protected void run()
        {
            //
            //  Cache a reference to the object before deleting it
            //
            MyObject myObject = m_a;
            String name = myObject.name;

            if (Transaction.hasWriteLock(myObject) == false)
            {
                System.out.println(name + ": does not have a write lock");
            }

            //
            //    Deleting an object takes a write lock
            //
            ManagedObject.delete(m_a);

            if (Transaction.hasWriteLock(myObject) == true)
            {
                System.out.println(name + ": now has a write lock");
            }
        }
    }
    private MyObject m_a;

    /**
     * Main entry point
     * @param args  Not used
     */
    public static void main(String[] args)
    {
        Locking locking = new Locking();
        CreateTransaction createTransaction = new CreateTransaction();
        DeleteTransaction deleteTransaction = new DeleteTransaction();

        createTransaction.execute();

        locking.m_a = createTransaction.m_a;
        locking.execute();

        deleteTransaction.m_a = createTransaction.m_a;
        deleteTransaction.execute();
    }

    /**
     * Transaction run method
     */
    @Override
    protected void run()
    {
        MyObject a = new MyObject("created");

        if (Transaction.hasWriteLock(a) == true)
        {
            System.out.println(a.name + ": has a write lock");
        }

        //
        //    This object does not have a write lock because it was created
        //    outside of this transaction.  Reading the name field will
        //    take a read lock.
        //
        if (Transaction.hasWriteLock(m_a) == false)
        {
            System.out.println(m_a.name + ": does not have a write lock");
        }
        if (Transaction.hasReadLock(m_a) == true)
        {
            System.out.println(m_a.name + ": now has a read lock");
        }

        //
        //    Take a write lock by setting the lock attribute.  This
        //    promotes the read lock taken above when name was read.
        //
        m_a.lock = true;

        if (Transaction.hasWriteLock(m_a) == true)
        {
            System.out.println(m_a.name + ": now has a write lock");
        }
    }
}

When Example 4.8, “Object locking” is run it generates the following output:

Example 4.9. Object locking output

[A] created: has a write lock
[A] existing: does not have a write lock
[A] existing: now has a read lock
[A] existing: now has a write lock
[A] existing: does not have a write lock
[A] existing: now has a write lock

Deadlocks are handled transparently and do not have to be explicitly handled by the application. When a deadlock occurs, the Transaction class detects the deadlock, rolls back the current transaction and restarts a new transaction by calling the run method again.

Explicit locking

It is possible to explicitly transaction lock objects. Explicit transaction locking is useful to avoid lock promotions. A lock promotion happens when an object has a read lock and then the object is modified. This is usually caused by first reading a field value and then modifying the object when using a serializable transaction isolation.

These mechanisms are available to explicitly lock objects:

  • Transaction.readLockObject - using a serializable isolation level, read locks an object. Performs a snapshot when using read committed - snapshot isolation level.

  • Transaction.writeLockObject - explicitly write lock an object

  • ManagedObject.extent(..., LockMode objectLock) - explicitly lock objects during extent iteration

  • explicitly lock an object during queries

The example below show how to avoid a lock promotion.

Example 4.10. Avoiding lock promotion

//     $Revision: 1.1.2.1 $
package com.kabira.snippets.transactions;

import com.kabira.platform.Transaction;
import com.kabira.platform.annotation.Managed;

/**
 * Explicit object locking.
 * <p>
 * <h2> Target Nodes</h2>
 * <ul>
 * <li> <b>domainnode</b> = A
 * </ul>
 */
public class LockPromotion extends Transaction
{
    @Managed
    public static class MyObject
    {
        String input;
        String output;
    }

    /**
     * Control program execution
     */
    public enum Action
    {
        /**
         * Create the object
         */
        CREATE,
        /**
         * Promote a read lock
         */
        PROMOTE,
        /**
         * Take a write lock - avoiding lock promotion
         */
        WRITELOCK
    }
    private MyObject m_b1;
    private Action m_action;

    /**
     * Main entry point
     * @param args  Not used
     */
    public static void main(String[] args)
    {
        LockPromotion lockPromotion = new LockPromotion();

        lockPromotion.m_action = Action.CREATE;
        lockPromotion.execute();

        lockPromotion.m_action = Action.PROMOTE;
        lockPromotion.execute();

        lockPromotion.m_action = Action.WRITELOCK;
        lockPromotion.execute();
    }

    /**
     * Report locks held on objects
     * @param msg   message to include in lock report
     */
    public void reportLock(String msg)
    {
        System.out.println(msg + " B1: read lock = "
            + Transaction.hasReadLock(m_b1)
            + ", write lock = "
            + Transaction.hasWriteLock(m_b1));
    }

    /**
     * Transaction run method
     * 
     * @throws com.kabira.platform.Transaction.Rollback
     */
    @Override
    protected void run() throws Rollback
    {
        if (m_action == Action.CREATE)
        {
            m_b1 = new MyObject();          
        }
        else if (m_action == Action.PROMOTE)
        {
            reportLock("promote: enter");

            //
            //     Accessing input takes a read lock
            //
            String i = m_b1.input;
            reportLock("promote: read");

            //
            //     Read lock is promoted to write lock. Note this
            //     also happens when the following is executed:
            //
            //     m_b1.output = m_b1.input;
            //
            m_b1.output = i;
            reportLock("promote: write");
        }
        else
        {
            assert (m_action == Action.WRITELOCK);

            reportLock("writelock: enter");

            //
            //     Explicitly take write lock to avoid promotion
            //
            Transaction.writeLockObject(m_b1);

            //
            //     Accessing input will already have write lock
            //
            String i = m_b1.input;
            reportLock("writelock: read");

            //
            //     No promotion of locks happen
            //
            m_b1.output = i;
            reportLock("writelock: write");
        }
    }
} 

When Example 4.10, “Avoiding lock promotion” is run it outputs the following (annotation added):

Example 4.11. Avoiding lock promotion output

[A] promote: enter B1: read lock = false, write lock = false

#
#     Read lock is taken when field on B1 is read
#
[A] promote: read B1: read lock = true, write lock = false

#
#     Write lock is taken when field on B1 is set
#
[A] promote: write B1: read lock = true, write lock = true

[A] writelock: enter B1: read lock = false, write lock = false

#
#    Explicitly write lock B1 causes both the read and write lock
#    to be taken on B1
#
[A] writelock: read B1: read lock = true, write lock = true
[A] writelock: write B1: read lock = true, write lock = true