上一篇花了点时间将同步器看了一下,心中对锁的概念更加明确了一点,知道我们所使用到的锁是怎么样获取同步状态的,我们也写了一个自定义同步组件Mutex,讲到了它其实就是一个简版的ReentrantLock,本篇文章我们就来看看ReentrantLock底层是怎么样的!

目录结构:

  • ReentrantLock
  • 公平锁与非公平锁
  • ReentrantReadWriteLock

ReentrantLock

ReentrantLock我们叫做可重入锁,也就是我们可以重复进入的意思,也就是表示,一个线程可以对指定资源进行重复加锁,并且还能够选择公平锁和非公平锁。

公平锁:先请求获取加锁的线程先被满足

非公平锁:XXXX

可重入锁可以对一个资源重复加锁,同时,在释放锁时也要进行多次release,所以不难想到,ReentrantLock只要维持一个值,用来控制这个资源加锁的次数就Ok了,初始化为零,当加锁时对这个值+1,release时-1。

public class ReentrantLock implements Lock, java.io.Serializable{
.
.
.
}

这个是ReentrantLock的定义,上一节我就无耻的把它贴出来了→.→

接下来我们来看看ReentrantLock是如何对资源进行加锁的!

首先肯定要定义一个继承自AbstractQueuedSynchronizer的内部类:

abstract static class Sync extends AbstractQueuedSynchronizer {
.
.
. }

构造函数的话,因为它可以选择公平和非公平锁,所以是否公平就是由构造函数决定的:

 public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

FairSync()和NonfairSync()都是构造函数,当然都是内部类啦,他们可以分别创建公平锁和非公平锁。

我自己粗略的看了一下源码,将ReentrantLock的大概结构画了一下:

刚才我们也说了,根据参数ReentrantLock会根据我们的需要创建对应的锁,当没有参数的时候我们来看看它默认的是什么?

 /**
* Creates an instance of {@code ReentrantLock}.
* 创建一个实例
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();//sync是Reentrant内的Sync类型的成员变量
}

从代码中我们可以知道,当我们使用无参构造的时候ReentrantLock会为我们创建一个非公平锁(事实上,大大多数情况下非公平锁的效率会更好);

那么我们先来看一下非公平锁是怎么获得同步状态的:

//非公平锁是继承Sync是毋庸置疑的
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());
//当被占用后开始调用这个方法了
//(当然后面的方法还会对这占用同步状态的线程是否是当前线程,
//因为这个锁是独占的,仅仅允许一个线程多次加锁)
else
acquire(1);
} protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}

(一)

上面的compareAndSetState我们已经不陌生了,上一篇文章我们在写叫Mutex的自定义同步组件的时候看到过,它的功能是:判断当前的state是否为0,如果为0那么获取锁,如果不为0,那么将state设置为1,:

//这个方法是同步器提供的方法,参数是expect和update,它继续调用了unsafe内的方法(内部是通过CAS来实现原子操作),
//将this(当前状态)和expect比较,如果相同返回true,如果不同则将state设置为update
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

(二)

接着便是setExclusiveOwnerThread(Thread thread),这个方法比较陌生,我们继续跟源码,

//这个方法是在AbstractOwnableSynchronizer内,从名字估计大家应该知道是干嘛的了,
//我们看看这个类的注释
/**
* A synchronizer that may be exclusively owned by a thread. This
* class provides a basis for creating locks and related synchronizers
* that may entail a notion of ownership. The
* {@code AbstractOwnableSynchronizer} class itself does not manage or
* use this information. However, subclasses and tools may use
* appropriately maintained values to help control and monitor access
* and provide diagnostics.
*博主英语不太好,勉强知道意思是:这个类是一个线程独占的同步器,
*这个类提供创建锁和相关同步器的基础(伴随着所有权的概念),
*这个类本身不控制和使用信息,子类和和工具可以适当使用维持的值去帮助控制和监视访问,提 * 供诊断(额,好几个单词不会0.0)
*/
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}

