JAVA JUC 并发编程 Part III (锁、AQS)

乐观锁和悲观锁

乐观锁和悲观锁是两种用于解决并发场景下的数据竞争问题的思想。

  • 乐观锁认为数据通常情况下不会引发冲突,所以操作不会加锁,只在更新时判断期间是否有其他事务修改了数据。如果有其他事务修改了数据,则放弃操作或重试,否则执行操作。常见的乐观锁实现方式包括CAS机制版本号机制

    image-20230922181407880

  • 悲观锁则持有相反的假设,认为数据非常容易引发冲突,所以操作时会加锁,保证其他事务不能同时修改数据,直到当前事务完成。数据库中的行锁、表锁、读锁、写锁以及Java中的synchronized关键字等都是悲观锁的常见实现方式。

    image-20230922184215235

CAS机制和版本号机制

CAS机制和版本号机制是解决并发问题的两种策略。

**CAS(Compare-and-Swap)机制,全称Compare-and-Swap,即比较并交换,是一种无锁算法。**CAS机制主要解决的是多线程并发访问同一资源时可能出现数据不一致的问题。在CAS操作中,有三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的当前值等于预期原值,则将该位置值更新为新值,否则,不做任何操作。在整个过程中,不需要获取锁。

**版本号机制是一种解决ABA问题的方案。**在版本号机制中,每个变量都有一个版本号,**每次修改变量时,版本号也会随之增加。**当需要进行CAS操作时,除了比较内存位置的值是否相等外,还需要比较版本号是否相等。如果版本号也相等,则可以执行CAS操作,否则,需要重新读取内存位置的值和版本号,然后再进行比较和操作。由于每次修改变量时,版本号都会增加,因此可以保证变量的唯一性和版本的一致性,从而避免ABA问题的出现。

这两种机制可以有效地解决并发环境下的数据一致性问题。

CAS:无锁、自旋锁、乐观锁、轻量级锁

原子类的底层也是CAS机制

我们知道i++是线程不安全的,那 atomicInteger.getAndIncrement()

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

AtomicInteger类主要利用CAS (compare and swap) + volatile 和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。

CAS的缺点

  • 循环时间长开销很大

    我们可以看到getAndAddInt方法执行时,有个do while

    如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销

  • 引出来ABA问题

    假设这样一种场景,当地一个线程执行CAS操作(V,E,U)操作,在获取到当前变量V,准备修改为新值U前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为旧值,这样的话,我们就无法正确判断这个变量是否已被修改过,如下图:

    image-20230922183809181

Unsafe类

Unsafe类中都是静态的native方法使Java拥有了橡C语言的指针一样操作内存空间的能力。有以下特点:

1、不受jvm管理,也就意味着无法被GC,需要我们手动GC,稍有不慎就会出现内存泄漏

2、Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常会导致整个JVM实例崩溃,表现为应用程序直接crash掉。

3、直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率。

自旋锁和互斥锁,借鉴CAS思想

自旋锁(spinlock)︰是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环

自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。

互斥锁,会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。但是自旋锁不会引起调用者堵塞,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。

自旋锁的实现基础是CAS算法基础,CAS自旋锁属于乐观锁,乐观地认为程序中的并发情况不那么严重,那么让线程不断去尝试更新。

Q:wait / sleep 的区别?

整体的区别其实是有四个:

1、所属类不同: sleep是线程中的方法,但是wait是Object中的方法。

2、语法不同: sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。

3、参数不同: sleep必须设置参数时间,wait可以不设置时间,不设置将一直休眠。

4、释放锁资源不同: sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。

5、唤醒方式不同: sleep不需要被唤醒(休眠之后推出阻塞),但是wait需要(不指定时间需要被别人中断)。

6、线程进入状态不同: 调用sleep方法线程会进入TIMED_WAITING有时限等待状态,而调用无参数的wait方法,线程会进入 WAITING无时限等待状态。

第三种线程休眠方法 LockSupport.park()

Java中的线程休眠大法系列(三)LockSupport.park()_Jianyang.liu的博客-CSDN博客

