Locks

Locks are a fundamental synchronization mechanism that allows exclusive access to a shared resource. When a thread acquires a lock, it gains the right to execute the critical section of code, while other threads trying to acquire the same lock are blocked until the lock is released.

In Java, the synchronized keyword is used to create a lock on an object or a block of code. Here’s an example:

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

In Golang, the sync package provides the Mutex type for locking. Here’s an example:

type Counter struct {
    mu    sync.Mutex
    count int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *Counter) GetCount() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

Semaphores

Semaphores are synchronization primitives that control access to a shared resource by maintaining a count of available permits. Threads can acquire permits from the semaphore and release them when they are done. If no permits are available, threads will block until a permit becomes available.

In Java, the Semaphore class is used to create semaphores. Here’s an example:

Semaphore semaphore = new Semaphore(2); // Create a semaphore with 2 permits

// Acquire a permit
semaphore.acquire();
// Critical section
// ...
// Release the permit
semaphore.release();

In Golang, the sync package provides the WaitGroup type, which can be used to implement semaphore-like behavior. Here’s an example:

var wg sync.WaitGroup
var semaphore = make(chan struct{}, 2) // Create a channel as a semaphore with 2 permits

func criticalSection() {
    semaphore <- struct{}{} // Acquire a permit
    defer func() { <-semaphore }() // Release the permit when done
    // Critical section
    // ...
    wg.Done()
}

func main() {
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go criticalSection()
    }
    wg.Wait()
}

Condition Variables

Condition variables provide a way for threads to wait for a specific condition to be met before proceeding. They are often used in conjunction with locks to create a monitor-like construct. Threads can wait on a condition variable until they are notified by another thread that the condition has been satisfied.

In Java, the Object class provides the wait() and notify() methods for condition variables. Here’s an example:

public class Buffer {
    private Queue<Integer> queue = new LinkedList<>();
    private int capacity;

    public Buffer(int capacity) {
        this.capacity = capacity;
    }

    public synchronized void put(int item) throws InterruptedException {
        while (queue.size() == capacity) {
            wait(); // Wait if the buffer is full
        }
        queue.add(item);
        notify(); // Notify waiting threads
    }

    public synchronized int get() throws InterruptedException {
        while (queue.isEmpty()) {
            wait(); // Wait if the buffer is empty
        }
        int item = queue.remove();
        notify(); // Notify waiting threads
        return item;
    }
}

In Golang, the sync package provides the Cond type for condition variables. Here’s an example:

type Buffer struct {
    queue []int
    cond  *sync.Cond
}

func NewBuffer() *Buffer {
    return &Buffer{
        queue: make([]int, 0),
        cond:  sync.NewCond(&sync.Mutex{}),
    }
}

func (b *Buffer) Put(item int) {
    b.cond.L.Lock()
    defer b.cond.L.Unlock()
    b.queue = append(b.queue, item)
    b.cond.Signal()
}

func (b *Buffer) Get() int {
    b.cond.L.Lock()
    defer b.cond.L.Unlock()
    for len(b.queue) == 0 {
        b.cond.Wait()
    }
    item := b.queue[0]
    b.queue = b.queue[1:]
    return item
}

Read-Write Locks

Read-write locks, also known as shared-exclusive locks, allow multiple threads to read a shared resource concurrently, but only one thread can write to the resource at a time. This is useful when reads are frequent and writes are less common, as it allows for better concurrency compared to using a regular lock.

In Java, the ReentrantReadWriteLock class is used for read-write locks. Here’s an example:

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

public void read() {
    readLock.lock();
    try {
        // Read operation
    } finally {
        readLock.unlock();
    }
}

public void write() {
    writeLock.lock();
    try {
        // Write operation
    } finally {
        writeLock.unlock();
    }
}

In Golang, the sync package provides the RWMutex type for read-write locks. Here’s an example:

var rw sync.RWMutex
var data int

func read() {
    rw.RLock()
    defer rw.RUnlock()
    // Read operation
    fmt.Println(data)
}

func write(value int) {
    rw.Lock()
    defer rw.Unlock()
    // Write operation
    data = value
}

These are just a few examples of the basic synchronization mechanisms available in Java and Golang. Each mechanism serves a specific purpose and can be used to coordinate access to shared resources and ensure the desired order of operations in concurrent environments.

It’s important to use synchronization mechanisms judiciously and only when necessary to avoid introducing unnecessary overhead and potential performance bottlenecks. By understanding and applying these synchronization tools effectively, you can write safe and efficient concurrent code in Java and Golang.