JUC并发原理

news2024/11/26 12:25:05

文章目录

  • JUC并发
    • synchronized
      • 锁对象
      • 底层原理
    • synchronized锁升级
      • 无锁
      • 偏向锁
      • 轻量级锁
      • 重量级锁
      • JIT对锁的优化:锁消除和锁粗化
    • reentrantlock
      • 公平锁和非公平锁
      • 可重入锁 / 递归锁
    • 死锁
      • 死锁产生条件
      • 如何排查死锁?如果解决死锁?
    • LockSupport与中断机制
      • 中断机制
        • 中断相关的三大API
        • 如何中断运行中的线程?
      • LockSupport
        • LockSupport原理与优势
  • JMM
    • happens-before(先行发生原则)
  • CAS
    • Unsafe
  • ThreadLocal
    • 基本使用
    • 使用规范
      • Thread、ThreadLocal、ThreadLocalMap三者关系?
      • ThreadLocalMap的Entry的key为啥使用threadlocal的弱引用?弱引用被GC了会怎么样?
  • 对象内存布局和对象头
    • 对象头
    • 类元信息
  • AQS
    • state与CLH队列
    • AQS自身属性与Node节点
    • ReentrantLock实现原理
      • 公平与非公平是咋实现的?

JUC并发

synchronized

锁对象

synchronized 有三种应用场景:

  • 作用于非静态实例方法:对当前实例加锁,进入同步方法前,需获得当前实例的锁,此时它是一个对象锁
  • 作用于静态实例方法:对实例所属类加锁,进入静态同步方法前,需要获得当前类对象的锁,此时它是一个类锁
  • 作用于代码块:对括号中配置的对象加锁。

