第 1 章 并发编程的挑战

1.1.3 如何减少上下文切换

减少上下文切换的方法有无锁并发编程、CAS 算法、使用最少线程和使用协程。

  • 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的 ID 按照 Hash 算法取模分段,不同的线程处理不同段的数据。
  • CAS 算法。Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。
  • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

1.1.4 减少上下文切换实战

如何通过 jstack 命令分析线程死锁:
https://www.jianshu.com/p/bb4ba6612392

1.2 避免死锁的几个常见方法

  • 避免一个线程同时获取多个锁。
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用 lock.tryLock(timeout)来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

1.3 资源限制的挑战

什么是资源限制

在进行并发编程时,要考虑资源的限制。硬件资源限制有带宽的上传/下载速度、硬盘读写速度和 CPU 的处理速度。软件资源限制有数据库的连接数和 socket 连接数等。

资源限制引发的问题

并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。

如何解决资源限制的问题

对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那
么就让程序在多机上运行。比如使用 ODPS、Hadoop 或者自己搭建服务器集群,不同的
机器处理不同的数据。可以通过“数据 ID%机器数”,计算得到一个机器编号,然后由对
应编号的机器处理这笔数据。
对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和
Socket 连接复用,或者在调用对方 webservice 接口获取数据时,只建立一个连接。

第 2 章 Java 并发机制的底层实现原理

2.1 volatile 的应用

可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果 volatile 变量修饰符使用恰当的话,它比 synchronized 的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

2.1.1.volatile 的定义与实现原理

Lock 前缀指令会引起处理器缓存回写到内存。
一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

2.1.2 volatile 的使用优化

LinkedTransferQueue, 这个内部类 PaddedAtomicReference 相对于父类 AtomicReference 只做了一件事情,就是将共享变量追加到 64 字节。
在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。Doug lea 使用追加到 64 字节的方式来填满高速缓冲区的缓存行,避免头节点和尾节点加载到同一个缓存行,使头、尾节点在修改时不会互相锁定。

2.2 synchronized 的实现原理与应用

先来看下利用 synchronized 实现同步的基础:Java 中的每一个对象都可以作为锁。
具体表现为以下 3 种形式。

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的 Class 对象。
  • 对于同步方法块,锁是 Synchonized 括号里配置的对象。
Synchonized 在 JVM 里的实现原理:

代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另外一种方式实现的,细
节在 JVM 规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。

monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处 #FF9800异常处,JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之配对。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。

2.2.1 Java 对象头

java 的对象头里面有些什么东西?

synchronized 用的锁是存在 Java 对象头里的 , 如果对象是数组类型,则虚拟机用 3 个字宽(Word)存储对象头,如果对象是非数组类型,则用 2 字宽存储对象头。

Mark work 的作用?

Mark Word 里默认存储对象的 HashCode、分代年龄和锁标记位
他是实现偏向锁的关键

2.2.2 锁的升级与对比

Java SE 1.6 锁的四种状态:
--------------- 无锁状态 -> 偏向锁 -> 轻量级锁 -> 重量级锁
锁标志位: 01 -> 01 -> 00 -> 10
偏向锁位: 0 -> 1

详细介绍锁升级过程:
多线程高并发:synchronized锁升级过程及其实现原理

1.偏向锁

什么是偏向锁?
所谓偏向锁,就是偏向某一个线程的意思,它的目的就是消除数据在无争用的情况下的同步操作,进一步提高运行性能
偏向锁的获得和撤销流程是怎么样的?

2.轻量锁

轻量锁的加锁及解锁过程

2.3 原子操作的实现原理

2.3.2.处理器如何实现原子操作

处理器提供总线锁定 #FF9800和缓存锁定 #FF9800两个机制来保证复杂内存操作的原子性

总线锁

所谓总线锁就是使用处理器提供的一个 LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。

缓存锁

频繁使用的内存会缓存在处理器的 L1、L2 和 L3 高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁,在 Pentium 6 和目前的处理器中可以使用 “缓存锁定” 的方式来实现复杂的原子性。

2.3.3 Java 如何实现原子操作

AtomicInteger.compareAndSet、AtomicBoolean、AtomicInteger、AtomicLong

CAS 实现原子操作的三大问题
ABA 问题
ABA 问题的解决思路是怎么样的?

