本文主要包含的内容:可重入锁(ReedtrantLock)、公平锁、非公平锁、可重入性、同步队列、CAS等概念的理解

显式锁

上一篇文章提到的synchronized关键字为隐式锁,会自动获取和自动释放的锁,而相对的显式锁则需要在编程时指明何时获取锁,何时释放锁。

通常,锁提供对共享资源的独占访问:一次只能有一个线程可以获取锁,并且对共享资源的所有访问都需要先获取锁;而有一些锁可能允许并发访问共享资源。

本文主要讲解可重入锁(ReentrantLock),该锁为独占共享资源锁,即独占锁。

1.可重入锁(ReentrantLock)

可重入锁指的是同一个线程可无限次地进入同一把锁的不同代码,又因该锁通过线程独占共享资源的方式确保并发安全,又称为独占锁

举个例子:同一个类中的synchronize关键字修饰了不同的方法。synchronize是内置的隐式的可重入锁,例子中的两个方法使用的是同一把锁,只要能执行testB()也就说明线程拿到了锁,所以执行testA()方法就不用被阻塞等待获取锁了;如果不是同一把锁或非可重入锁,就会在执行testA()时被阻塞等待。

public class Demo {

    public synchronized void testA(){
System.out.println("执行测试A");
} public synchronized void testB(){
System.out.println("执行测试B");
testA();
} }

1.1.可重入锁的类图关系

ReentrantLock实现了Lock接口和Serializable接口(都没画出来),它有三个内部类(SyncNonfairSyncFairSync),Sync是一个抽象类,它继承 AbstractQueuedSynchronizer 抽象同步队列,同时有两个实现类(NonfairSyncFairSync),其中父类AQS是个模板类提供了许多以锁相关的操作,子类分别是两种不同的获取锁实现(非公平锁和公平锁)。AQS 又继承了AbstractOwnableSynchronizer类,AOS用于保存锁被独占的线程对象。

ReentrantLock 类的构造方法有如下两种,很显然,在对象实例化时将决定同步器Sync是公平还是非公平。

// ReentrantLock类

private final Sync sync;
// 默认非公平
public ReentrantLock() {
sync = new NonfairSync();
} public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

先关注ReentrantLock类的方法lock() 和 unlock()。从源码可以发现ReentrantLock类的方法是交给内部类Sync 类来实现,而lock()方法在Sync类中是个抽象方法,具体实现在子类FairSync和NonfairSync类。其实ReentrantLock类中的其他方法也是交给Sync类去处理的,所以想要理解ReentrantLock类的重点是理解Sync类。

注意一个点:Sync类中lock()抽象方法不是Lock接口的抽象方法,它们是通过调用(如下)代码产生关联的。

// java.util.concurrent.locks.ReentrantLock类

public void lock() {
sync.lock();
}
public void unlock() {
sync.release(1);
}

结论一:

  • ReentrantLock 可重入锁获取锁有两种实现:公平和非公平;注意:从类图关系我们可以知道,公平和非公平内部类只有两个方法,都是与获取锁有关,公平与否仅针对获取锁而言,也即是lock()方法。PS:tryAcquire(int)最终会被lock()调用。

  • ReentrantLock的理解重点源码应该关注内部同步器Sync类和Sync的父类抽象同步队列AbstractQueuedSynchronizer。

1.2.怎么使用ReentrantLock

使用案例:并发安全访问共享资源

public class LockDemo {
public static void main(String[] args) {
// 简单模拟20人抢优惠
for(int i=0;i<20;i++){
new Thread(new ThreadDemo()).start();
}
} }
// 前十位可以获取优惠,凭号码兑换优惠
class ThreadDemo implements Runnable{
private static Integer num = 10;
private static final ReentrantLock reentrantLock = new ReentrantLock();
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} // 获取锁
reentrantLock.lock();
try {
if(num<=0){
System.out.println("已被抢完,下次再来");
return;
}
System.out.println(Thread.currentThread().getName()+"用户抢到的号码:"+num--);
}finally {
// 释放锁
reentrantLock.unlock();
} }
}

执行结果:

Thread-18用户抢到的号码:10

Thread-14用户抢到的号码:9

Thread-15用户抢到的号码:8

Thread-4用户抢到的号码:7

Thread-1用户抢到的号码:6

Thread-19用户抢到的号码:5

Thread-11用户抢到的号码:4

Thread-17用户抢到的号码:3

Thread-16用户抢到的号码:2

Thread-13用户抢到的号码:1

已被抢完,下次再来

已被抢完,下次再来

……

常用的一些方法

