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.