使用版本号,每次修改了变量则在变量前追加版本号

  • 循环时间长开销大
  • 只能保证一个共享变量的原子操作

第 3 章 Java 内存模型

什么是 JMM?

JMM 就是 Java 内存模型,因为在不同的硬件生产商和操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会有不同的问题,为此,Java 内存模型屏蔽掉各种硬件和操作系统之间的内存访问差异,以实现 Java 程序在不同的平台下都能达到一致的并发效果

Java 内存模型规定所有变量都存储在主内存中,包括实例变量、静态变量,但是不包括局部变量和方法参数,后者是线程私有的。每个线程都有自己的工作内存,线程的工作内存保存了该线程要用到的变量和主内存的副本拷贝,线程对变量的操作都在自己的工作内存中进行。线程不能直接读写主内存中的变量

不同的线程也无法访问对方的工作内存中的变量,线程之间的变量传递均需要主内存来完成。

JMM 定义了什么?

整个 Java 内存模型实际上是围绕着三个特征建立起来的: 原子性、可见性、有序性
原子性
JMM 只能保证基本的原子性,如果要保证一个代码块的原子性,提供了 monitorenter 和 moniterexit 两个字节码指令,也就是 synchronized 关键字。因此在 synchronized 块之间的操作都是原子性的。
可见性
可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。Java 是利用 volatile 关键字来提供可见性的。 当变量被 volatile 修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。而普通变量则不能保证这一点。
除了 volatile 关键字之外,final和 synchronized 也能实现可见性。
synchronized 的原理是,在执行完,进入 unlock 之前,必须将共享变量同步到主内存中。
final 修饰的字段,一旦初始化完成,如果没有对象逸出(指对象为初始化完成就可以被别的线程使用),那么对于其他线程都是可见的。
有序性
在 Java 中,可以使用 synchronized 保证多线程之间操作的有序性。实现原理有些区别:
Volatile 关键字是通过内存屏障达到静止指令重排序,以保证有序性。

synchronized 的原理是,一个线程 lock 之后,必须 unLock 后,其他线程才可以重新 lock,使得被 synchronized 包住的代码在多线程程序中是串行的

https://zhuanlan.zhihu.com/p/258393139

什么是内存屏障?

https://www.jianshu.com/p/2ab5e3d7e510

3.1.1 并发编程模型的两个关键问题

线程之间如何通信及线程之间如何同步?

线程之间的通信机制有两种:共享内存和消息传递。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。

Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

3.1.2 Java 内存模型的抽象结构

在 Java 中,所有实例域、静态域和数组元素都存储在堆内存 #FF9800中,堆内存在线程之间共享。局部变量,方法定义参数和异常处理器参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java 线程之间的通信由 Java 内存模型(本文简称为 JMM)控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见。
线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。

如果线程 A 与线程 B 之间要通信的话,必须要经历下面 2 个步骤。
⚫ 线程 A 把本地内存 A 中更新过的共享变量刷新到主内存中去。
⚫ 线程 B 到主内存中去读取线程 A 之前已更新过的共享变量。

3.1.3 从源代码到指令序列的重排序

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3 种类型。

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排
    语句的执行顺序。
  2. 指令级并行的重排序。现代处理器采用了指令级并行技术(InstructionLevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
    从 Java 源代码到最终实际执行的指令序列,会分别经历下面 3 种重排序,如图 3-3所示。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n3nicp08-1616245944158)(./images/1614229234213.png)]

3.1.4 并发编程模型的分类

3.1.5 happens-before 简介

happens-before 是什么东西?
我们无法枚举所有的场景来规定某个线程修改的变量何时对另一个线程可见。但可以制定一些通用的规则,这就是 happens-before

与程序员密切相关的 happens-before 规则如下。
⚫ 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
⚫ 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
⚫ volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个volatile 域的读。
⚫ 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before
C。

ps: 两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操
作之前执行!

3.2 重排序

什么是重排序?
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

3.2.1 数据依赖性

编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

3.2.2 as-if-serial 语义

什么是 as-if-serial ?
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。as-if-serial 语义把单线程程序保护了起来,遵守 as-if-serial 语义的编译器、runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。

3.2.3 程序顺序规则

