Detailed explanation of Java lock mechanism, optimistic lock, pessimistic lock, what is reentrant lock and how to achieve it

1. Optimistic Locking

  • Rationale: Optimistic locking assumes that in most cases, no conflicts will occur between multiple threads. When reading data, each thread is given an identifier (such as a version number or a timestamp). Before submitting the modification, it will compare whether the current identifier is equal to the previously read identifier. If they are equal, the submission is successful. Otherwise, it means that the data has been modified by other threads and conflict processing is required.
  • Implementation method: Usually implemented using a version number or a timestamp, an additional field can be added to the database as an identifier and compared during an update operation.
  • Application scenario: It is suitable for scenarios with frequent read operations and few write operations, which can reduce the use of locks and improve concurrency performance.
code example
import java.util.concurrent.atomic.AtomicInteger;

public class OptimisticLockExample {
    
    
    private static AtomicInteger version = new AtomicInteger(0);
    private static int sharedData = 0;

    public static void main(String[] args) {
    
    
        // 创建两个线程并启动
        Thread thread1 = new Thread(() -> {
    
    
            int currentVersion = version.get(); // 读取当前版本号
            int newValue = sharedData + 1; // 对共享数据进行修改
            // 模拟耗时操作
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            // 检查版本号是否仍为之前读取的版本号
            if (version.compareAndSet(currentVersion, currentVersion + 1)) {
    
    
                sharedData = newValue; // 提交修改
                System.out.println("Thread 1: Shared data updated to " + sharedData);
            } else {
    
    
                System.out.println("Thread 1: Failed to update shared data due to concurrent modification");
            }
        });

        Thread thread2 = new Thread(() -> {
    
    
            int currentVersion = version.get(); // 读取当前版本号
            int newValue = sharedData + 1; // 对共享数据进行修改
            // 模拟耗时操作
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            // 检查版本号是否仍为之前读取的版本号
            if (version.compareAndSet(currentVersion, currentVersion + 1)) {
    
    
                sharedData = newValue; // 提交修改
                System.out.println("Thread 2: Shared data updated to " + sharedData);
            } else {
    
    
                System.out.println("Thread 2: Failed to update shared data due to concurrent modification");
            }
        });

        thread1.start();
        thread2.start();
    }
}

In this example, we use the compareAndSet() method of the AtomicInteger class to implement optimistic locking. First, we define a version number versionfor tracking changes to shared data. We then create two threads, each of which reads the current version number and performs modification operations on the shared data. Before submitting the modification, the thread will check again whether the current version number is still the previously read version number. If yes, the modification is submitted successfully; otherwise, it means that the data has been modified by other threads and needs to be processed accordingly.

2. Pessimistic Locking

  • Principle: Pessimistic locking assumes that in a multi-threaded environment, access to shared resources will cause conflicts, so by default it is considered that conflicts will occur every time access, and locks are required to ensure exclusive access.
  • Implementation method: It can be implemented by using the synchronized keyword or the specific implementation of the Lock interface (such as ReentrantLock).
  • Application scenario: It is suitable for scenarios with frequent write operations, because it can ensure data consistency and thread safety.
code example
import java.util.concurrent.locks.ReentrantLock;

public class PessimisticLockExample {
    
    
    private static int sharedData = 0;
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
    
    
        // 创建两个线程并启动
        Thread thread1 = new Thread(() -> {
    
    
            lock.lock(); // 获取锁
            try {
    
    
                int newValue = sharedData + 1; // 对共享数据进行修改
                // 模拟耗时操作
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                sharedData = newValue; // 提交修改
                System.out.println("Thread 1: Shared data updated to " + sharedData);
            } finally {
    
    
                lock.unlock(); // 释放锁
            }
        });

        Thread thread2 = new Thread(() -> {
    
    
            lock.lock(); // 获取锁
            try {
    
    
                int newValue = sharedData + 1; // 对共享数据进行修改
                // 模拟耗时操作
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                sharedData = newValue; // 提交修改
                System.out.println("Thread 2: Shared data updated to " + sharedData);
            } finally {
    
    
                lock.unlock(); // 释放锁
            }
        });

        thread1.start();
        thread2.start();
    }
}

In this example, we use ReentrantLockthe class to implement pessimistic locking. First, we create an object called lockto ReentrantLockprotect the shared data. Then, we create two threads, call lock()the method to acquire the lock in the code block involving shared data, and call the method to release the lock after modifying the shared data unlock().

