Java并发那些事儿-锁

公平锁和非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞。

非公平锁是线程获取锁时,直接尝试获取锁,获取不到才会到等到队列的队尾等待。但如果此时锁刚好可用,那么这个线程可用无需阻塞直接获取锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

ReentrantLock中实现了公平锁和非公平锁:在代码中的具体体现如下:

Java并发那些事儿-锁_第1张图片
公平锁
Java并发那些事儿-锁_第2张图片
非公平锁

区别在于多了hasQueuedPredecessor()方法的限制条件,

Java并发那些事儿-锁_第3张图片
hasQueuedPredecessor

判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。

公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。

可重入锁和非可重入锁

可重入锁是在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

Java并发那些事儿-锁_第4张图片
ReentrantLock对可重入锁的支持
Java并发那些事儿-锁_第5张图片
NonReentrantLock非可重入锁

获取:ReentrantLock中维护一个同步状态status来计数重入次数,status初始值为0。当获取锁时,如果status==0表示没有其他线程在执行同步代码,就获取锁开始执行,并设置当前线程拥有锁。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。NonReentrantLock中的非可重入锁直接尝试获取锁,如果获取失败当前线程阻塞。

Java并发那些事儿-锁_第6张图片
ReentrantLock锁释放过程
Java并发那些事儿-锁_第7张图片
NonReentrantLock锁释放

释放:ReentrantLock先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程真正释放锁。NonReentrantLock在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。

独享锁和共享锁

独享锁也叫排他锁(写锁),是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。(synchronized)

共享锁(读锁)是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

ReentrantReadWriteLock中持有两把锁。

读写锁

读写锁是依靠内部Sync变量来实现的,是通过state字段来描述有多少线程持有锁。在ReentrantReadWriteLock中有读、写两把锁,所以需要在整型变量state上分别描述读锁和写锁的数量。将state变量“按位切割”切分成了两个部分,高16位表示读锁状态(读锁个数),低16位表示写锁状态(写锁个数)。

state变量
Java并发那些事儿-锁_第8张图片
tryAcquire方法:写锁的加锁

写锁的加锁步骤:

1,获取state变量的。表示有多少线程持有锁。

2,获取写锁的个数:w

3,如果state!=0,表示已经有线程持有了锁。

     3.1,如果写锁为0(这种情况下就存在读锁)&&持有锁的线程不是当前线程:返回失败。

     3.2,存在写锁,但是如果写入锁的数量大于65535,抛出一个error。

     3.3,state = state+1;

4,如果没有线程持有锁,并且当前线程需要阻塞||通过CAS增加写线程数失败:返回失败。

5,以上条件都不满足,设置当前线程是锁的拥有者:返回true。这样情况state和w的情况存在以下情况:c=0,w=0(第一次加锁的情况);c>0,w>0(写锁大于0,是因为这个锁是可重入锁)。

Java并发那些事儿-锁_第9张图片
tryAcquireShared加读锁的过程

if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1;  // 如果存在写锁,那么加读锁失败。

从以上读锁和写锁的加锁过程可以看出,一旦线程A成功获取到写锁之后,其他线程无论是读锁还是写锁都加不上。线程A可以利用到获取的写锁重复使用锁。只有等到线程A释放了写锁之后,其他线程才能加锁。在没有任何线程添加写锁的情况下,任何线程都可以添加读锁。

ReentrantLock中有公平锁和非公平锁,在获取锁时,如果同步资源没有被其他线程锁住,那么当前线程在使用CAS更新state成功后就会成功抢占该资源;如果公共资源被占用且不是被当前线程占用,那么就会加锁失败。所以ReentrantLock中的无论是读操作和写操作,加的锁都是排他锁。

参考:

不可不说的Java“锁”事 - 美团技术团队

你可能感兴趣的:(Java并发那些事儿-锁)