在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能提高并行度。编译器和处理器遵从这一目标,从 happens-before 的定义我们可以看出, JMM 同样遵从这一目标。

3.2.4 重排序对多线程的影响

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是 as-ifserial 语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

3.3 顺序一致性

3.3.1 数据竞争与顺序一致性

当程序未正确同步时,就可能会存在数据竞争。Java 内存模型规范对数据竞争的定义如下。
⚫ 在一个线程中写一个变量,
⚫ 在另一个线程读同一个变量,
⚫ 而且写和读没有通过同步来排序。

3.3.2 顺序一致性内存模型

顺序一致性内存模型是什么?
顺序一致性内存模型是一个理论参考模型,JMM 和处理器内存模型在设计时通常会以顺序一致性内存模型为参照。

顺序一致性内存模型有两大特性是什么?
⚫ 一个线程中的所有操作必须按照程序的顺序来执行。
⚫ (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一
致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

3.3.3 同步程序的顺序一致性效果

JMM在具体实现上的基本方针为:在不改变(正确同步的)程序执行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门。

3.3.4 未同步程序的执行特性

为什么JMM不保证对 64 位的 long 型和 double 型变量的写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性 ?
在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这一系列步骤称之为总线事务(Bus Transaction)。总线事务包括读事务(ReadTransaction)和写事务(WriteTransaction)。读事务从内存传送数据到处理器,写事务从处理器传送数据到内存,每个事务会读/写内存中一个或多个物理上连续的字。这里的关键是,总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其他的处理器和 I/O设备执行内存的读/写。

从 JDK5 开始),仅仅只允许把一个 64位 long/double型变量的写操作拆分为两个 32位的写操作来执行,任意的读操作在 JSR-133 中都必须具有原子性(即任意读操作必须要在单个读事务中执行)。

3.4 volatile 的内存语义

3.4.1 volatile 的特性

volatile变量自身具有下列特性。
⚫ 可见性。对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile变量最后的写入。
⚫ 原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++这种复合操作不具有原子性。

3.4.2 volatile 写-读建立的 happens-before 关系

3.4.3 volatile 写-读的内存语义

下面对 volatile写和 volatile 读的内存语义做个总结。
⚫ 线程 A 写一个 volatile 变量,实质上是线程 A 向接下来将要读这个 volatile 变量的某个线程发出了(其对共享变量所做修改的)消息。
⚫ 线程 B 读一个 volatile 变量,实质上是线程 B 接收了之前某个线程发出的(在写这个 volatile 变量之前对共享变量所做修改的)消息。
⚫ 线程 A 写一个 volatile 变量,随后线程 B 读这个 volatile变量,这个过程实质上是线程 A通过主内存向线程 B发送消息。

3.4.4 volatile内存语义的实现

volatile 内存语义是怎么实现的?
JMM针对编译器制定了一些 volatile 重排序规则:
⚫当第二个操作是 volatile写时,不管第一个操作是什么,都不能重排序。这个规
则确保 volatile写之前的操作不会被编译器重排序到 volatile写之后。
⚫ 当第一个操作是 volatile 读时,不管第二个操作是什么,都不能重排序。这个规
则确保 volatile读之后的操作不会被编译器重排序到 volatile读之前。
⚫ 当第一个操作是 volatile 写,第二个操作是 volatile读时,不能重排序。

3.4.5 JSR-133 为什么要增强 volatile 的内存语义

为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133 专家组决定增强 volatile 的内存语义:严格限制编译器和处理器对 volatile 变量与普通变量的重排序,确保 volatile 的写-读和锁的释放-获取具有相同的内存语义。

3.5 锁的内存语义

3.5.1 锁的释放-获取建立的 happens-before关系

3.5.2 锁的释放和获取的内存语义

对比锁释放-获取的内存语义与 volatile写-读的内存语义可以看出:锁释放与 volatile写有相同的内存语义;锁获取与 volatile读有相同的内存语义。下面对锁释放和锁获取的内存语义做个总结。
⚫ 线程 A 释放一个锁,实质上是线程 A 向接下来将要获取这个锁的某个线程发出了(线程 A 对共享变量所做修改的)消息。
⚫ 线程 B 获取一个锁,实质上是线程 B 接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
⚫ 线程 A 释放锁,随后线程 B 获取这个锁,这个过程实质上是线程 A 通过主内存向线程 B 发送消息。

