Java-ReentrantLock基本使用及底层原理
文章目录
概述/基本使用
- ReentrantLock相对于synchronized有一下特性:
1:可中断
2:可设置超时时间
3:可设置为公平锁
4:可设置多个条件变量
与synchronized相同的是:都是支持可重入。
ReentrantLock基本运用
可重入
可重入是指一个线程在获得一把锁后,因为它是这把锁的拥有者,所以有权利再次获得这把锁。
但是不可重入是指,线程获得锁后再次想获得锁但是被挡住了!
- 示例:让主线程连续获得method1/method2/method3方法里面的锁。
public class Test_Reentrant {
static ReentrantLock reentrantLock=new ReentrantLock();
public static void main(String[] args) {
method1();
}
private static void method1() {
reentrantLock.lock();
try {
System.out.println("方法1");
method2();
}finally {
reentrantLock.unlock();
}
}
private static void method2() {
reentrantLock.lock();
try {
System.out.println("方法2");
method3();
}finally {
reentrantLock.unlock();
}
}
private static void method3() {
reentrantLock.lock();
try {
System.out.println("方法3");
}finally {
reentrantLock.unlock();
}
}
}
可打断
可打断就是当t1线程通过lock或者synchronized获得锁后,如果此时t2线程来想访问临界区,那么因为已经加锁所以只能等待。
而可打断是t1通过lockInterruptibly()上锁,可以打断是指t2来了之后可以通过t1.interrupt()打断t1。打断后抛出异常。
public class Test_Reentrant {
static ReentrantLock reentrantLock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()-> {
try {
System.out.println("尝试获取锁");
reentrantLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("获取到锁");
}finally {
reentrantLock.unlock();
}
},"t1");
reentrantLock.lock();//开始先执行这句,主线程上锁,所以后来的t1就阻塞
t1.start();
sleep(1);//让主线程睡一会,t1拿到锁
t1.interrupt();//打断t1
}
当用lock加锁时,用interupt无法打断
public class Test_Reentrant {
static ReentrantLock reentrantLock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()-> {
reentrantLock.lock();
try {
System.out.println("获取到锁");
}finally {
reentrantLock.unlock();
}
},"t1");
t1.start();
Thread.sleep(1);//让主线程睡一会,保证t2拿到锁
t1.interrupt();//打断t1
}
锁超时
public class Test_trylock {
static ReentrantLock lock=new ReentrantLock();
public static void main(String[] args) {
Thread t1=new Thread(()-> {
try {
if (!lock.tryLock(2, TimeUnit.SECONDS)){
System.out.println("获得不到锁");
return;//获取不到则返回
};
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
System.out.println("获得锁");
}finally {
lock.unlock();
}
},"t1");
lock.lock();//让主线程获取锁
t1.start();
}
}
条件变量(await()/signalAll())
条件变量简单示例
- 简单示例:小明没有烟就不能干活,小芳没有饭就干不了活。还有两个小哥一个给小明送烟,一个给小芳送饭。
public class Test_has {
static Boolean hascigarette=false;//是否有烟
static Boolean hasfood=false;//是否有饭
static ReentrantLock lock=new ReentrantLock();
static Condition cigarette_room = lock.newCondition();//等烟线程的休息室
static Condition food_room = lock.newCondition();//等饭线程的休息室
public static void main(String[] args) {
new Thread(()->{
lock.lock();
try {
while (!hascigarette){
try {
cigarette_room.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("小明有了烟开始干活");
}finally {
lock.unlock();
}
},"要烟的小明").start();
new Thread(()->{
lock.lock();
try {
while (!hasfood){
try {
food_room.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("小芳吃到饭开始干活");
}finally {
lock.unlock();
}
},"要饭的小芳").start();
try {
Thread.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
lock.lock();
try {
System.out.println("我来给小明送烟");
hascigarette=true;
cigarette_room.signalAll();//唤醒在无烟休息室的线程
}finally {
lock.unlock();
}
},"送烟小哥").start();
new Thread(()->{
lock.lock();
try {
System.out.println("我来给小芳送饭");
hasfood=true;
food_room.signalAll();//唤醒在无饭休息室的线程
}finally {
lock.unlock();
}
},"送饭小哥").start();
}
}
ReentrantLock原理
AQS工作原理概要
AbstractQueuedSynchronizer又称为队列同步器(后面简称AQS),它是用来构建锁或其他同步组件的基础框架,内部通过一个int类型的成员变量state来控制同步状态,当state=0时,则说明没有任何线程占有共享资源的锁,当state=1时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待,AQS内部通过内部类Node构成FIFO的同步队列来完成线程获取锁的排队工作,同时利用内部类ConditionObject构建等待队列,当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。注意这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。
head和tail分别是AQS中的变量,其中head指向同步队列的头部,注意head为空结点,不存储信息。而tail则是同步队列的队尾,同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作。state变量则是代表同步状态,执行当线程调用lock方法进行加锁后,如果此时state的值为0,则说明当前线程可以获取到锁(在本篇文章中,锁和同步状态代表同一个意思),同时将state设置为1,表示获取成功。如果state已为1,也就是当前锁已被其他线程持有,那么当前执行线程将被封装为Node结点加入同步队列等待。其中Node结点是对每一个访问同步代码的线程的封装,从图中的Node的数据结构也可看出,其包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经被取消等。每个Node结点内部关联其前继结点prev和后继结点next,这样可以方便线程释放锁后快速唤醒下一个在等待的线程,Node是AQS的内部类
static final class Node {
//共享模式
static final Node SHARED = new Node();
//独占模式
static final Node EXCLUSIVE = null;
//标识线程已处于结束状态
static final int CANCELLED = 1;
//等待被唤醒状态
static final int SIGNAL = -1;
//条件状态,
static final int CONDITION = -2;
//在共享模式中使用表示获得的同步状态会被传播
static final int PROPAGATE = -3;
//等待状态,存在CANCELLED、SIGNAL、CONDITION、PROPAGATE 4种
volatile int waitStatus;
//同步队列中前驱结点
volatile Node prev;
//同步队列中后继结点
volatile Node next;
//请求锁的线程
volatile Thread thread;
//等待队列中的后继结点,这个与Condition有关,稍后会分析
Node nextWaiter;
//判断是否为共享模式
final boolean isShared() {
return nextWaiter == SHARED;
}
//获取前驱结点
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//.....
}
获得锁源码
- 我们通过源码一步一步来看ReentrantLock加锁原理,通过类图我们看到非公平锁NonfairSync()或者公平锁都有sync作为内部类这里sync为它的同步器,而sync都继承AQS,大家可以先看一下AQS原理这样对我们下面的理解有锁帮助。JUC-AQS原理
ReentrantLock lock=new ReentrantLock();
lock.lock();
try {
//被锁
}finally {
lock.unlock();
}
- 创建对象,默认调用无参构造方法,此时创建的是非公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
- lock方法:我们看到它是调用sync的lock方法,而sync是同步器(AQS里面讲过它里面封装加锁的方法)。
public void lock() {
sync.lock();
}
- 调用非公平锁lock的方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
我们看到这里的if-else块,这里用CAS方式尝试修改state来去加锁,compareAndSetState(0, 1)当加锁成功setExclusiveOwnerThread(Thread.currentThread())
将锁的owner改为当前线程。
当我们加锁失败调用acquire方法,就是当我们线程执行代码块时,此时锁被其他线程占用。
- acquire(1),加锁失败继续看源码,acquire是在AQS类里面的方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
!tryAcquire(arg) 这里在此尝试加锁,如果还是没有获取到那么前面有!取反那就是真,那么后面acquireQueued就是尝试创建节点对象,来将这个线程放入等待队列。
当我们第一次常见节点,会常见两个节点,一个空节点head指向它,第二个节点才是我们需要进入等待队列的线程,此时只有它一个线程tail指向它。
- acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- lock.unlock();释放锁
public void unlock() {
sync.release(1);
}
- release:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
可重入原理
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
加锁时自增每重入一次状态加一,解锁时逐步减一,知道状态为为0时释放锁,唤醒阻塞线程。
可打断原理
此模式下即使它被打断任然会驻留在AQS队列里,等获得锁后方能继续运行(是继续运行,打断标记被设置为true)。
使用可打断模式使用抛出异常方式。
公平锁
ReentrantLock有两个构造参数,一个有参,一个无参,默认的无参相当于有参数的false,默认为非公平锁。
- 如果为参数为false为非公平锁,非公平锁获取锁的方式为如果当前没有线程占有锁,当前线程直接通过cas指令占有锁,管他等待队列,就算自己排在队尾也是这样
- 如果为参数为true为公平锁,公平锁获取锁的方式为如果当前线程是等待队列的第一个或者等待队列为空或为当前占有线程,则通过cas指令设置state为1或增加。公平锁加锁的线程全部是按照先来后到的顺序,依次进入等待队列中排队的,不会盲目的胡乱抢占加锁。
条件变量的实现原理
- await流程
线程await后state值是-2,而等待队列
读写锁
读写锁,当多个线程对数据都是读操作时,这个时候我们可以使用读锁,读锁可以让多个线程并发同时访问临界区资源,因为多个线程都是读取操作,所以不涉及线程安全问题。而写锁,设计多个线程访问临界区资源,并修改临界区资源,这就牵扯到线程安全问题,所以它就要保证多个线程访问临界区时互斥访问。
class RWDemo{
private ReentrantReadWriteLock reentrantReadWriteLock=new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock readLock=reentrantReadWriteLock.readLock();//读锁,可并发应为不会造成线程安全问题
private ReentrantReadWriteLock.WriteLock writeLock=reentrantReadWriteLock.writeLock();//写锁,不可并发会造成线程安全问题
private Object data;
public Object readData(){//读数据
readLock.lock();
try{
return data;
}finally {
readLock.unlock();
}
}
public void writelock(Object value){//写数据
writeLock.lock();
try {
data=value;
}finally {
writeLock.unlock();
}
}
}
因为读锁不牵扯线程安全问题,所以读锁不支持条件变量。
读写锁,重入时不支持升级重入,就是读锁之后加写锁。
下面的代码时错误的
readLock.lock();
try{
writeLock.lock();
try {
}finally {
writeLock.unlock();
}
}finally {
readLock.unlock();
}
但可以重入可以锁降级,写锁之后加读锁。
读写锁的原理
StampedLock
StampedLock是一种有乐观读锁,用戳形式使用的加锁方式。
这里的戳是我代码里long类型的stamp。
我加写锁原理和写法与前面相似,但是读锁,它提供了一种特殊的方法,我们想要读取临界区的数据时,我们先用一种不加锁的方式,获取一个戳,然后在下面读代码里判断在读取时是否有别的线程修改的数据(如果修改了那么戳就会变化)。当戳没变化那么就读取。如果戳变化那么说明数据被改了,那么就使用读锁(因为读锁支持并发,但是读写操作互斥)。
class RWDemo{
StampedLock lock=new StampedLock();
Object data;
public Object read(){
long stamp = lock.tryOptimisticRead();
if(!lock.validate(stamp)){//真就是戳没被改变,临界区值没被改变
return data;
}
stamp = lock.readLock();//被改变了,那么就用读锁
try{
System.out.println("升级为读锁来读");
return data;
}finally {
lock.unlockRead(stamp);
}
}
public void write(){
long stamp = lock.writeLock();
try {
System.out.println("写");
}finally {
lock.unlockWrite(stamp);
}
}
}
本文地址:https://blog.csdn.net/qq_42411214/article/details/105100303
下一篇: 白天比夜里热