当前位置: 首页 > news >正文

synchronized关键字

synchronized关键字


锁相关知识

​ 多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:对象、变量、文件等。

  • 共享:资源可以由多个线程同时访问
  • 可变:资源可以在其生命周期内被修改引出的问题

​ 由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问!

互斥的两种方案

​ 最容易想到的在多线程编程下,对资源的保护那就是采用互斥的手段。 在操作系统种,对于进程互斥有两种实现方案:信号量与管程。事实上,它们只是两种方案,并不是实现。你完全可以用信号量去实现管程,也可以用管称去实现信号量。

管程

​ 管程也是一种互斥同步的解决方案,它其实就是将共享变量于对共享变量的操作封装起来,外部只能通过管程暴露的方法对共享变量进行操作。总结一下就是:管程指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。

互斥:
管称是互斥进入的
同步:
管程里还引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。在管程中通过设置条件变量及等待唤醒操作解决同步问题,让一个线程在不满足条件变量时进行等待,释放CPU使用权,其它线程执行时发现条件满足,再对它进行唤醒。

举个栗子,管程现在保护资源A,线程1来了发现条件不满足,于是执行到了一半sleep,让出CPU使用权,然后线程2执行了一半,发现条件满足了,对线程1进行唤醒,这时就有三种方案:

  • 线程2等待线程1执行完再继续执行

  • 线程1等待线程2执行完再执行

  • 规定唤醒为管程中最后一个可执行的操作

以上三种方案就是管程的三种模型:Horae、Mesa、Hansen。Java语言中的管程使用的是第二种方案:

在这里插入图片描述

在字节码指令中,管程是通过monitor实现的。Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。

在这里插入图片描述

信号量

信号量,可以理解为有几张通行证的思想。简单的来说,信号量维护一个计数器和等待队列,这个信号量还会有三个方法:初始化、P和V,P和V分别是荷兰语的test(proberen尝试)和increment(verhogen增量),P方法和V方法可以理解为尝试获取资源与归还资源。

当一个线程请求访问受到保护的临界区,这个线程首先要去尝试获取资源。怎么尝试呢,信号量就会先把计数器-1,然后查看当前计数器的值是否>0,如果>=0,就表明你可以获取资源;如果<0,就表示资源已经被人用完了,不好意思你要去等待队列等一等了。线程执行完业务逻辑,要归还资源,这时候信号量就会把计数器+1,如果当前计数器的值<=0,就表示等待队列还有人在等候,那么它就会去等待队列里唤醒一个线程,让它去重新尝试获取资源。

在这里插入图片描述


Java中的锁模型

如何解决线程并发安全问题?

​ 实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临 界资源,也称作同步互斥访问。

​ Java 中,提供了两种方式来实现同步互斥访问:synchronized 和 Lock,同步器的本质就是加锁

​ 加锁目的:

​ 序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)

​ 不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的 私有栈中,因此不具有共享性,不会导致线程安全问题。

简易锁模型

在这里插入图片描述

​ 我们把一段需要互斥执行的代码称为临界区。线程在进入临界区之前,首先尝试加锁 lock(),如果成功,则进入临界区,此时我们称这个线程持有锁;否则呢就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁 unlock()。

改进后的锁模型

​ 我们知道在现实世界里,锁和锁要保护的资源是有对应关系的,比如你用你家的锁保护你家的东西,我用我家的锁保护我家的东西。在并发编程世界里,锁和资源也应该有这个关系,但这个关系在我们上面的模型中是没有体现的,所以我们需要完善一下我们的模型。

在这里插入图片描述

首先,我们要把临界区要保护的资源标注出来,如图中临界区里增加了一个元素:受保护的资源 R;其次,我们要保护资源 R 就得为它创建一把锁 LR;最后,针对这把锁 LR,我们还需在进出临界区时添上加锁操作和解锁操作。另外,在锁 LR 和受保护资源之间,我特地用一条线做了关联,这个关联关系非常重要。很多并发 Bug 的出现都是因为把它忽略了,然后就出现了类似锁自家门来保护他家资产的事情,这样的 Bug 非常不好诊断,因为潜意识里我们认为已经正确加锁了。


synchronized的实现

synchronized的三种加锁方法

  • synchronized 对实例方法加锁,锁是当前实例对象

    方法的同步在字节码中是通过方法的 ACC_SYNCHRONIZED 标志来实现的: 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取 monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor对象,所以归根究底,还是monitor对象的争夺。

  • synchronized 应用在类方法时,锁是是当前类对象。

  • synchronized 应用在同步块上时,锁是括号里面的对象

    经过编译后,会在同步块的前后分别加上monitorenter 和 monitorexit两个字节码指令。这两个字节码都需要一个reference类型的参数来指明要锁定的对象。