生产者-消费者

图示

image-20230922191610889

代码示例

package com.zky;

import java.util.concurrent.ArrayBlockingQueue;

public class SharedQueue {
    // 生命队列的最大长度
    private int queueSize = 10;
	// 阻塞队列
    private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(queueSize);

    public static void main(String[] args) {
        SharedQueue test = new SharedQueue();

        // 消费者持续的运行
        Consumer consumer = test.new Consumer();
        consumer.start();

        // 生产10条消息
        for (int i = 0; i < 10; i++) {
            // 创建10个生产者的线程
            Producter producter = test.new Producter();
            producter.start();
        }
    }

    // 生产者
    class Producter extends Thread {
        @Override
        public void run() {
            // 保证生产者在整个过程中是线程安全的
            synchronized (queue) {
                // 1、判断当前队列长度是否小于最大长度
                if (queue.size() < queueSize) {
                    // 2、如果小于的话,生产者就可以生产消息了
                    // 2.1 往队列添加一条信息
                    queue.add(queue.size() + 1);
                    System.out.println("生产者往队列当中加入消息,队列当前长度:" + queue.size());
                    try {
                        // 模拟业务处理
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 2.2 唤醒消费者,有活了
                    queue.notify();
                } else {
                    // 3. 如果大于的话,生产者停止工作,稍微歇歇
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        queue.notify();
                    }
                }
            }
        }
    }

    // 消费者
    class Consumer extends Thread {
        @Override
        public void run() {
            // 消费者需要不断的工作
            while (true) {
                // 保证整个消费的过程是线程安全的
                synchronized (queue) {
                    // 如果队列为空, 消费者睡眠
                    if (queue.isEmpty()) {
                        System.out.println("当前队列为空...");
                        try {
                            queue.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            // 一旦出现异常,手动唤醒
                            queue.notify();
                        }
                    } else {
                        // 队列不为空 消费者进行消费
                        // 消费头部的信息
                        Integer value = queue.poll();
                        System.out.println("生产者从队列中消费了消息:" + value + " 队列当前长度:" + queue.size());
                        try {
                            // 模拟业务处理
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 唤醒生产者,可以继续工作了
                        queue.notify();
                    }
                }
            }
        }
    }
}

Synchronized 锁升级

其实在JDK1.6之前,Java内置锁还是一个重量级锁,是一个效率比较低下的锁,会由jvm用户态切换到操作系统的管程来实现互斥:

管程(Monitor)概念

  • Monitor,直译为"监视器",而操作系统领域一般翻译为"管程",在java领域就是"对象锁"。
  • 管程是指管理共享变量以及对共享变量操作过程,让它们支持并发,翻译成Java领域的语言,就是管理类的状态变量让这个类是线程安全的
  • synchronized关键字和wait(), notify(), notifyAll() 这三个方法是java中实现管程技术的组成部分
  • Monitor有两大作用:同步和互斥
  • wait/notify基于monitor做的,monitor中有owner、entryList,waitSet
  • synchronized关联了monitor,是在JVM层面实现的,源码是C++

java对象 与 Monitor 之间的关系

  • 每个java对象都可以关联一个Monitor对象,如果使用synchronized给对象加锁(重量级锁)之后,改对象头的Mark Word 中农就被设置指向Monitor对象的指针。

Monitor 结构

image-20230922204901189

锁升级

在JDK 1.6之后,JVM为了提高锁的获取与释放效率,对synchronized的实现进行了优化,引入了偏向锁和轻量级锁,从此以后Java内置锁的状态就有了4种(无锁、偏向锁、轻量级锁和重量级锁),并且4种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,也就是说只能进行锁升级(从低级别到高级别)

级别由低到高依次为: 无锁 一> 偏向锁 一> 轻量级锁 —> 重量级锁

markWord

如果了解对象的内存布局的可以略过此段。这个对象的内存布局是和JVM的实现有关。当一个对象被创建出来后它在内存中的布局如下,由四部分组成:

  • 8个字节的markword, (markword里面包含了其它的东西,比如GC标记,锁类型)