方法名称 描述
void lock() 获取锁
boolean tryLock() 尝试获取锁,调用该方法不会阻塞,会立即返回获取结果,获取到则返回true,获取不到则返回false
boolean tryLock(long timeout, TimeUnit unit) 尝试在阻塞的指定时间内获取锁
void lockInterruptibly() 获取锁,除非当前线程是interrupted,即发生中断时,结束锁的获取
void unlock() 释放锁
boolean isHeldByCurrentThread() 查询此锁是否由当前线程持有
boolean isLocked() 查询此锁是否由任何线程持有

2.一些概念的理解

2.1.锁和同步队列的关系

前面讲述过:ReentrantLock类的方法都是交给内部类Sync类来实现的。

Sync和它的子类都实现了,为什么还要ReentrantLock类来套这么一层呢?这关系到锁的使用和实现的问题。

  • 锁是面向开发者,隐藏细节让锁的开发变得更简洁;

  • 抽象同步队列是面向锁的实现,屏蔽了同步状态的管理、线程的排队、等待与唤醒等底层操作,简化了自定义同步器和锁的实现。

说白了,ReentrantLock(锁)类为了简化开发者的使用,具体实现交由其内部类自定义的同步器Sync去处理,而AQS则以模板的方式提供一系列有关锁的操作及部分可被子类Sync重写的模板方法。

2.2.公平锁与非公平锁概述

公平与非公平指的是获取锁的机制不同。

公平锁强调先来后到,表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定,即同步队列记录线程先后顺序,队列的特性FIFO(先进先出);

非公平锁只要CAS设置同步状态成功,当前线程就会获取到锁,没获取成功的依然放在同步队列中按FIFO原则等待,等待下一次的CAS操作。

从源码上可以知道它们的主要区别是多一个判断:!hasQueuedPredecessors()

该判断表示:加入了同步队列中当前节点是否有前驱节点,即在同步队列中有没有比当前线程更早的线程在队列中等待了,而非公平锁是没有这个判断的

// java.util.concurrent.locks.ReentrantLock.NonfairSync
// 非公平
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires); }
// java.util.concurrent.locks.ReentrantLock.Sync
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;
} // java.util.concurrent.locks.ReentrantLock.FairSync
// 公平:比非公平多了一步判断 !hasQueuedPredecessors()
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 主要区别:!hasQueuedPredecessors()
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;
}

附上获取锁时公平锁和非公平锁的源码区别图

结论二:

公平锁和非公平锁的主要区别是:!hasQueuedPredecessors(),表示同步队列中当前节点是否有前驱节点,即在同步队列中有没有比当前线程更早的线程在队列中等待了,而非公平锁没有这个判断

2.3.实现锁的可重入特性

前面在公平锁与非公平锁概述这点中,附上了对比两者的关键源码,其中可重入的源码是一样的

 ......
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}

判断当前线程和当前拥有独占访问权限的线程对比,是同一个线程则可以重新进入同一把锁。处理逻辑是:对同步状态state加上acquires=1,然后返回true,返回true即获取锁成功。

AbstractOwnableSynchronizer类用于保存锁被独占的线程对象,AOS类只有以下两个方法:

  • Thread getExclusiveOwnerThread()为获取当前拥有独占访问权限的线程,

  • void setExclusiveOwnerThread(Thread)为设置当前拥有独占访问权限的线程。

所以每次在获取锁成功后会做这么一步:setExclusiveOwnerThread(current)

if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}

ReentrantLock的内部类Sync继承AQS实现模板方法tryRelease(int) 实现锁的释放规则,源码如下方法参数releases=1。

先判断该线程是否为当前拥有独占访问权限的线程,再判断同步状态,如果状态不为0,则锁还没释放完,不执行 setExclusiveOwnerThread(null) 即不释放独占访问权限的线程。因为发生锁的重入时,同步状态state>1,所以锁释放时同步状态需要一层层出来,直到同步状态为0时,才会置空拥有独占访问权的线程。因此AQS的state状态表示锁的持有次数。

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;
}

结论三:公平和非公平的可重入性都一样,并且同步状态state的作用如下

  • 同步状态state<0 表示throw new Error("Maximum lock count exceeded");

  • 同步状态state=0 表示锁没有被占用

  • 同步状态state=1 表示锁被占用了

  • 同步状态state>1 表示锁发生了重新进入

即同步状态state等于锁持有的次数。

2.4.CAS概述

CAS的全称是Compare And Swap,意思是比较并交换,是一种特殊的处理器指令。

以方法compareAndSetState(int expect,int update)为例:

处理逻辑是:期望参数expect值跟内存中当前状态值比较,等于则原子性的修改state值为update参数值。

获取锁操作:compareAndSetState(0, 1),当同步状态state=0时,则修改同步状态state=1

compareAndSetState() 方法调用了Unsafe 类下的本地方法compareAndSwapInt(),该方法由JVM实现CAS一组汇编指令,指令的执行必须是连续的不可被中断的,不会造成所谓的数据不一致问题,但只能保证一个共享变量的原子性操作

