【Java 并发】 之 AQS 详解 & volatile关键字
Java并发之AQS详解
谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!
类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch...。
它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,见文末。state的访问方式有三种:
getState()
setState()
compareAndSetState()
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
acquire()的流程的流程图如下:
调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
AtomicInteger.increment
方法能保证原子性,而简单的++
运算却不能保证原子性。
CPU内存架构
现代计算机都是多处理机CPU,每个核心(Core)都有一套寄存器,CPU访问寄存器的速度是最快的,但是访问RAM内存速度相对来说要慢很多,所以为了解决寄存器与内存速度的不协调问题,每个CPU内核都会有一级或多级高速缓存(Cache):
当两个线程同时运行的时候,可能会出现下面的情况:两个线程同时使用一个共享变量,会在Cache中缓存该变量,当一个线程修改共享变量时,Cache未能及时将修改的值放回RAM,导致另一个线程不能读取修改后的值。
volatile关键字的作用
前面讲CPU内存架构就是为了说明volatile
关键字的作用:用来保证对变量修改后,能立即写回主存,从而保证共享变量的修改对所有线程是可见的。JVM语言规范将该特性称为happens-before
。
另外,在Java官方教程中讲“原子操作”时,提到平常写代码遇到的最简单的原子操作:
-
对引用变量(不是引用的对象)和大多数基本类型变量(除了long和double)的读写操作都是原子性的。
为什么long和double除外呢,我个人是这么理解的:因为long和double是8个字节长的,如果程序运行在32位的机器上,JVM需要执行更多的操作来实现long和double的运算。所以JVM 不能保证 long和double类型读写操作的原子性。
对于声明了
volatile
的所有变量(包括long和double)的读写操作都是原子性的。
从上面的说明我们可以了解到:volatile
关键字修饰的所有变量读写操作都是原子性的。那么是不是意味着对volatile
修饰的int
值进行++
操作也是原子性的。答案是否定的,volatile
不能保证++
,--
操作的原子性,这里所说的读写操作仅仅是指“取值”和“赋值”操作。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
手把手:Python加密货币价格预测9步走,视频+代码
YouTube网红小哥Siraj Raval系列视频又和大家见面啦!今天要讲的是加密货币价格预测,包含大量代码,还用一个视频详解具体步骤,不信你看了还学不会! 点击观看详解视频 时长22分钟 有中文字幕 ▼ 预测加密货币价格其实很简单,用Python+Keras,再来一个循环神经网络(确切说是双向LSTM),只需要9步就可以了!比特币以太坊价格预测都不在话下。 这9个步骤是: 数据处理 建模 训练模型 测试模型 分析价格变化 分析价格百分比变化 比较预测值和实际数据 计算模型评估指标 结合在一起:可视化 数据处理 导入Keras、Scikit learn的metrics、numpy、pandas、matplotlib这些我们需要的库。 ## Keras for deep learning from keras.layers.core import Dense, Ac
- 下一篇
阿里云主机ECS部署项目报:ERROR: cant resolve localhost address
一、我在阿里云买了个云主机ECS,在上面部署了一个Redis,并开启了远程连接,我在本地,通过IP+端口+用户名+密码,远程连接到Redis是一点问题都没有的。 二、现在我将项目部署到阿里云主机上去了。 我在启动项目的时候,一直给我报一个错误: [2018-05-04 15:50:04] ERROR: cant resolve localhost address java.net.UnknownHostException: iZbp17cj14ulhfrlj02rkaZ: iZbp17cj14ulhfrlj02rkaZ: Name or service not known at java.net.InetAddress.getLocalHost(InetAddress.java:1505) ~[?:1.8.0_171] at redis.clients.jedis.HostAndPort.getLocalHostQuietly(HostAndPort.java:105) [jedis-2.9.0.jar!/:?] at redis.clients.jedis.HostAndPort.&...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8编译安装MySQL8.0.19
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路