聊聊并发编程——多线程之AQS
目录
队列同步器(AQS)
独占锁示例
AQS之同步队列结构
解析AQS实现
队列同步器(AQS)
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组 件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获 取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。
-
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
-
使用同步器提供的3 个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))对同步状态进行更改,因为它们能够保证状态的改变是安全的。
同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交 互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者, 它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。
独占锁示例
Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的,通过独占锁了解下队列同步器(AQS)。
public class Mutex implements Lock { // 子类推荐被定义为自定义同步组件的静态内部类// 同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用private static class Sync extends AbstractQueuedSynchronizer {// 是否处于独占状态protected boolean isHeldExclusively() {return getState() == 1;}// 当状态为0的时候获取锁public boolean tryAcquire(int acquire) {if (compareAndSetState(0, 1)) {return true;}return false;}// 释放锁,将状态设置为0protected boolean tryRelease(int release) {if (getState() == 0) {throw new IllegalMonitorStateException();}setExclusiveOwnerThread(null);setState(0);return true;}// 返回一个Condition,每一个condition都包含了一个condition队列Condition newCondition() {return new ConditionObject();}} // 通过Sync进行代理操作,实现Lock接口的APIprivate final Sync sync = new Sync(); // 获取锁@Overridepublic void lock() {sync.acquire(1);} // 可中断地获取锁@Overridepublic void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);} // 尝试非阻塞的获取锁@Overridepublic boolean tryLock() {return sync.tryAcquire(1);} /*超时的获取锁,当前线程在以下3种情况下会返回:1.当线程在超时时间获得了锁2.当线程在超时时间被中断3.超时时间结束,返回false*/@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(time));} // 释放锁@Overridepublic void unlock() {sync.release(1);} /*获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用用,当前线程将释放锁*/@Overridepublic Condition newCondition() {return sync.newCondition();} }
AQS之同步队列结构
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其 加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
节点属性:
属性类型与名称 | 描述 |
---|---|
int waitStatus | 等待状态,包含如下状态: CANCELLED:值为1,表示节点已取消,通常是因为线程被中断或者等待超时而被取消。 SIGNAL:值为-1,表示后继节点需要被唤醒,即当前节点的释放(signal)会通知后继节点继续尝试获取锁或资源。 CONDITION:值为-2,表示节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中 PROPAGATE:值为-3,表示释放共享锁时需要向后继节点传播共享性质,以确保后继节点可以被唤醒。这在CountDownLatch等场景中会使用到。 INITIAL:值为0,初始状态。 |
Node prev | 前驱节点,当节点加入同步队列时被设置(尾部添加) |
Node next | 后继节点 |
Node nextWaiter | 等待队列中的后继节点。如果当前节点是共享的,那么这个字段将是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段 |
Thread thread | 获取同步状态的线程 |
节点是构成同步队列的基础,同步器拥有首节点(head)和尾结点(tail),没有成功获取同步状态的线程会成为节点加入该队列的尾部。
-
当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。
-
首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可
解析AQS实现
以ReentrantLock的非公平锁为例,看看lock的实现。
-
ReentrantLock.lock()—获取锁的入口
public void lock() {sync.lock();}
sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑。
Sync 有两个具体的实现类,分别是: NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他 线程等待,新线程都有机会抢占锁 FailSync: 表示所有线程严格按照 FIFO 来获取锁。
ReentrantLock的无参构造函数默认创建的是非公平锁。
public ReentrantLock() {sync = new NonfairSync();}
-
NonfairSync.lock()—获取同步状态/锁。
static final class NonfairSync extends Sync {private static final long serialVersionUID = 7316153563782823691L; /*** Performs lock. Try immediate barge, backing up to normal* acquire on failure.*/final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);} protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}
-
非公平锁的特点:抢占锁的逻辑,不管有没有线程排队,上来先CAS抢占一下。
-
CAS成功,表示成功获得锁。
-
CAS失败,调用获取独占锁acquire()走锁竞争逻辑。
-
-
AQS.acquire(1)—尝试获取独占锁or加入同步队列自旋获取锁。
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}
-
通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false
-
如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加 到 AQS 队列尾部。
-
acquireQueued(),将 Node 作为参数,通过自旋去尝试获取锁。
-
-
NonfairSync.tryAcquire(1)—尝试获取独占锁
protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}
它是重写 AQS 类中的 tryAcquire 方法
-
ReentrantLock.nofairTryAcquire(1)—尝试获取独占锁
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread(); // 获取当前执行的线程int c = getState(); // 获取state的值if (c == 0) { // 表示无锁状态if (compareAndSetState(0, acquires)) { // CAS替换state的值,case成功表示获取锁成功setExclusiveOwnerThread(current); // 保存当前获得锁的线程,下次再来的时候不用尝试竞争锁return true;}}else if (current == getExclusiveOwnerThread()) { // 如果同一线程竞争锁,直接增加重入次数int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
-
获取当前线程,判断当前的锁的状态
-
如果 state=0 表示当前是无锁状态,通过 cas 更新 state 状态的值
-
当前线程是属于重入,则增加重入次数
-
-
AQS.addWaiter(Node.EXCLUSIVE) —线程构造成节点加入同步队列 (static final Node EXCLUSIVE = null;)
private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;}
当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成 Node.
入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状 态。意味着重入锁用到了 AQS 的独占锁功能
-
将当前线程封装成 Node
-
当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的 node 添加到 AQS 队列
-
如果为空或者 cas 失败,调用 enq 将节点添加到 AQS 队列
-
-
enq(node)—通过自旋把当前节点加入到队列中
private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}
图解分析:
8.AQS.acquireQueued(node, 1)—把node加入到链表去争抢锁
-
获取当前节点的 prev 节点
-
如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁
-
抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head 节点
-
如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程
-
最后,通过 cancelAcquire 取消获得锁的操作
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor(); // 获取当前节点的prev节点if (p == head && tryAcquire(arg)) { // 如果是head节点,说明有资格去争抢锁setHead(node); // 获取锁成功,也就是ThreadA已经释放了锁,然后设置head为ThreadB获得执行权限p.next = null; // help GCfailed = false;return interrupted;}// ThreadA可能还没释放锁,使得ThreadB在执行tryAcquire返回falseif (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}
9.shouldParkAfterFailedAcquire—竞争锁失败后应该挂起
这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是 否应该被挂起。
-
如果 ThreadA 的 pred 节点状态为 SIGNAL,那就表示可以放心挂起当前线程
-
通过循环扫描链表把 CANCELLED 状态的节点移除
-
修改 pred 节点的状态为 SIGNAL,返回 false.
-
返回 false 时,也就是不需要挂起,返回 true,则需要调用 parkAndCheckInterrupt 挂起当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { // pred是前置节点int ws = pred.waitStatus; // 前置节点的waitStatusif (ws == Node.SIGNAL) // 如果前置节点为 SIGNAL,意味着只需要等待其前置节点的线程被释放return true;if (ws > 0) { // ws大于 0,意味着prev节点取消了排队,直接移除这个节点就行do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 利用cas设置prev节点的状态为SIGNAL(-1)}return false;}
图解分析:
waitStatus = -1(SIGNAL:值为-1,表示后继节点需要被唤醒,即当前节点的释放会通知后继节点继续尝试获取锁或资源。)
10.parkAndCheckInterrupt
Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识 true,并且对中断标识进行复位标识已经响应过了中断请求。如果返回 true,意味着在acquire方法中会执行 selfInterrupt()。
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted(); // 1.中断 2.复位}
selfInterrupt: 标识如果当前线程在 acquireQueued 中被中断过,则需要产生一 个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求的。
11.ReentrantLock.unlock()—锁释放
public void unlock() {sync.release(1);}
public final boolean release(int arg) {if (tryRelease(arg)) { // 释放锁成功Node h = head; // 获取aqs中的head节点if (h != null && h.waitStatus != 0) // 如果head节点不为空且状态!=0.调用unparkSuccessor(h)唤醒后续节点unparkSuccessor(h);return true;}return false;}
12.ReentrantLock.tryRelease()—设置锁状态
这个方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值 (参数是 1),如果结果状态为 0,就将排它锁的 Owner 设置为 null,以使得其它 的线程有机会进行执行。 在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时 候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock() 的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true.
protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}
13.AQS.unparkSuccessor()—唤醒后续节点
private void unparkSuccessor(Node node) {int ws = node.waitStatus; // 获取head节点的状态if (ws < 0)compareAndSetWaitStatus(node, ws, 0); // 设置head节点状态为0Node s = node.next; // 得到head节点的下一个节点//如果下一个节点为 null 或者 status>0 表示 cancelled 状态.if (s == null || s.waitStatus > 0) { s = null;//通过从尾部节点开始扫描,找到距离head最近的一个waitStatus<=0 的节点for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null) // next节点不为空,直接唤醒这个线程即可LockSupport.unpark(s.thread);}
-
相关文章:
聊聊并发编程——多线程之AQS
目录 队列同步器(AQS) 独占锁示例 AQS之同步队列结构 解析AQS实现 队列同步器(AQS) 队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组 件的基础框架&…...
DE0开发板交通灯十字路口红绿灯VHDL
名称:基于DE0开发板的交通灯十字路口红绿灯 软件:Quartus 语言:VHDL 要求: 设计一个十字路口交通信号灯的控制电路。分为两种情况,正常状态和报警状态。 1.正常状态:要求红、绿灯按一定的规律亮和灭&a…...
华为云云耀云服务器L实例评测使用 | 通过程序实现直播流自动分段录制
华为云云耀云服务器L实例评测使用 | 通过程序实现直播流自动分段录制 1. 准备工作2. 环境搭建3. 心得总结 1. 准备工作 随着云计算时代的进一步深入,越来越多的中小企业企业与开发者需要一款简单易用、高能高效的云计算基础设施产品来支撑自身业务运营和创新开发。基…...
前端教程-webpack
官网 webpack webpack基础 视频教程 尚硅谷Webpack5入门到原理(面试开发一条龙)...
white-space几种属性的用法(处理空格)
white-space:normal 文首的空格忽略,文本内部的换行符自动转成了空格。 white-space:nowrap 不换行,即使超出容器宽度 white-space:pre 与原文本一致,空格和换行符保留 white-space:pre-…...
Linux的历史
Linux的历史 前言: 关于Linux,你可能只是听说过它是一款操作系统,也许你还知道它是开源的,但在日常生活中,你更熟悉的是Windows。 那么我们为什么要了解、学习Linux,看完这一篇,你也许可以从…...
软考高级系统架构设计师系列论文真题八:论企业集成平台的技术与应用
软考高级系统架构设计师系列论文真题八:论企业集成平台的技术与应用 一、论企业集成平台的技术与应用二、找准核心论点三、理论素材准备四、精品范文赏析1.摘要2.正文3.总结软考高级系统架构设计师系列论文之:百篇软考高级架构设计师论文范文软考高级系统架构设计师系列之:论…...
[H5动画制作系列] 路径引导动画 Demo
代码参考1: <!DOCTYPE html> <html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>路径引导动画 Demo1</tit…...
[React] Context上下文的使用
文章目录 1.Context的介绍2.为什么需要Context3.Context的使用 1.Context的介绍 Context旨在为React复杂嵌套的各个组件提供一个生命周期内的统一属性访问对象,从而避免我们出现当出现复杂嵌套结构的组件需要一层层通过属性传递值得问题。 Context是为了提供一个组…...
高云FPGA系列教程(9):cmd-parser串口命令解析器移植
文章目录 @[toc]cmd-parser库简介cmd-parser库源码获取GW1NSR-4C移植cmd-parser实际测试cmd-parse命令解析器优化本文是高云FPGA系列教程的第9篇文章。 上一篇文章介绍片上ARM Cortex-M3硬核处理器串口外设的使用,演示轮询方式和中断方式接收串口数据,并进行回环测试。 本文…...
PHP8的静态变量和方法-PHP8知识详解
我们在上一课程讲到了public、private、protected这3个关键字,今天我们来讲解static关键字,明天再讲解final关键字。 如果不想通过创建对象来调用变量或方法,则可以将该变量或方法创建为静态变量或方法,也就是在变量或方法的前面…...
用AI写文章被百家号封禁
我是卢松松,点点上面的头像,欢迎关注我哦! 千万不要用AI创作,尤其是原文照搬!不要用ai,不要用,不要用!重要的事情说三遍。 近日ID名为“爸爸在家赚钱”用AI写了4-5篇文章投稿在百家号,随后百度就把他帐号…...
JVM--Java类加载器笔记
Java类加载器 代码经过编译变成了字节码打包成 Jar 文件。让 JVM 去加载需要的字节码,变成持久代/元数据区上的 Class 对象,接着执行程序逻辑。 类声明周期和加载过程 步骤:加载->链接(校验->准备->解析)-…...
【在Ubuntu部署Docker项目】— PROJECT#1
一、说明 让我们深入了解 Docker。用docker构建web服务器。我们正在计划开发JavaScript API,建立MySQL数据库,并创建一个 PHP 网站使用 API 服务。Php Node.js Mysql — DockerSeries — Episode#1 二、系统架构概述 我们要构建的容器,是三…...
【学习笔记】LOJ #6240. 仙人掌
毒瘤题😅 简单版本 CF235D Graph Game 首先,考虑建立圆方树,然后对于一个点双(简单环)上的两个点,有两条路径可以到达 和简单版本类似,考虑容斥。即枚举点对 i , j i,j i,j之间 哪些路径是联…...
java通过接口转发文件(上传下载)
java接口转发上传的文件 RequestMapping(value "/XXXX/fileUpload", method RequestMethod.POST) public String getFileUpload2(RequestParam("file") MultipartFile file, HttpServletRequest request) public static String hotMapPost3(String ur…...
Docker-部署docker-compose以及管理服务
部署docker-compose以及管理服务 文章目录 部署docker-compose以及管理服务[TOC] 前言一、docker-compose是什么?1、介绍2、 功能 二、安装docker-compose1.yum直接安装2.二进制安装3.pip安装 三、docker-compose部署服务1.编写docker-compose.yml文件 总结 前言 D…...
Android - Monkey 测试应用出现Crash报错IllegalStateException
问题描述 平时使用Lottie动画都是正常的,没出过这个crash问题,看下的报错信息,代码中文件夹也设置了,没看出来问题。 AndroidRuntime: java.lang.IllegalStateException: You must set an images folder before loading an imag…...
Spring源码分析 事务 实现原理
文章目录 什么是事务Spring事务管理Spring事务实现原理事务管理器事务定义事务的开启事务核心方法业务代码使用事务TransactionInterceptor 什么是事务 一般所指的事务是数据库事务,是指一批不可分割的数据库操作序列,也是数据库并发控制的基本单位。其…...
ADS-B及雷达显示终端8.3
新版本功能升级主要有如下: 1、地图更新 在上一版本8.2中使用的高程地图为由SRTM经过地形晕渲后,生成地形图片,然后对图片进行贴图,一一按规定位置、大小将地形图贴至底图上,而后在底图上进行二维矢量地图的绘制,包括…...
第二章:最新版零基础学习 PYTHON 教程(第二节 - Python 输入/输出–从 Python 控制台获取输入)
目录 Python 中的控制台是什么? 接受来自控制台的输入: 1. 将输入类型转换为整数:...
linux安装配置 flume
目录 一 解压安装包 二 配置部署 (1)修改配置 (2)下载工具 (3)创建配置文件 (4)启动监听测试 (5)flume监控文件 一 解压安装包 这里提供了网盘资源 链…...
SSM - Springboot - MyBatis-Plus 全栈体系(十五)
第三章 MyBatis 二、MyBatis 基本使用 4. CRUD 强化练习 4.1 准备数据库数据 首先,我们需要准备一张名为 user 的表。该表包含字段 id(主键)、username、password。创建SQL如下: CREATE TABLE user (id INT(11) NOT NULL AUT…...
win10默认浏览器改不了怎么办,解决方法详解
win10默认浏览器改不了怎么办,解决方法详解_蓝天网络 在使用Windows 10操作系统时,你可能会遇到无法更改默认浏览器的情况。这可能是因为其他程序或设置正在干扰更改。如果你也遇到了这个问题,不要担心,本文将为你提供详细的解决…...
C语言连接MySQL并执行SQL语句(hello world)
1.新建一个控制台项目 参考【VS2022 和 VS2010 C语言控制台输出 Hello World】VS2022 和 VS2010 C语言控制台输出 Hello World_vs2022源文件在哪_西晋的no1的博客-CSDN博客 2.安装MySQL 参考【MySQL 8.0.34安装教程】MySQL 8.0.34安装教程_西晋的no1的博客-CSDN博客 3.复制MySQ…...
react实现动态递增展示数字特效
在可视化展示界面时有一种场景,就是页面在初始化的时候,有些数字展示想要从某个值开始动态递增到实际值,形成一种动画效果。例如: 写一个数字递增的组件,有两种方式:1.固定步长,代码如下&#x…...
读取.nrrd和.dcm文件格式医学图片可视化与预处理
nrrd数据格式 MITK默认会将医学图像保存为格式为NRRD的图像,在这个数据格式中包含: 1、一个单个的数据头文件:为科学可视化和医学图像处理准确地表示N维度的栅格信息。 2、既能分开又能合并的图像文件。 nrrd_options输出 {u’dimension’:…...
VS CODE中的筛选器如何打开?
最近更新了vscode1.82版本,发现在git管理界面有一个“筛选器”功能,十分好用,后来关掉了,找了好久都没有找到办法打开这个筛选器功能,今天无意中不知道按到了哪个快捷键,打开了,就是下图这个&am…...
vue 多环境文件配置(开发,测试,生产)
1.经常我们在开发时候会有不同环境,要代理的路由等等都会出现不同 配置一下三个文件打包的时候,执行三个不同的指令就会打包不同的环境 npm run build:dev npm run build:test npm run build:prodpackage.json 中配置scripts 指令 以,env.development…...
在服务器上搭建pulseaudio的运行环境,指定其运行目录、状态目录和模块目录
如果想在搭建 PulseAudio 的服务器上指定其运行目录、状态目录和模块目录,可以通过修改 PulseAudio 的配置文件来实现。一般情况下所涉及的配置文件和相关选项如下所示: 1、配置文件路径:通常情况下,PulseAudio 的配置文件位于 /…...
粉丝社区网站怎么做/怎么制作网站教程
转载于:https://www.cnblogs.com/QMM2008/p/9628653.html...
杭州网站制作/榆林百度seo
自然界的颜色千变万化,为了给颜色一个量化的衡量标准,就需要建立色彩空间模型来描述各种各样的颜色,由于人对色彩的感知是一个复杂的生理和心理联合作用 的过程,所以在不同的应用领域中为了更好更准确的满足各自的需求,就出现了各种各样的色彩空间模型来量化的描述颜色。我…...
wordpress建m域名网站/seo优化网页
2.1、关键字与保留字 2.1.1、关键字(keyword)的定义和特点 定义:被Java语言赋予了特殊含义,用作专门用途的字符串(单词)特点:关键字种所有字母都是小写官方地址 2.1.2、保留字(…...
嘉定网站建设哪家好/廊坊seo外包
文章目录一、产品经理理解1.1 产品经理定义1.2 产品经理职责范围1.3 产品经理分类1.3.1 产品经理分类---行业1.3.2 产品经理分类---级别1.3.3 产品经理分类---用户群体1.3.4 产品经理分类---产品形态1.3.5 产品经理分类---按工作内容划分一、产品经理理解 1.1 产品经理定义 【…...
设计师网站推荐/我要推广网
使用环境(蓝色粗体字为特别注意内容) 1、软件环境:Keil MDK 5.15 2、硬件环境:STM32F103C8T6最小系统 在项目中打算用基本定时器6作为延时定时器,编译发现报错:main.c(77): error: #20: identifier "RCC_APB1ENR_TIM6EN&qu…...
wordpress hide title plugin/小红书推广
---------------------------------------------------------- 机械设计模型制作是设计前期中至关重要的一步 转载于:https://www.cnblogs.com/zff1933/p/7815511.html...