monitor:

​ 任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和 MonitorExit指令来实现。

monitorenter:

​ 每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行 monitorenter指令时尝试获取monitor的所有权,过程如下:

  • a. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor 的所有者;
  • b. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
  • c. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝 试获取monitor的所有权;

monitorexit:

  • 执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减 1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去 获取这个 monitor 的所有权。

​ monitorexit,指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁; 通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则 会抛出java.lang.IllegalMonitorStateException的异常的原因。

什么是monitor?

​ 可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的 是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的。

monitorenter的源码流程

​ 再深入到源码来说,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于 HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):

ObjectMonitor() {
2 _header = NULL;
3 _count = 0; // 记录个数
4 _waiters = 0,
5 _recursions = 0;
6 _object = NULL;
7 _owner = NULL;
8 _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet
9 _WaitSetLock = 0 ;
10 _Responsible = NULL ;
11 _succ = NULL ;
12 _cxq = NULL ;
13 FreeNext = NULL ;
14 _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
15 _SpinFreq = 0 ;
16 _SpinClock = 0 ;
17 OwnerIsThread = 0 ;
18 }

可以看到,ObjectMonitor实际上有两个队列waitSet(处于wait状态的线程)和entryList(处于等待锁 block 状态的线程)。

  1. 当多个线程进入同步代码块时,首先进入entryList
  2. 有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
  3. 如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
  4. 如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null

保证内存可见性

​ 从内存语义来说,加锁的过程会清除工作内存中的共享变量,再从主内存读取,而释放锁的过程则是将工作内存中的共享变量写回主内存。


Java中为什么任意对象可以作为锁

Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。

​ 监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。 那么有个问题来了,我们知道synchronized加锁加在对象上,对象是如何记录锁状态的呢?答案是锁状态是被记录在每个对象的对象头(Mark Word)中。
在这里插入图片描述

对象在内存中布局

​ 我们常用的Hotspot虚拟机中,对象在内存中布局实际包含3个部分:

  1. 对象头
  2. 实例数据
  3. 对齐填充

而对象头包含两部分内容,Mark Word中的内容会随着锁标志位而发生变化,所以只说存储结构就好了。

  1. 对象头:
  • Mark Word(标记字段):对象自身运行时所需的数据,也被称为Mark Word,也就是用于轻量级锁和偏向锁的关键点。具体的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程ID、偏向锁时间戳。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

  • 存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。

  1. 实例数据
  • 这部分主要是存放类的数据信息,父类的信息。
  1. 对其填充
  • 由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。

    Tip:不知道大家有没有被问过一个空对象占多少个字节?就是8个字节,是因为对齐填充的关系哈,不到8个字节对其填充会帮我们自动补齐。

在这里插入图片描述


重量级锁与锁升级

重量级锁

​ synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而monitorenter与monitorexit两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度。由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种切换是很耗资源的。

用户态和内核态

Linux系统的体系结构大家大学应该都接触过了,分为用户空间(应用程序的活动空间)和内核。

我们所有的程序都在用户空间运行,进入用户运行状态也就是(用户态),但是很多操作可能涉及内核运行,比我I/O,我们就会进入内核运行状态(内核态)。

在这里插入图片描述

这个过程是很复杂的,也涉及很多值的传递,我简单概括下流程:

  1. 用户态把一些数据放到寄存器,或者创建对应的堆栈,表明需要操作系统提供的服务。
  2. 用户态执行系统调用(系统调用是操作系统的最小功能单位)。
  3. CPU切换到内核态,跳到对应的内存指定的位置执行指令。
  4. 系统调用处理器去读取我们先前放到内存的数据参数,执行程序的请求。
  5. 调用完成,操作系统重置CPU为用户态返回结果,并执行下个指令。

所以大家一直说,1.6之前是重量级锁,没错,但是他重量的本质,是ObjectMonitor调用的过程,以及Linux内核的复杂运行机制决定的,大量的系统资源消耗,所以效率才低。

还有两种情况也会发生内核态和用户态的切换:异常事件和外围设备的中断。

JDK1.6的锁升级

​ JVM内置锁在1.5 之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、 偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,内置锁的并发性能已经基本与 Lock持平。

​ 从JDK 1.6 中默认是开启偏向锁和轻量级锁 的,可以通过-XX:-UseBiasedLocking来禁用偏向锁。

