目录
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 进行同步时,需要注意以下几点:
- ReentrantLock 是可重入锁,即同一个线程可以多次获取同一把锁,这样可以避免死锁的发生。但是,要注意在每次获取锁后要及时释放锁,否则会导致其他线程无法获取到锁而发生死锁。
- 当使用 ReentrantLock 进行同步时,需要显式地调用 lock() 方法来获取锁,然后在 finally 块中调用 unlock() 方法释放锁。如果在加锁之后没有正确释放锁,就会导致其他线程无法获取到锁而一直处于等待状态。
- ReentrantLock 提供了 tryLock() 方法来尝试非阻塞地获取锁,如果获取成功就返回 true,否则返回 false。这个方法可以用来避免线程因为获取不到锁而一直阻塞等待的情况,从而提高程序的效率。
- ReentrantLock 还提供了 lockInterruptibly() 方法来支持中断等待锁的线程的操作。如果一个线程正在等待获取锁的过程中,另外一个线程调用了该线程的 interrupt() 方法,那么该线程就会抛出 InterruptedException 异常,从而退出等待状态。
- ReentrantLock 还提供了 tryLock(long time, TimeUnit unit) 方法来支持限时等待锁的操作。如果在指定的时间内无法获取到锁,就会返回 false,否则返回 true。这个方法可以用来避免线程因为等待锁而一直阻塞的情况,从而提高程序的效率。
ReentrantLock 是一个非常实用的锁,它提供了更多的扩展功能和更好的性能表现,可以在多线程编程中发挥很大的作用。但是,在使用 ReentrantLock 时也需要注意正确地使用和释放锁,以避免出现死锁等问题。// 使用Lock对象最应该注意的就是需要手动释放锁
4、ReentrantReadWriteLock 详解
ReentrantReadWriteLock 是 Java 并发包中提供的一种锁机制,它支持读写锁分离的机制,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。它实现了 Lock 接口,因此可以作为替代 synchronized 关键字的锁机制。
ReentrantReadWriteLock 由两个锁组成:读锁和写锁。读锁可以被多个线程同时获取,但是写锁必须独占,也就是说,在任意时刻只能有一个线程获取到写锁。// 一个资源被读锁占据,必须要等待改资源的所有读锁都释放,才能够获取写锁去写数据。
ReentrantReadWriteLock 的主要特点包括:
- 支持多个读线程同时访问共享资源,从而提高并发性能。
- 写操作是互斥的,只允许一个线程进行写操作,从而保证数据一致性和安全性。
- 支持重入,即同一线程可以多次获取读锁或写锁。
- 支持锁降级,即一个线程先获取了写锁,然后再获取读锁,最后释放写锁,这样可以避免线程阻塞,提高并发性能。
使用 ReentrantReadWriteLock 时需要注意以下几点:
- 写锁必须独占,因此如果读线程很多,可能会导致写线程一直等待,从而影响性能。
- 写锁可能导致饥饿现象,即某些读线程可能永远无法获取到读锁。
- 由于 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 关键字的优势主要体现在以下两个方面:
-
粒度控制:Lock 锁提供了更细粒度的控制,可以灵活地控制锁的获取和释放。相比之下,synchronized 关键字只能对整个方法或者整个代码块进行加锁,无法进行更细粒度的控制。
-
非阻塞加锁:在某些情况下,Lock 锁可以采用非阻塞的方式进行加锁,避免了线程进入内核模式造成的性能损失。相比之下,synchronized 关键字的加锁过程是阻塞式的,一旦一个线程获取到了锁,其他线程就必须等待这个线程释放锁才能继续执行。
需要注意的是,Lock 锁相对于 synchronized 关键字也存在一些劣势,例如:
-
代码复杂度:相对于 synchronized 关键字,Lock 锁的使用方法更为复杂,需要显式地获取和释放锁。
-
内存消耗:Lock 锁需要占用额外的内存空间来存储锁对象等信息。
-
可读性:Lock 锁的代码相对于 synchronized 关键字可能更加冗长,可读性稍差。
因此,在实际开发中应该根据具体情况选择合适的锁机制。一般来说,如果只是简单的线程同步,使用 synchronized 关键字已经足够;如果需要更细粒度的控制或者需要避免线程阻塞,可以考虑使用 Lock 锁。