Lock与Condition

0. 用锁的最佳实践

  • 永远只在更新对象的成员变量时加锁

  • 永远只在访问可变的成员变量时加锁

  • 永远不在调用其他对象的方法时加锁

1. Lock与Synchronize区别

  • Lock是由代码实现,核心是CAS操作;synchronize则是关键字,通过修改对象头中的锁信息,由JVM实现调用。更详细的底层原理实现可见Java多线程——Lock和Synchronized底层原理比较synchronized和lock的区别(底层实现)

  • 由于Lock由代码实现,故需要在finally语句块中显示关闭锁,避免异常情况资源不释放。synchronize则会在线程崩溃时,由JVM自动释放锁资源。

  • synchronize在JDK1.5之前是重量级锁,每次都需要像操作系统申请资源,而JDK1.6后优化 synchronized 的性能给它的锁加入了四种状态,无锁状态 -> 偏向锁 -> 轻量级锁 -> 重量级锁(MarkWord储存状态中就可以看到),故后续性能和Lock差不多。

  • synchronize通常和object.wait()、object.notify()、object.notifyAll()搭配使用,没有显示的条件变量控制;Lock提供了更精细化的锁粒度,可在Lock.lock()及Lock.unlock()块中,通过Condition条件变量类搭配condition.await()、condition.signal()、condition.signalAll()使用。

  • Lock支持超时等待机制,synchronize不支持

2. Lock的引入

JDK1.5版本引入了java.util.concurrent.locks包,包含了Lock接口及其实现类。在Java已有管程实现的synchronize的基础下,作为引入Lock类的推动力,可以看看优化点:

  1. synchronize不支持超时等待,故在死锁原因层面,无法针对资源不可剥夺这个条件进行防范
  2. synchronize不支持显式的条件变量,无法更精细的控制并发粒度
  3. synchronize不能响应中断,Lock的lockInterruptibly()方法支持中断
  4. synchronize不支持获取锁失败时不进入阻塞状态,Lock的tryLock()方法支持
  5. 更好的性能(jdk1.6已对synchronize优化性能)

故,Java自JDK1.5版本后有了两种对于管程的实现。

3. Lock使用范例

class LockTest {
  private final Lock lock = new ReentrantLock();
  private int val;

  public void add() {
    lock.lock();
    try {
      val += 1;
    } finally {
      lock.unlock(); //必须在finally块中加unlock()操作
    }
  }
}

共享变量val的可见性保证分析如下。首先参考Lock相关类的类图
image

ReentrantLock继承自AbstractQueuedSynchronizer类,而AbstractQueuedSynchronizer类中有一个状态值state

// The synchronization status
private volatile int state;

可以看到state使用了volatile进行修饰,lock的时候会对state及unlock都会进行读写。利用JMM先行发生(Happens-Before)的规则(具体可参考Java内存模型:Java解决可见性和有序性问题的方案):

  1. 顺序性规则
    针对线程A,val += 1Happens-Before于lock.unlock()
  2. volatile变量规则
    针对volatile变量的读,永远在写之后。那么线程A的lock.unlock()Happens-Before于线程B的lock.lock()操作
  3. 传递性规则
    综合1和2的顺序,那么线程A的val += 1Happens-Before于线程B的val += 1

当前volatile只是保证了可见性,不同线程若是同时读到最新值state值并写入,还是存在并发问题。互斥性就由lock()底层中的CAS自旋操作来保证。

4. 异步转同步

某些请求是异步的,可以考虑通过回调函数及条件变量,将异步转为同步。
模拟如下:

public class RequestAsyncToSync {
    private final Lock lock = new ReentrantLock();
    private final Condition done = lock.newCondition(); // 接收完成条件变量
    private String resp; // 模拟响应对象

    // 模拟请求,两秒后响应并将数据写入到resp对象中
    public void request() {
        CompletableFuture<Void> asyncTask = CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(2000); // 模拟请求耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            resp = "Hello World"; // 模拟请求回调数据写回resp对象
            lock.lock();
            try {
                done.signalAll();
            } finally {
                lock.unlock();
            }
        });
    }

    // 判断是否完成
    public boolean isDone() {
        return !Objects.isNull(resp);
    }

    // 普通获取
    public String getResp() {
        return resp;
    }

    // 异步转同步等待获取
    public String getSyncResp() {
        lock.lock();
        try {
            while (!isDone()) {
                done.await();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        return resp;
    }

    public static void main(String[] args) {
        RequestAsyncToSync requestAsyncToSync = new RequestAsyncToSync();
        requestAsyncToSync.request();
        System.out.println("Resp:" + requestAsyncToSync.getResp()); // 打印Resp:null
        System.out.println("Resp:" + requestAsyncToSync.getSyncResp()); // 阻塞等待结果响应,并打印Resp:Hello World
    }
}

更多的可以深入解读CompletableFuture源码与原理,学习CompletableFuture中的异步转同步思想(CompletableFuture底层玩的都是CAS更新状态和回调函数,没有使用管程。但是Lock互斥性的实现底层也是CAS,可以一起了解下)。

posted @ 2023-11-28 17:18  kiper  阅读(9)  评论(0编辑  收藏  举报