所以我们可以看出这个是干嘛的了:

//同步器继承自AbstractOwnableSynchronizer,Sync继承自同步器,
//NonfairSync 继承自Sync,所以这下该该明白了!
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
}

NonfairSync 间接继承AbstractOwnableSynchronized,所以他可以调用它使用的方法,将exclusiveOwnerThread 变量设置为当前线程

//这个变量是AbstractOwnableSynchronized的成员变量
private transient Thread exclusiveOwnerThread;

所以为我们知道这个设置是用来帮助控制与监视用的!


(三)

接下来我们继续向下看,如果无法第一次无法获取同步状态,调用acquire方法,之前我们也见过这个方法,这个方法尝试获取同步状态,否则将当前线程组装成一个节点放入同步队列中,但是在这里,ReentrantLock对它进行了重写:

 public final void acquire(int arg) {
//其他的都没变,我们继续跟tryAcquire
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

这里的tryAcquire不再是 throw new UnsupportedOperationException();

而是:

 protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}

所以最终调用nonfairTryAcquire,至此我们真正的主角才上场!(鼓掌!!)

   final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//再次判断当前状态
if (c == 0) {
//下面这行代码我就不用说了吧(参考上面的lock方法)
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前的线程为我们之前存的线程(就是前面已经获取同步状态的线程)
else if (current == getExclusiveOwnerThread()) {
//c为当前的状态,acquires为参数为1
//当前状态最小值为0,表示当前无线程获取同步状态
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//将当前状态设置为nextc,也就是在原来c的基础上加1了
setState(nextc);
return true;
}
return false;
}

以上就是非公平锁的加锁方法,如果彻底明白了加锁的方法,那么release方法也不成问题了,接着看释放方法:


我们都知道ReentrantLock是同过unlock来释放锁的:

public void unlock() {
sync.release(1);
}

继续往下面跟release方法(记得参数是1):

//tryRelease方法就是不同的地方下面我们说
public final boolean release(int arg) {
//这个tryRelease方法被我们的ReentrantLock重写过,
//不再是抛出UnsupportedOperationException错误了
//下面我们详解tryRelease
if (tryRelease(arg)) {
//获取要释放的节点
Node h = head;
//如果节点不为空,且waitStatus不为0(0为初始状态)
if (h != null && h.waitStatus != 0)
//这个方法使用LockSupport来唤醒下一个节点
unparkSuccessor(h);
return true;
}
return false;
}

如果tryRelease释放成功(即不同步状态未0),那么获取要释放的节点,并判断当前节点是否存在,这个等待状态不能为0,只有这样才能进行下一步的唤醒操作!

