欢迎光临
我们一直在努力

java并发编程(四)可重入锁的基础

一、Lock和 synchronized 的异同点

不同点:

对比维度synchronizedLock(以 ReentrantLock 为例)
锁的性质 隐式锁(自动获取 / 释放) 显式锁(手动 lock()/unlock())
释放方式 代码块执行完毕 / 抛出异常自动释放 必须手动在 finally 中释放,否则易死锁
锁类型 仅支持非公平锁(部分 JDK 版本默认偏向锁 / 轻量级锁) 支持公平锁 / 非公平锁(可构造函数指定)
获取锁失败 只能阻塞等待,无法中断 支持 tryLock() 非阻塞获取、lockInterruptibly() 可中断获取
条件变量 仅支持一个条件队列(wait()/notify()) 支持多个条件队列(Condition),可精准唤醒线程
性能 JDK 1.6 后优化(偏向锁 / 轻量级锁),低竞争下性能接近 Lock 高竞争场景下性能更优,灵活度更高
可重入性 支持可重入 支持可重入(ReentrantLock 命名来源)
是否可尝试获取 不支持 支持 tryLock(long timeout, TimeUnit) 超时获取

相同点:
•1.ReentrantLock和synchronized都是独占锁, 只允许线程互斥的访问临界区。但是实现上两者不同: synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
•2.ReentrantLock和synchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

二、CAS vs 同步锁的应用场景

1.CAS 的使用前提

  • 操作必须是单一变量的原子操作(无法直接实现多变量的原子性);
  • 并发冲突概率低(1000 个线程同时更新同一个 AtomicInteger,CAS 自旋重试的开销远大于 ReentrantLock 的阻塞开销。);
  • 无需高级功能(如中断、超时、公平性),仅需 “成功更新” 或 “失败重试”。

2.同步锁适用场景

同步锁适用场景:
多变量 / 复杂逻辑、高冲突、需要阻塞 / 中断 / 超时等高级功能、临界区含阻塞操作;
(若临界区内有 IO 操作(如读写文件、网络请求)、Thread.sleep() 等阻塞操作,用 CAS 会导致自旋重试时一直占用 CPU(阻塞操作期间无法释放),而锁会释放 CPU 让其他线程执行,更合理。)

三、ReentrantLock的核心概念

1)可中断锁
响应中断的锁,Lock是可中断锁(Lock.lock()是死等,LOCK.lockInterruptibly();这个方法支持在等待锁的时候响应中断),synchronized不是。如果线程A正在执行锁中代码,线程B正在等待获取该锁。时间太长,线程B不想等了,可以让它中断自己。
2)公平锁
尽量以请求锁的顺序获取锁。比如同时有多个线程在等待一个锁,当锁被释放后,等待时间最长的获取该锁,跟京牌司法拍卖一个道理。非公平锁可能会导致有些线程永远得不到锁,synchronized是非公平锁,ReentrantLock是选非公平锁和公平锁。
3)读写锁
读写锁将对一个资源(如文件)的访问分为2个锁,一个读锁,一个写锁;读写锁使得多个线程的读操作可以并发进行,不需同步。而写操作就得需要同步,提高了效率
ReadWriteLock就是读写锁,是一个接口,ReentrantReadWriteLock实现了这个接口。可通过readLock()获取读锁,writeLock()获取写锁
4)绑定多个条件
一个ReentrantLock可以绑定多个Condition对象,仅需多次调用new Condition()即可;而在synchronized中锁锁对象的wait()、notify()/notifyAll()可以实现一个隐含的条件,如果要和多余的条件关联,就不得不额外的增加一个锁。

四、ReentrantLock公平锁和非公平锁

公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。在创建ReentrantLock的时候通过传进参数true创建公平锁,如果传入的是false或没传参数则创建的是非公平锁。
在这里插入图片描述
可以看到公平锁和非公平锁的实现关键在于成员变量sync的实现不同,这是锁实现互斥同步的核心。

总结

锁类型核心特性核心适用场景性能典型实现
公平锁 先到先得,无插队,顺序可控 金融交易、排队业务、有序写入、合规系统 低(吞吐量低 10%~30%) ReentrantLock(true)
非公平锁 先抢占后排队,允许插队,顺序不可控 互联网核心业务、中间件、缓存、高并发轻量操作 高(吞吐量 / 延迟更优) ReentrantLock(false)(默认)、synchronized、MySQL/Redis 锁

一句话结论:90% 以上的业务场景用非公平锁即可,仅当业务对执行顺序有强要求时,才选择公平锁。

五、ReentrantLock解决死锁

1.lockInterruptibly()

当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。**而ReentrantLock给我们提供了一个在等待锁的时候,可以响应中断的获取锁的方法*lockInterruptibly()。该方法可以用来解决死锁问题,避免长时间等待。**因为被中断的线程可以在finally中进行释放锁。

