概述
为什么会有锁升级的步骤呢,假设没有这个步骤,多个线程竞争时,抢到锁的线程直接运行,其他的都直接sleep/wait
,然后等第一个线程运行完成后,再由操作系统唤醒接下来的线程。这个一套动作下来就很费调度资源.
所以锁的升级相当于加了一层缓存,实在是竞争的很激烈,再由操作系统介入
平时分布式环境下控制并发都是用的redis
中间件。
但在一些mini项目
中对多线程的控制,简单操作可以用synchronized
关键字来控制;
synchronized
上锁时,锁的信息都放在对象头上,对象除了自己的数据外还有头部信息.
锁升级过程
锁的升级过程,其实就是线程争抢锁的过程;
第一个线程获取锁后,会切换到偏向锁,之后当前线程可重复进入锁住的代码块,此时第二个线程来获取锁,就会升级到轻量级级锁,然后第二个线程CAS自旋等待,自旋失败到一定次数后还没获取到锁,此时就会升级为重量级锁.
cas,比较并交换
cas算法的过程是这样的,cas包括有三个值:
v表示要更新的变量
e表示预期值,就是旧的值
n表示新值
更新时,判断只有e的值等于v变量的当前旧值时,才会将n新值赋给v,更新为新值。
否则,则认为已经有其他线程更新过了,则当前线程什么都不操作,最后cas放回当前v变量的真实值
AQS
AQS
,即AbstractQueuedSynchronizer
, 队列同步器,它是Java
并发用来构建锁和其他同步组件的基础框架。其定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch.
AQS
核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH
队列的变体实现的,将暂时获取不到锁的线程加入到队列中.
CLH:Craig、Landin and Hagersten
队列,是单向链表,AQS
中的队列是CLH
变体的虚拟双向队列(FIFO)
,AQS
是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。
获取同步状态(加锁)
假设线程A
要获取同步状态(这里想象成锁,方便理解),初始状态下state=0
,所以线程A
可以顺利获取锁,A
获取锁后将state置为1
。在A
没有释放锁期间,线程B
也来获取锁,此时因为state=1
,表示锁被占用,所以将B
的线程信息和等待状态等信息构成出一个Node
节点对象,放入同步队列,head
和tail
分别指向队列的头部和尾部(此时队列中有一个空的Node
节点作为头点,head
指向这个空节点,空Node
的后继节点是B
对应的Node
节点,tail
指向它),同时阻塞线程B
(这里的阻塞使用的是LockSupport.park()
方法)。后续如果再有线程要获取锁,都会加入队列尾部并阻塞。
释放同步状态(解锁后其他线程获取锁)
当线程A
释放锁时,即将state置为0
,此时A
会唤醒头节点的后继节点
(所谓唤醒,其实是调用
LockSupport.unpark(B)
方法),即B线程
从LockSupport.park()
方法返回,此时B
发现state
已经为0
,
所以B线程
可以顺利获取锁,B
获取锁后B
的Node
节点随之出队。
以ReentrantLock为例
其中非公平锁的加锁流程大致如下
java.util.concurrent.locks.ReentrantLock.NonfairSync
先cas
判断下当前是否有锁,没有锁直接占用;
有锁时判断是否是当前线程占用(相同线程可重复入锁)
非持有锁的线程则加入到等待队列中(双向链表)
一旦进入到AQS
链表中,都是按照FIFO
顺序去获取锁了(这点公平锁和非公平锁都一样)
非公平锁只是在一开始获取锁的时候可以不排队互相竞争,之后进入AQS
队列后线程之间还是得排队的;还有一个特殊的点是非公平锁在当前有其他线程在排队时会直接尝试获取锁,而不是像公平锁一样有其他线程在排队时会直接加入到AQS
队列中去排队