记得在上一篇文章中,我就提到了release方法,但是没有详细说明,只是说unparkSuccessor方法是用来唤醒下一个节点的,这次来看看unparkSuccessor方法:

 private void unparkSuccessor(Node node) {
//获取当前节点的waitStatus
int ws = node.waitStatus;
//如果ws小于零即表示当前节点满足可以通知下一个节点
if (ws < 0)
//CAS操作,将waitStatus设置为0(node的waitStatus一定是相等的)
compareAndSetWaitStatus(node, ws, 0);
//获取释放节点的下一个节点
Node s = node.next;
//如果s节点为null或者waitStatus > 0(即不是初始状态)
//那么s是空的
if (s == null || s.waitStatus > 0) {
s = null;
//以下为同步队列的变动
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//如果s不为空,那么使用LockSupport来唤醒s
//LockSupport是用来唤醒阻塞中断的线程的,后面一章我们详细来讲解
if (s != null)
LockSupport.unpark(s.thread);
}

以上就完成了锁的释放和唤醒下一个节点,貌似我们少说了什么,对就是tryRelease

//boolean类型,如果释放成功则返回true,否则返回false
protected final boolean tryRelease(int releases) {
//我们之前说过了这个参数为1
//获取当前同步状态并减一
int c = getState() - releases;
//判断当前线程是否为独占线程,如果不是抛出错误
//(错误释放错误,都不是你加锁的,你来凑什么热闹)
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果c为0,即getState为1,那么free为true,同步状态未空闲
if (c == 0) {
free = true;
//将独占线程成员变量设置为空
setExclusiveOwnerThread(null);
}
//将同步状态设置为0
setState(c);
这个时候返回free
return free;
}

以上便是ReentrantLock的解锁代码,因为是可重入的,所以当同步状态部位0的时候(大于零),我们可以多次调用unlock方法来调用释放同步状态!

以上就是非公平锁的的基本操作,接下来看看非公平锁是怎么样的:

static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L; final void lock() {
acquire(1);
} /**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
//获得当前线程
final Thread current = Thread.currentThread();
//获得同步状态
int c = getState();
//如果同步状态为0(同步状态空闲)
if (c == 0) {
//如果空闲,那么判断当前线程是否有前驱(意思是让当前线程不能插队)
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}

非公平锁里面同样头lock方法并重写了tryAcquire方法,lock方法里面调用acquire方法,acquire方法和之前的一样:

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

所以唯一不同的还是这个tryAcquire方法:这个方法和非公平锁不同在哪里?就是这个方法里多了一个判断hasQueuedPredecessors,这个方法判断同步队列中是否有前驱节点,如果这个方法返回true,表示有前驱节点,有线程比当前线程更早的获取锁,所以要前驱线程获取锁后释放才能继续获取锁,其他的代码都和上面的相同,我们不必纠结!


ReentrantReadWriteLock

在前面我写了一篇文章是关于读写锁的应用,主要的内容是:读读共享,读写互斥,写写互斥,读写锁维护了两把锁,一把读锁和一把写锁,通过分离读锁和写锁来提高并发性,因为在大多数并发情况下都是读数据,所以这样可以提升并发处理的效率。

我们先看下简单的结构图:

//ReentrantReadWriteLock 实现ReadWriteLock接口
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
.
.
.
}

ReadWriteLock的接口其实很简单,只是规范了两个方法:

public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*/
Lock readLock();
}

看看ReentrantReadWriteLock有哪些字段:

//这个是读锁ReadLock,这个类是ReentrantReadWriteLock的内部类
private final ReentrantReadWriteLock.ReadLock readerLock; 这个是读锁WriteLock,这个类是ReentrantReadWriteLock的内部类
private final ReentrantReadWriteLock.WriteLock writerLock; final Sync sync;

构造方法:

//这个构造函数通过this调用下面的这个构造函数
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
//读写锁同样有公平锁和非公平锁
//通过无参构造和当前的构造方法我们可以看出默认的是new一个非公平锁
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}

实现接口的方法:

//分别返回读锁和写锁的方法
public ReentrantReadWriteLock.WriteLock writeLock() {
return writerLock;
}
public ReentrantReadWriteLock.ReadLock readLock() {
return readerLock;
}

我们上面看到了final类型的 sync,当然毋庸置疑,ReentrantReadWriteLock里面也有继承同步器的内部类:

//这里的内部类是抽象类型的哟
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L; //下面定义了四个常亮
//这个常亮表示共享位移为16
static final int SHARED_SHIFT = 16;
//共享单元
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//最大数目
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//独占掩码
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//共享数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
//独占数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
} //为当前类的内部类,继承ThreadLocal,以调用线程为key,
//HoldCounter为value的对象
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
} //一系列的字段 //当前线程读锁的持有量
private transient ThreadLocalHoldCounter readHolds; //最后一个线程成功获取读锁的持有量
private transient HoldCounter cachedHoldCounter; //第一个获取读锁的线程
private transient Thread firstReader = null; //第一个获取读锁线程的锁持有量
private transient int firstReaderHoldCount; //无参构造
Sync() {
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
} abstract boolean readerShouldBlock(); abstract boolean writerShouldBlock(); //这个和ReentrantLock中的相同(因为继承了同步器,所以这些方法方法要实现)
//以下如果我们看见和之前和同步器内的方法名相同,那么你不用怀疑,它就是一样的
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
} protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
//这个是读写锁特有的方法:释放共享锁(继承同步器后自定义的方法)
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
} private IllegalMonitorStateException unmatchedUnlockException() {
return new IllegalMonitorStateException(
"attempt to unlock read lock, not locked by current thread");
}
//这个是读写锁特有的方法:获得共享锁(继承同步器后自定义的方法)
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
} .
.
.
//这里省略了一些代码

