[toc]
前言
java提供了种类丰富的锁,每种锁因其特性不同,在适当的场景下能够展现出非常高的效率,本文旨在对锁的相关源码、使用场景举例,以及不同锁的适用场景,Java中往往是按照是否含有某一特性来定义锁的,我们通过特性将锁进行分组分类,在使用对比方式进行介绍,帮助大家更快捷的理解相关知识,下面,给出本文内容的总体分类目录:
乐观锁VS悲观锁
乐观锁与悲观锁是一种广义上的概念,提现了看待线程同步的不同角度,在Java和数据库中都有此概念对应的实际应用。
先说概念,悲观锁对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取的时候会先加上锁,确保数据不会被别的线程修改。Java中synchronized
和Lock
的实现类都是悲观锁。
而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没用被更新,当前线程将自己修改的数据成功写入,如果数据已经被其他线程更新,则根据不同的方式执行不同的操作(例如报错或者自动重试)。
乐观锁在Java中是通过使用无锁编程来实现的,最常用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的
根据上面的概念描述我们发现:
- 悲观锁适合写操作很多的场景,先加锁可以保证写操作时数据正确
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升
光说概念有些抽象,我们来看下乐观锁和悲观锁的调用示例:
// ------------------------- 悲观锁的调用方式 -------------------------
//synchronized实现的悲观锁
public synchronized void testMethod(){
//操作同步资源
}
//ReentrantLock实现的悲观锁
private ReentrantLock lock=new ReentrantLock();//需要保证一个线程一个锁
public void modifyPublicResources(){
//加锁
lock.lock();
//同步资源
//释放锁
lock.unlock();
}
// ------------------------- 乐观锁的调用方式 -------------------------
private AtomicInteger atomicInteger = new AtomicInteger(); // 需要保证多个线程使用的是同一个AtomicInteger
atomicInteger.incrementAndGet(); //执行自增1
通过调用方式示例,我们可以发现悲观锁基本都是显示锁定之后再操作同步资源,而乐观锁则直接操作同步资源,那么为何乐观锁能够做到不锁定同步资源也可以正确的实现线程同步呢?我们通过介绍乐观锁的主要实现方式CAS
的技术原理解惑。
CAS乐观锁
CAS全称Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线之间变量同步.JUC包中的原子类就是通过CAS来实现的乐观锁。
CAS算法涉及到三个操作数:
- 需要读写的内存值V
- 进行比较的值A
- 要写入的新值B
当且仅当V的值等于A时,CAS通过原子方式用新值B来更新V的值("比较+更新"整体是一个原子操作),否则不会执行任何操作。一般情况下更新是一个不断重试的操作。
之前提到的JUC包中的原子类,就是通过CAS实现了乐观锁,我们进入原子类AtomicInteger源码,看一下AtomicInteger的定义:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
根据定义我们可以看出各属性的作用:
- unsafe:获取并操作内存的数据
- valueOffset:存储value在AtomicInteger的偏移量
- value:存储的AtomicInteger的int值,该属性借助volatile关键字保证其在线程间是可见的
接下来我们查看一下AtomicInteger的自增函数incrementAndGet()源码,发现自增函数底层调用的是unsafe.getAndAddInt()
但是由于JDK本身只有Unsafe.class,只通过class文件中参数名,并不能很好了解方法的作用,我们看一下JDK的Unsafe的源码:
//AtomicInteger中的自增方法以原子方式将当前值增加一。
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
// ------------------------- OpenJDK 8 -------------------------
// Unsafe.java
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
根据OpenJDK8源码可以看出getAndAddInt()循环获取给定对象o中的偏移量存处v的值,然后判断内存值是否等于v,如果相等则将内存值设为v+delta,否则返回false,继续循环进行重试,知道设置成功你那个才推出循环,并将旧值返回,整个比较-更新操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。
后续JDK通过CPU的cmpxchg指令,去比较寄存器中的A和内存中的值V。如果相等,就把写入的新值B存入内存中,如果不相等,就将内存值V赋值给寄存器中的值A,然后通过Java代码中while循环再次调用,cpmxchg指令进行充实,直到设置成功为止。
CAS虽然很搞笑,但是他存在三个问题,这里简单说一下:
- ABA问题:CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值,但是如果内存值原来是A,后来变成了B,然后又变成A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的,ABA问题的解决思路就是在遍历前面添加版本号,每次变量更新的时候把版本号加1,这样变化过程就就从A-B-A变成了1A-2B-3A。
- JDK从1.5开始提供了AtomicStampedReferencel来解决ABA问题,集体操作封装在
compareAndSet()
中,首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志值设置为给定的更新值
- JDK从1.5开始提供了AtomicStampedReferencel来解决ABA问题,集体操作封装在
- 循环时间开销大:CAS操作如果长时间不成功,会导致一直自旋,给CPU带来非常大的开销
- 只能保证一个共享遍历的原子操作:对一个共享变量执行操作是,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性。
- JDK1.5开始JDK提供了AtomicReferencr类来保证引用对象之间的原子性,可以把过的变量放在一个对象里面进行CAS操作。
自旋锁VS适应性自旋锁
自旋锁
在介绍自旋锁前,我们需要介绍一下前提只是来帮助大家明白自旋锁的概念。
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间,如果同步代码块中的内容过于简单,状态转换消耗的时间可能比用户代码执行的时间还要长。
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失,如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程稍等一下,我们需要让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免期货线程的开销,这就是自旋锁。
自旋锁本身是有缺点的,他不能代替阻塞,自旋锁虽然避免了线程切换的开销,但他占用处理器时间。如果锁被占用时间很短,自旋等待的效果就会非常好,反之如果锁被占用时间很长,那么自旋锁的线程只会白白浪费处理器资源。所以自旋锁等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用
-XX:preBlockSpin来更改
)没有成功获取到锁,就应该挂起线程。
自旋锁的实现原理同样也是CAS,例如AtomicInteger中调用unsafe进行自增操作的源码do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行,自旋,直至修改成功。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
//自旋
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
自适应自旋锁
自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning
来开启,JDK6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。
自适应意味着自旋的时间(次数)不在固定,而是由前一次在同一个锁上的自旋时间以及锁的拥有这状态来决定。如果在同一个锁对象上,自旋锁等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而他将自旋等待持续相对更长的时间,如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
无锁VS偏向锁VS轻量级锁VS重量级锁
这四种锁是指状态,专门针对synchronized的,在介绍这四种锁之前还需要介绍一些额外的知识。
首先为什么synchronized能够实现线程同步?
在回答这个问题之前我们需要了解两个重要概念:java对象头、Monitor。
java对象头
synchronized是悲观锁,在操作同步资源需要给同步资源先加锁,这把锁就是存在Java对象头里面,而Java对象头又是什么?
我们以HotSpot虚拟机为例,HotSpot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
- Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,他会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着标志位的变化而变化。
- Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
Monitor
Monitor可以理解为一个同步工具或一中同步机制,通常被描述为一个对象,每个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。
Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表,每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标志,表示该锁被这个线程占用。
synchronized的同步实现
现在回到synchronized,synchronized是通过Monitor
来实现线程同步,Minitor是依赖于底层的操作系统Mutex Loc(互斥锁)来实现线程同步。
如同我们在自旋锁中提到的阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间,如果同步代码中的内容过于简单,转台转换消耗的时间可能比用户代码执行的时间还要长。这种方式就是synchronized最初实现的同步方式,JDK6之前的synchronized的效率低的原因,这种依赖于操作系统Mutex Lock所实现的锁我们称之为重量锁,JDK6中为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁。
所以目前锁一共有四种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。
通过上面介绍,我们对synchronized的加锁机制以及相关知识有了一定了解,那么下面我们给出四种锁的状态对应的Mark Word内容,然后在分别讲解四种锁状态的思路以及特点:
锁状态 | 存储内容 | 存储内容 |
---|---|---|
无锁 | 对象的HashCode、对象分代年龄、是否是偏向锁(0) | 01 |
偏向锁 | 偏向线程ID、偏向时间戳、对象的HashCode对象分代年龄、是否是偏向锁(1) | 01 |
轻量锁 | 指向栈中锁记录的指针 | 00 |
重量锁 | 指向互斥量(重量级锁)的指针 | 10 |
无锁
无锁没有对资源进行锁定,所有线程都能反问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能够修改成功,而其他线程修改失败不断重试知道修改成功。上面我们介绍的CAS的原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。
偏向锁
偏向锁:偏向锁是指一段同步代码一直被一个线程锁访问,那么该线程会自动获取锁,降低获取锁的代价。
在大多数情况下,锁总是由同一个线程多长获得,不存在多线程竞争,所以出现了偏向锁,其目的就是在只有一个线程执行同步代码块时提高性能。
当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID,在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁,引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁获取和释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadId的时候依赖一次CAS原子指令即可。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁,偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),他会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁恢复到无锁(标志位01)或轻量级锁(标志位00)状态。
偏向锁在JDK6中是默认开启的,可以通过JVM参数关闭偏向锁:-XX:UseBiasedLocking=false
,关闭之后程序默认进入轻量级锁的状态。
轻量锁
轻量锁:是指当锁是偏向的时候,被另外的线程所访问,偏向锁就会升级为轻量锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(说标志位为01状态,是否为偏向锁为0),虚拟机首先将在当前线程的栈帧中建立一个锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中Mark Word复制到锁记录中。
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word.
如果更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志设置为00,表示对象处于轻量级锁定状态。
如果轻量锁更新失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步代码块,否则说明多个线程竞争锁。
若当前只有一个等待线程,则该线程通过自旋等待。但是当自旋超过一定次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量锁升级为重量锁,轻量锁,等待线程会处于自旋状态,升级为重量锁会变为阻塞状态。
重量锁
升级为重量锁时,锁的标志位变为10,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。整体锁的状态升级如下:
综上偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作,而轻量级锁是通过CAS操作和自旋来解决加锁问题,避免线程阻塞而影响性能,重量级锁是将除了拥有锁的线程以为的线程都阻塞。
公平锁VS非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。
- 优点:等待锁的线程不会饿死,总归会拿到锁
- 缺点:整体吞吐效率相对非公平锁要低,等待队列中除了第一个线程以外所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
非公平锁:是多个线程加锁时直接尝试获取锁,获取不到才会等待队列的队尾等待,但如果此时锁刚好可用,那么这个线程无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。
- 优点:是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。
- 缺点:是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
公平锁如图所示:
如上图所示,假设一口水井,有管理员看守,管理员有一把锁,只有拿到锁的人才能够打水,打完水需要把锁还给管理员,每个过来打水的人都有管理员允许并拿到锁才能打水,如果前面有人正在打水,那么这个想要打水的人就必须排队,管理员会查看下一个要去打水的人是不是队伍中排在最前面的人,如果是,才会给你锁让你去打水,如果不是,就必须队尾排队,这就是公平锁。
非公平锁,管理员对打水的人没有要求,即使等待队伍里有排队等待的人,但如果在上一个人刚打完把锁还给管理员,而且管理员还没有运行等待队伍里下一个人去打水的时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待,如下图所示:
下面通过ReentrantLock的源码来讲解公平锁和非公平锁:
根据代码可知,ReentrantLock里面有一个内部类Synnc,Sync继承了AQS(AbstractQueueSynchronizer),添加所和释放锁大部分操作实际上都是在Sync中实现的,他有公平锁FairSync和非公平锁NonfairSync两个子类.ReentraantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。
下面看一下公平锁与非公平锁的加锁方式源码:
通过源码对吧,我们明显的看出公平锁和非公平锁的lock()方法唯一区别就在于公平锁在获取同步状态时多一个限制条件:hasQueuedPredecessors()。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
通过hasQueuedPredecessors源码可以看出,该方法主要是做一件事:主要是判断当前线程是否位于同步队列的第一个,如果是则返回true,否则返回false。
综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取说,从而实现公平的特性,非公平锁,加锁时不靠谱排队等问题,直接尝试获取锁,所以存在后申请却先获得锁的情况
可重入锁VS非重入锁
可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入该线程的内层方法或自动获取锁(前提锁对象是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中的ReentrantLock和synchronized都是可重入锁,重入锁的优点可一定程度上避免死锁,下面是代码示例:
public class Widget {
public synchronized void doSomething() {
System.out.println("方法1执行...");
doOthers();
}
public synchronized void doOthers() {
System.out.println("方法2执行...");
}
}
在上面代码中,类中两个方法都被synchronized修饰的,doSomething()方法调用doOthers()方法,因为内置说是可重入的,所以同一个线程调用doOthers()可以直接获得当前对象锁,进入doOthers()进行操作。如果不是可重入锁,那么当前线程在调用doOthers()之前需要执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁,已经被当前线程持有,且释放无效。所以会出现死锁。
为什么可重入锁可以嵌套调用,是可以自动获得锁,我们通过下图分别解析一下:
还是打水的雷子,有多人在排队打水,此时管理员允许锁和同一个人的多个水桶绑定。这个人多个水桶打水时,第一个水桶和锁绑定并打完水之后,第二个水桶也可以直接和锁绑定并开始打水,所有的水桶都打完了之后打水之人才会将锁还给管理员,这个人所有的打水过程能够成功执行,后序等待人也能够打到水,这就是可重入锁。
如果是非可重入锁,此时管理员只允许锁和同一个人的一个水桶绑定,第一个水桶和锁绑定打完水之后并不会释放锁,导致第二个水桶不能和锁绑定也无法打水,当前线程出现死锁,整个等待队列的所有线程都无法被唤醒。
独享锁VS共享锁
独享说和共享锁同样也是一种概念,我们先介绍一下具体概念,然后通过ReentrantLock和ReentrantReadWriteLock的源码来介绍独享锁。
- 独享锁:也叫排它锁,是指该锁一次只能被一个线程所持有,如果线程T对数据A加上排它锁,则其他线程不能再对A加如何类型的锁,获得排它锁的线程即能读取又能修改数据,JDK中synchronized和JUC中的lock的实现类就是独享锁(互斥锁)。
- 共享锁:是指该锁可被多个线程所持有,如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能再加排它锁,获得共享锁的线程只能读取数据,不能修改数据。
独享锁与共享锁也是通过AQS实现的,通过实现不同的方法,来实现独享和共享。
下面是ReentrantReadWriteLock的部分源码:
我们看到ReentrantReadWriteLock有两把锁;ReadLock和WriteLock,由词意可知,一个是读锁,一个是写锁,合称读写锁,在进一步观察可以发现ReadLock和WriteLock是靠内部的Sync实现的锁,Sync是AQS的子类,这种结构在CountDownLatch、ReentrantLock、Semaphore里面都有。
在ReentranReadWriteLock里面,读锁和写锁的锁主体都是Sync,但读锁和写锁的加锁方式不一样,读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写过程互斥,因为读锁和写锁分离的,所以ReentranReadWriteLock的并发性相比一般的互斥锁有了很大提升。
在读锁和写锁的具体加锁方式有什么区别呢?在了解源码之前我们你需要回顾一下其他知识,在最开始提及的AQS的时候我们也提到了state变量(int类型32位),该字段用来描述有多少线程持有锁。
在独享锁中这个值通常是0或者1,(如果重入锁的话,state值就是重入的次数),在共享锁中state就是持有锁的数量,但是在ReentrantReadWriteLock中有读、写两把锁,所以需要一个整形遍历state上分别描述读锁和写锁的数量(或者也可以叫状态)。于是将state变量按位分隔切分成两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数),如下图所示:
了解概念之后我们看一下代码,先看写锁加锁源码:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//获取当前锁的个数
int c = getState();
//获取写锁的个数
int w = exclusiveCount(c);
//如果已经持有锁c!=0
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
//如果写线程w为0(意思就是只有读锁),如果读锁存在,则不能获取写锁,或持有锁的线程不是当前线
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
//如果写锁大于最大数(65535,2的16次方-1)就抛出error
throw new Error("Maximum lock count exceeded");
// 重入获取
setState(c + acquires);
return true;
}
//如果当前写线程为0,并且当前线程需要阻塞那么就返回失败,或者如果通过CAS增加写线程失败也返回失败
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//如果c=0,w=0或者c>0,w>0(可重入),则设置当前线程活锁的拥有者
setExclusiveOwnerThread(current);
return true;
}
- 这段代码首先取到当前线程锁的个数c,然后在通过c来获取写锁个数w,因为写锁是低16位,所以去低16位于当前c做与运算(
int w=exclusiveCount(c)
),高16位和0与运算后是0,剩下的就是低位运算的值,同时也是持有写锁的线程数目。 - 在取到写锁线程数目后,首先判断是否已经有线程持有了锁,如果已经有线程持有锁了(
c!=0
),则查看当前写锁的数目,如果写线程数为0(即存在读锁),或者持有锁的线程不是当前线程就返回失败(涉及到公平锁和非公平锁的实现)。 - 如果写入所的数量大于最大数(65535,2的16次方-1)就抛出error
- 如果当前写线程为0(那么读线程也应该为0,因为上面已经处理了c!=0的情况了)并且当前线程需要阻塞那么就返回失败,如果通过CAS增加写线程失败也返回失败。
- 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者,返回成功。
tryAcquire()
处理重入条件外(当前线程为了获取了写锁状态)之外,增加一个读锁是否存在的判断,如果存在读锁,则写锁不能被获取,原因在于:必须保证写锁的操作对读锁可见,如果允许读锁存在已被获取情况下对写锁获取,那么正在运行的其他读锁线程就无法感知到当前写锁操作,因此只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程访问均被阻塞。写锁的释放与ReentrantLock释放过程基本类似,每次释放均减少写锁状态,当写锁状态为0时表示写锁已被释放,然后等待读写锁线程才能继续访问写锁,同时前次写线程的修改对后面的读写线程可见。
接着读锁的代码:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
// 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
可以看到在tryAcquireShard(int unused)中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态,如果是当前线程获取了写锁(写锁是被当前线程获取)或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。读锁的每一次释放(线程安全,可能有多个读线程同时释放读锁)均减少读状态,减少的值是1<<16,所以读写锁才能实现读读共享,读写、写读、写写互斥。
此时在看一下互斥锁ReentrantLock
中公平锁和非公平锁加锁源码:
我们发现在ReentrantLock虽然有公平锁和非公平锁两种,但是他们添加的都是独占锁,根据源码所示,当前某个线程调用lock方法获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用CAS更新state成功后就会成功抢占该资源,而如果公共资源被占用且不是被当前线程占用,那么就会加锁失败,所以可以确定ReentrantLock无论是读操作海慧寺写操作,添加锁都是独享的。