(2.1.27.8)Java并发编程:Lock显示锁

版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/fei20121106/article/details/83268499

一、concurrent包的设计

要了解Java为我们提供的基于Lock接口(以及相关实现类)实现的锁功能,我们首先要看一下整个concurrent包下的设计。具体设计如下所示:

在这里插入图片描述
【concurrent包的设计】

在上图中,我们大致可以看出courrent包下的整体结构。整个包大致分为了三层。

  1. 高层:Lock、同步器、阻塞队列等。
  2. 中层:AQS(AbstractQueuedSynchronizer)、非阻塞数据结构、原子变量类(java.util.concurrent.atomic包中的类)。
  3. 底层:volatile变量的读/写、CAS操作。

其中每个层中的依赖关系也很明显,AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),都是基于底层实现,而高层类又依赖中层这些基础类

特别需要注意的是于Lock接口(以及相关实现类)相关的锁功能在整个高层中起着非常重要的重要。虽然没有直接在图中表述Lock接口在高层中的关系,但是在高层中我们所罗列的同步器、阻塞队列、并发容器等,或多或少都依赖或使用其Lock接口(以及相关实现类)实现的锁功能。

二、Lock接口(以及相关实现类)UML类图

总所周知锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁就能够防止多个线程同时访问共享资源(但是有些锁可以允许多线程并发的访问共享资源,比如我们后期将会讲解的读写锁)

在Lock接口出现之前,Java程序是靠synchronized关键字来实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口以及相关实现类,来实现锁的功能。

在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性,只是代码写法上有点区别,一个表现为API层面的互斥锁(lock()和unlock()方法配合try/finally语句块来完成),另一个表现为原生语法层面的互斥锁。

不过,相比synchronized,ReentrantLock增加了一些高级功能,主要有以下3项:

  • 等待可中断
    • 指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
  • 公平锁
    • 指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。
    • synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
  • 锁绑定多个条件
    • 指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或notifyAll()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而ReentrantLock则无须这样做,只需要多次调用newCondition()方法即可。

关于 Lock接口(以及相关实现类)的UML类图,具体如下所示:

在这里插入图片描述
【Lock接口(以及相关实现类)UML类图】

我们来简单分析下

  • Lock接口
    • 实现类: ReentrantLock
    • 实现类: WriteLock
    • 实现类: ReadLock

Lock的实现类普遍持有AQS的实现类,实质上lock的实现主要依赖AQS实现调度控制(主要实现并发阻塞),依赖Condition实现信号量控制(主要实现等待通知),依赖LockSupport实现底层控制

  • AbstractOwnableSynchronizer 抽象类
    • 实现类: AbstractQueuedLongSynchronizer
    • 实现类: AbstractQueuedLongSynchronizer

AbstractOwnableSynchronizer的实现类普遍持有 Condition的实现类,作为信号量控制

我们在下文进行逐一的分析,现在先给一个简单示例:

class Test2 {
    private static volatile int condition = 0;
    private static Lock lock = new ReentrantLock();
    private static Condition lockCondition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    while (!(condition == 1)) {
                        lockCondition.await();
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    lock.unlock();
                }
                System.out.println("a executed by condition");
            }
        });
        A.start();
        Thread.sleep(2000);
        condition = 1;
        lock.lock();
        try {
            lockCondition.signal();
        } finally {
            lock.unlock();
        }
    }
}

可以看到通过 lock.newCondition() 可以获得到 lock 对应的一个Condition对象lockCondition ,lockCondition的await()、signal()方法分别对应之前的Object的wait()和notify()方法。整体上和Object的等待通知是类似的。

三、Lock接口

方法名称 描述
void lock() 获取锁. 成功则向下运行,失败则阻塞
void lockInterruptibly() throws InterruptedException 可中断地获取锁,在当前线程获取锁的过程中可以响应中断信号
boolean tryLock() 尝试非阻塞获取锁,调用方法后立即返回,成功返回true,失败返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 在超时时间内获取锁,到达超时时间将返回false,也可以响应中断
void unlock(); 释放锁
Condition newCondition(); 获取等待通知组件实现信号控制,等待通知组件实现类似于Object.wait()方法的功能

从Lock提供的接口可以看出来,显示锁至少比synchronized多了以下功能:

  1. 可中断获取锁:使用synchronized关键字获取锁的时候,如果线程没有获取到被阻塞了,那么这个时候该线程是不响应中断(interrupt)的,而使用Lock.lockInterruptibly()获取锁时被中断,线程将抛出中断异常。
  2. 可非阻塞获取锁:使用sync关键字获取锁时,如果没有成功获取,只有被阻塞,而使用Lock.tryLock()获取锁时,如果没有获取成功也不会阻塞而是直接返回false。
  3. 可限定获取锁的超时时间:使用Lock.tryLock(long time, TimeUnit unit)。
  4. 其实显示锁还有其他的优势,比如同一锁对象上可以有多个等待通知队列(相当于Object.wait()),我们后面会讲。

其实除了更多的功能,显示锁还有一个很大的优势:synchronized的同步是jvm底层实现的,借助物理设备的指令控制,对一般程序员来说程序遇到出乎意料的行为的时候,除了查官方文档几乎没有别的办法;而显示锁除了个别操作用了底层的Unsafe类之外,几乎都是用java语言实现的

当然显示锁也不是完美的,否则java就不会保留着synchronized关键字了,显示锁的缺点主要有两个:

  1. 使用比较复杂,这点之前提到了,需要手动加锁,解锁,而且还必须保证在异常状态下也要能够解锁。而synchronized的使用就简单多了。
  2. 效率较低,synchronized关键字毕竟是jvm底层实现的,因此用了很多优化措施来优化速度(偏向锁、轻量锁等),而显示锁的效率相对低一些。

因此当需要进行同步时,优先考虑使用synchronized关键字,只有synchronized关键字不能满足需求时,才考虑使用显示锁。

猜你喜欢

转载自blog.csdn.net/fei20121106/article/details/83268499