底层原理

  • 1、管程(英语:monitors,也称为监视器)是一种程序结构,结构内的多个子程序(对象或模块〉形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。对共享变量能够进行的所有操作集中在一个模块中。(把信号量及其操作原语“封装”在一个对象内部) 管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。
  • 2、Java中每个对象都继承自Object,而每个Object 的对象都关联一个monitor,该monitor在C语言底层中对应一个ObjectMonitor结构体,有_owner属性记录持有该对象的线程,有_count属性,记录该线程获取monitor锁的次数,同一时间只有一个线程能够持有对象的monitor,这是Java中所有对象都可以成为锁对象,且可以重复获取锁的基础。
    在这里插入图片描述
  • 3、字节码标识:
    • synchronized同步代码块实现使用的是monitorentermonitorexit指令来标识获取到对象的monitor和释放对象的monitor,每次monitorenter进入,锁重入次数加1,每次monitorexit退出,锁重入次数减1。
    • synchronized同步方法底层使用 ACC_SYNCHRONIZED访问标志,标识同步方法,并使用ACC_STATIC区分该方法是否静态同步方法,即有ACC_SYNCHRONIZEDACC_STATIC标识的是静态同步方法,只有ACC_SYNCHRONIZED标识的是普通同步方法。静态同步方法会持有类对象的monitor,而普通同步方法会持有实例对象的monitor.

synchronized锁升级

锁会带来并发时的性能下降,Java8对synchd锁关键字做了锁升级的优化。
对象锁升级过程:

  • 无锁(锁标志01,且是否偏向锁标志位为0)
  • 偏向锁(锁标志01,且是否偏向锁标志位为1),前54位指向当前线程id
  • 轻量级锁(锁标志位为00),前62位指向线程栈中Lock Record的指针
  • 重量级(自旋)锁(锁标志位为10),前62位指向堆中的monitor对象的指针。
  • GC标志(锁标志位为11)

在这里插入图片描述

在这里插入图片描述

无锁

前25位unused,全0
后31位,存放哈希码,仅当有调用的时候才生成
1位unused
4位分代年龄,因此分代年龄最大只能是15,因为对象头中只有4个bit记录。
偏向锁标志位,无锁为0
锁标志位01.

偏向锁

当线程A第一次竞争到锁,通过操作修改MarkWord中的偏向锁线程ID、偏向锁标志位,使其从无锁状态升级为偏向锁。

如果不存在其他线程竞争,持有偏向锁的线程将永远不需要进行同步。

偏向锁的出现是为了解决在一个线程执行同步时,提高性能,线程的id被记录到对象头,如果id符合,则不需要从用户态切换到内核态去进行加锁解锁的过程。

  • 当非偏向线程id的线程尝试获取锁:
    • 如果没有发生竞争,即偏向线程已经退出来同步代码块,非偏向线程才进入,那么会将对象头设置为无锁状态,并撤销偏向锁,重新偏向为当前线程。
    • 如果发生了竞争,即偏向线程还未退出同步代码块,那么出现锁升级为轻量级锁,正在竞争的线程会进入自旋状态,等待获得该轻量级锁。
  • 如果对象处在已经偏向状态,且又调用了hashcode()方法,它的偏向状态会被撤销,并膨胀为重量级锁,对象头的前62位指向重量级锁的位置,其中存储有原来的哈希码。
  • 如果在偏向锁状态调用了wait()方法,那么偏向锁也会直接升级为重量级锁,因为wait()方法也是重量级锁独有的

在这里插入图片描述

由于偏向锁的维护成本比较高,Java15废除了偏向锁。最终就是,JDK 15 之前,偏向锁默认是 enabled,从 15 开始,默认就是 disabled,除非显示的通过 UseBiasedLocking 开启

轻量级锁

轻量级锁:多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻寒,轻量级锁本质是自旋锁。
在这里插入图片描述
在这里插入图片描述
自旋锁自旋的有最大的自旋次数,如果自旋达到一定次数,会将自旋锁升级为重量级锁,自旋次数取决于自适应自旋锁机制。自适应自旋锁的大致原理:

  • 线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。
  • 反之如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。

重量级锁

重量级锁,对象头的前62位指向互斥量。
在这里插入图片描述

JIT对锁的优化:锁消除和锁粗化

JIT即时编译器(JIT compiler,just-in-time compiler)
下边的代码,每次new一个对象,对其加锁,实际上加锁完全无用,JIT会对它优化,发生锁消除。
在这里插入图片描述
锁粗化就是加大锁的锁定范围,如下边的代码,多个同步代码块相连,锁对象相同,JIT编译器会执行锁粗化,用一个更大范围的synchronized,代替多个同步代码块,避免多次加锁、释放锁。
在这里插入图片描述

reentrantlock

公平锁和非公平锁

在这里插入图片描述
reentrantlock默认是非公平锁,主要有以下两点考虑:

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

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

可重入锁 / 递归锁

可重入锁的优点在于,可以在一定程度上避免死锁,一个加了锁的方法,如果递归调用自身,此时使用不可重入锁,就会自己把自己锁住。

死锁

死锁产生条件

  • 存在互斥访问的锁
  • 已经获取锁不可被强行剥离
  • 没有完全获取到所需要的锁时,会持有锁并等待。
  • 存在循环等待锁释放

举一个MySQL中,由于两个事务的间隙锁互相兼容,而插入意向锁与间隙互斥导致死锁的案例:
在这里插入图片描述

  • time1阶段:事务A加间隙锁,范围(20, 30)
  • time2阶段:事务B加间隙锁,范围(20, 30)
  • time3阶段:事务A尝试给id为25的位置加插入意向锁,但是发现事务B在(20,30)间设置了间隙锁,加锁失败,阻塞,等待事务B释放间隙锁。
  • time4阶段:事务B尝试给id为26的位置加插入意向锁,但是发现事务A在(20,30)间设置了间隙锁,加锁失败,阻塞,等待事务A释放间隙锁。

事务A和事务B相互等待对方释放锁,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。

如何排查死锁?如果解决死锁?

命令行版:

  1. jps -l
    列出所有Java进程号,找到发生死锁的进程,假如是1123
  2. jstack 1123
    查看进程栈信息,就可以定位到死锁的情况

图形工具版:
使用jconsole工具,也可以定位正在运行的Java进程的死锁情况。

LockSupport与中断机制

中断机制

  • 一个线程不应该由其他线程来强制中断或停止,而是应该山线程自己自行停止,自己来决定自己的命运。
  • 在Java中没有办法立即停止一条线程,然而停止线程却非常重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的协商机制——中断,也即中断标识协商机制。
  • 中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法也仅仅是将线程对象的中断标识设成true;接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断,此时究竟该做什么需要你自己写代码实现。
  • 每个线程对象中都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。

中断相关的三大API

在这里插入图片描述

  1. interrupt():
    • 当一个线程调用interrupt()时,如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已,被设置中断标志的线程将继续正常运行,不受影响。即interrupt()仅是设置中断标识,并不实际中断线程,需要被调用的线程进行配合。就像你让一个人闭嘴,但最终那人是否闭嘴,需要它的配合。
    • 如果线程处于被阻塞的状态(例如sleep、wait、join等状态),在别的线程中调用当前线程对象的interrupt()方法,那么线程将立即退出被阻塞状态,中断状态标志位将被清除,并抛出一个InterruptedException异常。
    • 对于不活动的线程,调用interrupt()没有任何影响。
  2. public boolean isInterrupted():检测当前实例的中断标志位是否被设置为中断。
  3. public static boolean interrupted():静态方法,会调用当前线程的isInterrupted(),并传入参数,清除中断标志位。

从源码中可以看到,实例方法默认是不清除中断状态的,而静态方法则会。
在这里插入图片描述

如何中断运行中的线程?

  1. 通过volatile关键字,进行线程间通信,线程每次都会读取volatile关键字最新的值。
public class demo1 {
    private static volatile boolean isStop;
    public static void main(String[] args) {
        new Thread( () -> {
            while(!isStop) {}
            System.out.println(Thread.currentThread().getName() + "收到停止!");
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "发送停止信号!");
            isStop = true;
        }, "B").start();
    }
}