​ 针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高。

锁是否会降级?

​ 有可能会,当JVM进入到安全点safePoint时,会检查是否有闲置的monitor,然后试图降级。

锁消除

​ 消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并 且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。锁消除的依据是逃逸分析的数据支持。 锁消除,前提是java必须运行在server模式(server模式会比client模式作更多的优化),同时必须开启逃逸分析。

:-XX:+DoEscapeAnalysis 开启逃逸分析

-XX:+EliminateLocks 表示开启锁消除。

使用逃逸分析,编译器可以对代码做如下优化:

  • 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
  • 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分 配。
  • 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存, 而是存储在CPU寄存器中。
锁粗化

​ 锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。

偏向锁

​ 偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞 争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心 思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时, 无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

​ 所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的 是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。

轻量级锁

​ 倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时 Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞 争”,注意这是经验数据。

​ 轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁

​ 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情 况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要 从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或 100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

自适应锁

​ 自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得锁,那么虚拟机就认为这次也有可能成功,进而会允许自旋等待持续更长的时间,比如100个循环。

简单总结

​ 简单点说,偏向锁就是通过对象头的偏向线程ID来对比,甚至都不需要CAS了,而轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现,重量级锁则是除了拥有锁的线程其他全部阻塞。

在这里插入图片描述


synchronized的作用

有序性

​ synchronizedkey保证有序性,synchronized 保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

可见性

​ synchronized可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。

原子性

​ synchronized确保同一时间只有一个线程能拿到锁,能够进入代码块,所以也就保证了原子性。

可重入性

​ synchronized锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执行完对应的代码块之后,计数器就会-1,直到计数器清零,就释放锁了。所以synchronized是可重入锁,可以避免一些死锁的情况,也可以让我们更好封装我们的代码。

不可中断性

​ 不可中断就是指,一个线程获取锁之后,另外一个线程处于阻塞或者等待状态,前一个不释放,后一个也一直会阻塞或者等待,不可以被中断。

​ 值得一提的是,Lock的tryLock方法是可以被中断的。


对比

synchronized与volatile

​ 其实这二者的使用场景并不一样,volatile的能力是保证可见性与防止指令重排,可以用来实现乐观锁,但并不能保证原子性;而synchronized则是典型的悲观锁。

volatile 能为我们提供如下特性:

  • 确保实例变量和类变量的可见性;
  • 确保 volatile 变量前后代码的重排序以 volatile 变量为界限。

volatile 的局限性:

  • volatile 的可见性和有序性只能作用于单一变量;
  • volatile 不能确保原子性;
  • volatile 不能作用于方法,只能修饰实例或者类变量

​ volatile 的以上特点,决定了它的使用场景是有限的,并不能完全取代 synchronized 同步方式。一般使用 volatile 的 场景是代码中通过某个状态值 flag 做判断,flag 可能被多个线程修改。如果不使用 volatile 修饰,那么 flag 不能保 证最新的值被每个线程读取到。而在使用 volatile 修饰后,任何线程对 flag 的修改,都立刻对其它线程可见。此外 其它线程看到 flag 变化时,所有对 flag 操作前的代码都已生效,这是 volatile 的有序性确保的。

​ 正是由于 volatile 有如上局限性,所以我们只能在上述场景或者其它适合的场景使用 volatile。反推 volatile 不适用 的场景如下:

  1. 一个变量或者多个变量的原子性操作;

  2. 不以 volatile 变量操作作为分界线的有序性保证。

volatile 无法解决的问题最终还得通过 sychronized 或者其它加锁方式来确保同步。

synchronized与lock

  • synchronized是关键字,是JVM层面的底层啥都帮我们做了,而Lock是一个接口,是JDK层面的有丰富的API。
  • synchronized会自动释放锁,而Lock必须手动释放锁。
  • synchronized是不可中断的,Lock可以中断也可以不中断。
  • 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  • synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  • Lock可以使用读锁提高多线程读效率。
    两者一个是JDK层面的一个是JVM层面的,我觉得最大的区别其实在,我们是否需要丰富的api,还有一个我们的场景。

synchronized与ReentrantLock