  • 4个字节的ClassPoint(此指针指向的Class),默认是开启指针压缩所以是四个字节,关闭指针压缩后是八个字节

  • 实例对象中的成员属性大小

  • 字节填充(有的JVM需要8字节对齐,如果上面的字节相加后不能被8整除,则需要在此补齐)

image-20230922205742429

image-20230923121358352

偏向锁

在没有线程竞争的条件下,第一个获取锁的线程通过CAS将自己的threadId写入到该对象的markword中,若后续该线程才再次获取锁, 需要比较当前线程ThreadId和对象markword中的threadId是否一致,如果一致那么可以直接获取,并且锁对象时钟保持对该线程的偏向,也就是说偏向锁不会主动释放

简单说就是同一个线程来不断调用,如果线程id一样,就不会升级为轻量级锁只要加上了就不会主动释放

  • 偏向锁存

jdk中偏向锁存在延迟4秒启动,也就是说在jvm启动后4秒后创建的对象才会开启偏向锁

可以通过jvm参数来取消这个延迟时间

偏向锁也可以关闭

创建的对象状态为 对象处于无锁状态 + 可偏向状态

偏向锁位(biased_lock) 1bit + 锁标志位(lock) 2bit = 1 01

轻量级锁

当两个以上线程交替获取锁,但并没有在对象上并发的获取锁时,偏向锁升级为轻量级锁

在此阶段,线程采取CAS的自旋方式尝试获取锁,避免阻塞线程造成的cpu在用户态和内核态间转换的消耗

简单说就是多线程串行情况下获取锁,这时不存在并发,偏向锁就会升级为轻量级锁

重量级锁

两个或以上线程并发的在同一个对象上进行同步时,为了避免无用自旋消耗cpu,轻量级锁会升级为重量级锁,这时markword中的指针指向的monitor对象(也被称为管程或监视器锁)的起始地址

从用户态切换为内核态

可重入锁(递归锁)

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

可:可以

重:再次

入:进入

锁:同步锁

package com.zky;

public class Main {

    private  static final Object objectLockA = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            relock();
        },"a").start();
    }
    private static Integer i = 0;

    public static void relock() {
        synchronized (objectLockA) {
            if (i == 3){
                return;
            }else {
                i++;
                System.out.println(i + " 调用");
                relock();  // 自己调用自己 不会自己阻塞自己啊!
            }
        }
    }
}

ReentrantLock

Sync是隐式锁,Lock是显示锁

ReentrantLock是Lock的默认实现,在聊ReentranLock之前,我们需要先弄清楚一些概念:

  • 可重入锁: 可重入锁是指同一个线程可以多次获得同一把锁; ReentrantLock和关键字Synchronized都是可重入锁

  • 可中断锁:可中断锁时子线程在获取锁的过程中,是否可以相应线程中断操作。

    synchronized是不可中断的,ReentrantLock是可中断的

  • 公平锁和非公平锁:公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程

    到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而

package com.zky;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;


// * ReentrantLock 更灵活,粒度更细,手动显示上锁和解锁 java实现
//               一定解锁 最好解锁前 lock.isHeldByCurrentThread() 适用动态解锁场景
// * synchronized    隐式上锁和解锁 jvm 实现 c++

public class Math {

    private static Integer stock = 100000;

    // 1. 创建ReentrantLock 对象
    private static ReentrantLock lock = new ReentrantLock();