线程状态被中断后的行为
正在等待锁(未获取到) 立即停止等待,抛出 InterruptedException
已经获取到锁(持有锁) 不会立即抛出异常,但线程的「中断标记」会被置为 true;只有当线程再次调用 lockInterruptibly()/wait()/sleep() 等可中断方法时,才会触发异常

public class ReetrantLockTest3 {

static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {

Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
thread.start();
thread1.start();
thread.interrupt();//是第一个线程中断
}

static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
firstLock.lockInterruptibly();
TimeUnit.MILLISECONDS.sleep(10);//更好的触发死锁
secondLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"正常结束!");
}
}
}
}

在这里插入图片描述
构造死锁场景:创建两个子线程,子线程在运行时会分别尝试获取两把锁。其中一个线程先获取锁1在获取锁2,另一个线程正好相反。如果没有外界中断,该程序将处于死锁状态永远无法停止。**我们通过使其中一个线程中断,来结束线程间毫无意义的等待。**被中断的线程将抛出异常,释放锁,而另一个线程将能获取锁后正常结束。

2.获取锁限时等待:tryLock()

ReentrantLock还给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。

public class ReetrantLockTest4 {

static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();

public static void main(String[] args) throws InterruptedException {

Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
thread.start();
thread1.start();
}

static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;

public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}

@Override
public void run() {
try {
// 第一步:循环尝试获取第一个锁,获取不到就休眠10ms重试
while (!firstLock.tryLock()) {
TimeUnit.MILLISECONDS.sleep(10);
System.out.println(Thread.currentThread().getName() + "尝试获取锁1失败,正在重试…");
}
System.out.println(Thread.currentThread().getName() + "成功获取锁1");
// 第二步:循环尝试获取第二个锁
while (!secondLock.tryLock()) {
// 核心:获取第二个锁失败 → 立即释放第一个锁(避免持有锁等待)
firstLock.unlock();
System.out.println(Thread.currentThread().getName() + "未获取到锁2,释放锁1");
// 循环尝试获取第二个锁,获取不到就休眠10ms重试
TimeUnit.MILLISECONDS.sleep(10);
System.out.println(Thread.currentThread().getName() + "尝试获取锁2失败,正在重试…");
}
System.out.println(Thread.currentThread().getName() + "成功获取锁2");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName() + "正常结束!");
}
}
}
}

在这里插入图片描述
Thread-0获取到自己的第一个锁lock1,Thread-1获取到自己的第一个锁lock2,这个时候Thread-0尝试获取到自己的第二个锁lock2,但是此时lock2被Thread-1占用,所以他会释放自己的第一个锁lock1,这个时候Thread-1尝试获取到自己的第二个锁lock1,因为Thread-0释放了自己的第一个锁lock1,所以Thread-1成功获取到所有的锁并正常结束。
而Thread-0一直在尝试获取到自己的第二个锁lock2,直到Thread-1成功获取到所有的锁并正常结束,Thread-0获取到自己的第二个锁lock2,但是因为他已经释放了第一个锁,所以在finally里面对第一个锁释放的时候会报错。

六、结合Condition实现线程等待通知

Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁。之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用该Condition的signal()方法唤醒线程。使用方式和wait,notify类似。
阻塞队列是一种特殊的先进先出队列,它有以下几个特点:
1.入队和出队线程安全
2.当队列满时,入队线程会被阻塞;当队列为空时,出队线程会被阻塞。

public class ReetrantLockTest6 {

static class TxBlockQueue<E>{
//队列容量
private int size ;

private LinkedList<E> list = new LinkedList<>();

private ReentrantLock lock = new ReentrantLock();

private Condition empty = lock.newCondition();
private Condition full = lock.newCondition();

public TxBlockQueue(int size) {
this.size = size;
}

public void enqueue(E e){
lock.lock();
try {
while (list.size() == size){
full.await();
}
list.add(e);
System.out.println("进队列:"+e);
empty.signal();
} catch (InterruptedException e1) {
e1.printStackTrace();
} finally {
lock.unlock();
}
}

public E dequeue(){
E e = null;
lock.lock();
try {
while (list.size() == 0){
empty.await();
}
e = list.removeFirst();
System.out.println("出队列:"+e);
full.signal();

} catch (InterruptedException e1) {
e1.printStackTrace();
} finally {
lock.unlock();
}
return e;
}

}

public static void main(String[] args) {
TxBlockQueue<Integer> queue = new TxBlockQueue<>(2);
for (int i = 0; i < 10; i++) {
int data = i;
new Thread(){
@Override
public void run() {
queue.enqueue(data);
}
}.start();
}

for (int i = 0; i < 10; i++) {
int data = i;
new Thread(){
@Override
public void run() {
queue.dequeue();
}
}.start();
}

}
}

赞(0)
未经允许不得转载:171主机测评 » java并发编程(四)可重入锁的基础
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址