3.5.3 锁内存语义的实现

ReentrantLock 的实现依赖于 Java 同步器框架 AbstractQueuedSynchronizer.
由于 AQS 是用一个整型的 volatile 变量来维护同步状态,所以这个变量实际上 ReentrantLock 通过 volatile 来实现内存语义

ReentrantLock 分为公平锁和非公平锁,我们首先分析公平锁。使用公平锁时,加锁方法 lock()调用轨迹如下。

  1. ReentrantLock:lock()。
  2. FairSync:lock()。
  3. AbstractQueuedSynchronizer:acquire(int arg)。
  4. ReentrantLock:tryAcquire(int acquires)。

加锁方法首先读 volatile变量 state

在使用公平锁时,解锁方法 unlock()调用轨迹如下。

  1. ReentrantLock:unlock()。
  2. AbstractQueuedSynchronizer:release(int arg)。
  3. Sync:tryRelease(int releases)。

根据 volatile的 happens-before 规则,释放锁的线程在写 volatile 变量之前可见的共享变量,在获取锁的线程读取同一个 volatile 变量后将立即变得对获取锁的线程可见。

非公平锁的释放和公平锁完全一样,
所以这里仅仅分析非公平锁的获取。使用非公平锁时,加锁方法 lock()调用轨迹如下。

  1. ReentrantLock:lock()。
  2. NonfairSync:lock()。
  3. AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)。

CAS 如何同时具有 volatile 读和 volatile 写的内存语义?
首先,编译器不会对 volatile 读与其后面的任意内存操作重排序。同样的,编译器也不会对 volatile 写与 volatile 写前面的任意内存操作重排序。组合这两个条件,意味着为了同时实现 volatile读和 volatile 写的内存语义,编译器不能对 CAS与 CAS 前面和后面的任意内存操作重排序。

在 CAS 的源码实现中,比如 X86 处理器,程序会根据处理器的类型来决定是否为其添加 cmpxchg(口诀:村民培训成果) 指令添加 LOCK 前缀,多线程处理器则添加,单处理器则不添加;

这个 LOCK 前缀有什么用处呢?
在奔腾处理器出来之前的处理器中,带有 lock 前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。 但是这会带来昂贵的开销,从 Pentium 4、Intel Xeon及 P6 处理器开始,Intel 使用 缓存锁定(Cache Locking)来保证指令执行的原子性。缓存锁定将大大降低 lock 前缀指令的执行开销。

那么什么是缓存锁定呢?
缓存锁定是某个 CPU 对缓存数据进行更改时,会通知缓存了该数据的 CPU 抛弃缓存的数据或者从内存重新读取。

总结 从本文对 ReentrantLock 的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种方式。
1)利用 volatile 变量的写-读所具有的内存语义。
2)利用 CAS 所附带的 volatile 读和 volatile 写的内存语义。

3.5.4 concurrent 包的实现

由于 Java 的 CAS 同时具有 volatile 读和 volatile 写的内存语义,因此 Java 线程之间的通信现在有了下面 4 种方式。

  1. A 线程写 volatile 变量,随后 B 线程读这个 volatile 变量。
  2. A 线程写 volatile 变量,随后 B 线程用 CAS 更新这个 volatile 变量。
  3. A 线程用 CAS 更新一个 volatile 变量,随后 B 线程用 CAS 更新这个 volatile 变量。
  4. A 线程用 CAS 更新一个 volatile 变量,随后 B 线程读这个 volatile 变量。

concurrent 包的源代码实现是这样的:
首先,声明共享变量为 volatile。
然后,使用 CAS 的原子条件更新来实现线程之间的同步。
同时,配合以 volatile 的读/写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。

3.6 域的内存意义

3.6.1 final 域的重排序规则

