JUC总结3

CAS

简介

CAS的全称是"比较并交换",是一种无锁的原子操作,其体现了乐观所的思想,在无锁的情况下保证线程操作共享数据的原子性。

CAS一共有3个值:

1、V:要更新的值;

2、E:预期值;

3、N:新值。

在比较和交换的过程中,需要比较V与E是否相等,若相等,则将V设置为N;若不相等,则说明有其他线程对共享变量进行了更新,当前线程应放弃。若CAS失败,则继续尝试获取新值,直至更新成功。

CAS底层实现

CAS底层是通过Unsafe类来直接调用操作系统底层的CAS指令,Unsafe类中的方法都是native方法,由系统提供接口实现,Unsafe类对CAS实现是通过C++进行的。

CAS存在的问题

ABA问题

ABA问题是指一个位置原来是A,后面被改为B,再后来又被改为A,进行CAS操作的线程无法知道在该位置的值发生过改变。

可通过加入时间戳或版本号的方式,解决ABA问题,在进行CAS操作时,需要值和版本号(或时间戳)均匹配时才能进行修改操作。在Java中,AtomicStampedReference类就实现了这种机制,它会同时检查引用值和stamp是否都相等。

循环性能开销问题

CAS在进行自旋操作时,若一直不成功,则会给CPU代理巨大的开销,可通过限制自旋次数来解决这个问题。

只能保证一个变量的原子操作

CAS只能保证对一个变量进行原子操作,当存在多个变量时,CAS无法直接保证对他们的原子操作。可通过以下两种方式解决:

1、考虑改用锁实现原子操作;

2、合并多个变量,将多个变量封装成一个对象,通过AtomicReference来保证原子性。

volatile

volatile的主要作用

保证变量在线程间的可见性

当一个线程修改某个变量的值时,volatile关键字会将修改的值刷新到主存中,该新值就对其他线程可见,对于volatile修饰的关键字,禁止使用JIT(即时编译器)进行优化。

禁止指令重排序

使用volatile修饰的变量,会在读、写共享变量时加上屏障,阻止其他读写操作越过屏障对共享变量进行读写操作。

对于volatile的写操作,会在其前后加上屏障,其中在写操作前的屏障,禁止前面的普通读操作与该写操作重排;写操作后的屏障,禁止后面的volatile读写操作与该写操作重排序,如下图所示:

对于volatile的读操作,会在其后面加上屏障,禁止后面的普通读写操作与重排序,该屏障强制让本地内存中的变量值失效,从而重新从主内存中读取最新的值,如下图所示:

对于volatile的写操作前的操作,不会被编译器重排到该写操作后;对于volatile的读操作后的操作,不会被编译器重排到该写操作前。

AQS

AQS是阻塞式锁和相关同步工具的框架,中文名为抽象队列同步器,它是构建锁或其他同步组件的基础框架。其思想是,若被请求的资源空闲,则线程申请该资源成功,反之该线程则进入一个等待队列,其他线程释放该资源时,系统随机选择一个在等待队列中的线程,赋予其资源。

AQS是悲观锁,通过Java实现,其开启和释放都需要手动进行,其工作机制是:

同步状态state由volatile修饰,保证其他线程对其可见,同时采用一个FIFO双端队列存储线程,如下图所示:

当线程要获取锁时,会尝试改变state的状态,若state状态为0,则可将其改为1,该线程抢占锁成功。若线程强制锁失败,则线程进入FIFO队列中等待。

AQS通过CAS自旋锁保证线程的原子性,保证每次只能有一个线程修改state成功。

AQS既可实现公平锁,又可实现非公平锁。当新来的线程与队列中的线程共同强资源时,该锁为非公平锁(如AQS实现类ReentrantLock),新来的线程进入等待队列中等待,只允许队列中head线程占用资源时,该锁为公平锁。

ReentrantLock

概述

ReentrantLock是可重入锁,其可中断、可设置超时时间、可设置公平锁、支持多个条件变量、支持重入。ReentrantLock主要利用CAS+AQS实现的,通过new ReentrantLock()创建的锁默认为非公平锁,要将其设置为公平锁,则应该通过有参构造函数的方式创建,并将变量设置为true(该变量设置为false时实现的非公平锁),代码:

//实现公平锁
ReentrantLock lock = new ReentrantLock(true);

构造函数如下图:

NonfairSync、FairSync的父类为Sync,Sync的父类为AQS。

加锁

调用lock()方法可实现加锁,unlock()方法实现解锁。在公平锁的条件下,锁会授予给等待时间最长的线程,在非公平锁的条件下,其加锁的方式如下:

当线程调用lock()尝试获取锁时,首先通过CAS方式修改state变量,若成功将其修改为1,则让exclusiveOwnerThread线程指向这个线程,该线程获取锁成功。若修该失败,则线程获取锁失败,则线程进入等待队列中。当exclusiveOwnerThread为null时,则持有锁的线程释放了锁,则会唤醒双向队列中在head位置的线程,公平锁和非公平锁的情况见上述AQS介绍。

为了实现锁的可重入,ReentrantLock内部有一个计数器跟踪线程持有锁的次数,当线程首次获取锁时,计数器的值变为1,如果同一线程再次获取锁,计数器增加;每释放一次锁,计数器减 1。当线程调用unlock()方法时,ReentrantLock会将持有锁的计数减1,如果计数到达0,则释放锁,并唤醒等待队列中的线程来竞争锁。

Synchronized和Lock对比

1、Synchronized是关键字,源码在JVM中,通过C++实现;而Lock是接口,源码由JDK提供,通过Java实现;

2、使用Synchronized时,退出同步代码块会自动释放锁,而使用lock时,需要通过unlock()释放;

3、两种都是悲观锁,支持互斥、同步、锁重入等功能;

4、相比于Synchronized,Lock支持获取锁状态、设置公平锁、可打断、可超时等,同时支持ReentrantLock、ReentrantReadWriteLock不同适合条件的实现;

5、在没有竞争时,Synchronized做了如偏向锁、轻量级锁等优化,但在竞争激烈时,Lock的性能更好。

相关推荐
Kisorge19 分钟前
【C语言】指针数组、数组指针、函数指针、指针函数、函数指针数组、回调函数
c语言·开发语言
路在脚下@1 小时前
spring boot的配置文件属性注入到类的静态属性
java·spring boot·sql
森屿Serien1 小时前
Spring Boot常用注解
java·spring boot·后端
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
苹果醋32 小时前
React源码02 - 基础知识 React API 一览
java·运维·spring boot·mysql·nginx
晓纪同学2 小时前
QT-简单视觉框架代码
开发语言·qt
威桑2 小时前
Qt SizePolicy详解:minimum 与 minimumExpanding 的区别
开发语言·qt·扩张策略
Hello.Reader2 小时前
深入解析 Apache APISIX
java·apache
飞飞-躺着更舒服2 小时前
【QT】实现电子飞行显示器(简易版)
开发语言·qt
明月看潮生2 小时前
青少年编程与数学 02-004 Go语言Web编程 16课题、并发编程
开发语言·青少年编程·并发编程·编程与数学·goweb