In this example, pessimistic locking is used by explicitly acquiring and releasing locks. When a thread acquires a lock, other threads are blocked until the lock is released. This can ensure that only one thread can access shared resources at the same time, ensuring data consistency and thread safety.

3. Reentrant Lock

  • Principle: A reentrant lock is a special type of lock that allows the same thread to acquire the lock multiple times, also known as reentrancy. When a thread already holds a lock, it is allowed to request to acquire the lock again without causing the thread to be blocked. This mechanism can avoid deadlock.
  • Implementation method: In Java, the ReentrantLock class and the synchronized keyword are all implementations of reentrant locks.
  • Application scenario: It is suitable for scenarios where a thread needs to recursively call a synchronization method or code block to improve the flexibility of the code.
code example
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    
    
    private static int sharedData = 0;
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
    
    
        // 创建两个线程并启动
        Thread thread1 = new Thread(() -> {
    
    
            lock.lock(); // 获取锁
            try {
    
    
                int newValue = sharedData + 1; // 对共享数据进行修改
                // 模拟耗时操作
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                sharedData = newValue; // 提交修改
                System.out.println("Thread 1: Shared data updated to " + sharedData);
                updateSharedData(); // 调用可重入方法
            } finally {
    
    
                lock.unlock(); // 释放锁
            }
        });

        Thread thread2 = new Thread(() -> {
    
    
            lock.lock(); // 获取锁
            try {
    
    
                int newValue = sharedData + 1; // 对共享数据进行修改
                // 模拟耗时操作
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                sharedData = newValue; // 提交修改
                System.out.println("Thread 2: Shared data updated to " + sharedData);
                updateSharedData(); // 调用可重入方法
            } finally {
    
    
                lock.unlock(); // 释放锁
            }
        });

        thread1.start();
        thread2.start();
    }

    private static void updateSharedData() {
    
    
        lock.lock(); // 获取锁
        try {
    
    
            int newValue = sharedData + 1; // 对共享数据进行修改
            // 模拟耗时操作
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            sharedData = newValue; // 提交修改
            System.out.println("Shared data updated inside the reentrant method to " + sharedData);
        } finally {
    
    
            lock.unlock(); // 释放锁
        }
    }
}

In this example, we use ReentrantLockthe class to implement reentrant locks. First, we create an object called lockto ReentrantLockprotect the shared data. Then, we create two threads, call lock()the method to acquire the lock in the code block involving shared data, and call the method to release the lock after modifying the shared data unlock().

It is worth noting that reentrant locks allow the same thread to acquire the lock multiple times. updateSharedData()In the example, after thread 1 acquires the lock, the method is called during the modification of the shared data , and the lock also needs to be acquired in this method. Due to the characteristics of reentrant locks, thread 1 can acquire the lock again without causing a deadlock.

There are many ways to implement reentrant locks in Java, the most common of which are ReentrantLockclasses. Reentrant locks provide a flexible and powerful mechanism for managing and securing access to shared resources.

4. Fair Lock

  • Principle: Fair lock is a lock mechanism that guarantees that the order in which threads acquire locks is the same as the order in which they apply for locks. It will allocate lock resources according to the order of thread application, so as to prevent a thread from starvingly waiting for a lock.
  • Implementation method: You can use the constructor of the ReentrantLock class to specify a fair lock.
  • Application scenario: When multiple threads compete for the same resource, it is hoped to allocate lock resources fairly to avoid scenarios where a certain thread cannot acquire a lock for a long time.
import java.util.concurrent.locks.ReentrantLock;

public class FairLockExample {
    
    
    private static int sharedData = 0;
    private static ReentrantLock lock = new ReentrantLock(true); // 创建公平锁

    public static void main(String[] args) {
    
    
        // 创建两个线程并启动
        Thread thread1 = new Thread(() -> {
    
    
            lock.lock(); // 获取锁
            try {
    
    
                int newValue = sharedData + 1; // 对共享数据进行修改
                // 模拟耗时操作
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                sharedData = newValue; // 提交修改
                System.out.println("Thread 1: Shared data updated to " + sharedData);
            } finally {
    
    
                lock.unlock(); // 释放锁
            }
        });

        Thread thread2 = new Thread(() -> {
    
    
            lock.lock(); // 获取锁
            try {
    
    
                int newValue = sharedData + 1; // 对共享数据进行修改
                // 模拟耗时操作
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                sharedData = newValue; // 提交修改
                System.out.println("Thread 2: Shared data updated to " + sharedData);
            } finally {
    
    
                lock.unlock(); // 释放锁
            }
        });

        thread1.start();
        thread2.start();
    }
}

