Java锁深入理解2——ReentrantLock

前言

本篇博客是《Java锁深入理解》系列博客的第二篇,建议依次阅读。
各篇博客链接如下:
Java锁深入理解1——概述及总结
Java锁深入理解2——ReentrantLock
Java锁深入理解3——synchronized
Java锁深入理解4——ReentrantLock VS synchronized
Java锁深入理解5——共享锁

概述

虽然我们常用的可能是Synchronized,但我们还是先看JDK锁。因为它由JDK实现,有可见的源代码。分析起来会方便一些。

理解了之后,在去看Synchronized,会容易很多(毕竟都是锁,不管是谁实现的,大致的思想应该有共同之处)。

由于后面要从Demo一路深入到JDK源码。而看多线程源码和普通单线程源码还不太一样。如果还没尝试过多线程debug的,可以先看一下Java锁深入理解1——概述及总结,其中讲了如何多线程debug。

Demo1

JDK锁有很多,我们就以最常用的ReentrantLock(可重入锁,也是一种排他锁)来举例

public void testReentrantLock() {
    ReentrantLock mylock = new ReentrantLock();

    mylock.lock();//抢锁 加锁
    System.out.println("------do something....");//线程安全操作
    mylock.unlock();//释放锁
}

在这段demo中,如果有多个线程都会执行这个方法。那么同一时间,只会有一个线程进入到mylock.lock();mylock.unlock();之间。可以在其中做一些需要线程安全的操作。

Demo2

Demo1只是一种最基本的使用方式,通过lock-unlock来圈定一个安全区(也叫临界区),来保证线程安全。

