Lecture 13: Lock Implementations

Overview

  • Peterson Lock
  • Test-and-set Lock
  • Test-and-test-and-set Lock

Peterson Lock

The Peterson Lock

class Peterson implements Lock {
    private boolean[] flag = new boolean[2];
    private int victim;
	
    public void lock () {
       int i = ThreadID.get(); // get my ID, 0 or 1
       int j = 1 - i;          // other thread's ID
	   
       flag[i] = true;         // set my flag
       victim = i;             // set myself to be victim
       while (flag[j] && victim == i) {};
    }
	
    public void unlock () {
        int i = ThreadID.get();
        flag[i] = false;
    }
}

A Challenge

Peterson lock assumes 2 threads, with IDs 0 and 1

  • How do we accomplish this?

Make a Thread Subclass

Manually set an ID for threads

public class PetersonThread extends Thread {
    private int id;
    private LockedCounter ctr;
    private int numIncrements;

    public PetersonThread (int id, LockedCounter ctr, int numIncrements) {
	super();
	this.id = id;
	this.ctr = ctr;
	this.numIncrements = numIncrements;
    }

    public int getPetersonId() {
	return id;
    }

    @Override
    public void run () {
	for (int i = 0; i < numIncrements; ++i) {
	    ctr.increment();
	}
    }
}

Next week: A better way

Making a PetersonLock

class PetersonLock {
    private boolean[] flag = new boolean[2];
    private int victim;

    public void lock () {
	int i = ((PetersonThread)Thread.currentThread()).getPetersonId();
	int j = 1 - i;
	flag[i] = true;
	victim = i;
	while (flag[j] && victim == i) {};
    }

    public void unlock () {
	int i = ((PetersonThread)Thread.currentThread()).getPetersonId();	
	flag[i] = false;
    }
}

And Now: A Locked Counter

public class LockedCounter {
    private int count = 0;
    PetersonLock lock = new PetersonLock();

    public void increment () {
	lock.lock();
	try {
	    ++count;
	} finally {
	    lock.unlock();
	}
    }

    public int read () {
	return count;
    }
}

Finally, We’re Ready to Test It!

D’oh!

What happened?

Memory Consistency!

volatile Variables

Java can make variables visible between threads:

  • use volatile keyword
  • individual read/write operations to volatile are atomic

Drawbacks:

  • volatile variables are less efficient
  • only single read/write operations are atomic
    • e.g. count++ not atomic
  • only primitive datatypes are visible
    • if volatile SomeClass..., only the reference is treated as volatile

What Variables Should be volatile?

  • In PetersonLock?
  • In LockedCounter?

PetersonLock Again

class PetersonLock {
    private boolean[] flag = new boolean[2];
    private int victim;

    public void lock () {
	int i = ((PetersonThread)Thread.currentThread()).getPetersonId();
	int j = 1 - i;
	flag[i] = true;
	victim = i;
	while (flag[j] && victim == i) {};
    }

    public void unlock () {
	int i = ((PetersonThread)Thread.currentThread()).getPetersonId();	
	flag[i] = false;
    }
}

A Problem

Only primitive datatypes can be volatile

  • volatile boolean[] flag makes the reference volatile, not the data itself

How to fix this?

A Fix

Just make 2 boolean variables, flag0 and flag1

  • Yes, I know this is ugly

LockedCounter Again

public class LockedCounter {
    private int count = 0;
    PetersonLock lock = new PetersonLock();

    public void increment () {
	lock.lock();
	try {
	    ++count;
	} finally {
	    lock.unlock();
	}
    }

    public int read () {
	return count;
    }
}

Testing Our Counter Again

Finally!!!

What have we done?

  1. Proven correctness of a lock
    • idealized model of computation
    • atomic read/write operations
  2. Implemented lock
    • used Java to resemble idealized model
  3. Used lock
    • saw expected behavior

Theory and practice converge!

Limitations

  • Limitations of PetersonLock
    • only two threads
    • weird Java gymnastics to deal with thread IDs
      • (more on this next time)
  • Limitations of volatile variables
    • can ony perform atomic read/write operations
    • only for primitive data-types
    • need at least n registers (variables) for lock with n threads

Simplicity through Atomicity

Better locks through atomics:

  • AtomicBoolean supports atomic operations
  • For example:
    • ab.getAndSet(boolean newValue)
    • sets ab’s value to newValue and returns previous value

How could you use a single AtomicBoolean to implement a lock?

The Test-and-set Lock

public class TASLock {
    AtomicBoolean isLocked = new AtomicBoolean(false);
	
    public void lock () {
        while (isLocked.getAndSet(true)) {}
    }
	
    public void unlock () {
        isLocked.set(false);
    }
}

Advantages/Disadvantages?

public class TASLock {
    AtomicBoolean isLocked = new AtomicBoolean(false);
	
    public void lock () {
        while (isLocked.getAndSet(true)) {}
    }
	
    public void unlock () {
        isLocked.set(false);
    }
}

How Does TASLock Perform?

  • test on laptop
  • test on Remus/Romulus

A Tweak: TTASLock

In TASLock, we do a lot of getAndSet() operations under contention

    public void lock () {
        while (isLocked.getAndSet(true)) {}
    }

Since getAndSet is expensive, we could try:

  • only attempt getAndSet if we’ve seen isLocked.get() == false

Test-and-test-and-set Lock

    public void lock () {
	while (true) {
	    while (isLocked.get()) {};

	    if (!isLocked.getAndSet(true)) {
		return;
	    }
	}
    }

How does this affect performance?

Next Time

  • Knowing when to back off
  • Using queues to lock
    • thread-local objects