Java 中的Lock锁对象(ReentrantLock/ReentrantReadWriteLock)详解

目录

1、Lock Objects 详解

2、Java 中的 Lock Objects 的实现原理

3、ReentrantLock 详解

4、ReentrantReadWriteLock 详解

5、Lock锁的等待和唤醒

6、Lock 和 synchronized 的异同


1、Lock Objects 详解

        Java 中的 Lock Objects 是用于线程同步的机制,它们允许多个线程同时访问共享资源,并确保线程安全。与 synchronized 块相比,Lock Objects 提供了更多的灵活性和控制权

        Lock Objects 可以分为两种类型:ReentrantLock 和 ReentrantReadWriteLock。

  • 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 的三个独有功能时(等待可中断,实现公平锁,条件通知)

2、Java 中的 Lock Objects 的实现原理

        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 时需要谨慎并严格遵守最佳实践。

3、ReentrantLock 详解

        ReentrantLock 是 Java 并发包中提供的一种可重入的独占锁,它可以用来代替 synchronized 关键字进行同步操作。与 synchronized 关键字相比,ReentrantLock 提供了更多的扩展功能,例如可以中断等待锁的线程、可以尝试非阻塞地获取锁、可以限时地等待锁等

        ReentrantLock 的基本使用方法如下:

        (1)创建 ReentrantLock 对象:

Lock lock = new ReentrantLock();

        (2)在需要同步的代码块前后加上 lock() 和 unlock() 方法:

lock.lock();
try {
    // 同步代码块
} finally {
    lock.unlock();
}

        在使用 ReentrantLock 进行同步时,需要注意以下几点:

  1. ReentrantLock 是可重入锁,即同一个线程可以多次获取同一把锁,这样可以避免死锁的发生。但是,要注意在每次获取锁后要及时释放锁,否则会导致其他线程无法获取到锁而发生死锁。
  2. 当使用 ReentrantLock 进行同步时,需要显式地调用 lock() 方法来获取锁,然后在 finally 块中调用 unlock() 方法释放锁。如果在加锁之后没有正确释放锁,就会导致其他线程无法获取到锁而一直处于等待状态。
  3. ReentrantLock 提供了 tryLock() 方法来尝试非阻塞地获取锁,如果获取成功就返回 true,否则返回 false。这个方法可以用来避免线程因为获取不到锁而一直阻塞等待的情况,从而提高程序的效率。
  4. ReentrantLock 还提供了 lockInterruptibly() 方法来支持中断等待锁的线程的操作。如果一个线程正在等待获取锁的过程中,另外一个线程调用了该线程的 interrupt() 方法,那么该线程就会抛出 InterruptedException 异常,从而退出等待状态。
  5. ReentrantLock 还提供了 tryLock(long time, TimeUnit unit) 方法来支持限时等待锁的操作。如果在指定的时间内无法获取到锁,就会返回 false,否则返回 true。这个方法可以用来避免线程因为等待锁而一直阻塞的情况,从而提高程序的效率。

        ReentrantLock 是一个非常实用的锁,它提供了更多的扩展功能和更好的性能表现,可以在多线程编程中发挥很大的作用。但是,在使用 ReentrantLock 时也需要注意正确地使用和释放锁,以避免出现死锁等问题。// 使用Lock对象最应该注意的就是需要手动释放锁

4、ReentrantReadWriteLock 详解

        ReentrantReadWriteLock 是 Java 并发包中提供的一种锁机制,它支持读写锁分离的机制,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。它实现了 Lock 接口,因此可以作为替代 synchronized 关键字的锁机制。

        ReentrantReadWriteLock 由两个锁组成:读锁和写锁。读锁可以被多个线程同时获取,但是写锁必须独占,也就是说,在任意时刻只能有一个线程获取到写锁。// 一个资源被读锁占据,必须要等待改资源的所有读锁都释放,才能够获取写锁去写数据。

        ReentrantReadWriteLock 的主要特点包括:

  1. 支持多个读线程同时访问共享资源,从而提高并发性能。
  2. 写操作是互斥的,只允许一个线程进行写操作,从而保证数据一致性和安全性。
  3. 支持重入,即同一线程可以多次获取读锁或写锁。
  4. 支持锁降级,即一个线程先获取了写锁,然后再获取读锁,最后释放写锁,这样可以避免线程阻塞,提高并发性能。

        使用 ReentrantReadWriteLock 时需要注意以下几点:

  1. 写锁必须独占,因此如果读线程很多,可能会导致写线程一直等待,从而影响性能。
  2. 写锁可能导致饥饿现象,即某些读线程可能永远无法获取到读锁。
  3. 由于 ReentrantReadWriteLock 是基于内部类 Sync 实现的,因此在使用时需要注意 Sync 类中的方法和属性的访问权限。

        下面是一个示例代码:

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]

5、Lock锁的等待和唤醒

        在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()方法类似

6、Lock 和 synchronized 的异同

        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 关键字的优势主要体现在以下两个方面:

  1. 粒度控制:Lock 锁提供了更细粒度的控制,可以灵活地控制锁的获取和释放。相比之下,synchronized 关键字只能对整个方法或者整个代码块进行加锁,无法进行更细粒度的控制。
  2. 非阻塞加锁:在某些情况下,Lock 锁可以采用非阻塞的方式进行加锁,避免了线程进入内核模式造成的性能损失。相比之下,synchronized 关键字的加锁过程是阻塞式的,一旦一个线程获取到了锁,其他线程就必须等待这个线程释放锁才能继续执行。

        需要注意的是,Lock 锁相对于 synchronized 关键字也存在一些劣势,例如:

  1. 代码复杂度:相对于 synchronized 关键字,Lock 锁的使用方法更为复杂,需要显式地获取和释放锁。
  2. 内存消耗:Lock 锁需要占用额外的内存空间来存储锁对象等信息。
  3. 可读性:Lock 锁的代码相对于 synchronized 关键字可能更加冗长,可读性稍差。

        因此,在实际开发中应该根据具体情况选择合适的锁机制。一般来说,如果只是简单的线程同步,使用 synchronized 关键字已经足够;如果需要更细粒度的控制或者需要避免线程阻塞,可以考虑使用 Lock 锁。

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

swadian2008

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值