浅谈--Lock锁 与 Condition

一 Lock锁简介

1.用处:

Lock锁与synchronized一样,都是可以用来控制同步访问的。

2.有了synchronized,为什么还要Lock锁呢?

那就要谈到synchronized的缺点,主要是三个方面。

A 有时候用synchronized修饰的代码,访问它需要很长时间,下一个要访问同一代码块的线程就要等待阻塞很长的时间。如果我想要下一个线程在等待一段时间后,如果还没有得到锁的话,就放弃等待,这就可以使用Lock锁,来设置等待时间。

B synchronized 是互斥锁,同一时间只能有一个线程可以访问被它修饰的代码块。而Lock锁可以实现互斥锁,也可以实现共享锁(同一时间支持多条线程访问)。

C 有些情况下,获取与释放锁的情况比较复杂。比如:用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。在这种场景中synchronized关键字就不那么容易实现了,使用Lock接口容易很多

3.Lock锁的缺点:

     相比于synchronized,Lock需要手动的获取锁与释放锁。

4.Lock锁的高级特性:

A    尝试非阻塞地获取锁       当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁,如果当前时刻,锁被其它线程占有,则直接返回fasle;

B    能被中断地获取锁   在锁的获取过程中可以响应中断。调用 lockInterruptibly() 方法,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过 lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用 threadB.interrupt()方法能够中断线程B的等待过程。

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。

 而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

C    超时获取锁         在指定的截止时间之前获取锁, 超过截止时间后仍旧无法获取则返回

 

二  ReentrantLock

ReentrantLock作为Lock接口的子类,可以实现Lock接口的所有特性。内部有两个同步器实现类,一个为公平锁,另一个为非公平锁。

1.特性:

ReentrantLock的特性就是支持重进入,即任何线程在获取到锁之后能够再次获取该锁而不会发生堵塞,注意是两次获取的是同一个锁。
ReentrantLock还支持定义公平锁与非公平锁,关于同步器与公平锁,可以看一下 https://mp.csdn.net/postedit/85238441

2.使用API

ReentrantLock lock = new ReentrantLock(true) 定义是否为公平锁。
lock.tryLock(); 非阻塞 的获取锁。
lock.lockInterruptibly();可中断的获取锁

tryLock(long timeout, TimeUnit unit); 设置时间的获取锁。

lock。unlock(); 释放锁。

三  ReentrantReadWriteLock

ReentrantReadWriteLock是读写锁,把对一个资源的锁分为读锁与写锁。

1.特性

ReentrantReadWriteLock内部维护了一对锁。一个是读锁,是共享锁,同一时刻可以允许多个线程同时进行访问,

    还有一个是写锁,是排它锁,同一时刻只允许一个线程访问。

当写锁被获取到时,后续的读写操作都会被阻塞,当写锁释放后,所有的操作继续执行。

ReentrantReadWriteLock也支持重进入与公平性选择。

2.API

ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false); 创建公平锁与非公平锁。
ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); 获取读锁。
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); 获取写锁。

其他的API实现Lock 的高级特性,如非阻塞的获取锁,可中断的获取锁。

3.锁降级

当前线程遵循先获取写锁、获取读锁、释放写锁 、释放读锁的过程,称为锁降级

目的:为了保证数据的可见性。如果当前线程不获取读锁,而是直接释放写锁。假设此时有另一线程T获取写锁,并修改了数据。则当前线程无法感知到线程T的数据更新。如果遵循锁降级的步骤,线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

四  Condition

Condition作为接口使用的是ConditionObject对象,ConditionObject是AQS的内部类,每个Condition对象都有一个FIFO等待队列,用于存储处于等待状态的线程。

1.作用:

        可以实现比  wait()和notify/notifyAll()方法更高级的    等待/通知机制

2 wait()和notify/notifyAll()的介绍

   在使用notify/notifyAll()方法进行通知时,被通知的线程是有JVM选择的,

   执行notifyAll()方法的话就会通知所有处于等待状态的线程

3.Condition

 一个Lock对象中可以创建多个Condition实例,一个Condition可以注册多个线程,从而可以有选择性的进行线程通知

而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。

Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程

4.用法

Condition condition1 = lock.newCondition();     创建Condition
condition1.await();            使当前线程释放锁,构造成节点,加入等待队列尾部,进入等待状态。
condition1.signal();          唤醒注册在此Condition上等待时间最长的线程。即唤醒在等待队列首节点的线程
condition1.signalAll();      唤醒注册在此Condition上所有等待的线程。

5.注意

A     必须在condition.await()方法调用之前调用lock.lock()代码获得锁,不然会报错。

B    在使用wait/notify实现等待通知机制的时候我们知道必须执行完notify()方法所在的synchronized代码块后才释放锁。在这里也差不多,必须执行完signal所在的try语句块之后才释放锁,condition.await()后的语句才能被执行。

6.关于同步队列与等待队列的问题

Object模型只有一条同步队列与一条等待队列,所有处于等待的线程都位于等待队列中。调用await()方法时,由于此时已经在同步队列中,并获取了锁,使当前线程释放锁,构造成节点,加入等待队列尾部,进入等待状态。当从等待队列中将线程唤醒时,是随机唤醒的(synchronized 与 notify组合),使等待队列中被唤醒的线程,加入到同步队列中,进入锁的获取竞争中。成功获取了锁的线程,才从await()方法后开始运行。

Lock模型有一条同步队列与多条等待队列,即上文关于Condition与synchronized的分析。加入等待队列与唤醒线程与Object模型相同,只是当唤醒线程时,不是随机通知的,而是唤醒在等待队列中等待时间最长的线程,即等待队列中的首节点。

欢迎点赞与关注小编,小编水平有限,错误的地方请指出来。

欢迎评论与讨论,小编会尽力解答。

猜你喜欢

转载自blog.csdn.net/qq_36647176/article/details/85267661