赞
踩
目录
Java 中的 Lock Objects 是用于线程同步的机制,它们允许多个线程同时访问共享资源,并确保线程安全。与 synchronized 块相比,Lock Objects 提供了更多的灵活性和控制权。
Lock Objects 可以分为两种类型:ReentrantLock 和 ReentrantReadWriteLock。
以下是使用 ReentrantLock 实现线程同步的示例:
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
-
- public class SharedResource {
- private final Lock lock = new ReentrantLock();
- private int value;
-
- public void increment() {
- lock.lock(); // 获取锁
- try {
- value++;
- } finally {
- lock.unlock(); // 释放锁
- }
- }
-
- public int getValue() {
- lock.lock(); // 获取锁
- try {
- return value;
- } finally {
- lock.unlock(); // 释放锁
- }
- }
- }
在上面的示例中,SharedResource 类具有一个私有的 ReentrantLock 对象 lock,并且在 increment() 和 getValue() 方法中都使用了该锁来保证线程安全。在 increment() 方法中,线程会获取锁,并对共享变量 value 进行递增操作,最后释放锁。在 getValue() 方法中,线程会获取锁,返回共享变量 value 的值,最后释放锁。
使用 Lock Objects 的好处在于它们提供了更细粒度的控制,例如可以指定锁的公平性、超时时间等。同时,与 synchronized 块不同,Lock Objects 还提供了 tryLock() 方法,该方法会尝试获取锁,并立即返回结果。如果锁已被其他线程持有,则返回 false。这样,我们可以在等待锁的过程中做一些其他的操作,而不是一直阻塞等待锁的释放。// 利用等待时间
// Lock对象相对于隐式锁(synchronized)的最大优点是它们能够退出获取锁的尝试。
什么情况下使用 ReentrantLock?
需要使用 ReentrantLock 的三个独有功能时(等待可中断,实现公平锁,条件通知)
Java 中的 Lock Objects 实现原理主要依赖于 Java 的 AQS(AbstractQueuedSynchronizer)框架。AQS 是 Java 并发包中的一个基础框架,它提供了一种同步机制,允许自定义同步器的实现,同时提供了可重入锁和条件变量等常见的同步机制的实现。
ReentrantLock 和 ReentrantReadWriteLock 都是基于 AQS 实现的。它们的实现基本上都是通过维护一个等待队列,将线程放入等待队列中来实现线程同步的。// Semaphore、CountDownLatch 和 CyclicBarrier 等也是基于AQS 实现的
当一个线程尝试获取锁时,它会调用 Lock 对象的 lock() 方法。如果此时锁没有被其他线程占用,则该线程将成功获取到锁,否则该线程将进入等待队列中等待。当锁被释放时,等待队列中的线程将被唤醒,竞争锁的机会被重新分配。ReentrantLock 还支持可重入,即同一线程可以多次获取同一把锁而不被阻塞,这是通过维护一个计数器来实现的。// 可重入机制的实现
ReentrantReadWriteLock 的实现原理与 ReentrantLock 类似,但它采用了一种更加灵活的方式来支持读写操作的并发性。它维护了一个读锁和一个写锁,多个线程可以同时持有读锁,但只能有一个线程持有写锁。当有线程获取写锁时,读锁将被阻塞,直到写锁释放。当有线程获取读锁时,如果当前有线程持有写锁,则读锁将被阻塞,直到写锁释放。当有线程获取读锁时,如果当前没有线程持有写锁,则读锁将立即被获取,读锁计数器加一,表示当前有一个线程持有读锁。
Lock Objects 的实现原理比 synchronized 块更为复杂,但由于它们提供了更高的灵活性和控制力,因此在一些高并发场景下更为适用。但需要注意的是,由于 Lock Objects 的实现较为复杂,使用不当可能会带来一些潜在的问题,例如死锁、竞态条件等。因此,在使用 Lock Objects 时需要谨慎并严格遵守最佳实践。
ReentrantLock 是 Java 并发包中提供的一种可重入的独占锁,它可以用来代替 synchronized 关键字进行同步操作。与 synchronized 关键字相比,ReentrantLock 提供了更多的扩展功能,例如可以中断等待锁的线程、可以尝试非阻塞地获取锁、可以限时地等待锁等。
ReentrantLock 的基本使用方法如下:
(1)创建 ReentrantLock 对象:
Lock lock = new ReentrantLock();
(2)在需要同步的代码块前后加上 lock() 和 unlock() 方法:
- lock.lock();
- try {
- // 同步代码块
- } finally {
- lock.unlock();
- }
在使用 ReentrantLock 进行同步时,需要注意以下几点:
ReentrantLock 是一个非常实用的锁,它提供了更多的扩展功能和更好的性能表现,可以在多线程编程中发挥很大的作用。但是,在使用 ReentrantLock 时也需要注意正确地使用和释放锁,以避免出现死锁等问题。// 使用Lock对象最应该注意的就是需要手动释放锁
ReentrantReadWriteLock 是 Java 并发包中提供的一种锁机制,它支持读写锁分离的机制,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。它实现了 Lock 接口,因此可以作为替代 synchronized 关键字的锁机制。
ReentrantReadWriteLock 由两个锁组成:读锁和写锁。读锁可以被多个线程同时获取,但是写锁必须独占,也就是说,在任意时刻只能有一个线程获取到写锁。// 一个资源被读锁占据,必须要等待改资源的所有读锁都释放,才能够获取写锁去写数据。
ReentrantReadWriteLock 的主要特点包括:
使用 ReentrantReadWriteLock 时需要注意以下几点:
下面是一个示例代码:
- public class ReadWriteLockDemo {
- private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
- private final List<String> data = new ArrayList<>();
-
- public void readData() {
- lock.readLock().lock();
- try {
- // 读取共享数据
- System.out.println("read data: " + data);
- } finally {
- lock.readLock().unlock();
- }
- }
-
- public void writeData(String newData) {
- lock.writeLock().lock();
- try {
- // 写入共享数据
- data.add(newData);
- System.out.println("write data: " + newData);
- } finally {
- lock.writeLock().unlock();
- }
- }
- }
在上面的示例中,readData()方法获取读锁并读取共享数据,writeData()方法获取写锁并写入共享数据。注意,在获取锁之后,需要在finally语句块中释放锁,以确保锁总是能被正确释放。
ReentrantReadWriteLock的使用可以提高并发性能,特别是在读操作比写操作更频繁的场景中。但是,它也需要更多的内存和处理器时间来维护状态信息。在使用时需要根据实际场景选择适合的锁机制。
ReentrantReadWriteLock 的锁降级
锁降级是指先获取写锁,然后再获取读锁,最后释放写锁的过程。在这个过程中,线程可以先访问共享资源,然后放弃写权限,转而访问读资源。这样可以避免写操作期间读操作的阻塞,提高并发性能。// 由写锁降为读锁,释放写锁后,仍然持有读锁
下面是一个示例代码:
- public class LockDowngradeDemo {
- private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
- private final List<String> data = new ArrayList<>();
-
- public void writeData(String newData) {
- lock.writeLock().lock();
- try {
- // 写入共享数据
- data.add(newData);
- System.out.println("write data: " + newData);
-
- // 获取读锁
- lock.readLock().lock();
- } finally {
- // 释放写锁
- lock.writeLock().unlock();
- }
- }
-
- public void readData() {
- lock.readLock().lock();
- try {
- // 读取共享数据
- System.out.println("read data: " + data);
- } finally {
- // 释放读锁
- lock.readLock().unlock();
- }
- }
- }
在上面的示例中,writeData()方法先获取写锁,然后写入共享数据。接着,它获取读锁,释放写锁,这样就实现了锁降级。最后,readData()方法获取读锁并读取共享数据。
需要注意的是,在锁降级的过程中,线程必须先获取写锁,然后再获取读锁,这是因为读锁是共享锁,可以被多个线程同时持有。如果先获取读锁,再获取写锁,那么写锁就会一直被阻塞,可能导致死锁的发生。
另外,在实现锁降级的过程中,需要注意锁的释放顺序,即先释放写锁再释放读锁。这是因为写锁是独占锁,不能被多个线程同时持有,而读锁是共享锁,可以被多个线程同时持有。如果先释放读锁,可能会导致其他线程获取读锁而阻塞,无法释放写锁。因此,必须先释放写锁,再释放读锁。
锁降级过程需要注意的是,释放完写锁后,线程仍然持有读锁,如果此时读锁不释放,其他线程获取写锁时,将会被一致阻塞。下列程序演示了这一过程
- import java.util.ArrayList;
- import java.util.List;
- import java.util.concurrent.locks.ReentrantReadWriteLock;
-
- public class LockDowngradeDemo {
-
- private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
- private final List<String> data = new ArrayList<>();
-
- public void writeData(String newData) {
- lock.writeLock().lock();
- try {
- // 写入共享数据
- data.add(newData);
- System.out.println(Thread.currentThread().getName() + "-write data: " + newData);
-
- // 获取读锁
- lock.readLock().lock();
- } finally {
- // 释放写锁,此时线程仍持有读锁
- lock.writeLock().unlock();
- // 此处如果不释放读锁,其他线程获取写锁时将被阻塞,放开此段代码查看输出的区别
- // lock.readLock().unlock();
- }
- }
-
- public void readData() {
- lock.readLock().lock();
- try {
- // 读取共享数据
- System.out.println(Thread.currentThread().getName() + "-read data: " + data);
- } finally {
- // 释放读锁
- lock.readLock().unlock();
- }
- }
-
- public static class LockDowngradeTask implements Runnable {
-
- private LockDowngradeDemo downgradeDemo;
-
- private String writeData;
-
- public LockDowngradeTask(LockDowngradeDemo downgradeDemo, String writeData) {
- this.downgradeDemo = downgradeDemo;
- this.writeData = writeData;
- }
-
- @Override
- public void run() {
- downgradeDemo.writeData(writeData);
- downgradeDemo.readData();
- }
- }
-
- public static void main(String[] args) {
- LockDowngradeDemo downgradeDemo = new LockDowngradeDemo();
- LockDowngradeTask task1 = new LockDowngradeTask(downgradeDemo, "write a data");
- LockDowngradeTask task2 = new LockDowngradeTask(downgradeDemo, "write another data");
- new Thread(task1, "thread-1").start();
- new Thread(task2, "thread-2").start();
- }
- }
如果只是释放写锁,不释放读锁,线程2获取写锁时,将一直被阻塞。输出结果如下:
- thread-1-write data: write a data
- thread-1-read data: [write a data]
在Java中,Lock锁的等待和唤醒机制是由Condition对象实现的。Condition对象提供了类似于Object的wait和notify方法的等待和唤醒机制。
Lock锁中的Condition对象可以通过Lock对象的newCondition方法创建。线程可以通过调用Condition的await方法来等待某个条件满足,然后通过调用Condition的signal方法来唤醒等待在该条件上的线程。
下面是一个使用Lock的等待和唤醒的示例代码:
- import java.util.concurrent.locks.Condition;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
-
- public class LockConditionExample {
- private final Lock lock = new ReentrantLock();
- private final Condition condition = lock.newCondition();
- private volatile boolean flag = false;
-
- public void waitForFlag() throws InterruptedException {
- lock.lock();
- try {
- while (!flag) {
- condition.await();
- }
- } finally {
- lock.unlock();
- }
- }
-
- public void setFlag() {
- lock.lock();
- try {
- flag = true;
- condition.signalAll();
- } finally {
- lock.unlock();
- }
- }
-
- public static void main(String[] args) {
- LockConditionExample example = new LockConditionExample();
- new Thread(() -> {
- try {
- example.waitForFlag();
- System.out.println("Thread 1 is finished");
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }).start();
- new Thread(() -> {
- try {
- Thread.sleep(1000);
- example.setFlag();
- System.out.println("Thread 2 is finished");
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }).start();
- }
- }
在这个示例中,我们定义了一个Lock和一个Condition。waitForFlag方法获取锁,如果flag为false,则在Condition上等待。setFlag方法设置flag为true并唤醒在Condition上等待的线程。
在main方法中,我们创建了两个线程。第一个线程等待flag变为true,第二个线程在1秒后设置flag为true并唤醒第一个线程。当第一个线程被唤醒后,它将输出"Thread 1 is finished"。
这个示例展示了如何使用Lock和Condition实现线程之间的等待和唤醒机制。需要注意的是,在使用await()和signal()方法时,必须先获取到锁对象才能调用这些方法,否则会抛出IllegalMonitorStateException异常。// 这点和object类中的wait()和notify()方法类似
Lock 和 synchronized 的对比:
特性 | Lock | synchronized |
本质 | Lock锁是接口 | synchronized是关键字 |
作用范围 | 只能作用于代码块上 | 作用于方法和代码块上 |
底层 | 基于AQS,FIFO先进先出队列实现的 | 基于object Monitor对象锁来实现的 |
支持 | 支持公平锁和非公平锁 | 只支持非公平锁 |
加锁方式 | 非阻塞式加锁,并且支持可中断式加锁,支持超时时间加锁 | 阻塞式加锁 |
加锁和解锁 | Lock锁有一个同步队列和支持多个等待队列(condition) | 在加锁和解锁时,只有一个同步队列和一个等待队列 |
等待和唤醒 | lock锁使用的是condition接口的await()和signal()方法 | 使用的是object类中的wait()和notify()方法 |
(1)Lock锁需要用到内核模式吗?
Java 中的 Lock 锁通常不需要使用内核模式。相比于 synchronized 关键字,Lock 锁更多地依赖于用户空间的 CAS(Compare and Swap)操作和 volatile 关键字来实现锁的操作。CAS 操作是一种原子操作,它可以在不使用锁的情况下实现线程同步。volatile 关键字可以保证变量的可见性,从而保证锁的状态对所有线程可见。
具体地说,Java 中的 Lock 锁通常采用的是自旋锁的方式,即线程不断地尝试获取锁,如果获取失败就不断重试,直到获取到锁为止。这种方式避免了线程进入内核模式从而造成的性能损失。只有当自旋的次数达到一定的阈值,或者发现当前锁已经被其他线程占用时,线程才会进入内核模式进行等待。
需要注意的是,在某些特定的情况下,Java 中的 Lock 锁可能会使用到内核模式。例如,如果一个线程在尝试获取锁的过程中遇到了饥饿现象(即一直获取不到锁),那么系统可能会采用类似于睡眠的方式,让该线程暂时让出 CPU 资源,等待一段时间后再次尝试获取锁。这个过程可能会涉及到内核模式的操作。但是,这种情况只是极少数的情况,通常情况下 Java 中的 Lock 锁不会使用到内核模式。
(2)Lock锁比synchronized 的性能要高吗?
在某些情况下,使用 Lock 锁比 synchronized 关键字可以获得更高的性能,但并不是在所有情况下都是如此。具体来说,Lock 锁相对于 synchronized 关键字的优势主要体现在以下两个方面:
需要注意的是,Lock 锁相对于 synchronized 关键字也存在一些劣势,例如:
因此,在实际开发中应该根据具体情况选择合适的锁机制。一般来说,如果只是简单的线程同步,使用 synchronized 关键字已经足够;如果需要更细粒度的控制或者需要避免线程阻塞,可以考虑使用 Lock 锁。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。