    static class StockRunnable implements Runnable {
        @Override
        public void run() {
            try {
                // 2. 手动上锁
                lock.lock();
                stock--;
            } finally {
                // 判断当前锁是否在当前线程加锁
                if (lock.isHeldByCurrentThread()) {
                // 3. 手动解锁
                    lock.unlock();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        StockRunnable task = new StockRunnable();

        try {
            for (int i = 0; i < 100000; i++) {
               threadPool.execute(task);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
            // 等待关闭
            threadPool.awaitTermination(1000, TimeUnit.SECONDS);
            System.out.println("库存:" + stock);
        }
    }
}

申请等待限时

申请锁等待限时是什么意思? 一般情况下,获取锁的时间我们是不知道的,synchronized关键字获取锁的过程中,只能等待其他线程把锁释放之后才能够有机会获取到锁。所以获取锁的时间有长有短。如果获取锁的时间能够设置超时时间,那就非常好了

ReentrantLock刚好提供了这样功能,给我们提供了获取锁限时等待的方法trytock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果: true表示获取锁成功,false表示获取锁失败。

trytock无参方法

尝试获取锁的方法,也可以传入时间和单位进行等待限时,synchronized就完全没有这个特性

lock1.tryLock(2, TimeUtil.SECONDS)

image-20230923131919844

获取锁的过程是可中断的

对于synchronized关键字,如果一个线程在等待获取锁,最终只有2种结果:

  • 要么获取到锁然后继续后面的操作
  • 要么一直等待,直到其他线程释放锁为止

而ReentrantLock提供了另外一种可能,就是在等待获取锁的过程中(发起获取锁请求到还未获取到锁这段时间内是可以被中断的,也就是说在等待锁的过程中,程序可以根据需要取消获取锁的请求。有些使用这个操作是非常有必要的。

比如: 你和好朋友越好一起去打球,如果你等了半小时朋友还没到,突然你接到一个电话,朋友由于突发状况,不能来了,那么你一定达到回府。中断操作正是提供了一套类似的机制,如果一个线程正在等待获取锁,那么它依然可以收到一个通知,被告知无需等待,可以停止工作了。

注意:

  • ReentrankLock中必须使用实例方法1cckInter-uptibly()获取锁时,在线程调用interrupt()方法之后,才会引发: InterruptedException异常
  • 线程调用interrupt()之后,线程的中断标志会被置为true
  • 触发InterruptedException异常之后,线程的中断标志会被清空,即置为false
  • 所以当线程调用interrupt()引发InterruptedException异常,中断标志的变化是:false-> true- >false

公平锁和不公平锁

在大多数情况下,锁的申请都是非公平的,也就是说,线程1首先请求锁A,接着线程2也请求了锁A。那么当锁A可用时,是线程1可获得锁还是线程2可获得锁呢?这是不一定的,系统只是会从这个锁的等待队列中随机挑选一个,因此不能保证其公平性。

这就好比买票不排队,大家都围在售票窗口前,售票员忙的焦头烂额,也顾及不上谁先谁后,随便找个人出票就完事了,最终导致的结果是,有些人可能一直买不到票。而公平锁,则不是这样,它会按照到达的先后顺序获得资源。

公平锁的一大特点是: 它不会产生饥饿现象,只要你排队,最终还是可以等到资源的;

  • synchronized关键字默认是有jvm内部实现控制的,是非公平锁。

  • ReentrantLock运行开发者自己设置锁的公平性。

package com.zky;

import java.util.concurrent.locks.ReentrantLock;

public class Main {

    private static int num = 0;

    // 默认非公平锁 当传入true时会使用公平锁
    private static ReentrantLock fairLock = new ReentrantLock(true);

    public static class T extends Thread {
        public T(String name) {
            super(name);
        }

        @Override
        public void run() {
            for (int i = 0; i < 5; i++) {
                fairLock.lock();
                try{
                    System.out.println(this.getName() + "获得锁!");
                } finally {
                    fairLock.unlock();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        T t3 = new T("t3");
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
    }
}

Q:为什么会有公平锁/非公平锁的设计为什么默认非公平?

  1. 恢复挂起的线程到真正的锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间
  2. 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程再此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

Q:使用公平锁会有什么问题?

公平锁保证了排队的公平性,非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的“锁饥饿”

Q:什么时候用公平?什么时候用非公平?

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省了很多线程切换时间,吞吐量自然就上去了,否则那就用非公平锁,大家公平使用

共享锁和排它锁

排它锁

  • 排它锁又称独占锁,获得了以后既能读又能写,其他没有获得锁的线程不能读也不能写,典型的synchronized就是排它锁

共享锁

  • 共享锁又称读锁,获得了共享锁以后可以查看但无法修改和删除数据,其他线程也能获得共享锁,也可以查看但不能修改和删除数据
  • 在没有读写锁之前,我们虽然保证了线程安全,但是也浪费了一定的资源,因为多个读操作同时进行并没有线程安全问题
  • ReentrantReadWriteLock中读锁就是共享锁,写锁是排它锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果不这样,读是无限阻塞的,这样提高了程序的执行效率

ReentrantReadWriteLock

ReentrantReadWriteLock.ReadLock

ReentrantReadWriteLock.WriteLock

读写锁的规则

多个线程只申请读锁,都能申请到

如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放该锁

如果有一个线程已经占用写锁,则其他线程申请写锁或读锁都要等待它释放也就是说,要么多读要么一写

简单说就是读的时候可以读但不能写,写的时候不能读也不能写

Q:synchronized与Lock的区别?

  1. 首先synchronizedjava内置关键字(底层是C++),在jvm层面,Lock是个java类(底层是java);
  2. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  3. synchronized自动释放锁(a线程执行完同步代码会释放锁; b线程执行过程中发生异常会释放锁),Lock需在finally手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  4. synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  5. synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可)

扩展

原子性问题:lock cmpxchgq 在硬件级别对内存所在的缓存行加锁/总线锁

ABA问题:采用AtomicStampedReference类进行版本控制

打印对象内部组成结构 pom依赖

<dependency>
	<groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

Q:轻量级锁一定比重量级锁性能高?

不一定,CAS处理方法如果线程过多会导致cpu一直空转,极大耗费cpu资源

轻量级锁升级优化

轻量级锁这里有个机制就是一个线程自旋次数超过某个值的话会升级为重量级锁,但是如果只有两个线程这样很影响性能啊!

AbstractQuenedSynchronizer (AQS)

AQS: AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。

AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包

AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果

被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获

取不到锁的线程加入到队列中。

CLH (Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。

AQS 详解 | JavaGuide(Java面试 + 学习指南)

ReentrantLock 源码解析

为什么放在这里来看ReentrantLock 源码,诶当然是因为他的源码就是用AQS机制实现的

这里只看公平锁的源码

先说两者的区别

  • 非公平锁相对于公平锁会尝试两次去加锁,如果还没获取锁的话才会去排队

  • 公平锁是先判断要不要排队,不用排队直接加锁,要排队直接去排队就行了

tryAcquire

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1); // 真正加锁的方法
    }

    protected final boolean tryAcquire(int acquires) { // 这里就是尝试加锁的代码
        final Thread current = Thread.currentThread();
        int c = getState(); // 获取state的值
        if (c == 0) { // 如果state为0表示当前这把锁是空闲状态,可以直接加锁
            // 因为是公平锁 这里会判断队列中是否有线程在排队 有的话无法获取锁!非公平锁就没有这个if判断!
            if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) { // 修改状态
                setExclusiveOwnerThread(current); // 记录得到锁的线程
                return true;
            }
        }
        // 这里锁被占用了再判断这把锁是属于哪个线程的 如果是同一个线程将state + 1 这里就是可重入锁
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false; // 获取锁的线程不是我自己 加锁失败,加入队列等待唤醒! 
        // 这里就会去执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 方法
    }
}

acquire

可以看到加锁的方法就存在AbstractQueuedSynchronizer类中

// AQS
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ...
    public final void acquire(int arg) {
    	if (!tryAcquire(arg) && // 尝试加锁 如果成功了就不会执行后面的代码 
        	acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 加锁失败 就入队然后不断循环尝试加锁
        	selfInterrupt();
	}
    ...
}

hasQueuedPredecessors

判断有没有线程在排队

// 判断有没有线程在排队
public final boolean hasQueuedPredecessors() { 
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
		// 这部分考虑到并发时表示头节点的next还没来得及赋值的情况 导致的排队顺序错误的问题
        ((s = h.next) == null || s.thread != Thread.currentThread()); 
}

getExclusiveOwnerThread

记录当前获取锁的线程

    // 记录当前获取锁的线程
	private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
	
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }

acquireQueued

这个方法用于控制线程的出队和跳过已经中断的线程

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor(); // 拿前一个节点
            // 如果前一个节点就是head的话 表示这个节点就是队首节点 直接再次尝试加锁
            if (p == head && tryAcquire(arg)) { 
                setHead(node);   
                p.next = null; // help GC    这一系列操作相当于是出队操作,表示获取到锁了,出队就行了
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) // 如果没有获取到锁那么阻塞到队列中
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire

其中waitStatus 的集中状态

/**waitStatus值,指示线程已取消 就是中断了*/
static final int CANCELLED=1/**waitStatus值,指示后续线程需要取消标记*/
static final int SIGNAL=-1/**waitStatus值,指示线程正在等待条件*/
static final int CONDITION=-2/**waitStatus值,指示下一个acquireShared应该无条件传播*/
static final int PROPAGATE=-3
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL) // 判断前面节点的 waitStatus
        // 如果前面节点的waitStatus = -1 那么直接返回true 表示需要park阻塞
        return true;
    if (ws > 0) {
        // 表示当前线程节点被中断了 直接抛弃这个节点 然后继续找下一个未中断的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 表示现在前一个节点waitStatus = 0  这里就将前面节点的waitStatus改为-1 表示后面还有节点需要唤醒
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

parkAndCheckInterrupt

所有入队的线程节点对象都会调用LockSupport.park方法阻塞当前线程 等待锁的释放

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

compareAndSetState

这个方法用于修改state状态

protected final boolean compareAndSetState(int expect, int update) {
    // CAS 机制修改状态
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

addWaiter

生成队列节点对象并加入到队列中

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred; // 新节点的prev指向尾结点
        if (compareAndSetTail(pred, node)) { // 这里处理多个线程情况下节点入队的情况 也是通过CAS机制处理的
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

compareAndSetTail

处理多个线程情况下节点入队的情况 也是通过CAS机制处理的

private final boolean compareAndSetTail(Node expect, Node update) {
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

enq

在addWaiter中CAS失败的线程会走这个方法继续尝试加入队列 这里使用了for循环 其中也是CAS机制

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        // 这里意思就是队列中无节点的时候 初始化了队列 再次循环会把自己的node对象prev指向尾结点
        // 这里可以看做会有个虚拟头结点或者说是哨兵节点
        if (t == null) { 
            if (compareAndSetHead(new Node())) 
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

利用Node节点中的waitStatus属性控制节点唤醒

先来看 Node 节点的几个属性 :

        volatile int waitStatus; // 表示有无下一节点

        volatile Node prev; // 指向上一节点

        volatile Node next;	 // 指向下一节点

        volatile Thread thread; // 当前线程

        Node nextWaiter; // 链表化

这里waitStatus是个重点,如果waitStatus = 0 的话表述这个线程之后不需要去唤醒其他的线程了,如果waitStatus = -1 表示当前节点要去unpark后面节点让后面节点得到锁,这个waitStatus有可能是其他值,假如表示中断,那么就不需要去唤醒了,大致结构如下:

Node node) {
for (;😉 {
Node t = tail;
// 这里意思就是队列中无节点的时候 初始化了队列 再次循环会把自己的node对象prev指向尾结点
// 这里可以看做会有个虚拟头结点或者说是哨兵节点
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}


### 利用Node节点中的waitStatus属性控制节点唤醒

先来看 Node 节点的几个属性 : 

```java
        volatile int waitStatus; // 表示有无下一节点

        volatile Node prev; // 指向上一节点

        volatile Node next;	 // 指向下一节点

        volatile Thread thread; // 当前线程

        Node nextWaiter; // 链表化

这里waitStatus是个重点,如果waitStatus = 0 的话表述这个线程之后不需要去唤醒其他的线程了,如果waitStatus = -1 表示当前节点要去unpark后面节点让后面节点得到锁,这个waitStatus有可能是其他值,假如表示中断,那么就不需要去唤醒了,大致结构如下:

image-20230924135634859

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kaiyue.zhao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值