还有两个操作await, signal也挺常见。分别是用来把自己阻塞,把别人唤醒。其实这两个操作对线程安全并没有什么直接作用。已经不属于“解决多线程客观问题”的范畴,而是属于“把多线程玩出更多花样”的范畴。如果说lock-unlock是锁的核心功能,那么await/signal则属于锁的附属功能。

    ReentrantLock mylock = new ReentrantLock();
    Condition c = mylock.newCondition();    

	public void testReentrantLock2() {

        mylock.lock();//抢锁 加锁
        System.out.println("------do something....");//线程安全操作
        try {
            c.await();//把自己阻塞
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        mylock.unlock();//释放锁
    }

    public void testReentrantLock2_1() {

        mylock.lock();//抢锁 加锁
        System.out.println("------do something....");//线程安全操作
        c.signal();//把阻塞的线程唤醒(配合await使用)
        mylock.unlock();//释放锁
    }

Demo2中,首先是增加了Condition c = mylock.newCondition();,不知道怎么翻译。自面意思就是“条件”,一般我们就直接称之为Condition。

语言和语言体系之间必然不可能一一对应。而专业领域的翻译有“精度要求”。当含义误差比较大时,就没必要硬翻译。

此时中文里夹杂英文专业词汇不叫装逼,而是为了表意更准确。(日常表达是没必要的)

testReentrantLock2方法中,在lock-unlock划定的临界区里(这个条件很重要),使用了c.await();。当某个线程A执行到这里的时候,会被阻塞在这里。

此时该线程A会失去锁(它虽然还身在临界区里,但却处于休眠状态)。相当于其他线程忽略线程A的存在,可以继续抢锁。

testReentrantLock2_1中,在lock-unlock划定的临界区里(这个条件很重要),使用了c.signal();。当线程B执行到这里的时候,会把阻塞的线程唤醒(比如上面的线程A)。

此时你可能有个疑问:那如果另一个线程B立马抢到锁,并唤醒A。是不是会和刚醒来的线程A同时身处临界区。

答:是的。 而且如果B使用的是signalAll(),还有可能唤醒一堆被阻塞线程。(所以不要误认为“临界区”同一时间只能有一个线程)

但区别就是:B手中有锁,只要B不出来,其他线程就进不来。而那些被B唤醒的线程能做的 只能默默的把剩下的路走完。

问题

如果用过锁,或许会产生一些疑问:

  • 代码为什么会在mylock.lock()位置停下来
  • 代码为什么会在c.await()位置停下来
  • 抢到锁的本质是什么
  • 怎么保证只有一个线程抢到锁
  • 什么时候才能抢锁

内部机制

下面就正式进入ReentrantLock类的内部,来解答上面的疑惑。

代码结构

在这里插入图片描述

这张图就表示ReentrantLock类的总体结构。(图中并没有严格按照URL的规范画。包含关系直接使用了更直观的嵌套,而不是用线条表示。箭头含义是按规范画的:A—>B表示A继承B)

当new ReentrantLock()时,其实使用的是FairSync(公平锁)或者NonfairSync(非公平锁)。
在这里插入图片描述
也可以通过传参数true,来创建公平锁
在这里插入图片描述

而这两种锁的顶级父类就是AbstrateQueuedSynchronizer(AQS)。

AQS

先整体看一下这个锁的核心类,AQS原理示意图

在这里插入图片描述

这张图相当于图代码结构示意图中,AQS部分的进一步放大,可以看到其中更多丰富的细节。

图中关键的两个东西:一个是state,一个是同步队列

队列中的一个个节点封装着一个个线程。绿色代表是当前获得锁的,在队列中位列第一。后面的黄色节点则处于阻塞状态。AQS就是通过这个队列来管理线程,实现“先来后到”的方式顺序执行。

state是一个标志,相当于一个红绿灯(更像公共厕所的锁上的显示:有人/无人):1表示有线程正在占有锁,其他线程不用白费力气去抢了。0表示当前没有占用,其他线程有机会去抢。当同一个线程在前一个锁还没释放的时候,就又再次抢锁也是可以的,此时state会加到2,以此类推,重入几次,state就是几。

图中的另外一种队列(红色的那种),画了两个,表示这种队列可以有多个(也可以没有)。叫条件队列。也就是代码中,我们使用await之后,线程节点被放置的位置。再被signal唤醒之后,线程节点就从这个红色队列中脱离出来(脱离的优先级也是按照先来后到的方式,从队列头部一个一个的脱落),然后重新回到同步队列中排队。

名词统一

关于AQS中的两种队列的名字,有点乱(有些博客自己都前后不一致)。我根据源码上的注释,给本文统一如下:

等待队列(wait queues):上面两种队列的统称。这两种队列都是有AQS类中的内部类Node类组成的,都是阻塞等待状态(除了同步队列的头节点)。(参考AQS源码中的Node类上的注释的第一句:Wait queue node class)

同步队列(sync queue):也就是实现lock-unlock的核心队列,图中第一条队列。(参考AQS源码中的transferForSignal方法的注释:Transfers a node from a condition queue onto sync queue.)

条件队列(condition queue):就是图中的红色队列。(参考AQS源码中的transferForSignal方法的注释:Transfers a node from a condition queue onto sync queue.)

Transfers a node from a condition queue onto sync queue.意思是:将节点从条件队列转移到同步队列。

Node

上面那个AQS的原理图中,Node只是一个小方块,我们继续放大这个方块,以及两个队列链表

Node节点示意图:
在这里插入图片描述

同步队列示意图:
在这里插入图片描述

条件队列示意图:
在这里插入图片描述

可以看到Node节点之间通过prev和next,组成了同步队列的双向链表。通过nextWaiter,组成了条件队列的单向链表。

线程组织成队列的逻辑场景

那这个两个队列是怎么用Node节点自动组织起来的呢。以同步队列为例介绍一下。

一般情况下,我们会把锁的定义

ReentrantLock mylock = new ReentrantLock();
Condition c = mylock.newCondition();

写在方法外面,因为只需要定义一个即可,后面不需要重复定义。

有了这两句话,我们的AQS容器,以及其中的Condition就生成了。后面只要有线程碰到这个容器,它就像一个高速公路检查站一样,在里面触发一系列的操作。看一下AQS的初始化时的示意图(注意观察和【AQS原理图】的差异):

在这里插入图片描述

在容器里,除了有state之外,还有headtail(组织队列的关键元素)。

当某个线程进来之后,在state的指挥下,被包装成Node节点,然后被head和tail引用。

然后是第二个,它会自动被追加到第一个节点的后面,然后是第三个,第四个,,,

最后就形成了前面我们看到的【AQS原理图】的样子。

可重入锁逻辑

通过上面的介绍,我们基本就掌握了ReentrantLock以及AQS的基本原理。下面是一些源码细节。

流程图

在这里插入图片描述
这个流程图很重要。结合这张图,会帮助理解后面各种操作的逻辑。

lock()

公平锁上锁逻辑

看看不能抢(看state状态,是不是锁定中(其他线程正在运行中))

  • 1-1. 如果能抢,就抢就抢一抢(不断循环尝试)
    • 1-1-1. 抢到了,就把老大给踢出队列(如果有老大的话),自己做老大
    • 1-1-2. 没抢到,自己就阻塞
  • 1-2. 如果不能抢,就进队列去等
    • 1-2-1. 进了队列,发现自己是老二,那么就去尝试抢一抢(进入1-1的循环)
    • 1-2-2. 进了队列,发现自己不是老二,那么就阻塞

解释

  • 老大:也就是头节点,抢到锁的线程。

  • 这里忽略了一些细节:

    • 抢的过程中也可能发现是自己重入(上一次抢到锁的就是自己,现在绕了一圈又进来了),那么也算抢成功(自己是老大,抢完之后还是老大)
    • 等待中:被取消的,会被踢出队列
    • 我们看到类似中断的一些代码,仔细看这些代码,其实并不会引起中断。只是在收到中断信号之后,这个中断信号会唤醒阻塞(但因为在循环里面,所以并不影响结果),然后这个中断信号被抹去,最后又给恢复了(如果感觉有点晕,没关系,你只要知道这个逻辑无伤大雅,不用去刻意理解这部分逻辑,后面会讲到中断这块)。
  • 我们一开始可能认为:一个线程队列,如果简单设计的话。前一个运行完,触发后一个运行似乎是最简单的。

    但实际设计的方案是:老大运行完,确实“通知”老二了。但这个“通知”的意思是:唤醒后一个线程(从阻塞变为非阻塞)。

    就是说:老大退位了,并不意味值老二自动变老大。只是告诉老二,你有权利上位了(上位的过程还是老二主动循环尝试去争取)。

    其实想想也好理解:线程和线程之间都是独立的,没有很强的耦合关系。最大的耦合就是signal唤醒了。

  • 老二怎么踢掉的老大:源码

    setHead(node);
    p.next = null; // help GC

这里的p就是当前节点(老二)的前面的节点(老大)。就是说把老大的next引用指到null。
第一句的setHead方法里,把原本指向前节点的引用指向null。
也就是把双向的引用都断掉。而且把head也指向了老二。老大彻底“失联”,等着被GC回收。

非公平锁上锁逻辑

直接抢抢试试(不去判断state)

  • 1-1. 如果成功,自己直接做老大
  • 1-2. 如果失败,进入“公平锁”流程

unlock()

解锁流程,无论是公平锁还是非公平锁都一样

  1. 把锁的状态改为“非锁定中”
  2. 唤醒下一个节点(unpark)
  3. 从节点上退下来? 【并没有这一步!老大的位置是被老二踢下来的】

await()

  1. 排进条件队列
  2. 释放锁(这一步就是unlock的操作)
  3. 阻塞

signal()

  1. 找到条件队列的第一个节点
  2. 让这个节点从条件队列脱离掉(first.nextWaiter = null;)
  3. 让这个节点排到同步队列的队尾(tail.next = node;)
  4. 唤醒这个节点(unpark)

小结

  • 在AQS中,试图抢锁的只有老大,老二和还未入队列“外来者”,其他节点都处于阻塞状态
  • unlock和await(注意:能做这两个动作的只有拿到锁的头节点),都会调用同一个释放锁的过程(改锁状态为0,唤醒同步队列里的第二个节点)。
    不同点是:await后还会把自己加入条件队列,然后阻塞自己(其实可以说await流程中包含unlock的流程)。
  • 无论是同步队列里的自动阻塞(那些黄色节点),还是使用await后的阻塞(红色节点),本质原理是一样的,都是用park阻塞,都需要被别的线程用unpark唤醒。区别在于:
    • 在哪阻塞:前者在同步队列里阻塞,后者在条件队列里阻塞。
    • 被谁唤醒:前者被头节点释放锁后唤醒,后者被其他线程(其实还是头节点)使用signal唤醒。
  • unlock和signal,都会涉及到唤醒节点(unpark)的操作。
    前者是唤醒的是同步队列里的第二个节点,后着是唤醒条件队列里的第一个节点。
  • 唤醒(unpark):就是是给指定节点“解穴”,让它继续动起来。
  • 被唤醒之后,至于去干什么,取决于线程当前执行到哪了,后面还要做什么。如果是同步队列的节点,被唤醒后就是继续抢锁。而条件队列里的节点,正常就是默默的继续往下执行代码。当然,如果它身处一个循环语句之中,转一圈,它也许还会再次去抢锁。

其实前面的流程已经把取消等流程都给省略了,但还是太细节,太复杂。再画一个更简化版的整体动态概览图(两条实线表示节点的变换位置的方向)
在这里插入图片描述

可能的困惑

  1. lock是为了实现线程安全,那么lock源代码本身的线程安全怎么保证?
    比如:lock()源码中,抢锁(改锁状态),线程入队列都是用了CAS(也是AQS的核心),保证了线程安全。而await的时候在节点入队列时,却直接使用的=,不会出现线程安全问题吗?
    请添加图片描述

答:这是一个思维盲区。或许有读者已经想到问题出在哪了。
因为await只能在lock和unlock之间(临界区)的线程安全区里调用,所以await内不用担心线程安全问题。
整个过程,其实只有抢锁的时候,需要考虑线程安全。后面的操作一直到unlock其实都是线程安全的,其他线程都被阻止在抢锁那一步了。

  1. 线程被唤醒后,在哪复活?是不是像打游戏一样,在泉水(出生地)里复活?

答:这就是想多了。它在哪阻塞,就在哪被唤醒。例如下面的await方法代码
请添加图片描述
线程在LockSupport.park(this);阻塞,那么当它被其他线程唤醒时,就还是从这句话开始执行。
但是,之所以可能引起困惑。从await()开始,到park最终停下,最后再次被唤醒开始往下执行,中间经历了很长的流程,如下图所示:

在这里插入图片描述

这个一维流程图看着晕?再换个二维流程图视角看看:
在这里插入图片描述

我们还看到park这句话被while语句包裹着。也就意味着:即使被唤醒,也又可能立马又阻塞。
这个写法也值得我们学习:线程被唤醒后别晕着头就往下执行,最好看看当前什么状况,如果不能往下执行,也许还得继续阻塞。

  1. 如果锁重入了多次,比如重入了三次,state的值被加到3。此时做await()操作。state值需要清零吗。
    答:不需要,这里的重入就有点像事务,你进了多少层事务,最后都得一层层的出来。除非程序报错。

CAS(compareAndSet)和自旋锁

在说AQS的时候总会有人说CAS和自旋锁。
首先明确一点:CAS本身是不会自旋的,只试一次:返回true或者false
那自旋体现在哪呢,有两段循环语句:

  1. 这是当前线程节点 作为一个“外来节点”(还没排到同步队列里)的接下来的行为:入队
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    

代码逻辑:

  • 如果队列的结尾是空(根本没人排队),就去尝试当那第一个节点(也可能尝试失败)。
  • 否则就尝试排到队尾(不一定能排进去)。
  • 这两个条件内的方法都是CAS尝试,如果失败了,就再次循环执行一遍,直到排进去为止。
  1. 这是当前节点,作为同步队列里一个节点,接下来的行为:抢锁或阻塞
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                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);
        }
    }

