并发编程学习笔记(6)----公平锁和ReentrantReadWriteLock使用及原理
(一)公平锁
1、什么是公平锁?
公平锁指的是在某个线程释放锁之后,等待的线程获取锁的策略是以请求获取锁的时间为标准的,即使先请求获取锁的线程先拿到锁。
2、在java中的实现?
在java的并发包中提供了ReentrantLock提供了重入锁并且也提供了公平锁(FairSync)和非公平锁(NonfairSync)。
RenntranLock类的构造方法可传入一个boolean值作为标记是否是否用公平锁,默认是非公平的,非公平锁我们与我们之前学习时实现的可重入锁原理相似,这里就不再详说,接下来看看公平锁的源码:
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;
}
FairSync重写了AQS中的tryAcquire()方法,跟我们前面的知识点中的需要什么要的锁就重写tryXX方法吻合。这里看代码。
可以看到这里跟我们前两两章学习到的非公平锁的实现只多了一个hasQueuePredecessors()方法的判断,这个方法就是判断当前线程的前一个线程是否也有资格去获取锁,只有没有资格获取锁时,当前的线程才可以返回true.否则就不能获取锁,接下来看看hasQueuePredecessors()方法:
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());
}
这段代码表示如果当链表为空时,是肯定可以获取的锁的,所以h!=t返回false,前面的方法会继续向下执行,链表不为空时,当头节点的下一个节点为空时,没有可以执行的线程,不能获取锁,当头节点的下一个节点不为空时,但是头节点的下一个节点的线程不等于当前线程,这样也是不能获取的,表示当前线程的前面还有先进来的线程在等待,所以不能获取锁,这样实现了一个公平锁。公平锁的使用只需要在实例化ReentrantLock的时候传入true即可。
(二)读写锁(ReentrantReadWriteLock)的使用及原理。
1、什么是读写锁?
在实际的应用中,读的操作是远远大于写操作的,并且读操作是不会产生线程安全问题的,如果我们给读和写的所有线程都加上互斥锁,那么在读的过程中会影响很大的性能,所以在java中提供了读写锁,读写锁分为读锁和写锁,其中读锁和读锁共享,读锁和写锁互斥,写锁和写锁互斥,这也是前面AQS的独享和共享模式的具体实现。
2、读写锁的运用方式
直接上代码:
package com.wangx.thread.t5; import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; public class Demo { private Map<String,Object> map = new HashMap<>(); private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock write = readWriteLock.writeLock();
private Lock read = readWriteLock.readLock();
public Object get (String key) {
read.lock();
System.out.println(Thread.currentThread().getName() + "读操作在执行...");
try {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return map.get(key);
} finally {
read.unlock();
System.out.println(Thread.currentThread().getName() + "读操作执行完毕...");
}
} public void put(String key, Object value) {
write.lock();
System.out.println(Thread.currentThread().getName() + "写操作在执行...");
try {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(key, value);
} finally {
write.unlock();
System.out.println(Thread.currentThread().getName() + "写操作执行完毕...");
} } }
由于hashMap是线程不安全的,但是又是需要读写都进行的,所以使用map的get和put方法可以很好的模拟线程安全问题和读写锁的使用,运用很简单,就是在线实例化ReentrantReadWriteLock,在通过该对象分别获取读写锁,在读的地方加上读锁,写的地方加上写锁。
3、读写锁源码实现
首先获取看写锁源码:
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
} public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
ReentrantReadWriteLock的writeLock方法返回writeLock,在构造方法中又实例化了WriterLock,WriteLock是ReentrantReadWriteLock的内部类,实现了Lock接口,保证了锁的所有方法的功能。再看WriteLock中的lock方法,
public void lock() {
sync.acquire(1);
}
ReentrantReadWriteLock的内部帮助器,所以这里调用的实际上是外部类的Sync内部类,接着进入该类的acquire()方法:
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
继承AQS框架的实现都是通过操作state来进行锁的获取和释放,所以getState()方法就不说了,接下来看看exclusiveCount()方法:
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
这里是通过十六位来保存读锁和写锁的,高8位保存写锁,低八位保存读锁,exclusiveCount()传入1时返回值为本身。表示当前只能有一个线程持有独享锁。c等于0时,第一个线程第一次进来,操作和原理与之前的自己实现锁类似,就不详细说了,当c不等于0时,表示此时是有线程在执行的,但是w等于0则表示当前执行的是持有共享锁的县城,或者当前线程不是重入线程的情况下,返回false,获取线程失败,并且当写线程个数操过了最大数,也会获取锁失败,否则更改状态,返回true,获取重入锁成功。这就是写锁的实现,就是判断在写线程的情况下是否有其他线程在使用锁。
写线程的unlock也是调用WriteLock的中的unlock,最后调用sync中的tryRelease方法,所以重点来看tryRelease方法:
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
当线程释放时,先判断,如果不是独享线程,肯定是有异常的,直接抛出异常,这个释放方法只能是独享线程来调用,做重线程的减减操作,当持有锁的线程为0,切重入锁为0时,释放锁,更改状态。完成写锁的释放。
读锁的实现源码,前面说过,读锁就是AQS的共享模式的具体运用,跟写锁类似,我们直接在ReentrantReadWriteLock的内部类的Sync中找到tryAcquireShared()方法:
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
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);
}
第一步:判断是否有线程持有独享锁的存在,即exclusiveCount不为0,并且当前锁不是独享锁,返回-1表示获取锁失败。
第二步:获取持有共享锁的线程数量,readerShouldBlock()线程公平的判断,判断当前持有共享锁的线程是否小于最大线程数,是更改状态。
第三步:当r等于0时,表示当前线程是第一个,则将第一个线程保存,并将第一个线程的重入次数计为1。如果进来的线程是第一个的线程的重入,则重入次数加1。
第四步:HoldCounter用来保存持有持有共享锁的线程个数及线程id,当第二个能共享的线程进入时,rh==null时,readHolds获取到当前线程,readHolds通过ThreadLocal<HoldCounter> 保证线程安全。
第五步:当rh != null 时,且rh.tid != getThreadId(current),即缓存的线程不是当前线程,即是进来的一个新的持共享锁的线程,则也获取当前线程的信息存到cachedHoldCounter中。
第六步:以上条件不成立时,表示是一个重入线程进入,当他的重入次数为0,表示第一次进入,将其添加到readHolds中,并将count++记录某个线程重入的次数,这样就能保证存入的共享线程个数和每个线程重入的次数。所有操作成功,则放回1,表示获取锁成功,否则fullTryAcquireShared()对获取失败的各种原因进行处理,最后返回结果。
tryReleaseShared()方法:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
这里只分析跟独享锁不同的部分,如果当前线程是第一个进来的共享线程,且重入次数为1时,释放该线程的共享锁,否则重入次数减1,当前线程不等于缓存的线程时,获取readholds中的线程,得到获取到的线程冲入次数,当小于等于1时移除该线程,也就是释放该线程锁,小于等于0时抛出异常,不为1时则重入次数减减,当持有共享锁的所有线程都移除后,不断自旋,直到成功释放锁,返回状态nextc是否为0。表示该锁是否可以释放。这样就成功地释放了共享锁。
(三)这里有个降级锁,就是在读和写可能在同一个操作中是,我们需要将写锁降级为读锁,以保证线程安全,示例代码:
package com.wangx.thread.t6; import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; public class Demo { private Map<String,Object> map = new HashMap<>(); private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock write = readWriteLock.writeLock();
private Lock read = readWriteLock.readLock();
private volatile boolean isUpdate; public void readWrite() {
read.lock();
if (isUpdate) {
read.unlock();
write.lock();
map.put("xxx", "xxx");
read.lock();
write.unlock();
}
Object object = map.get("xxx");
System.out.println(object);
read.unlock();
}
}
以上就是读写锁和公平锁我所了解的知识,今天的分享就到此为止,不足之处,忘各位指出。
原文 并发编程学习笔记(6)----公平锁和ReentrantReadWriteLock使用及原理
并发编程学习笔记(6)----公平锁和ReentrantReadWriteLock使用及原理的更多相关文章
- 并发编程学习笔记(10)----并发工具类CyclicBarrier、Semaphore和Exchanger类的使用和原理
在jdk中,为并发编程提供了CyclicBarrier(栅栏),CountDownLatch(闭锁),Semaphore(信号量),Exchanger(数据交换)等工具类,我们在前面的学习中已经学习并 ...
- JUC并发编程学习笔记
JUC并发编程学习笔记 狂神JUC并发编程 总的来说还可以,学到一些新知识,但很多是学过的了,深入的部分不多. 线程与进程 进程:一个程序,程序的集合,比如一个音乐播发器,QQ程序等.一个进程往往包含 ...
- Java并发编程学习笔记
Java编程思想,并发编程学习笔记. 一.基本的线程机制 1.定义任务:Runnable接口 线程可以驱动任务,因此需要一种描述任务的方式,这可以由Runnable接口来提供.要想定义任务,只需实现R ...
- 并发编程学习笔记(15)----Executor框架的使用
Executor执行已提交的 Runnable 任务的对象.此接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节.调度等)分离开来的方法.通常使用 Executor 而不是显式地创建 ...
- 并发编程学习笔记(14)----ThreadPoolExecutor(线程池)的使用及原理
1. 概述 1.1 什么是线程池 与jdbc连接池类似,在创建线程池或销毁线程时,会消耗大量的系统资源,因此在java中提出了线程池的概念,预先创建好固定数量的线程,当有任务需要线程去执行时,不用再去 ...
- 并发编程学习笔记(13)----ConcurrentLinkedQueue(非阻塞队列)和BlockingQueue(阻塞队列)原理
· 在并发编程中,我们有时候会需要使用到线程安全的队列,而在Java中如果我们需要实现队列可以有两种方式,一种是阻塞式队列.另一种是非阻塞式的队列,阻塞式队列采用锁来实现,而非阻塞式队列则是采用cas ...
- 并发编程学习笔记(8)----ThreadLocal的使用及源码分析
1. ThreadLocal的理解 ThreadLocal,顾名思义,就是线程的本地变量,ThreadLocal会为每个线程创建一个本地变量副本,使得使用ThreadLocal管理的变量在多线程的环境 ...
- 并发编程学习笔记(5)----AbstractQueuedSynchronizer(AQS)原理及使用
(一)什么是AQS? 阅读java文档可以知道,AbstractQueuedSynchronizer是实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量.事件,等等)提供一个框架, ...
- 并发编程学习笔记(4)----jdk5中提供的原子类及Lock使用及原理
(1)jdk中原子类的使用: jdk5中提供了很多原子类,它会使变量的操作变成原子性的. 原子性:原子性指的是一个操作是不可中断的,即使是在多个线程一起操作的情况下,一个操作一旦开始,就不会被其他线程 ...
随机推荐
- POJ2155 Matrix 【二维树状数组】+【段更新点查询】
Matrix Time Limit: 3000MS Memory Limit: 65536K Total Submissions: 17766 Accepted: 6674 Descripti ...
- VS2012 ASP.NET 母版页的创建与使用
在做牛腩新闻公布系统的过程中,须要使用ASP.NET的母版页来抽出全部网页的公共部分,以便更好的复用自己的网页布局和设计. 首先我们来看怎样创建一个新的母版页,例如以下图所看到的: 加入之后,例如以下 ...
- WEB端应该使用DataTable/DataSet吗?
有一次和同事讨论起具体的技术细节,同事说不要用什么实体类,从数据库访问到的数据,直接用DataTable.DataSet 就好.理由是,从获取到的数据集转换成实体类,有一定的性能损耗. 呵呵,性能.我 ...
- regulator_get 调用过程【转】
本文转载自:http://blog.csdn.net/u012719256/article/details/52083961 Touch panel DTS 分析(MSM8994平台,Atmel 芯片 ...
- NaN in JavaScript
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN The global NaN ...
- react-native 项目更名步骤
刚开始开发项目的时候,更没有想好一个项目名称,如何才能更名一个RN APP名称呢,可按照如下方式操作即可. 使用说明 更改package.json { "name": " ...
- mysql —— 利用Navicat 导出和导入数据库
Navicat for MySql 导出数据库方法: 打开Navicat for MySql,在要导出的数据库上面右击鼠标,点击“转储SQL 文件”→“数据和结构”. 找到合适的路径,点击“保存”. ...
- bzoj1036 [ZJOI2008]树的统计Count——LCT
题目:https://www.lydsy.com/JudgeOnline/problem.php?id=1036 LCT水题! 然而没有1A(咬牙)! 注意值有负数,所以取 max 的话要把作为“哨兵 ...
- 9. Ext基础1 -- Ext中 getDom、get、getCmp的区别
转自:https://blog.csdn.net/huobing123456789/article/details/7982061 要学习及应用好Ext框架,必须需要理解Html DOM.Ext El ...
- java运行jdk连接mysql出现了:Establishing SSL connection without server's identity verification is not recommended
注意:出现这类提示也不会影响对数据库的增删改查操作,所以不用紧张.. 在运行练习时出现下面的错误信息提示: Establishing SSL connection without server's i ...