在这里插入图片描述

  1. 通过AtomicBoolean 原子布尔型,
public class demo1 {
    private static volatile boolean isStop;
    private static AtomicBoolean flag = new AtomicBoolean(false);
    public static void main(String[] args) {
        new Thread( () -> {
//            while(!isStop) {}
            while(!flag.get()) {}
            System.out.println(Thread.currentThread().getName() + "收到停止!");
        },"A").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "发送停止信号!");
//            isStop = true;
            flag.set(true);
        }, "B").start(
        );
    }
}
  1. 通过线程自带的中断API,线程2中使用线程1的interrupt()方法,而线程1中使用isInterrupted()方法来监听。
public class demo1 {
    private static volatile boolean isStop;
    private static AtomicBoolean flag = new AtomicBoolean(false);
    public static void main(String[] args) {
        Thread t1 = new Thread( () -> {
//            while(!isStop) {}
//            while(!flag.get()) {}
            while (!Thread.currentThread().isInterrupted()) {}
            System.out.println(Thread.currentThread().getName() + "收到停止!");
        },"A");
        t1.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {
            e.printStackTrace();
        }
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "发送停止信号!");
//            isStop = true;
//            flag.set(true);
            t1.interrupt();
        }, "B").start(
        );
    }
}

LockSupport

在这里插入图片描述
synchronized和reentrantlock的线程等待和线程唤醒有如下局限性:

  1. 对象的wait()和notify()方法必须要在同步代码块或者同步方法里边运行,且成对出现必须先wait后notify才ok。
  2. reentrantlock构建的condition的await()方法和signal()方法必须先获取到reentrantlock锁,才可以使用,且成对出现,必须先await()后signal();

基于synchronized和reentrantlock实现三个线程交替打印A、B、C:

package Interrupt;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class demo1 {
    public static void main(String[] args) {
        Data d = new Data();
        for (int i = 0; i < 3; ++i) {
            final int temp = i;
            new Thread( () -> {
                for (int j = 0; j < 10; ++j) {
                    d.print2(temp, (char) ('A' + temp));
                }
            }).start();
        }
    }
}
class Data {
    private int flag = 0;
    private final Object lock = new Object();
    private final Lock lk = new ReentrantLock();
    private final Condition cd = lk.newCondition();
    void print(int i, char letter)  {
        synchronized (lock) {
            try {
                while (i != flag) lock.wait();
                System.out.println(letter);
                flag = (flag + 1) % 3;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.notifyAll();
            }
        }
    }
    void print2(int i, char letter) {
        lk.lock();
        new Object();
        try {
            while (i != flag) cd.await();
            System.out.println(letter);
            flag = (flag + 1) % 3;
            cd.signalAll();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lk.unlock();
        }
    }
}