Java 并发编程艺术 读书笔记相关推荐

  1. Java并发编程艺术----读书笔记(二)

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/a724888/article/details/64214595  java并发编程艺术2 jav ...

  2. JAVA并发编程艺术读书笔记(1,2章节)

    第一章 并发编程的挑战 为什么要使用并发编程? 主要是为了更有效地利用资源.即使是单核的CPU也可以多线程执行程序,多线程实际上是CPU分配时间片给各个线程,因为时间片非常短,所以看起来就像在同事执行 ...

  3. Java并发编程艺术读书笔记

    1.多线程在CPU切换过程中,由于需要保存线程之前状态和加载新线程状态,成为上下文切换,上下文切换会造成消耗系统内存.所以,可合理控制线程数量. 如何控制: (1)使用ps -ef|grep appn ...

  4. Java并发编程艺术阅读笔记(一)

    Java并发编程艺术阅读笔记(一) 1.什么是上下文切换 CPU通过时间片分配算法循环执行任务,执行完一个任务的时间片后就会切换到下一个任务.但是在切换任务之前会保存上一个任务的状态,在切换回该任务, ...

  5. Java并发编程艺术学习笔记(五)

    Java并发编程艺术学习笔记(五) Java并发容器和框架 Java为开发者也提供了许多开发容器和框架,可以从每节的原理分析来学习其中精妙的并发程序. 一.ConcurrentHashMap的实现原理 ...

  6. java并发编程实践 读书笔记_Java - 并发编程实践(读书笔记)

    [注] 同步机制保证:1)原子性 2)内存可见性: Volatile变量只能保证:1)可见性: - 恰当的同步,同步的弱形式,确保对一个变量的更新以可预见的方式告知其他线程. [注] 用锁来协调访问变 ...

  7. Java并发编程实战读书笔记

    Java并发编程 标签(空格分隔): 并发 多线程 基础 线程 在执行过程中,能够执行程序代码的一个执行单元,在Java语言中,线程有四种状态:运行,就绪,挂起,结束. 并发特性 原子性 一个操作不会 ...

  8. Java并发编程实战读书笔记三

    第七章 取消和关闭 Java没有提供任何机制来安全的终止线程,虽然 Thread.stop 和 suspend 等方法提供了这样的机制,但由于存在着一些严重的陷,因此应该避免使用 7.1任务取消 7. ...

  9. Java并发编程实战读书笔记(一)——线程安全性、对象共享

    一.线程安全性 一个对象是否需要是线程安全的,取决于它是否被多个线程访问. 当多个线程访问,并且其中有一个执行写入时,必须采用同步机制,Java中主要的同步关键字是 synchronized 独占加锁 ...

最新文章

  1. 转载 - 10个基于jQuery实现的漂亮网站赏析
  2. 新手探索NLP(九)——文本摘要
  3. postman怎么导出测试用例_postman---postman文件夹介绍以及批量执行用例
  4. Swin Transformer升级版来了!30亿参数,刷榜多项视觉任务
  5. 遇到这样的程序员,你怎么办?
  6. Perl中的替换(七)
  7. Java的数据库编程之入门案例
  8. 常用 Git 命令清单(阮一峰)
  9. (WPF)XAML 过程式代码
  10. 要注意了!这样使用MyBatis框架,被攻击了!
  11. java cropper_cropper 使用总结
  12. 大神级ppt作品欣赏_中班美术课件星空PPT课件教案图片音乐
  13. iGoogle自定义
  14. 基于SimpItk的下肢全景X光图像拼接
  15. 实现一个行内三个div等分_一个div,包含三个小的div,平均分布的样式
  16. IDEA的使用大全(快捷键、TomCat、Maven......)
  17. 用友NC系统考勤机自动同步方案
  18. 对角矩阵np.diag()
  19. python异步socket接收_Python简易聊天工具-基于异步Socket通信
  20. 温伯格:量子力学的困境

热门文章

  1. 网易杭研 java 校招_09网易杭研校园招聘面试题
  2. uni-app 188修复弹框问题
  3. 10M独享带宽,能承受多少人下载文件?
  4. vue集成Luckyexcel实现在线编辑Excel,可自行导入,也可从服务器端获取
  5. leetcode剑指offe刷题-第一题-用两个栈实现队列
  6. python查看bit_python 实现12bit灰度图像映射到8bit显示的方法
  7. ECC RDIMM 服务器内存条简介
  8. nes游戏开发_NES Classic运行Linux,新的0 AD alpha,以及更多游戏新闻
  9. 模拟器和平精英连接服务器没有响应,和平精英模拟器卡顿解决方法一览
  10. Qt常用的模板函数说明