Lock
Posted zpp1234
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Lock相关的知识,希望对你有一定的参考价值。
ReentrantLock:表示重入锁,它是唯一一个实现了Lock接口的类。重入锁指的是 线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次;
syschronized和reenttrantlock都支持重入锁;
重入锁的设计目的
比如调用demo方法获得了当前的对象锁,然后在这个方法中再去调用 demo2,demo2中的存在同一个实例锁,这个时候当前线程会因为无法获得 demo2的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死 锁。
ReentrantReadWriteLock
我们以前理解的锁,基本都是排他锁(互斥锁),也就是这些锁在同一时刻只允许一个线程进 行访问,而读写所在同一时刻可以允许多个线程访问,但是在写线程访问时,所有 的读线程和其他写线程都会被阻塞。读写锁维护了一对锁,一个读锁、一个写锁; 一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读 多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量.
public class LockDemo {
static Map<String,Object> cacheMap=new HashMap<>();
static ReentrantReadWriteLock rwl=new
ReentrantReadWriteLock();
static Lock read=rwl.readLock();
static Lock write=rwl.writeLock();
public static final Object get(String key) {
System.out.println("开始读取数据");
read.lock(); // 读锁
try {
return cacheMap.get(key);
}finally {
read.unlock();
}
}
public static final Object put(String key,Object value){
write.lock();
System.out.println("开始写数据");
try{
return cacheMap.put(key,value);
}finally {
write.unlock();
}
}
}
在这个案例中,通过hashmap来模拟了一个内存缓存,然后使用读写所来保证这 个内存缓存的线程安全性。当执行读操作的时候,需要获取读锁,在并发访问的时候,读锁不会被阻塞,因为读操作不会影响执行结果。 在执行写操作是,线程必须要获取写锁,当已经有线程持有写锁的情况下,当前线 程会被阻塞,只有当写锁释放以后,其他读写操作才能继续执行。使用读写锁提升 读操作的并发性,也保证每次写操作对所有的读写操作的可见性
? 读锁与读锁可以共享
? 读锁与写锁不可以共享(排他)
? 写锁与写锁不可以共享(排他)
ReentrantLock 的实现原理
我们知道锁的基本原理是,基于将多线程并行任务通过某一种机制实现线程的串 行执行,从而达到线程安全性的目的。在 synchronized 中,我们分析了偏向锁、 轻量级锁、乐观锁。基于乐观锁以及自旋锁来优化了 synchronized 的加锁开销, 同时在重量级锁阶段,通过线程的阻塞以及唤醒来达到线程竞争和同步的目的。 那么在ReentrantLock中,也一定会存在这样的需要去解决的问题。就是在多线程 竞争重入锁时,竞争失败的线程是如何实现阻塞以及被唤醒的呢?
AQS 是什么
在 Lock 中,用到了一个同步队列 AQS,全称 AbstractQueuedSynchronizer,它 是一个同步工具也是Lock用来实现线程同步的核心组件。如果你搞懂了AQS,那 么J.U.C中绝大部分的工具都能轻松掌握
AQS 的两种功能
从使用层面来说,AQS的功能分为两种:独占和共享 独占锁,
每次只能有一个线程持有锁,比如前面给大家演示的ReentrantLock就是 以独占方式实现的互斥锁
共 享 锁 , 允 许 多 个 线 程 同 时 获 取 锁 , 并 发 访 问 共 享 资 源 , 比 如 ReentrantReadWriteLock
AQS 的内部实现
AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点。所以双向链表可以从任 意一个节点开始很方便的访问前驱和后继。每个 Node 其实是由线程封装,当线 程争抢锁失败后会封装成Node加入到ASQ队列中去;当获取锁的线程释放锁以 后,会从队列中唤醒一个阻塞的节点(线程)。
ReentrantLock 的源码分析
以ReentrantLock作为切入点,来看看在这个场景中是如何使用AQS来实现线程 的同步的
ReentrantLock 的时序图
调用ReentrantLock中的lock()方法,源码的调用过程我使用了时序图来展现。
ReentrantLock.lock()
这个是reentrantLock获取锁的入口 public void lock() { sync.lock(); }
sync实际上是一个抽象的静态内部类,它继承了AQS来实现重入锁的逻辑,我们前面说过AQS是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备 业务功能,所以在不同的同步场景中,会继承AQS来实现对应场景的功能 Sync有两个具体的实现类,
分别是: NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他 线程等待,新线程都有机会抢占锁
FailSync:表示所有线程严格按照FIFO来获取锁
NofairSync.lock(Reentrantlock.lock的具体实现)
以非公平锁为例,来看看lock中的实现
1. 非公平锁和公平锁最大的区别在于,在非公平锁中我抢占锁的逻辑是,不管有 没有线程排队,我先上来cas去抢占一下
2. CAS成功,就表示成功获得了锁
3. CAS失败,调用acquire(1)走锁竞争逻辑
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
CAS 的实现原理
以上是关于Lock的主要内容,如果未能解决你的问题,请参考以下文章