LockSupport原理与优势

  • LockSupport是concurrent包中一个工具类,不支持构造,提供了一堆static方法,比如park(),unpark()等。
  • LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程LockSupport和每个使用它的线程都有一个许可(permit)关联。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。
  • 当调用park():
    • 如果有凭证,直接消耗掉这个凭证,然后正常退出。
    • 如果无凭证,就必须阻塞等待凭证可用。
  • 当调用unpark():
    • 给指定线程增加一个凭证,但凭证最多只能有一个,不可累加。

LockSupport优势在于灵活性:

  • LockSupport的park()和unpark()方法不需要在同步代码块内,或者持有锁。
  • unpark()可以先于park()方法调用,不用担心线程间的执行的先后顺序,因为如果先执行unpark(),为指定线程提前赋予了凭证,那么该线程在调用park()时,直接低效已有的凭证。

下边是一个主线程等待子线程任务执行完毕,再唤醒主线程的例子:
注意在使用时,LockSupport的unpark()方法需要指定线程,因此第一步,我们需要获取到主线程md

public class demo1 {
    public static void main(String[] args) {
        Thread mt = Thread.currentThread();
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("子线程执行完毕。去唤醒主线程");
                LockSupport.unpark(mt);
            }
        }).start();
        System.out.println("等待子线程执行完毕");
        LockSupport.park();
        System.out.println("主线程被唤醒");
    }
}

JMM

JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开的。

happens-before(先行发生原则)

CAS

CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg

执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就对总线加锁,只有一个线程能加速成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。

Unsafe

Unsafe是CAS核心类,由于Java无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe类相当于一个后门,其内部方法都是native修饰的,可以像C的指针一样直接操作内存。

Unsafe类的compareAndSwapObject,相对于Java中调用的函数,多个两个与内存操作相关的属性,var1和var2,var1表示要操作的对象,而var2表示操作对象属性地址的偏移量。
在这里插入图片描述
以unsafe类实现的原子类Int为例:
当compareAndSwap()执行失败时,会陷入循环,即自旋,仅当执行成功时,跳出循环。
在这里插入图片描述

ThreadLocal

基本使用

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

常用方法:
在这里插入图片描述
设置初始化方法:
protected T initialValue()
每个线程在第一次访问这个值的get()方法,会返回这个初始化方法的结果。
在这里插入图片描述

JDK8,可以用withInitial() 简化上边的写法:
在这里插入图片描述
案例:多线程模拟买票统计

public class demo1 {
    public static void main(String[] args) {
        SaleData sd = new SaleData();
        for (int i = 0; i < 5; ++i) {
            new Thread(() -> {
                int j = Math.abs(new Random().nextInt()) % 20;
                while (j-- != 0) {
                    sd.sale();
                }
                System.out.println(Thread.currentThread().getName() + "卖出:" + sd.saleNum.get());
            }).start();
        }
    }
}
class SaleData {
    ThreadLocal<Integer> saleNum = ThreadLocal.withInitial(() -> 0);
    public void sale() {
        saleNum.set(saleNum.get() + 1);
    }
}

使用规范

每个线程都有自己的线程栈,栈空间大小是有效的,因此必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,造成ThreadLocal变量累积,可能会影响后续业务逻辑和造成内存泄露问题。

尽量在try - finally 块中,try中使用,finally中进行remove清除。

public class demo1 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        SaleData sd = new SaleData();
        try {
            for (int i = 0; i < 10; i++) {
                threadPool.submit(() -> {
                    int r = Math.abs(new Random().nextInt() % 15);
                    for (int j = 0; j < r; ++j) {
                        sd.sale();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖出:" + sd.saleNum.get());
                });
            }
        } finally {
            // 必须在用完后remove
            sd.saleNum.remove();
            threadPool.shutdown();
        }
    }
}
class SaleData {
    ThreadLocal<Integer> saleNum = ThreadLocal.withInitial(() -> 0);
    public void sale() {
        saleNum.set(saleNum.get() + 1);
    }
}

