Lock的作用
由于多线程访问共享资源时,线程间因在同一时刻操作共享资源,可能导致共享资源不准确,线程获取的结果可能有误 。
所以为保障每个线程获取 / 操作的结果是准确的,就需要用 Lock 来实现。
Lock究竟是怎样的一个东西?🙃
饭要一口一口吃,想要弄明白lock 这个东西,得先知道对象在内存中究竟是一种怎样的结构。
对象的内存结构分为3块:对象头(markword、类型指针)、实例数据、对齐填充(忽略)
📌实例数据 指的是 对象的成员变量," 其实对象在堆中所占的空间都是用来存放成员变量的!"
📌对象头中的类型指针 指的是 当前对象是哪个Class的,在JVM堆中存放着Class对象,而每个实例对象就是根据这个类型指针来归纳到属于哪个Class(大类)中。
📌对齐填充 指的是 将对象的大小填充至8的倍数。为什么?作为CPU、寄存器的总线每次读取的数据大小应该要刚好填满寄存器效率才高。以64位操作系统来说,对应的寄存器大小也是64位,所以每次往寄存器放东西也应该放8个字节的数据大小,才保证寄存器刚好满负载,得到最大限度使用,因此对象不是8的倍数,就得填充够。
重点是对象头中的markword
我们来看看 markword 的结构(以64位操作系统为例)🧐
当对象处于不同锁状态下,它的内存分配就是这样子的了。
观察到它的 【无锁、轻量级锁状态】=【00】,【重量级锁】=【10】,【偏向锁】=【101】
.....................so easy👌
事不宜迟,现在就进行实战验证😉,上代码:
无锁对象🔽
偏向锁对象🔽
ps. 要看到偏向锁的效果,需要等JVM启动后才可以看到。在JVM启动的过程中会创建多个线程,这些线程都是启动JVM所必须的,避免不了出现内存资源的争抢,因此需要待JVM启动后,才进行对象的创建,避免在创建对象的过程中也参与进内存资源的争夺战中。
轻量级锁对象🔽
暂时扩展到这里....👍
接下来回归正题
Lock其实就是一个标识,当对象被打上这个标识的时候,就意味着同一时刻只能允许一个线程对其进行使用。
Lock它只是一个接口,具体有哪些实现类?🙄
当对象被打上锁的标识后==资源被一个线程独占,其它的线程要想去使用这个对象,这时JVM或者说操纵系统(OS)应该怎么样处理?
同样的,饭要一口一口吃,需要知道 " java语言的线程模型 " 是怎样的!为什么要强调是java 语言的线程模型? 因为不同语言对应内核
都有属于各自的线程模型。
📌而 java语言的线程模型是 【java通过new Thread()创建的一个线程 对应 内核的一个线程,意思就是 java线程:内核线程 = 1:1】,
为什么通过 new Thread() 创建的线程 就和 OS的线程 成 1:1关系呢?答案是:new Thread()方法实际调用的是本地方法创建的。
目光转回来。
基于这个线程模型,当对象被独占,而其他线程想要使用的话,就需要通过 Lock接口提供的实现类进行上锁和解锁操作和线程间的通信(这个等下说),看一下具体有哪些实现类(我是看jdk 1.8版本的)🔽
ReentrantLock 剖析
ReentrantLock 是一把可重入的锁,什么叫可重入?意思就是当一个线程获取到这个对象的锁之后,可以再次获取这把锁,不需要因可能拿锁失败而被阻塞等待。
这个很好理解......😁
📌其工作原理是咋样的?
在分析之前,需要知道一个极其重要的概念,相信有了解并发编程的伙伴都知道—— CAS 和 AQS 😫
📌CAS(compareAndSet.....)是轻量级锁的实现原理,这里又可以进行扩展了😄
-------有轻量级锁就有重量级锁,那什么是轻量级锁?什么是重量级锁?两者间的关系又是怎样的?
在老版本的 jdk中,是通过重量级锁来进行线程同步操作,通过对系统总线加锁从而达到资源独占的效果;当有一定并发量
但并发又不过高的情况下,这种加锁方式就显得尤为耗性能。鉴于此就衍生出了轻量级锁,轻量级锁是一个概念,目的是
将操作系统管理线程的工作交给了JVM进行管理,JVM对线程进行资源的访问进行控制。继续探究,它们之间的关系是怎样的?
它们之间存在一个升级的过程,是这样的🔽
CAS处理过程是这样的🔽
但CAS也存在一些问题:ABA问题,自旋方式的局限性,范围不能灵活控制。
✍ABA问题具体是什么?
线程A和线程B同时改变内存v(值为0),线程A正在准备开始执行CAS时,线程B已经将内存v的值改为1后又改为0,对于线程A进行CAS时是察觉不到值已经被修改过了。
解决办法:加版本号 或者 使用AtomicStampedReference
✍自旋方式的局限性?
由于一次CAS并不一定能够成功,所以往往会将CAS操作放入到循环中,而每次CAS都需要CPU切换线程来进行比较,一旦线程数多了,CPU来回切换频繁而且又不成功,白白浪费CPU资源,所以在高并发场景下CAS效率并不高。
✍范围不能灵活控制?
CAS只能对单个变量进行比较,而非多个变量。
CAS的底层是通过C++实现的:
Unsafe类中的compareAndSwapXX是一个本地方法,该方法的实现位于unsafe.cpp中
1. 先想办法拿到变量value在内存中的地址;
2. 通过Atomic::cmpxchg实现比较替换,其中参数X是即将更新的值,参数e是原内存的值;
📌AQS(AbstractQueueSynchronizer)是一个除了synchronized关键之外的锁机制,实现了锁是怎样工作的,也可以视作一个框架,通过继承的方式进行锁机制的重写从而实现各种锁处理的方式,最重要的是其中含有一个双向的同步队列(CLH)。
📌CLH同步队列 是将多个Node节点(由线程包装而成)连接在一起的一个抽象队列。
💡. 现在就可以结合AQS、CAS、CLH去看看ReentrantLock是如何实现锁功能的。
从加锁和解锁两方面进行探究.........
首先看看ReentrantLock的类结构🔽
好戏来了,要有心理准备,过程比较枯燥🤕
从一个简单的例子入手:
public class ReentrantLockTest {
public static void main(String[] args) {
// 默认非公平锁实现
ReentrantLock lock = new ReentrantLock();
// 公平锁实现
//ReentrantLock lock = new ReentrantLock(true);
new Thread(()->{
//1. 上锁
lock.lock();
try{
System.out.println("..........");
TimeUnit.SECONDS.sleep(60*60);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//2. 解锁
lock.unlock();
}
}).start();
}
}
📌上锁
//1. 进入lock方法
public void lock() {
//2.使用非公平锁实现
sync.lock();
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
//1.进入到非公平锁实现
final void lock() {
//2. 试想一下,当多个线程走到这里,是通过CAS操作来争抢修改state的值(代表持有锁的状
//态),谁修改成功就谁获得到锁(这里就是公平与公平实现的不用之处)
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//3. 如果当前线程的锁被抢走了,就要排队了...
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//看看它是怎样排队的...
//第一步:排队前,还不死心,看看还能不能拿到锁---tryAcquire()
//第二部:死心了,就把自己打包成Node然后去排队---addWaiter()
//第三步:在队列中挣扎吧,哈哈!!---acquireQueued()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
对这三步进行分析:
第一步🔽
//非公平实现
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//进入到这个方法
final boolean nonfairTryAcquire(int acquires) {
//1. 拿到当前的线程
final Thread current = Thread.currentThread();
//2. 拿到当前锁的状态
int c = getState();
//3. 状态为0表示没有线程拿到锁,锁处于空闲状态
if (c == 0) {
//4. 试想一下,当有多个线程同时走到这里进行抢锁,抢到的就返回true
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//4. 就算锁被某线程拿到了,但拿锁的那个线程再次进入到这里就不需要抢了,直接把锁给
//它(重入锁的效果)
else if (current == getExclusiveOwnerThread()) {
//5. 锁状态+1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//6. 抢又抢不到,也不是可重入,那只好返回false,安心排队去
return false;
}
第二步🔽
private Node addWaiter(Node mode) {
//1. 将当前线程包装成一个Node节点(下面的注释都用 "当前节点" 代替)
Node node = new Node(Thread.currentThread(), mode);
//2. 拿到等待对列的尾节点
Node pred = tail;
//3. 队列不为空,将当前节点 前置指向尾节点
if (pred != null) {
node.prev = pred;
//4. 试想一下,如果多个线程都死心了,也跑到这一步争先排队,这里就要进行CAS操作
if (compareAndSetTail(pred, node)) {
//5. 将原来的尾节点 后置指向当前节点,这样就完成了入队操作
pred.next = node;
return node;
}
}
//6. 队列为空,这里应该是创建一个队列,走进这个方法看看
enq(node);
return node;
}
//进入到这个方法,验证一下是否创建队列,答案是正确的
private Node enq(final Node node) {
//1. 哟,是一个死循环。试想一下,当多个线程冲破重重关卡跑到这里来,看谁能抢到头节点这
//个位置了
for (;;) {
//2. 再次获取一下尾节点(被volatile关键字修饰的,此关键字的作用接下来就会说);
//为什么要再次获取一下尾节点呢?
//答:当某个线程占了先机,当了头节点,同时也是尾节点,因为一开始就只有自己那么一个
//节点嘛~然后第二个线程进行compareAndSetHead()的时候,肯定失败的,接着就会进入下
//一轮循环,其他线程也是同样的命运;到达第二轮循环的时候,就会进入到else{}这里->
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
//3. ->这里,将当前节点 前置指向尾节点,这里多个线程都以同一个尾节点作为前置
//是没问题的,毕竟是预设,能CAS成功才算入队成功呢
node.prev = t;
//4. CAS操作,争抢当尾节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
第三步🔽
//挣扎吧,少年~~~
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//多个线程都走到这里挣扎,就像一个滚筒洗衣机,一堆衣服在这转...
for (;;) {
//1. 先拿到当前节点的 前置节点
final Node p = node.predecessor();
//2. 如果前置节点是头节点(那当前节点就是排在第二位咯),有希望了,再次尝试去
//获取锁,其实进入到这里都是能拿到锁的了
if (p == head && tryAcquire(arg)) {
//3. 获取锁成功之后,将当前节点变成队头(其实只有老二才能有资格拿锁,老大
//在此前已经拿过锁了,所以要把机会留给老二,毕竟要人人有份才公平嘛!)
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//3. 而不是老二的线程们,就跑到这里了,这里干了什么呢?估计是挂起这些线程,进
//入方法验证一下,推理是正确的!
//如果达不到被挂起的资格,就继续执行循环,直到当前节点是老二就可以有资格被挂起
//了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//4. 线程一旦被中断,就抛出中断异常,直接执行下面finally代码块。如果是正
//常唤醒,就直接进入下一轮循环
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//-->这里 看这个方法名所表达的意思是 “当获取锁失败时应该要挂起?”
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//1. 看看当前节点 的前置节点正处于何种状态
//CANCELLED = 1
//SIGNAL = -1
//CONDITION = -2
//PROPAGATE = -3
int ws = pred.waitStatus;
//2. 前置节点处于SIGNAL状态的就表明(当老大的标识),当前节点(老二)需要被挂起了
if (ws == Node.SIGNAL)
return true;
//3. 前置节点大于0意味着放弃获取锁,所以它会往前找,找到状态不是大于0的节点,作为当前
//节点的前置节点。说白了就是忽略被放弃的节点,重组队列
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//4. 还没有被挂起的资格,也没被取消的线程们就会集中到这里来,都会尝试去修改各自对
//应的前置节点的状态,修改好之后直接返回false,进行下一轮循环(把一个个线程挂起
//来)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//有资格去挂起的话
private final boolean parkAndCheckInterrupt() {
//1. 挂起当前线程呗~~
LockSupport.park(this);
//--------挂起后,这里会被阻塞,不会往下执行代码-----------
//两种情况:
//1. 直到线程被中断,抛出异常,就会执行到上面的finally代码段中了
//2. 直到线程被唤醒而非中断的话,就返回false
return Thread.interrupted();
}
在代码里面走了一遭,感觉不难吧,弄懂了非公平锁的源码,再去理解公平锁就很简单了,我就不细说了。😎在上面的代码中,有一个虚拟的等待队列(CLH),在整个上锁过程,这个队列究竟是怎样的呢?下面就画一下🔽
到这里就说清上锁的过程了。
📌解锁
解锁过程相对来说简单很多了,直接分析源码
//进入unlock方法
public void unlock() {
//使用sync同步器方法解锁,解锁就不需要分公平或者非公平了
sync.release(1);
}
//进入方法
public final boolean release(int arg) {
//1. 尝试着释放锁,进去瞧瞧->
if (tryRelease(arg)) {
//2. 获取CLH队列的头节点(虚拟的头节点)
Node h = head;
//3. 队列不为空
if (h != null && h.waitStatus != 0)
//4. 唤醒头节点中的线程,进入这个方法->
unparkSuccessor(h);
return true;
}
return false;
}
//->看看是怎样修改锁状态的
//这里不涉及争抢解锁,因为同一时刻就只要一个线程拥有锁,所以这里就不担心线程安全问题
protected final boolean tryRelease(int releases) {
//1. 先将锁状态-1(重入的状态值>1)
int c = getState() - releases;
//2. 必须得是持有锁的线程才能解自己的锁。总不能让别的线程来帮自己解吧
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//3. 如果state=0,代表锁空闲
free = true;
setExclusiveOwnerThread(null);
}
//4. 更新一下状态
setState(c);
return free;
}
//->到这个方法
private void unparkSuccessor(Node node) {
//1. 拿到头节点的状态
int ws = node.waitStatus;
if (ws < 0)
//2.如果在释放锁前,节点没被取消,就把头节点状态还原为 0
//TODO 明明只有一个线程在进行解锁,为什么要CAS呢?
compareAndSetWaitStatus(node, ws, 0);
//2. 到这里就去“叫醒老二”,拿到第二个节点
Node s = node.next;
//3. 如果节点为空或者已经被取消了
if (s == null || s.waitStatus > 0) {
s = null;
//4. 从队尾开始遍历,往前找,找到节点状态是-1(这个就是在队列中等待的正常状态)的
//紧随老二的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//5. 唤醒线程
if (s != null)
LockSupport.unpark(s.thread);
}
到这里就分析完解锁过程了,说白了就是找到没被取消而且紧随队首的节点,进行唤醒罢了~~👌
接下来要说的是🔽
线程间的通信
以往线程间进行通信(线程间你叫我等,或者我叫醒你的过程),是使用 object的 wait() 、notify() 、notifyAll() 方法且需要结合synchronized关键字来实现。而到了现在,有一种新
的方式来替代这种实现 —— Condition。
我们先看一下以往的方式,直接上例子:
package locks;
public class Synchronized_ {
public static void main(String[] args){
Object o = new Object();
Synchronized_ synchronized_= new Synchronized_();
new Thread(()->{
try {
synchronized_.m1(o);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
).start();
new Thread(()->{
try {
synchronized_.m3(o);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
).start();
//这里会存在问题
//当此线程优先完成唤醒的时候,而其他线程正在挂起,那么这些线程就永远不会被唤醒了
new Thread(()->{ synchronized_.m2(o); }).start();
}
public void m1 (Object obj) throws InterruptedException {
synchronized (obj){
System.out.println("m1执行");
obj.wait();
System.out.println("m1唤醒");
}
}
public void m3 (Object obj) throws InterruptedException {
synchronized (obj){
System.out.println("m3执行");
obj.wait();
System.out.println("m3唤醒");
}
}
public void m2(Object obj){
synchronized (obj){
obj.notifyAll();
}
}
}
wait():当线程执行这个方法的时候,会释放对这个对象的锁,然后进入等待队列挂起;
notify() \ notifyAll():当线程执行到这个方法的时候,会唤醒等待队列中一个或者全部线程;
我们再看看condition是怎样处理的:
package locks;
import java.util.PriorityQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Condition_ {
private int queueSize = 10;
private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Condition_ test = new Condition_();
Producer producer = test.new Producer();
Consumer consumer = test.new Consumer();
producer.start();
consumer.start();
Thread.sleep(10);
producer.interrupt();
consumer.interrupt();
}
class Consumer extends Thread{
@Override
public void run() {
consume();
}
volatile boolean flag=true;
private void consume() {
while(flag){
lock.lock();
try {
while(queue.isEmpty()){
try {
System.out.println("队列空,等待数据");
notEmpty.await();
} catch (InterruptedException e) {
flag =false;
}
}
queue.poll(); //每次移走队首元素
notFull.signal();
System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");
} finally{
lock.unlock();
}
}
}
}
class Producer extends Thread{
@Override
public void run() {
produce();
}
volatile boolean flag=true;
private void produce() {
while(flag){
lock.lock();
try {
while(queue.size() == queueSize){
try {
System.out.println("队列满,等待有空余空间");
notFull.await();
} catch (InterruptedException e) {
flag =false;
}
}
queue.offer(1); //每次插入一个元素
notEmpty.signal();
System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));
} finally{
lock.unlock();
}
}
}
}
}
await() == wait()
signal() == notify()
signalAll() == notifyAll()
两者的区别是:condition 更安全,更灵活
通过示例都知道怎么进行线程间的通信了,接下来剖析一下condition的工作原理🔽
📌挂起等待
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//1. 添加到condition等待队列,看看是怎样添加到队列的->
Node node = addConditionWaiter();
//2. 释放当前线程持有的锁,其他线程可以获取锁了
int savedState = fullyRelease(node);
int interruptMode = 0;
//3. 如果在同步队列就跳出循环,否则直接挂起不加入同步队列。
while (!isOnSyncQueue(node)) {
//4. 当前线程一直在这里阻塞住,等到被唤醒才继续往下执行
LockSupport.park(this);
//5. 线程被唤醒
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
//->进入此方法
private Node addConditionWaiter() {
//1. 这个lastWaiter是ConditionObject中的成员变量,是一个Node节点
Node t = lastWaiter;
//2. 如果队列不为空,且 尾节点的状态不是 -2
if (t != null && t.waitStatus != Node.CONDITION) {
//3. 移除节点
unlinkCancelledWaiters();
//4. 等待队列经过处理后,再拿尾节点
t = lastWaiter;
}
//5. 将当前线程包装为 状态-2 的节点(代表是condition类型的)
Node node = new Node(Thread.currentThread(), Node.CONDITION);
//6. 加入conditionObject里面的队列,一个lock有多个conditionObject
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
//删除节点
private void unlinkCancelledWaiters() {
//1. 拿到第一个等待节点
Node t = firstWaiter;
Node trail = null;
//2. 遍历整个等待队列
while (t != null) {
//3. 拿到下一个节点
Node next = t.nextWaiter;
//4. 节点不等于 -2
if (t.waitStatus != Node.CONDITION) {
//5. 将当前节点移出队列
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
📌唤醒
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
//1. 唤醒condition队列中的第一个节点
doSignal(first);
}
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
//主要是这个方法进行唤醒操作
final boolean transferForSignal(Node node) {
//1. 将condition队首节点状态改为0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//2. 将队首节点加入CLH同步队列中
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
//3. 唤醒
LockSupport.unpark(node.thread);
return true;
}
结合流程图有助于理解代码,来看看整个流程是怎样的🔽
以上就是condition的内容了。