代码逻辑:

  • 如果前面那个节点是头节点,就抢锁(不一定抢到)
  • 否则就阻塞(等着前面的节点执行完,唤醒我)
  • 循环上面两步,一直到抢到为止

简化代码写法分析及思考

在ReentrantLock源代码中有这么两处典型的if判断语句

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
和

if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;

以第一段为例,他的逻辑其实是

public final void acquire(int arg) {
        if (!tryAcquire(arg)) {
            if(acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
            
    }

这并不难看出。因为&&的作用,if条件语句中的第一个条件其实也相当于一个判断,对第二个条件的执行与否造成影响。
但如果按照我日常开发的习惯,我基本会写成第二种拆开的写法。甚至写成这样:

public final void acquire(int arg) {
        boolean tryAcquireRes = !tryAcquire(arg);
        if (tryAcquireRes) {
        	//看代码我们就会明白,下面两句话是顺序执行的两句,也给拆开
            Node newWaiter = addWaiter(Node.EXCLUSIVE);
            boolean acquireQueuedRes = acquireQueued(newWaiter, arg);
            if(acquireQueuedRes) {
                selfInterrupt();
            }
                
        }
            
    }

原因无他,只是为了让代码更易读。减少团队合作中的沟通成本,一眼就看出逻辑(这相当于团队之间用代码在沟通)。
但是,这里的写法我是认可的。因为这是在封装工具包,而且是多线程这种对性能要求极高的代码。当然是能多榨取一点性能就多榨取一点。作为开源软件,测试是非常到位的,不担心出bug。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

QT Qwidget 事件处理机制

qlineEdit Qt事件处理是指在Qt应用程序中处理各种事件的过程。事件是指在应用程序中发生的各种操作,例如按键、鼠标点击、窗口移动等。Qt提供了一个事件处理机制,使得开发者可以对这些事件进行处理,以实现应用程序的各种功能。 Qt中的事件处…

CMake设置Visual Studio工程的调试环境变量和工作目录cwd的方法

1、设置在Visual Studio中调试的环境变量,此设置仅仅在VS中点击那个绿色三角运行时有效,与你直接双击打开exe文件运行无关,有效避免多版本动态库全部写入系统环境变量的污染问题; # Visual Studio中调试依赖的独立环境变量 set_p…

代码随想录算法训练营第五十天| ● 309.最佳买卖股票时机含冷冻期 ● 714.买卖股票的最佳时机含手续费 ●总结

309.最佳买卖股票时机含冷冻期 看完题后的思路 dp[i][] 0: 第i天不持有股票的最大利润 1: 持有 递推公式 dp[i][0]max(第i-1天不持有,第i-1天持有,在第i天卖了) dp[i][1]max(第i-1天持有, 第i-2天不持有,第i天持有) 初始化 dp[0][0]0; dp[0][1]-price[i]; dp[1][0]max(x,x) d…

PCB模块化设计23——LDO线性稳压电源模块PCB布局布线设计规范

目录PCB模块化设计23——LDO线性稳压电源模块PCB布局布线设计规范一、LDO线性稳压电源组成与概念二、LDO的电路的主干道布局要点PCB模块化设计23——LDO线性稳压电源模块PCB布局布线设计规范 一、LDO线性稳压电源组成与概念 LDO线性稳压器是最基本的稳压电源变换,…

STM32时钟与定时器

目录 一、STM32的时钟系统 二、SysTick定时器 三、HAL_Delay的实现 四、通用定时器 一、STM32的时钟系统 概念时钟系统是由振荡器(信号源)、定时唤醒器、分频器等组成的电路。常用的信号源有晶体振荡器和RC振荡器。 意义时钟是嵌入式系统的脉搏&…

自然语言处理中的数据预处理:理论与实践

❤️觉得内容不错的话,欢迎点赞收藏加关注😊😊😊,后续会继续输入更多优质内容❤️👉有问题欢迎大家加关注私戳或者评论(包括但不限于NLP算法相关,linux学习相关,读研读博…

2020-2022年美赛C题信件汇总

2020-2022年美赛O奖C题写信汇总(机翻) 2020 <1> Dear Marketing Director of Sunshine Company , According to your requirements, we analyze the ratings and reviews of competitive products on Amazon for baby pacifier, microwave and hair dryer to be intro…

C++ 模板、函数模板、类模板

函数模板、类模板 文章目录函数模板、类模板前言1.模板1.1 模板的概念1.2 模板的特点2. 函数模板2.1 函数模板语法2.2 函数模板注意事项2.3 普通函数与函数模板的区别2.4 普通函数与函数模板的调用规则2.5 模板的局限性2.6 函数模板案例3. 类模板3.1 类模板语法3.2 类模板与函数…

2023年Java程序员生存指南,8个中肯建议度过寒潮!

最近小源看到一个2023Java程序员生存指南&#xff0c;觉得挺有意思的&#xff0c;今天分享给大家&#xff01; 第一&#xff0c;不要在互联网公司基于低代码做开发。 第二&#xff0c;保证一定强度的刷题&#xff0c;原因嘛你懂的~ 第三&#xff0c;凡是要求名校的岗位&#xf…

C#,码海拾贝(08)——埃特金(Aitken)逐步曲线插值算法,《C#数值计算算法编程》源代码升级改进版

埃特金逐步线性插值法&#xff08;Aitken successive linear interpolation method&#xff09;一种能逐步升阶的插值方法.用拉格朗日插值多项式计算函数近似值时&#xff0c;如果精度不满足要求&#xff0c;需增加插值节点以提高插值多项式次数时&#xff0c;原来算出的结果均…

字节二面,原来我对自动化测试的理解太浅了

如果你入职一家新的公司&#xff0c;领导让你开展自动化测试&#xff0c;作为一个新人&#xff0c;你肯定会手忙脚乱&#xff0c;你会如何落地自动化测试呢&#xff1f; 01 什么是自动化 有很多人做了很长时间的自动化但却连自动化的概念都不清楚&#xff0c;这样的人也是很悲…

七、网络安全

&#xff08;一&#xff09;网络安全设计 1、网络安全体系设计 &#xff08;1&#xff09;物理安全 通信线路的可靠性、硬软件设备的安全性、设备的备份、防灾害防干扰能力、设备的运行环境、不间断电源 eg机房安全 &#xff08;2&#xff09;系统安全 操作系统本身的缺陷…

vue尚品汇商城项目-day01【4.完成非路由组件Header与Footer业务】

文章目录4.完成非路由组件Header与Footer业务4.1使用组件的步骤&#xff08;非路由组件&#xff09;本人其他相关文章链接4.完成非路由组件Header与Footer业务 在咱们项目开发中&#xff0c;不在以HTML CSS 为主&#xff0c;主要搞业务、逻辑 开发项目的流程&#xff1a; (1)…

模式识别 —— 第六章 支持向量机(SVM)与核(Kernel)

模式识别 —— 第六章 支持向量机&#xff08;SVM&#xff09;与核&#xff08;Kernel&#xff09; 文章目录模式识别 —— 第六章 支持向量机&#xff08;SVM&#xff09;与核&#xff08;Kernel&#xff09;硬间隔&#xff08;Hard-Margin&#xff09;软间隔&#xff08;Soft…

52LeetCode刷题_LeetCode刷题手册

虽然刷题一直饱受诟病&#xff0c;不过不可否认刷题确实能锻炼我们的编程能力&#xff0c;相信每个认真刷题的人都会有体会。现在提供在线编程评测的平台有很多&#xff0c;比较有名的有 hihocoder&#xff0c;LintCode&#xff0c;以及这里我们关注的 LeetCode。 LeetCode收录…

Spring 注解和 XML 配置文件重复定义 Bean,会怎样?

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;蚂蚁集团高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《EffectiveJava》独家解析》专栏作者。 热门文章推荐…

iPhone屏幕适配(之屏幕尺寸)

Device screen size 各设备屏幕尺寸 DeviceDimensions (portrait)iPhone 14 Pro Max430x932 pt (1290x2796 px 3x)iPhone 14 Pro393x852 pt (1179x2556 px 3x)iPhone 14 Plus428x926 pt (1284x2778 px 3x)iPhone 14390x844 pt (1170x2532 px 3x)iPhone 13 Pro Max428x926 pt (…

Element Plus 实例详解(七)___Typography 排版

Element Plus 实例详解&#xff08;七&#xff09;___Typography 排版 目录 一、前言 二、搭建Element Plus试用环境 1、搭建Vue3项目&#xff08;基于Vite Vue&#xff09; 2、安装Element Plus 三、Element Plus Typography 排版功能试用 1、字号 2、行高 3、Font-fam…

C语言:位运算符----与(),或(|),非(~),异或(^),左移(<<)和右移(>>)

C语言 基础开发----目录 一、位运算符----简介 位运算符 就是按二进制位进行运算。 C语言中位运算符主要包括六种&#xff0c;具体如下&#xff1a; 与(&)&#xff0c;或(|)&#xff0c;非(~)&#xff0c;异或(^)&#xff0c;左移(<<)和右移(>>) 位运算符含…

【C++】类和对象(三)

类和对象&#xff08;三&#xff09; 拷贝构造函数&#xff1a; 当我们想要将一个已确定的类变量的值拷贝给另外一个相同类型的类变量&#xff0c;有什么快捷的方法吗&#xff1f; 就相当于定义了一个int类型的i10&#xff0c;想将i复制给一个刚初始化的遍历j&#xff0c;in…
最新文章