Lock 的实现
Lock 本质上是一个接口,它定义了释放锁和获得锁的抽象方法,定义成接口就意
味着它定义了锁的一个标准规范,也同时意味着锁的不同实现。实现 Lock 接口的类有很多,以下为几个常见的锁实现。
ReentrantLock:表示重入锁,它是唯一一个实现了 Lock 接口的类。重入锁指的是
线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入
次数
ReentrantReadWriteLock:重入读写锁,它实现了 ReadWriteLock 接口,在这个
类中维护了两个锁,一个是 ReadLock,一个是 WriteLock,他们都分别实现了 Lock
接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则
是: 读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的
操作都会存在互斥。
StampedLock: stampedLock 是 JDK8 引入的新的锁机制,可以简单认为是读写
锁的一个改进版本,读写锁虽然通过分离读和写的功能使得读和读之间可以完全
并发,但是读和写是有冲突的,如果大量的读线程存在,可能会引起写线程的饥饿。
stampedLock 是一种乐观的读策略,使得乐观锁完全不会阻塞写线程
ReentrantLock 重入锁
重入锁的目的
重入锁,表示支持重新进入的锁,也就是说,如果当前线程 t1 通过调用 lock 方
法获取了锁之后,再次调用 lock,是不会再阻塞去获取锁的,直接增加重试次数
就行了。synchronized 和 ReentrantLock 都是可重入锁。
比如调用 demo 方法获得了当前的对象锁,然后在这个方法中再去调用
demo2,demo2 中的存在同一个实例锁,这个时候当前线程会因为无法获得
demo2 的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死
锁。
public class ReentrantDemo{
public synchronized void demo(){
System.out.println("begin:demo");
demo2();
}
public void demo2(){
System.out.println("begin:demo1");
synchronized (this){
} }
public static void main(String[] args) {
ReentrantDemo rd=new ReentrantDemo();
new Thread(rd::demo).start();
} }
ReentrantReadWriteLock
以前理解的锁,基本都是排他锁,也就是这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个线程访问,但是在写线程访问时,所有的读线程和其他写线程都会被阻塞。读写锁维护了一对锁,一个读锁、一个写锁; 一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量.
public class ReentranLockReadWriteLockDemo {
private static HashMap<String,String> hashMap = new HashMap(); // 假设要操作的内存
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock read = rwl.readLock(); // 读锁
static Lock write = rwl.writeLock(); // 写锁
public String getKey(String key ){
read.lock();
System.out.println("读锁: 开始读数据操作");
try {
return hashMap.get(key);
}finally {
read.unlock();
}
}
public void put(String key ,String value){
write.lock();
System.out.println("写锁: 开始写数据操作");
try {
hashMap.put(key,value);
}finally {
write.unlock();
}
}
public static void main(String[] args) {
}
}
通过 hashmap 来模拟了一个内存缓存,然后使用读写锁来保证这个内存缓存的线程安全性。当执行读操作的时候,需要获取读锁,在并发访问的时候,读锁不会被阻塞,因为读操作不会影响执行结果。
在执行写操作是,线程必须要获取写锁,当已经有线程持有写锁的情况下,当前线程会被阻塞,只有当写锁释放以后,其他读写操作才能继续执行。使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性
- 读锁与读锁可以共享
- 读锁与写锁不可以共享(排他)
- 写锁与写锁不可以共享(排他)
ReentrantLock 实现原理
锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串行执行,从而达到线程安全的目的。 在synchronize中,分析了偏向锁、轻量级锁,乐观锁。基于乐观锁以及自旋锁来优化了synchronize的加锁开销,同时在重量级锁阶段,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。
在ReentrantLock中,在多线程竞争重入锁时,竞争失败的线程是如何实现阻塞以及被唤醒的??
AQS是啥?
全称是AbstractQueuedSynchronizer。它是一个同步工具也是Lock用来实现线程同步的核心组件。如果你搞懂了AQS,那J.U.C中绝大部分的工具基本都能掌握。
AQS的两种功能
分别为: 独占 和 共享
独占锁:每次只能有一个线程持有锁,ReentrantLock就是以独占方式实现互斥锁。
共享锁: 允许多个线程同时获取锁,并发访问共享资源。ReentrantReadWriteLock就运用了共享锁。
AQS的内部实现
AQS队列内部维护的是一个FIFO的双向链表,这种结构的特点是每个Node数据结构都有两个指针,分别指向后续节点和前驱节点。所以双向链表可以从任意一个节点开始很方便的访问前驱和后续。
每个Node其实是由线程封装,当线程争抢失败后会封装成Node加入到AQS队列中去;当获取锁的线程释放锁以后,会从AQS对列中唤醒一个阻塞的节点(就是线程封装的)。
Node 源码:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
释放锁以及添加线程对于AQS队列的变化
当出现锁竞争以及释放锁的时候,AQS同步队列中的节点会发生变化。
这里添加节点会设计两个变化:
- 新的线程封装成Node节点追加到同步队列中,设置prev节点以及修改当前节点的前置节点的next节点指向自己
- 通过CAS将tail重新指向新的尾部节点
head节点表示获取锁成功的节点,当头节点在释放同步状态时,会唤醒后续节点,如果后续节点获得锁成功,会把自己设置为头节点,节点的变化过程如下:
这个释放节点也会有两个变化
- 修改head节点指向下一个获得锁的节点
- 新的获得锁的节点,将prev的指针指向null
设置head节点不需要用CAS,原因是设置head节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所有不需要CAS保证,只需要把head节点设置为原节点的后续节点,并且断开原head节点的next引用即可。
公平锁 和 非公平锁的区别
锁的公平性是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO。
非公平锁不一样,差异点有两个:
FairSync.tryAcquire
final void lock(){
acquice(1);
}
非公平锁在获取锁的时候,会先通过CAS进行抢占,而公平锁不会。
FairSync.tryAcquire
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
这个方法与nofairTryAcquire(int acquires)比较,不同的地方在于判断条件多了hasQueuedPredecessors()方法,也就加入了同步对列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
Condition
Condition 是一个多线程协调通讯工具,可以让某个线程一起等待某个条件(condition),只要满足条件时,线程才会被唤醒。
public class ConditionWaitTest implements Runnable{
private Lock lock;
private Condition condition;
public ConditionWaitTest(Lock lock , Condition condition){
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
System.out.println("begin - condition wait ");
try {
lock.lock();
condition.await();
System.out.println("end - condition wait ");
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
}
}
public class ConditionSignalTest implements Runnable {
private Lock lock;
private Condition condition;
public ConditionSignalTest(Lock lock , Condition condition){
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
System.out.println("begin - condition signal");
try {
lock.lock();
condition.signal();
System.out.println("end - condition signal");
}catch (Exception e){
e.printStackTrace();
}
lock.unlock();
}
}
通过这个案例实现了wait和notify的功能,当调用await方法后,当前线程会释放锁并等待,而其他线程调用condition对象的signal或者signalall方法通知并被阻塞的线程,然后自己执行unlock释放锁,被唤醒的线程获得之前的锁继续执行,最后释放锁。
所以,condition中两个最重要的方法,一个是await,一个是signal方法
阻塞:await()方法中,在线程释放锁资源之后,如果节点不在 AQS 等待队
列,则阻塞当前线程,如果在等待队列,则自旋等待尝试获取锁
释放:signal()后,节点会从 condition 队列移动到 AQS 等待队列,则进入
正常锁的获取流程。signalall()释放全部的,雷同notifyall方法。