In this example, we use ReentrantLockthe class to implement fair locks. By ReentrantLockpassing the parameter when creating the object true, we create a fair lock, that is, the thread that waits the longest gets the lock first.

In a fair lock, when multiple threads compete for the same lock, the lock is assigned to them in the order in which the threads waited. This ensures that threads waiting earlier get the lock first, avoiding a starvation situation where some threads are consistently unable to get the lock.

It should be noted that fair lock may sacrifice certain performance, because it needs to maintain a queue to manage waiting threads. Therefore, when performance requirements are high and there are no special requirements, unfair locks can be used.

In actual development, the selection of fair locks should be comprehensively considered based on specific business needs and performance requirements.

5. Mutex

  • Principle: A mutex is a lock mechanism used to protect shared resources from concurrent access. It ensures that only one thread can execute the protected code at the same time by locking the code block or method.
  • Implementation method: In Java, you can use the synchronized keyword or the ReentrantLock class to implement a mutex.
  • Application Scenario: It is suitable for scenarios that need to protect the code in the critical section to ensure data consistency and thread safety.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MutexLockExample {
    
    
    private static int sharedData = 0;
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
    
    
        // 创建两个线程并启动
        Thread thread1 = new Thread(() -> {
    
    
            lock.lock(); // 获取锁
            try {
    
    
                int newValue = sharedData + 1; // 对共享数据进行修改
                // 模拟耗时操作
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                sharedData = newValue; // 提交修改
                System.out.println("Thread 1: Shared data updated to " + sharedData);
            } finally {
    
    
                lock.unlock(); // 释放锁
            }
        });

        Thread thread2 = new Thread(() -> {
    
    
            lock.lock(); // 获取锁
            try {
    
    
                int newValue = sharedData + 1; // 对共享数据进行修改
                // 模拟耗时操作
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                sharedData = newValue; // 提交修改
                System.out.println("Thread 2: Shared data updated to " + sharedData);
            } finally {
    
    
                lock.unlock(); // 释放锁
            }
        });

        thread1.start();
        thread2.start();
    }
}

In this example, we use ReentrantLockthe class to implement a mutex. By creating a ReentrantLockobject, we obtain a reentrant lock. This means that the same thread can acquire the same lock multiple times without deadlock.

In the example, when a thread acquires the lock, other threads will be blocked until the lock is released. This ensures that only one thread can modify shared data at a time, thereby avoiding data races and concurrency issues.

It should be noted that when using a mutex, be sure to call unlock()the method at an appropriate place to release the lock to avoid deadlock and resource leaks.

Mutex locks are a common and effective mechanism for protecting shared resources and are widely used in concurrent programming. The use of mutexes can ensure the consistency and thread safety of shared data.

Six, spin lock (Spin Lock)

  • Principle: Spin lock is a busy-waiting lock mechanism. When a thread tries to acquire a lock, it will not block immediately. Instead, it will check the state of the lock in a loop until the lock is successfully acquired or the maximum number of spins is reached.
  • Implementation method: In Java, you can use the compareAndSet method of the AtomicInteger class to implement a simple spin lock.
  • Application scenario: It is suitable for situations where the lock holding time is very short, avoiding frequent blocking and waking up of threads.
import java.util.concurrent.atomic.AtomicBoolean;

public class SpinLockExample {
    
    
    private static int sharedData = 0;
    private static AtomicBoolean lock = new AtomicBoolean(false);

    public static void main(String[] args) {
    
    
        // 创建两个线程并启动
        Thread thread1 = new Thread(() -> {
    
    
            while (!lock.compareAndSet(false, true)) {
    
    
                // 自旋等待锁释放
            }
            try {
    
    
                int newValue = sharedData + 1; // 对共享数据进行修改
                // 模拟耗时操作
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                sharedData = newValue; // 提交修改
                System.out.println("Thread 1: Shared data updated to " + sharedData);
            } finally {
    
    
                lock.set(false); // 释放锁
            }
        });

        Thread thread2 = new Thread(() -> {
    
    
            while (!lock.compareAndSet(false, true)) {
    
    
                // 自旋等待锁释放
            }
            try {
    
    
                int newValue = sharedData + 1; // 对共享数据进行修改
                // 模拟耗时操作
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                sharedData = newValue; // 提交修改
                System.out.println("Thread 2: Shared data updated to " + sharedData);
            } finally {
    
    
                lock.set(false); // 释放锁
            }
        });

        thread1.start();
        thread2.start();
    }
}