Thread、ThreadLocal、ThreadLocalMap三者关系?

Thread中有一个ThreadLocalMap的成员,
ThreadLocalMap的每个Entry以ThreadLocal实例的弱引用为key,任务对象为value。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

ThreadLocalMap的Entry的key为啥使用threadlocal的弱引用?弱引用被GC了会怎么样?

假设我们在一个方法中,设置一个threadlocal变量t1存入到Thread上,该变量是一个强引用,而在存放到ThreadlocalMap上的是一个弱引用。

 public void func1() {
     ThreadLocal<String> t1 = new ThreadLocal<>();
     t1.set("1111");
 }

在这里插入图片描述
为什么使用弱引用?
当func1执行完毕,栈帧销毁,那么强引用t1也就没有了。但是此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象,若这个key引用就是弱引用,就会导致key指向的threadlocal对象及指向的对象不能被gc回收,导致内存泄露。

而使用弱引用,每次gc时都会找到该弱引用对象,就不会发生内存泄露。

当gc发生后,存在ThreadlocalMap中的Entry的key会变为null,那么其对应的value就无法被访问,为解决该问题。threadlocal的get()、set()、remove方法,都会寻找key为null的脏Entry,然后对它进行删除。

对象内存布局和对象头

对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据( Instance Data)和对齐填充(Padding)

对齐填充的目的是为了保证8个字节的倍数。
在这里插入图片描述
数组对象比Java对象在对象头会多个记录数组长度的字段。

在这里插入图片描述

对象头

对象头分为两部分:

  • 对象标记(Mark Word)
    • 哈希码
    • GC标记
    • GC次数
    • 同步锁标记
    • 偏向锁持有者
  • 类型指针( Class Pointer):指向实例在方法区的类元信息。

在这里插入图片描述
在64位系统中,对象头中的对象标记(Mark Word)占用8个字节,类型指针占了8个字节,一共是16个字节。
Mark Word 默认存储对象的哈希code、分代年龄和锁标志位等信息,这些信息都是与对象自身定义无关的数据,所有MarkWord 被设计成一个非固定的数据结构,以便在极小的空间内存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说再运行期间MarkWord里的存储数据会随着的锁的标志位的变化而变化
在这里插入图片描述

在这里插入图片描述
对象分代年龄使用4个bit位,最大是15

类元信息

类元信息在类加载的时候创建在元空间。(元空间在Java8以后的概念,用于存储类元信息、静态变量、常量池等)
每个实例对象都有指向它的类元信息的指针class pointer,虚拟机通过这个指针来确定这个对象是哪个类的实例。
在这里插入图片描述

AQS

AQS(AbstractQueuedSynchronizer,抽象队列同步器)整体就是一个抽象的FIFO队列,来完成资源获取线程的排队工作,并通过一个int类型的state表示持有锁的状态。

每次尝试持有锁,检查state是否为0,如果为0,则将state修改为1,表示持有了锁。
在这里插入图片描述
AQS是JUC的实现基石,JUC中的ReentrantLock、CountDownLatch、ReentrantReadWriteLock、Semaphore都是基于AQS实现的。

锁面向是锁的使用者,而AQS(抽象队列同步器)面向的是锁的设计者。

state与CLH队列

state变量是一个volatile修饰的int变量,如果为0,表示空闲,如果大于等于1,则表明锁被占用。

CLH队列:CLH是三个大牛名字缩写,队列本质是一种双端队列。

在这里插入图片描述

AQS自身属性与Node节点

在这里插入图片描述

ReentrantLock实现原理

公平与非公平是咋实现的?

ReentrantLock中维护了Sync,FairSync,NonFairSync3个内部类。

在这里插入图片描述
调用Reentrant的lock方法时,实际上是调用sync的lock方法,sync进而调用fairSync或者NonFairSync的lock方法。

这里可以发现公平锁与非公平锁的第一个区别:
非公平锁: 调用lock时,首先就会执行一次CAS,如果失败,才会进入acquire方法 公平锁: 不会进行CAS抢占锁,直接进入acquire方法
在这里插入图片描述