​ 接下来来看Lock一个重要的实现类,跟synchronized都是可重入锁的ReentrantLock。ReentrantLock其实具备synchronized所有特性,可以完全取代synchronized。不过ReentrantLock设计之初并不是为了替换掉synchronized,而是当synchronized不能满足需求时,才考虑使用ReentrantLock。看下二者对比:

  • 从性能角度上,JDK1.5,随着线程的增加,内置锁的性能急剧下降,而 ReentrantLock 的下降并不明显。线程增加到一定数量后,ReentrantLock 性能会达到内置锁的 4-5 倍。而在JDK1.6中,两者差距并不明显,ReentrantLock 略占一点点优势。

  • 从特性角度讲,ReentrantLock 提供了公平和非公平锁、可定时、可轮询和可中断的锁获取方式、非块状锁结构,而 synchronized 不支持

  • 从公平性的选择,ReentrantLock 支持公平锁,而synchronized 不支持公平锁。不过公平性的选择,意味着需要放弃一部分性能。大多数情况下,公平锁的性能都要低于非公平锁。这是因为挂起和恢复线程都有很大开销。选择公平锁时,从释放锁到等待队列中最前面线程被唤醒能够去 tryLock,中间有很大的时间延迟,那么这就造成了公平锁的性能会更差。

    如果线程获取锁到释放锁之间的程序执行时间较长,那么公平锁的性能不会那么差。因为不会有很多的线程唤醒操作,也就是说不会有过多的时间间隙被浪费点。那么公平锁有能带来更好的公平性,所以此时我们优先选择公平锁。

    如果线程持有锁执行逻辑的时间很短,而多线程并发量又很大。这造成了获取和释放锁频繁发生,从而大量时间浪费在从锁被释放到排队线程被唤醒工作的过程上。因此,此时我们更好的选择是非公平锁。

  • 从使用的角度讲,synchronized 使用简单,只需要把同步代码放入 synchronized 代码块中即可,程序执行完同步代码块自动解锁。而 Lock 需要显式获取锁,然后需要配合 try 和 finally 来使用,尤其注意一定要在 finally 中释放锁。

相关文章:

  • 分布式锁的几种实现方式
  • 延时队列的几种实现方式(只有原理,并没有源码)
  • DDD整理(概念篇)
  • DDD的分层架构设计
  • 面试记录之synchronized的惨败经历
  • 面试复盘整理
  • Go语言基础_数据类型、基本语法篇
  • Go学习笔记_环境搭建
  • Markdown学习
  • Markdown下载客户端
  • JDK,JRE,JVM三者的区别
  • 2020-12-01
  • 2020-12-02
  • 比较三个数字,求出最大值
  • Scanner限制次数猜数字
  • CSS相对定位
  • eclipse(luna)创建web工程
  • Java程序员幽默爆笑锦集
  • Mac 鼠须管 Rime 输入法 安装五笔输入法 教程
  • MySQL Access denied for user 'root'@'localhost' 解决方法
  • PAT A1120
  • react 代码优化(一) ——事件处理
  • Spring技术内幕笔记(2):Spring MVC 与 Web
  • UEditor初始化失败(实例已存在,但视图未渲染出来,单页化)
  • webpack+react项目初体验——记录我的webpack环境配置
  • Zsh 开发指南(第十四篇 文件读写)
  • 官方解决所有 npm 全局安装权限问题
  • 计算机在识别图像时“看到”了什么?
  • 让你成为前端,后端或全栈开发程序员的进阶指南,一门学到老的技术
  • 如何将自己的网站分享到QQ空间,微信,微博等等
  • 使用docker-compose进行多节点部署
  • 使用Maven插件构建SpringBoot项目,生成Docker镜像push到DockerHub上
  • 用mpvue开发微信小程序
  • 在Docker Swarm上部署Apache Storm:第1部分
  • ​人工智能书单(数学基础篇)
  • ​如何防止网络攻击?
  • ​业务双活的数据切换思路设计(下)
  • #1014 : Trie树
  • #NOIP 2014# day.1 生活大爆炸版 石头剪刀布
  • #pragma pack(1)
  • $().each和$.each的区别
  • (C语言)fread与fwrite详解
  • (C语言)深入理解指针2之野指针与传值与传址与assert断言
  • (Matlab)遗传算法优化的BP神经网络实现回归预测
  • (八)Flask之app.route装饰器函数的参数
  • (三)mysql_MYSQL(三)
  • (学习日记)2024.04.04:UCOSIII第三十二节:计数信号量实验
  • (原)记一次CentOS7 磁盘空间大小异常的解决过程
  • (转)程序员技术练级攻略
  • (转)总结使用Unity 3D优化游戏运行性能的经验
  • .apk文件,IIS不支持下载解决
  • .net core 实现redis分片_基于 Redis 的分布式任务调度框架 earth-frost
  • @Autowired标签与 @Resource标签 的区别
  • @ModelAttribute使用详解
  • [20190416]完善shared latch测试脚本2.txt