上面我代码做了简单的注释,也pass了一些方法,为了代码的完整性所以将Sync所有的的代码都贴了出来,整体上看着有点乱,我们理一下,首先ReentrantReadWriteLock和ReentrantLock结构基本相同,都有公平锁和非公平锁,实现方式是一样的,不同的地方是读写锁内部维护了两把锁,读锁和写锁,ReentrantLock同步状态表示被同一个线程获取的次数,ReentrantReadWriteLock同样需要有一个同步状态值来表示当前锁(读写锁),被同一个线程获取的次数,在读写锁中如果在一个整型变量中维护多种状态,就需要“按位切割使用”这个变量,它的高16位用来记录读状态,低16位用来记录写状态:

我们来看看具体是如何操作的:

写锁:

当重进入时候仅需加1,当释放的时候减1,获取当前状态的时候进行与(&)操作(0000000000000000 1111111111111111)将高的16位置为0即可获得当前的写状态。

读锁:

当重进入的时候仅需加1<<16,释放的时候减去1>>>16(无符号补零右移16位),获取当前状态时整体右移16位(左边补零)。

写所得获取与释放,在读写锁中,同样是重写了同步器的tryAcquire方法,和ReentrantLock不同的是在获取之前需要判定一下读锁是否存在,如果读锁存在那么获取失败!写锁的释放操作和ReentrantLock基本一致,无其他特别。

读锁的获取与释放,在获取读锁的时候判断当前读锁容量是否充足(因为存储16位,所以这个读锁会有一个最大值),如果充足还要判断当前状态是否大于零,如果大于零,那么无非三种情况,

①读锁状态位为0,写锁状态位不为→当前有写锁占用读锁进入等待(01)

②读锁状态位不为0,写锁状态位0→当前读锁占用读锁可以获得锁(10)

③读状态位和写状态位都为0→读锁可以获取(00)

以上的线程安全靠CAS进行保证!

读锁的每次释放是线程安全的,每次状态位减1<<16.

读写锁还有一个特性就是锁降级,指的是将写锁降级为读锁,是指当前线程获取的是写锁,先获取读锁然后释放写锁,保留读锁。

本章到此结束!还有很多分析不到的地方,望指正,不胜感激!

2018 3.29 10:34

