Lock接口

锁主要是能用来控制多个线程访问共享变量的方式,在Lock接口出现之前,Java主要是靠我们前面介绍的synchronized关键字来实现锁功能的,在Java SE 5之后,开始提供Lock接口来实现锁的功能。他有与synchronized关键字类似的同步功能,只是说有一些差别,Lock需要显式的进行加锁和解锁操作。虽然损失了一些隐式加解锁的便捷性,但是增强了可操作性。另外Lock还提供了一些特有的可中断获取锁。超时获取锁等同步特性。

Lock使用与api接口

1
2
3
4
5
6
Lock lock = new ReentrantLock();
lock.lock();
try{
}finally{
lock.unlock();
}

·解锁过程放入finally块,确保锁能够被释放
·加锁过程不要写在try里面,因为加锁过程如果发生异常,也会进入finally块导致锁无故释放

假设lock 方法放在内部,正常获取锁,finally 就会正常释放锁;但一旦异常,并没有真正拿到锁,此时执行 finally 中的释放锁,就会发生IllegalMonitorStateException 异常。如果 lock 方法放在外部,正常获取锁,finally 就会正常释放锁;但一旦异常,就会立即跳出线程不会执行try-finally代码块,也就不会再次进入到finally 中再次引发其他异常。

Lock接口提供的api,底层几乎都是由AQS实现的,可以先留一个印象:

Lock与Synchronized对比

可以从加锁解锁的原理、实现方法、对象、目的和效果从头到尾对比一下:

相同点:
·synchronized 和 Lock 都是用来保护资源线程安全的。
·都可以保证可见性。
·synchronized 和 ReentrantLock 都拥有可重入的特点。

不同点:

1.加解锁控制差别: synchronized的加锁和解锁是由]vm实现的(内置锁),而Lock的加解锁需要手动控制,通过lock()和 unlock(),一般会把unlock操作放入finally块来解锁,以防忘记解锁。

2.是否可以设置公平/非公平: 公平锁是指多个线程在等待同一个锁时,根据先来后到的原则依次获得锁。非公平锁接近于随机选取,但是也可以允许插队。ReentrantLock 等 Lock 实现类可以根据自己的需要来设置公平或非公平,synchronized 则不能设置(默认非公平)。

3.synchronized 锁不够灵活:synchronized一个线程获取锁之后,其他线程想要获取锁只能等待,只能进入阻塞状态,直到持有锁的线程释放这个锁,可能这个等待过程会持续很久。相比之下,Lock可以使用lockInterruptibly方法,不想等了可以中断退出,也可以使tryLock获取锁,能获取就获取,不能获取线程也可以去干别的事情,更加灵活。

4.synchronized 锁只能同时被一个线程拥有,但是 Lock 锁没有这个限制:例如读写锁中的读锁时可以被多个线程
同时拥有的。

其他:例如实现方式的不同等。

AQS队列同步器