同步队列中还有很多CAS相关方法,比如:

compareAndSetWaitStatus(Node,int,int):等待状态的原子性修改

compareAndSetHead(Node):设置头节点的原子性操作

compareAndSetTail(Node, Node):从尾部插入新节点的原子性操作

compareAndSetNext(Node,Node,Node):设置下一个节点的原子性操作

除了同步队列中提供的CAS方法,在Java并发开发包中,还提供了一系列的CAS操作,我们可以使用其中的功能让并发编程变得更高效和更简洁。

java.util.concurrent.atomic一个小型工具包,支持单个变量上的无锁线程安全编程。

比如:num++ 或num--,自增和自减这些操作是非原子性操作的,无法确保线程安全,为了提高性能不考虑使用锁(synchronized、Lock),可以使用AtomicInteger类的方法来完成自增、自减,其本质是CAS原子性操作。

AtomicInteger num = new AtomicInteger(10);
// 自增
System.out.println(num.getAndIncrement());
// 自减
System.out.println(num.getAndDecrement());

注意:只是在自增和自减的过程是原子性操作。

如下代码下面整块代码是非线程安全的,只是num.getAndDecrement()自减时是原子性操作,也即是并发场景下num.get()无法确保获取到最新值。

private static AtomicInteger num = new AtomicInteger(10);
......
if(num.get()<=0){
System.out.println("已被抢完,下次再来");
return;
}
System.out.println("号码:"+num.getAndDecrement());

支持哪些数据类型呢?

    基本数据类型

  • AtomicBoolean:原子更新布尔值类型

  • AtomicInteger:原子更新整数类型

  • AtomicLong:原子更新长整型

  • 数组类型

  • AtomicIntegerArray:原子更新整型数组里的元素

  • AtomicLongArray:原子更新长整型数组里的元素

  • AtomicReferenceArray:原子更新引用类型数组里的元素

  • 引用类型

  • AtomicReference:原子更新引用类型

  • AtomicMarkableReference:原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)

  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。

  • 更新类型中的字段

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器

  • AtomicLongFieldUpdater:原子更新长整型字段的更新器

  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段

3.抽象同步队列AQS

AbstractQueuedSynchronizer 抽象同步队列,它是个模板类提供了许多以锁相关的操作,常说的AQS指的就是它。AQS继承了AbstractOwnableSynchronizer类,AOS用于保存线程对象,保存什么线程对象呢?保存锁被独占的线程对象

抽象同步队列AQS除了实现序列化标记接口,并没有实现任何的同步接口,该类提供了许多同步状态获取和释放的方法给自定义同步器使用,如ReentrantLock的内部类Sync。抽象同步队列支持独占式或共享式的的获取同步状态,方便实现不同类型的自定义同步器。一般方法名带有Shared的为共享式,比如,尝试以共享式的获取锁的方法int tryAcquireShared(int),而独占式获取锁方法为boolean tryAcquire(int)

AQS是抽象同步队列,其重点就是同步队列如何操作同步队列

3.1同步队列

双向同步队列,采用尾插法新增节点,从头部的下一个节点获取操作节点,节点自旋获取同步锁,实现FIFO(先进先出)原则。

理解节点中的属性值作用

  • prev:前驱节点;即当前节点的前一个节点,之所以叫前驱节点,是因为前一个节点在使用完锁之后会解除后一个节点的阻塞状态;

  • next:后继节点;即当前节点的后一个节点,之所以叫后继节点,是因为“后继有人”了,表示有“下一代”节点承接这个独有的锁;

  • nextWaiter:表示指向下一个Node.CONDITION状态的节点(本文不讲述Condition队列,在此可以忽略它);

  • thread:节点对象中保存的线程对象,节点都是配角,线程才是主角;

  • waitStatus:当前节点在队列中的等待状态

因篇幅原因,关于抽象同步队列AQS、锁的获取过程、锁的释放过程、自旋锁、线程阻塞与释放、线程中断与阻塞关系等内容将在下一篇文章展开讲解。

图是新增节点的过程

Java中的线程安全与线程同步

Java线程状态(生命周期)--一篇入魂

自己编写平滑加权轮询算法,实现反向代理集群服务的平滑分配

Java实现平滑加权轮询算法--降权和提权

Java实现负载均衡算法--轮询和加权轮询

Java全栈学习路线、学习资源和面试题一条龙

更多优质文章,请关注WX公众号:Java全栈布道师