从源码来看ReentrantLock和ReentrantReadWriteLock的更多相关文章

  1. Java读源码之ReentrantLock(2)

    前言 本文是 ReentrantLock 源码的第二篇,第一篇主要介绍了公平锁非公平锁正常的加锁解锁流程,虽然表达能力有限不知道有没有讲清楚,本着不太监的原则,本文填补下第一篇中挖的坑. Java读源 ...

  2. 多线程与高并发(五)—— 源码解析 ReentrantLock

    一.前言 ReentrantLock 是基于 AQS 实现的同步框架,关于 AQS 的源码在 这篇文章 已经讲解过,ReentrantLock 的主要实现都依赖AQS,因此在阅读本文前应该先了解 AQ ...

  3. Java并发——结合CountDownLatch源码、Semaphore源码及ReentrantLock源码来看AQS原理

    前言: 如果说J.U.C包下的核心是什么?那我想答案只有一个就是AQS.那么AQS是什么呢?接下来让我们一起揭开AQS的神秘面纱 AQS是什么? AQS是AbstractQueuedSynchroni ...

  4. [源码分析]读写锁ReentrantReadWriteLock

    一.简介 读写锁. 读锁之间是共享的. 写锁是独占的. 首先声明一点: 我在分析源码的时候, 把jdk源码复制出来进行中文的注释, 有时还进行编译调试什么的, 为了避免和jdk原生的类混淆, 我在类前 ...

  5. [源码分析]ReentrantLock & AbstractQueuedSynchronizer & Condition

    首先声明一点: 我在分析源码的时候, 把jdk源码复制出来进行中文的注释, 有时还进行编译调试什么的, 为了避免和jdk原生的类混淆, 我在类前面加了"My". 比如把Reentr ...

  6. Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析

    目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...

  7. java分析源码-ReentrantLock

    一.前言 在分析了 AbstractQueuedSynchronier 源码后,接着分析ReentrantLock源码,其实在 AbstractQueuedSynchronizer 的分析中,已经提到 ...

  8. 【JDK】JDK源码分析-ReentrantLock

    概述 在 JDK 1.5 以前,锁的实现只能用 synchronized 关键字:1.5 开始提供了 ReentrantLock,它是 API 层面的锁.先看下 ReentrantLock 的类签名以 ...

  9. 从OkHttp的源码来看 HTTP

    先来了解一下OkHttp的历史,最早是square公司觉得Android给的HttpClient这块的库不太好用,于是乎做了一层包装,再后来他们包装的这个库被Android官方给收回去了,而Andro ...

随机推荐

  1. VC++信息安全编程(13)Windows2000/xp/vista/7磁盘扇区读写技术

    有些时候,我们读取磁盘文件,会被hook.我们读到的可能并非实际的文件. 我们直接读取磁盘扇区获取数据. 实现磁盘数据的读写,不依赖WindowsAPI. [cpp] view plaincopy v ...

  2. Flex上传文件报“Error #2038”

    1.错误描述 ioerror: [IOErrorEvent type="ioError" bubbles=false cancelable=false eventPhase=2 t ...

  3. DataTable复制数据,深度复制

    /**/ /// <summary> /// 复制数据,深度复制 /// </summary> /// <param name="dataSourceRow&q ...

  4. 拥抱.NET Core系列:MemoryCache 缓存选项

    在上一篇 "拥抱.NET Core系列:MemoryCache 缓存过期" 中我们详细的了解了缓存过期相关的内容,今天我们来介绍一下 MSCache 中的 Options,由此来介 ...

  5. CF384 div2 E. Vladik and cards

    题意 给你一个的排列,求一个满足条件的最长子序列 每种数字的差小于等于,并且每种数字之内是连续的 解法 首先单纯认为用肯定不行的 所以应该考虑二分答案(所求长度具有二分性) 再用dp判断是否可行,这个 ...

  6. How to verify Certificate Pinning?

    Some friends of mine they worry about the risk of Man-in-the-middle so they ask me how to verify the ...

  7. HALCON学习-资料

    HALCON学习网: http://www.ihalcon.com/ 学习资料推荐博客: http://k594081130.blog.163.com/blog/static/218359013201 ...

  8. python 内置函数range和xrange

    range range 是一种类型(type),它是一个数字的序列,而且是不可变的,通常用在for循环中. class range(stop)class range(start, stop [, st ...

  9. 用css以写代码形式画一个皮卡丘

    我的个人网站是通过写代码的形式来形成一个网站的,前一阵子在某个大神的github上看到他用写代码的形式来完成一个皮卡丘,于是心血来潮花了半个小时,也完成了一个作品. 这其中涉及到的知识点也不是很复杂, ...

  10. Redis进阶实践之十六 Redis大批量增加数据

    一.介绍      有时,Redis实例需要在很短的时间内加载大量先前存在或用户生成的数据,以便尽可能快地创建数百万个键.这就是所谓的批量插入,本文档的目标是提供有关如何以尽可能快的速度向Redis提 ...