具体tryAcquire方法交给子类来实现:
在这里插入图片描述
公平锁的tryAcquire()方法,在获取同步状态时多了一个限制条件:hasQueuedPredecessors(),判断等待队列中是否存在有效前驱节点,只有队列为空或者当前线程节点是AQS中的第一个节点,则返回false,代表后续可以去获取锁;否则其他情况都表示,已经有前驱节点,返回true代表后续不能获取锁!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/867618.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【SWAT水文模型】SWATCUP率定参数实例-以洮河流域为例

SWATCUP率定参数实例 以SWATCUP首次模拟结果为例&#xff0c;具体模拟结果如下&#xff1a; 模拟参数结果如下&#xff1a; Parameter_Name Fitted_Value Min_value Max_value 1:R__CN2.mgt -0.180000 -0.200000 0.200000 2:V__SUR…

罗勇军 → 《算法竞赛·快冲300题》每日一题:“推箱子” ← 差分及前缀和

【题目来源】http://oj.ecustacm.cn/problem.php?id1819http://oj.ecustacm.cn/viewnews.php?id1023【题目描述】 在一个高度为H的箱子前方&#xff0c;有一个长和高为N的障碍物。 障碍物的每一列存在一个连续的缺口&#xff0c;第i列的缺口从第l个单位到第h个单位&#xff0…

【c语言】通讯录(静态)

小张刚学习完结构体&#xff0c;枚举&#xff0c;联合相关的知识&#xff0c;实践实践&#xff0c;写一个通讯录呗&#xff01; 通讯录的功能 1.通讯录可存放100的信息 2.信息的内容姓名&#xff0c;性别&#xff0c;年龄&#xff0c;电话&#xff0c;地址 3.支持增加联系人 4.…

许可是开源的基石,协作才是未来! KCC@南京第二次活动

自从疫情结束后&#xff0c;作为“穷游”之城的南京&#xff0c;且具有高性价比的六朝古都&#xff0c;来旅行的人们也是络绎不绝&#xff0c;恰逢暑假之时更为火爆。即便在炎炎夏日&#xff0c;也依然抵挡不住开源人的那颗赤诚之心。 此次活动不仅是本地的开源人&#xff0c;花…

树莓派RP2040 用Arduino IDE安装和编译

目录 1 Arduino IDE 1.1 IDE下载 1.2 安装 arduino mbed os rp2040 boards 2 编程-烧录固件 2.1 打开点灯示例程序 2.2 选择Raspberry Pi Pico开发板 2.3 编译程序 2.4 烧录程序 2.4.1 Raspberry Pi Pico开发板首次烧录提示失败 2.4.2 解决首次下载失败问题 2.4.2.1…

uniapp的UI框架组件库——uView

在写uniapp项目时候&#xff0c;官方所推荐的样式库并不能满足日常的需求&#xff0c;也不可能自己去写相应的样式&#xff0c;费时又费力&#xff0c;所以我们一般会去使用第三方的组件库UI&#xff0c;就像vue里我们所熟悉的elementUI组件库一样的道理&#xff0c;在uniapp中…

基于grpc从零开始搭建一个准生产分布式应用(1) - 开始准备

开始前必读&#xff1a;​​基于grpc从零开始搭建一个准生产分布式应用(0) - quickStart​​ 本来笔者并不想开设这个系列&#xff0c;因为工作量比较大&#xff0c;另外此专题的技术点也偏简单。最近复盘了下最近的工作&#xff0c;发现一个问题就是各个互联网大厂一般都会有…

hbase 报错 Master passed us a different hostname to use; was=

原因 wsl2的 /etc/hosts 配置的不兼容,我这里是ubuntu22 修改为 127.0.0.1 ubuntu22 即可

Blazor 调试控制台

文章目录 设置 设置 Blazor项目启动之后&#xff0c;有好几种项目设置&#xff0c;我其实想要这一种控制台 直接Console.log就行了 public void LoginBtn(){Console.WriteLine("登录");//navigationManager.NavigateTo("/index");}

STM32 LL库开发

一、STM32开发方式 标准库开发&#xff1a;Standard Peripheral Libraries&#xff0c;STDHAL库开发&#xff1a;Hardware Abstraction Layer&#xff0c;硬件抽象层LL库开发&#xff1a;Low-layer&#xff0c;底层库 二、HAL库与LL库开发对比 ST在推行HAL库的时候&#xff0c;…