感觉这部分对底层实现方法的要求不高,只要知道这里是干啥的就行(

队列同步器AbstractOueuedSynchronizer(以下简称同步器)是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量state表示同步状态通过内置的FIFO队列来完成资源获取线程的排队工作,是实现大部分同步需求的基础。

可重入锁ReentrantLock,读写锁ReentrantReadWriteLock,CountDownLatch,Semephore这些同步组件都是基于AQS的经典应用。AQS子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件(锁,CountDownLatch等)使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件。

这里给一个共享锁Mutex的自定义实现代码:

显然,Lock是面向锁的使用者的,他定义了使用者与锁的交互接口,隐藏了实现细节。而AQS是面向锁的实现者的,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待与唤醒等底层操作。锁和同步器很好的隔离 了使用者和实现者所需关注的领域。

AQS实现方法

还是非常复杂的。好像是出于不要让人混淆的原因吧,AQS的实现被分成了三类方法:

使用者继承AQS类并重写5个指定的方法(第二类)。重写同步器指定的方法时需要使用同步器提供的3个方法来访问或修改同步状态**(第一类)。最后将AQS组合在自定义同步组件的实现中,并调用其9个模板方法(第三类)和 5个重写过的方法**来实现。另外,模板方法也会调用使用者重写的方法实现。

但其实画张图厘清逻辑就也还好:

总结: 实现一个自定义锁的步骤
·继承Lock接口
·定义一个AQS的子类,作为锁的静态内部类。该类继承自AQS,也就拥有了AQS定义的9个模版方法。另外还需要实现AQS定义的5个可重写方法,重写实现过程中需要用到3个同步状态的修改方法。
·实现Lock规定的接口方法,原理就是将”操作”代理到AQS的子类sync上,即利用sync的14种方法来实现Lock。

AQS框架原理

就像刚刚说的,AQS最重要的就是一个int类型的state变量,一个同步队列。不同锁对于state变量的用法是不同的,所以下面我们先介绍同步队列的基本结构。

以及加入和释放节点的过程如下:

不同类型锁的获取与释放

总结

​ AQS主要依赖于一个双向链表和一个volatile类型的整数state来实现同步控制。该整数state用来表示同步状态一般情况下,state=0表示没有线程占用同步资源,state>0表示有线程占用同步资源,state>1表示同步资源已经被争用了多次,比如ReentrantLock可以允许一个线程多次获得锁,每次state值加1。
​ AOS实现同步的关键在于,它提供了一个基于FIFO队列的同步队列,通过将等待线程加入同步队列中,然后在释放同步状态的时候,从同步队列中唤醒等待线程,从而实现了同步机制。
​ AQS的实现主要有两种方式:独占式(Exclusive)和共享式(Shared)。独占式指只有一个线程可以占用同步资源,比如ReentrantLock,而共享式是指多个线程可以同时占用同步资源,比如CountDownLatch。在AQS中,这两种方式的实现是基本相同的,区别在于获取和释放同步状态的方式不同。

ReentrantLock 可重入锁

公平锁和非公平锁有什么区别?为什么非公平锁比公平锁性能更好?

公平锁:锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。

非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好但可能会导致某些线程永远无法获取到锁。


公平锁执行流程:获取锁时,先将线程自己添加到同步队列的队尾并休眠,当某线程用完锁之后,会去唤醒同步队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。

非公平锁执行流程:当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入同步队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。

当尝试获取的是一个公平锁,那在拿到锁之前,会首先调用 hasQueuedPredecessors方法来检查是否有其他线程在等待队列中排在当前线程前面,即检查队列中是否有该节点的前驱。(按照公平原则,有的话,你就该排在它之后。)导致的结果就是当前线程需要让出 CPU,进入同步队列中,这就是上下文切换。当有大量线程尝试获取锁时,这个上下文切换就是很频繁的。
非公平锁没有上面那个hasQueuedPredecessors 对程而是尝试获取锁时,如果获取到了,就直接返回成功了,线程获取到了锁,继续执行。并不会进入同步队列中,也就不会发生上下文切换。

ReentrantLock是如何实现公平锁的?非公平锁的?

ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync,Sync类继承自AbstractQueuedSynchronizer抽象类。
非公平锁是ReentrantLock的默认实现。公平锁对比非公平锁的实现差异主要体现在tryAcquire方法(获取锁)这里。非公平锁(NonfairSync)的tryAcquire实现直接调用了父类Sync中的nonfairTryAcquire。而公平锁tryAcquire的唯一不同的点为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁(这就叫公平)。

ReentrantReadWriteLock 读写锁

之前提到锁(如Mutex和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而ReentrantReadWriteLock在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

Java并发包中提供的读写锁是ReentrantReadWriteLock,具有如下特性:

·公平性选择:支持公平和非公平(默认)的锁获取方式;

·支持重入:同一线程获取读锁之后能再次获取;同一线程获取写锁之后能再次获取写锁,并且还能获取读锁;

·锁降级:按照获取写锁,再获取读锁,再释放写锁的顺序,写锁能够降级为读锁。

读写状态设计

读写加锁与释放规范

这里是针对多线程竞争同一把锁的时候做的规范。简单说,写锁是独占的,读锁是共享的;如果一个线程先占据写锁,那么虽然别的线程不能读,但是该线程本身可以获取读锁;如果一个线程占据读锁,那么其他线程都可以访问读锁,但是都不能获取写锁。

读锁和写锁都是支持可重入的。但是,获取读锁的实现从Java 5到Java 6变得复杂许多,主要原因是新增了一 些功能,例如getReadHoldCount()方法,作用是返回当前线程获取读锁的次数。读状态是记录所有线程获取读锁次数的总和getReadLockCount(),而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中(关于ThreadLocal可以查看目Java并发线程基础 ),由线程自身维护。

锁降级

指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

也就是说,如果一个线程先写后读,那么写操作完成之后直接“锁降级“到读锁进行读操作;当写操作和读操作间隔的比较久,就是第一种情况。

LockSupport

和Thread.sleep的区别:

Thread.sleep 的灵活性较低,因为它只能根据指定的时间延迟来暂停线程,并且不能被其他线程提前唤醒。
LockSupport.parkNanos 提供了更高的灵活性,因为它可以被其他线程通过 unpark 方法提前唤醒,而且它还提供了纳秒级别的精度(尽管实际的精度可能受到操作系统和硬件的限制)。
Thread.sleep 是一个更简单、更易于使用的方法,适用于基本的延迟需求,而LockSupport.parkNanos 则是一个更强大、更灵活的工具,适用于需要更高级并发控制的场景。

Condition

这里是需要和AQS联合起来理解更好。

任意一个]ava对象,都拥有一组监视器方法(定义在java.lang.0bject上),主要包括wait()、wait(long timeout)、notify()以及notifyAIl()方法,这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口也提供了类似0bject的监视器方法,与Lock配合可以实现等待/通知模式。但是,Condition 提供了更加精细的等待、唤醒的控制,同时允许创建多个等待队列实现更加复杂的线程通信场景。

首先需要了解,除了同步队列,Monitor还拥有等待队列。这两个的区别相当于是有没有“排队许可”的问题,在同步队列里的节点线程可以竞争锁,但是等待队列里的还在休眠,不会产生竞争。

如何实现这种效果呢?最主要的是Condition中的await和signal方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

public void conditionWait() throws InterruptedException {
lock.lock();
try {
//等待
condition.await();
} finally {
lock.unlock();
}
}

public void conditionSignal() throws InterruptedException {
lock.lock();
try {
// 通知
condition.signal();
} finally {
lock.unlock();
}
}

分析:
·使用: 一般都会将Condition对象作为成员变量,获取一个condition必须通过Lock的newCondition方法

·await():当调用await()方法后,当前线程会释放锁并在此等待

·signal():其他线程调用Condition对象的signal()方法,通知当前线程后,当前线程才从等待状态进入锁的同步队列,尝试获取锁,如果获取到了锁,则从await()方法返回。

等待与通知