In this example, we use AtomicBooleanthe class to implement a spinlock. A spin lock is a lock mechanism that repeatedly checks the state of the lock when acquiring the lock.

In the example, each thread uses compareAndSetthe method to try to acquire the lock. If the current state of the lock is unlocked ( false), set it to locked ( true), thus successfully acquiring the lock. If the lock has been acquired by other threads, it will spin and wait until the lock is released.

It should be noted that when using spin locks, deadlocks and livelocks should be avoided. Therefore, it is very important to choose the number of spins and the spin waiting time reasonably. Too long spin time may cause performance degradation, while too short spin time may cause excessive thread switching overhead.

Spin locks are suitable for scenarios where the access time to shared data is short and the competition is not very intense. In high concurrency situations, spin locks may waste CPU resources. Therefore, it is necessary to consider whether to use spin locks based on specific business scenarios and performance requirements.

7. Latch

  • Rationale: A lock is a synchronization tool used to wait for other threads to complete operations. It allows one or more threads to wait for other threads to complete specific tasks before continuing.
  • Implementation method: In Java, CountDownLatch and CyclicBarrier are common locking implementations.
  • Application scenario: It is suitable for scenarios that need to wait for other threads to complete a task before continuing to execute.
import java.util.concurrent.CountDownLatch;

public class CountdownLatchExample {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        int workerCount = 3; // 工作线程数目

        CountDownLatch latch = new CountDownLatch(workerCount);

        // 创建工作线程并启动
        for (int i = 0; i < workerCount; i++) {
    
    
            Thread thread = new Thread(() -> {
    
    
                // 模拟工作
                System.out.println("Worker thread start");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println("Worker thread finish");

                latch.countDown(); // 工作完成,计数减一
            });
            thread.start();
        }

        System.out.println("Main thread waiting for workers to finish");
        latch.await(); // 主线程等待所有工作线程完成
        System.out.println("All workers have finished");

        // 继续主线程的后续操作
    }
}

In this example, we use CountDownLatchthe class to implement the latch. A lock is a synchronization tool that enables one or more threads to wait for other threads to complete some operations before proceeding.

In the example, the main thread first creates an CountDownLatchobject and specifies the number of worker threads to wait for workerCount. The main thread then creates multiple worker threads and calls countDown()the method at the beginning and end of each worker thread to indicate that the work is complete.

The main thread will be blocked after calling await()the method until the counter is reduced to zero, that is, all worker threads have completed their work. Then, the main thread can continue to perform the next operation.

Locking is suitable for scenarios where a group of threads need to wait for a certain condition to be met before continuing to execute at the same time. Through locking, you can better control the concurrent execution of threads.

Eight, semaphore (Semaphore)

  • Principle: A semaphore is a synchronization tool used to control the number of threads accessing a resource at the same time. It can specify the number of threads that can access resources at the same time, and provides a mechanism for acquiring and releasing permissions.
  • Implementation method: In Java, the Semaphore class is the implementation of a semaphore.
  • Application scenario: Applicable to scenarios that need to limit the number of threads that concurrently access a resource or control traffic.
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    
    
    public static void main(String[] args) {
    
    
        int workerCount = 5; // 工作线程数目
        Semaphore semaphore = new Semaphore(2); // 信号量,初始许可证数量为2

        // 创建工作线程并启动
        for (int i = 0; i < workerCount; i++) {
    
    
            Thread thread = new Thread(() -> {
    
    
                try {
    
    
                    semaphore.acquire(); // 获取许可证,如果没有可用的许可证,则阻塞等待
                    System.out.println("Worker thread start");
                    // 模拟工作
                    Thread.sleep(1000);
                    System.out.println("Worker thread finish");
                    semaphore.release(); // 释放许可证
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            });
            thread.start();
        }
    }
}

In this example, we use Semaphorethe class to implement semaphores. A semaphore is a synchronization tool that controls the amount of access to a resource.

In the example, a semaphore ( semaphore) is created with an initial license quantity of 2. Then, multiple worker threads are created and started.

Each worker thread calls the method to obtain a license before starting work acquire(). If a license is currently available, the thread acquires a license and continues to perform work. If no license is currently available, the thread blocks and waits until another thread releases a license.

After the worker thread completes the work, it calls release()the method to release the license, so that other waiting threads can obtain the license and continue to perform work.

Through semaphores, we can control the number of threads that access a certain resource at the same time, and realize the control and limitation of concurrent access.

Guess you like

Origin blog.csdn.net/u012581020/article/details/132211685
Recommended