手机怎样换ip地址 更改手机IP地址有哪些方式

ip地址怎么改&#xff1a;使用深度ip转换器 在互联网时代&#xff0c;IP地址扮演着网络世界中的独特标识符。它是我们在上网时必不可少的元素&#xff0c;负责为设备提供独立的身份&#xff0c;并将信息传输到正确的目的地。然而&#xff0c;有时我们需要改变IP地址&#xff0c…

婚恋交友h5多端小程序开源版开发

婚恋交友h5多端小程序开源版开发 以下是婚恋交友H5多端小程序的功能列表&#xff1a; 用户注册和登录&#xff1a;用户可以通过手机号码或第三方账号注册和登录。个人信息填写&#xff1a;用户可以填写个人基本信息&#xff0c;包括姓名、性别、年龄、身高、体重、学历、职业等…

第11集丨Vue 江湖 —— 过滤器(filter)

目录 一、过滤器1.1 局部过滤器1.2 全局过滤器1.3 过滤器的串联1.4 案例 二、BootCDN2.1 在BootCDN下载dayjs2.2 dayjs官方文档 一、过滤器 过滤器&#xff1a; 定义&#xff1a;对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理&#xff09;。语法: a. 注册过…

阿里巴巴面试题---考察对底层源代码的熟悉程度

题目如图所示: 很多人可能会觉得两个输出都会是false,因为我们都会觉得""比较的是引用类型的地址,虽然放入的值都一样但是重新创造了新对象,地址不一样,所以结果都是false. 然而,当我们运行程序会发现结果都是false. 下面,我们来分析为什么是这样的结果. 我们知道…

nodejs+vue+elementui健身俱乐部网站rix1z

为设计一个安全便捷&#xff0c;并且使用户更好获取本健身俱乐部管理信息&#xff0c;本文主要有安全、简洁为理念&#xff0c;实现用户快捷寻找健身课程、健身器材、会员卡信息、新闻公告等信息&#xff0c;从而解决健身俱乐部管理信息复杂难辨的问题。该系统以vue架构技术为基…

Linux 目录和文件常见操作

就常见的命令&#xff1a; pwd pwd 显示当前的目录 目录迁移 我以如下的目录大致结构做一个简单的例子 cd 迁移到指定的路径&#xff0c;可以指定相对路径和绝对路径&#xff0c;默认相对 .指向当前路径&#xff0c;…/ 指向上一级的目录。 ls 列出文件及其目录 命令选…

取个对象值导致系统崩溃

取个对象值导致系统崩溃 前言 想必各位小伙经常在项目中遇到一些错误&#xff0c;取对象值的时候&#xff0c;经常报错,又或者某些项目突然就挂经常都是出现在一些对象取值上面&#xff0c;然后就被领导一顿训斥 报错分析 例如&#xff1a; 下面这个报错大家想必不会陌生&am…

华尔街之狼

&#xff08;1&#xff09; 我最近在重看一本书《物理学家走进华尔街》&#xff0c;顺便我还又重看了一遍《华尔街之狼》。 华尔街之狼&#xff0c;我最感兴趣三点&#xff1a; 垃圾产品&#xff1a;退市的垃圾股粉单 垃圾顾客&#xff1a;没有钱&#xff0c;但想发财的平民。就…

掌握Python的X篇_28_python包管理工具pip命令

本篇将会介绍在实际使用python中最能节省效率的内容&#xff0c;利用第三方库拿来就用。 文章目录 1. pip命令是什么2. pip相关操作2.1 list2.2 install2.3 uninstall2.4 导出和导入2.4.1 freeze命令2.4.2 “-r” 3. 国内镜像4. Python Packges Index网站 1. pip命令是什么 p…

mysql安装配置教程

mysql下载 mysql官网 mysql英文官网下载 mysql安装 解压安装包,如下效果 2.配置初始化文件my.ini 在根目录下创建一个txt文件&#xff0c;名字叫my&#xff0c;文件后缀为ini 之后复制下面这个代码放在文件下 &#xff08;新解压的文件没有my.ini文件&#xff0c;需自行创建…