Java 可重入锁的那些事(一)的更多相关文章

  1. 轻松学习java可重入锁(ReentrantLock)的实现原理

    转载自https://blog.csdn.net/yanyan19880509/article/details/52345422,(做了一些补充) 前言 相信学过java的人都知道 synchroni ...

  2. java 可重入锁ReentrantLock的介绍

    一个小例子帮助理解(我们常用的synchronized也是可重入锁) 话说从前有一个村子,在这个村子中有一口水井,家家户户都需要到这口井里打水喝.由于井水有限,大家只能依次打水.为了实现家家有水喝,户 ...

  3. 轻松学习java可重入锁(ReentrantLock)的实现原理(转 图解)

    前言 相信学过java的人都知道 synchronized 这个关键词,也知道它用于控制多线程对并发资源的安全访问,兴许,你还用过Lock相关的功能,但你可能从来没有想过java中的锁底层的机制是怎么 ...

  4. Java可重入锁如何避免死锁

    本文由https://bbs.csdn.net/topics/390939500和https://zhidao.baidu.com/question/1946051090515119908.html启 ...

  5. Java 多线程 重入锁

    作为关键字synchronized的替代品(或者说是增强版),重入锁是synchronized的功能扩展.在JDK 1.5的早期版本中,重入锁的性能远远好于synchronized,但从JDK 1.6 ...

  6. Java 可重入锁

    一般意义上的可重入锁就是ReentrantLock http://www.cnblogs.com/hongdada/p/6057370.html 广义上的可重入锁是指: 可重入锁,也叫做递归锁,指的是 ...

  7. Java不可重入锁和可重入锁的简单理解

    基础知识 Java多线程的wait()方法和notify()方法 这两个方法是成对出现和使用的,要执行这两个方法,有一个前提就是,当前线程必须获其对象的monitor(俗称“锁”),否则会抛出Ille ...

  8. java可重入锁reentrantlock

    public class ReentrantDemo { //重入锁 保护临界区资源count,确保多线程对count操作的安全性 /*public static ReentrantLock rtlo ...

  9. Java可重入锁与不可重入锁

    可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的. synchronized 和   ReentrantLock 都是可重入锁. 可重入 ...

随机推荐

  1. 区分 python 爬虫或者是写自动化脚本时遇到的 content与text的作用

    通常在使用过程中或许我们能够轻而易举的会使用requsts模块中的content 与 text ,从print结果来看根本看不出任何区别: 总结精髓,text 返回的是unicode 型的数据,一般是 ...

  2. CSS 网页字体最佳实践

    一般在网页的字体设置中,可以将字体分类三类: 系统字体:使用系统自带的字体 兜底字体:当系统字体无法正常使用,而兜底的字体 Emoji 字体:显示网页中的表情字体 为了满足不同平台,以及 Emoji ...

  3. JVM学习笔记-从底层了解程序运行(一)

    1:JVM基础知识 什么是JVM 1. java虚拟机,跨语言的平台,实现java跨平台 2. 可以实现多种语言跨平台,只要该语言可以编译成.class文件 3. 解释执行.class文件 java是 ...

  4. 19.Tomcat多实例部署及负载均衡、动静分离

    Tomcat多实例部署及负载均衡.动静分离 目录 Tomcat多实例部署及负载均衡.动静分离 Tomcat多实例部署 安装jdk 设置jdk环境变量 安装tomcat 配置 tomcat 环境变量 修 ...

  5. SAP 实例 12 List Box with Value List from PBO Module

    REPORT demo_dynpro_dropdown_listbox. DATA: name TYPE vrm_id, list TYPE vrm_values, value LIKE LINE O ...

  6. Maven + SSM环境搭建

    Maven + SSM 之前Maven+SSM都是照着搭建的,自己想写点什么的时候发现搭建的过程不清楚. 于是花了时间边整理思路边搭建,并把搭建过程记录下来. 视频看来终觉浅,还是需要自己动手实践,捋 ...

  7. 基于thinkphp6 layui的优秀极速后台开发框架推荐

    很多时候我们在做项目开发的时候,苦于没有好一点的轮子,自己动手开发的话,太耗费时间了,如果采用VUE的话,学习成本跟调试也比较麻烦, 而且有时候选用的东西甲方也不太容易接受,现在给大家介绍一款优秀的极 ...

  8. zabbix监控mysql主从同步

    获取主从复制sql线程和I/O线程状态是否为yes #!/bin/bash HOSTNAME="数据库IP" PORT="端口" USERNAME=" ...

  9. vmware修改虚拟机网卡mac地址

    选中"虚拟机" 右键 "设置",然后选中"网络适配器",然后点击"高级",设置"MAC地址"

  10. 【微服务专题之】.Net6下集成消息队列上-RabbitMQ

    ​ 微信公众号:趣编程ACE关注可了解更多的.NET日常实战开发技巧,如需源码 请公众号后台留言 源码;[如果觉得本公众号对您有帮助,欢迎关注] .Net中RabbitMQ的使用 [微服务专题之].N ...