一:synchronized的弊端
第二篇中我们介绍了synchronized,为啥有了synchronized还需要又Lock呢?
- synchronized无法手动释放锁。有些操作对于我们来说实现不了,比如说一个线程sleep的时候,需要让出锁,但synchronized我们是控制不了的。
- 无法实现读锁。两个线程同时读实现不了。
二:简单介绍Lock
Lock是一个接口,我们可以看见它下面又三个实现。ReentrantLock,Condition,ReadWriteLock。 Lock接口的抽象方法:然后我们开始介绍Lock下面重要实现。
三:ReentrantLock
1.是个class
实现了Lock,说明是一个锁,实现了Serializable,说明了支持序列化。2.构造器(两个,对应公平锁、非公平锁)
- 首先是其内部有一个Sync类型的变量,Sync其实是一个ReentrantLock的抽象静态内部类。我们只用知道ReentrantLock内部操作其实是由Sync类型的变量sync来实现的即可。
-
然后看ReentrantLock的构造器。
我们会发现两个构造器会给sync赋不同的对象引用,一个是FairSync,另一个是NonfairSync。很明显一个是公平锁一个是非公平锁。
- 公平锁和非公平锁:
- 公平锁能保证:老的线程排队使用锁,新线程仍然排队使用锁。
- 非公平锁保证:老的线程排队使用锁;但是无法保证新线程抢占已经在排队的线程的锁。
3:方法
- 我们看ReentrantLock内部的实现就会发现,其实一直都是sync的方法调用。
- 所以现在我们除了知道ReentrantLock可以设置成公平锁、非公平锁之外,我们想要更深层次的了解,还是要深入看ReentrantLock的内部类FairSync和NonFairSync是怎样实现的。
- FairLock、NonFairLock其实是继承了Sync,看一下继承关系,我画的那个大框框就是ReentrantLock,有内部类Sync,FairSync,NonFairSync。
4:顶顶大名AQS(看AbstractQueuedSynchronize源码)
[https://www.zhihu.com/search?type=content&q=AQS]
(这个链接的文章很不错)
其实我也是学着源代码,写着博客。一直都找不到ReentrantLock的切入点,后来想起来了AQS,这就是个不错的切入点。
- AQS机制(AQS的功能分为两种:独占和共享)
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。 - 其实AQS就是我们上面AbstractQueuedSynchronizer的缩写
-
AQS的实现:
AbstractQueuedSynchronizer内部有一个类Node,Node内部定义了是线程状态的常量,而且我们通过Node的构造器发现,每一个Node里面确实是由一个线程的。AQS的实现依赖内部的同步队列,也就是FIFO的双向队列,如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
5.总结:
- ReentrantLock内部是是一个公平锁class+非公平锁class,两个class都是继承于Sync。Sync由实现了AbstractQueuedSynchronizer(AQS)。AQS由两种功能,一种是独占(例如ReentrantLock),另一种是共享(例如ReentrantReadWriteLock)。(独占的时候,这个锁只能由一个线程掌控,共享的时候,这个锁可以由多个线程公用。)
- AQS内部是维护了一个FIFO的双向队列,如果当前线程竞争锁失败,那么AQS会把当前线程以及等待状态信息构造成一个Node加入到同步队列中,同时再阻塞该线程。当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程)。
- AQS内部当然也是用了CAS技术,就是队列里添加元素的时候,是通过CAS实现的。
四:ReentrantReadWriteLock (读写锁)
参考了https://www.zhihu.com/search?type=content&q=ReentrantReadWriteLock
1. 为啥引入ReentrantReadWriteLock
很多并发情况下,之前介绍的锁机制例如ReentrantLock重入锁,只允许单个线程执行受保护的程序,而在大部分应用中都是读的操作次数远远大于写操作次数。如果使用ReentrantLock锁会严重影响整体的性能,如果使用ReentrantReadWriteLock可以实现并发访问的情况下,读可以多线程同时访问,而写只有一个线程可以执行。他允许多读,但是不允许读和写同时发生。(1)读-读不互斥:读读之间不阻塞(2)读-写互斥:读会阻塞写,写也会阻塞读(3)写-写互斥:写写阻塞
2. 内部大体实现(和ReentrantLock一样都是AQS)
和ReentrantLock一样,构造函数传参的时候,可以传进去一个boolean变量,指定是公平锁还是非公平锁(默认是非公平锁)。ReentrantReadWriteLock还继承了ReadWriteLock接口(ReadWriteLock内部只有两个方法)。ReentrantReadWriteLock 比 ReentrantLock内部多了一个WriteLock,ReadLock,顾名思义一个是读锁,一个是写锁。 Sync类型sync其实也是继承了AbstractedQueeuedSynchronizer。
3.使用
public class ReentrantReadWriteLockTest {
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();
public String read() {
readLock.lock();
try {
//执行的代码快
return "xxx";
} finally {
readLock.unlock();
}
}
public void write() {
writeLock.lock();
try {
//执行写操作代码快
System.out.println("执行写操作代码快");
} finally {
writeLock.unlock();
}
}
}
4. 读锁-读锁 写锁-写锁 写锁-读锁之间的关系是怎样处理的?
ReentranLock里面已经说过了是怎样实现公平锁、非公平锁的
然后我们只说说读锁-写锁的处理:ReentrantReadWriteLock中一个state怎么维持两种状态,其实很简单,state是个32位的int变量,那我们就分成两份,高16位表示读,低16位表示写,假设状态为S,读状态为S>>>16,写状态为S&0x0000FFFF。也是就说我是读锁变写锁就S>>16,写锁变读锁就是S<<16。
5.总结
理解了AQS再来看ReentrantLock、ReentReadWriteLock就觉得一目了然。
网友评论