多线程学习-基础(十三)(学习参考·网摘) ArrayBlockingQueue源代碼解析(base jdk 1.8)
前记:
这个得首先声明一下,以下大部分内容均参考于:https://blog.csdn.net/wx_vampire/article/details/79585794,本随笔只作为学习作用,侵权删!
说一下我看的学习心得吧!对于BlockingQueue这个接口以及常用的实现类的用法,真的是不看不知道,一看吓一跳!有点超出了我的现有水平的理解范畴了!主要是里面的一些对Java基础中一些不常用的方法,修饰符的使用,这个对我来说真的算是涨姿势了。
还有就是,一些链表在处理数据的算法,这些也让我有点头大,学习的过程中处于“半懂”状态,这让我很受打击,不过好处就是让我知道接下来要往哪方面学习了。
好了,开始正文吧,以下算是一个网上转摘学习资料备份了。
正文:
ArrayBlockingQueue 是数组结构的堵塞队列的一种实现,那么肯定要实现的BlockingQueue接口。
解释一下接口含义
- boolean add(E e); 队列添加元素,返回成功标识,队列满了抛出队列满的异常,无堵塞。
- boolean offer(E e);队列添加元素,返回成功标识,无堵塞。
- void put(E e);队列添加元素,无返回值,队列满了会堵塞。
- boolean offer(E e, long timeout, TimeUnit unit);队列添加元素,队列满了堵塞,设置有超时时间。
- E poll();队列拉取元素,无堵塞,没有值返回null。
- E take();队列拉取元素,队列空了会堵塞,等待能拉取到值为止
- E poll(long timeout, TimeUnit unit);队列拉取元素,队列空了等待,设置有等待超时时间
- E peek() ; 只读队首元素的值,没有返回空
- int remainingCapacity(); 计算剩余容量
- boolean remove(Object o); 移除元素
- int drainTo(Collection<? super E> c, int maxElements); 移除元素放到入参的集合当中
- public Iterator<E> iterator() jdk 1.8以后ArrayBlockingQueue还增加了迭代器功能,这个模块下面会重点介绍,很有意思。
堵塞队列提供的功能:
- 在多线程环境下提供一个类似于生产者和消费者这样的一个模型
- 提供一个FIFO的顺序读取和插入
那就引起我的思考:
- 怎么实现的堵塞机制和堵塞的超时机制?
- 作为一个集合类,数组结构的怎么在多线程环境下实现安全扩容?
- 1.8jdk版本为什么会增加迭代器功能?
下面的代码说明:部分是我自己按照自己的立即翻译,当然多数是参考原作者的注释。
package java.util.concurrent; //所在包
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;//重入锁
import java.util.AbstractQueue;//抽象队列
import java.util.Collection;//集合
import java.util.Iterator;//迭代器
import java.util.NoSuchElementException;//异常
import java.lang.ref.WeakReference;//弱引用
import java.util.Spliterators;//分割迭代
import java.util.Spliterator;//分割迭代
class
public class ArrayBlockingQueue<E>
extends AbstractQueue<E> //继承了抽象队列
implements BlockingQueue<E>, //实现了BlockingQueue接口
java.io.Serializable//实现了Serializable接口,说明此类可以被序列化
{
//序列化ID
private static final long serialVersionUID = -817911632652898426L; /** 阻塞队列中存放的对象 */
default final Object[] items; /** 消费者获取对象的下一个对象下标,具体的操作有poll take peek remove */
default int takeIndex /** 生产者放入对象的下一个对象的下标,具体的操作有 put offer add */
default int putIndex; /** 队列中元素的数量 */
default int count; /** 这个锁就是实现生产者,消费者模型的锁模型,并且所有和并发相关的堵塞控制都是通过这个锁来实现的*/
default final ReentrantLock lock; /** 这个是有ReentrantLock 中的Condition一个标识队列中有元素非空标志,用于通知消费者队列中有数据了,快来取数据 */
private final Condition notEmpty; /** 这个也是ReentrantLock 中的Condition的一个标识,标识队列中的元素不满用于通知生产者队列中空地,快来塞数据*/
private final Condition notFull; /**
* 这是一个迭代器集合,是之前没有的特性,
* 细节:transient 标示变量是序列化忽略这个变量。
*/
default transient Itrs itrs = null;
/***********************【阻塞队列常用的方法】*************************************************************************************/
/**
*
*堵塞提交,超时返回false
*/
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
checkNotNull(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
//获取锁
lock.lockInterruptibly();
try {
while (count == items.length) {
if (nanos <= 0)
return false;
//这里是使用同步队列器的超时机制,在nanos的时间范围内,方法会在这里堵塞,超过这个时间段nanos的值会被赋值为负数,方法继续,然后在下一个循环返回false。这个标志是未满标志,队列里面未满就可以放进元素嘛。然后判断成功就是一个入队列操作
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);
return true;
} finally {
lock.unlock();
}
}
/**
* 入队列操作,因为putIndex已经是当前该放入元素的下标了,放入元素之后,
* 需要将putIndex+1,并且元素数量加1。然后直接调用非空标志通知等待中的消费者
* 质疑:如果我没有等待中的消费者,那也要通知,那不是浪费么?
* 解释:下端代码是signal的实现
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first)
}
signal方法已经在里面已经对队列的首元素判断空,不通知了,
这个引起我的一个思考,确实在函数里面就应该对这些条件做判断要比外面判断更好一些,一个是更健壮,一个是更友好,但是这个最小作用模块还是功能模块,别一个调用链做了多次的这种条件的判断,这就让阅读者难受了。
*/
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
/***
* poll的操作和offer基本一样,就是做的是出队列的操作。还有就是一个drainTo方法也很类似,有一个细节有意思就是drainTo是
*一个批量操作,但是通知却是一个一个通知的。没有调用singalAll()。因为堵塞队列强调一个顺序。一进一出原则。还有就是在外面判断了有无等待者。因为这**样却是省不必要的循环了。
*/
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
return dequeue();
} finally {
lock.unlock();
}
} /**
* 出队列操作,跟入队列操作正好是相反的,多了一个清理操作
*
*/
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
//@key jdk1.8的新特性迭代器特性,这里是因为元素的出队列所以清理和这个元素相关联的迭代器
itrs.elementDequeued();
//对于生产者的通知
notFull.signal();
return x;
}
/**
* 根据下标移除元素,那么会分成两种情况一个是移除的是队首元素,一个是移除的是非队首元素,移除队首元素,就相当于出队*列操作,移除非队首元素那么中间就有空位了,后面元素需要依次补上,然后如果是队尾元素,那么putIndex也就是插入操作的*下标也就需要跟着移动。这里面同样有无用迭代器的清理和notFull标志的通知。elementDequeued 和removedAt 这两个函数差*不多主要做的就是清理。但是不一样的是第一种情况当成出队列来处理了。而第二种就相当于这个元素就没有进过队列来处理,*轻轻地来,轻轻地走不带走一片云彩
*/
void removeAt(final int removeIndex) {
final Object[] items = this.items;
//当移除的元素正好是队列首元素,就是take元素,正常的类似出队列的操作,
if (removeIndex == takeIndex) {
// removing front item; just advance
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
//
} else {
//因为是队列中间的值被移除了,所有后面的元素都要挨个迁移
final int putIndex = this.putIndex;
for (int i = removeIndex;;) {
int next = i + 1;
if (next == items.length)
next = 0;
if (next != putIndex) {
items[i] = items[next];
i = next;
} else {
items[i] = null;
this.putIndex = I;
break;
}
}
count—;
if (itrs != null)
itrs.removedAt(removeIndex);
}
notFull.signal();
}
/**
* 当元素出队列的时候调用的方法这个出队列方法
*/
void elementDequeued() {
// 在队列为空的时候调用清空所有的迭代器;
if (count == 0)
queueIsEmpty();
// 当拿元素进行循环的时候,清理所有过期的迭代器
else if (takeIndex == 0)
takeIndexWrapped();
}
}
/**
* 因为takeIndex等于0了,意味着开始下一个循环了.
* 然后通知所有的迭代器,删除无用的迭代器。
*/
void takeIndexWrapped() {
//循环了一次cycle加1
cycles++;
for (Node o = null, p = head; p != null;) {
final Itr it = p.get();
final Node next = p.next;
//需要清理的条件,和清理代码
if (it == null || it.takeIndexWrapped()) {
p.clear();
p.next = null;
if (o == null)
head = next;
else
o.next = next;
} else {
o = p;
}
p = next;
}
//没有迭代器了,就关掉迭代器的集合
if (head == null) // no more iterators to track
itrs = null;
}
/**这个takeIndexWrapped 是内部类Itr 的方法跟上面不是一个类的方法
*这里就是判断这个迭代器所持有的元素还在队列里面么,那么有两个条件,1.isDetached()
* 2.就是看这个的循环次数,比建立这个迭代器的时候的循环次数,如果大于1,说明发生过两次以上的循环
* 拿里面的元素都换了个遍,拿肯定是不对了,拿这个迭代器就被关闭了。
* @return true if this iterator should be unlinked from itrs
*/
boolean takeIndexWrapped() {
// assert lock.getHoldCount() == 1;
if (isDetached())
return true;
if (itrs.cycles - prevCycles > 1) {
// All the elements that existed at the time of the last
// operation are gone, so abandon further iteration.
shutdown();
return true;
}
return false;
}
//将所有的标志位都标记成remove ,null
void shutdown() {
cursor = NONE;
if (nextIndex >= 0)
nextIndex = REMOVED;
if (lastRet >= 0) {
lastRet = REMOVED;
lastItem = null;
}
prevTakeIndex = DETACHED;
}
/***
* 迭代器的基本方法之一,获取下一个元素,会发生缓存器失效的情况,如果是缓存器失效了,能重组就重组,即从takeIndex开始遍历,如果不行就标记失效, *返回none
* @return
*/
public E next() {
// assert lock.getHoldCount() == 0;
final E x = nextItem;
if (x == null)
throw new NoSuchElementException();
final ReentrantLock lock = ArrayBlockingQueue.this.lock;
lock.lock();
try {
//当判定该迭代器失效了,会重组迭代器,以takeIndex为起点开始遍历,或者标记失效
if (!isDetached())
incorporateDequeues();
lastRet = nextIndex;
final int cursor = this.cursor;
//cursor这个值会在incorporateDequeues方法中修改,
if (cursor >= 0) {
nextItem = itemAt(nextIndex = cursor);
this.cursor = incCursor(cursor);
} else {
nextIndex = NONE;
nextItem = null;
}
} finally {
lock.unlock();
}
return x;
} /**
* 发现元素发生移动,通过判定cycle等信息,然后cursor取值游标就重新从takeIndex开始
* 下面如果发现所有记录标志的值发生变化,就直接清理本迭代器了。
* */
private void incorporateDequeues() {
final int cycles = itrs.cycles;
final int takeIndex = ArrayBlockingQueue.this.takeIndex;
final int prevCycles = this.prevCycles;
final int prevTakeIndex = this.prevTakeIndex;
if (cycles != prevCycles || takeIndex != prevTakeIndex) {
final int len = items.length;
// 从本迭代器建立开始,到目前堵塞队列出队列的个数,也就是takeIndex的偏移量
long dequeues = (cycles - prevCycles) * len
+ (takeIndex - prevTakeIndex);
// 判断所记录的last,next cursor 还是不是原值如果不是,这个迭代器就判定detach
if (invalidated(lastRet, prevTakeIndex, dequeues, len))
lastRet = REMOVED;
if (invalidated(nextIndex, prevTakeIndex, dequeues, len))
nextIndex = REMOVED;
if (invalidated(cursor, prevTakeIndex, dequeues, len))
cursor = takeIndex;
if (cursor < 0 && nextIndex < 0 && lastRet < 0)
detach();
else {
//重新记录cycle值
this.prevCycles = cycles;
this.prevTakeIndex = takeIndex;
}
}
}
/***********************【迭代器类的链表集合管理类】*************************************************************************************/
/**
* 下面是一个内部类:迭代集合链表类(用于处理迭代器)
* 作用:管理当前阻塞队列的迭代器
*/
class Itrs { /**
* 内部类中的内部类,自定义了一个节点
* 将里面的元素设置成弱引用,目标就是当成缓存使用的
* WeakReference:帮助JVM合理的释放对象,造成不必要的内存泄漏!!
*/
private class Node extends WeakReference<Itr> {
//下一个节点
Node next;
//节点构造器
Node(Itr iterator, Node next) {
super(iterator);
this.next = next;
}
} /** 记录循环的次数,当take下标到0的时候为一个循环 cycle+1 */
int cycles = 0; /** 定义一个头节点 **/
private Node head; /** 用于删除无用的迭代器 */
private Node sweeper = null;
/**
* 这个标识删除探针
*/
private static final int SHORT_SWEEP_PROBES = 4;
private static final int LONG_SWEEP_PROBES = 16;
//迭代器链表集合的构造器
Itrs(Itr initial) {
register(initial);
}
/**
* 注册逻辑的实现,在链表的最前面加元素
*/
void register(Itr itr) {
head = new Node(itr, head);//创建一个头节点
}
void doSomeSweeping(boolean tryHarder) {} //删除旧的,过期的,无用的迭代器
void takeIndexWrapped() {} //
void removedAt(int removedIndex) {} //
void queueIsEmpty() {} //
void elementDequeued() { } //
}
/***********************【迭代器类】*************************************************************************************/
/**
*创建一个内部类:当前阻塞队列的迭代器
*/
private class Itr implements Iterator<E> {
/** 光标,是迭代器下一次迭代时的坐标,迭代器没有需要遍历的对象了,这个值会为负值*/
private int cursor; /** 下一个元素内容,调用Iterator.next方法拿到的值 */
private E nextItem; /** 下一个元素的下标,none 是-1 被移除了是-2对应下面的static int */
private int nextIndex; /** 上一个元素的内容 */
private E lastItem; /** 上一个元素的的下标,none 是-1 被移除的是-2 同样对应下面的static int */
private int lastRet; /** 记录之前的开始遍历的下标,当这个迭代器判定为失效了这个值就是DETACHED */
private int prevTakeIndex; /* 记录之前循环次数的值,和Cycles进行比对,就知道有没有再循环过 */
private int prevCycles; /** 当阻塞队列中无数据时的状态值 */
private static final int NONE = -1; /**元素被调用remove方法移走,的状态值*/
private static final int REMOVED = -2; /**元素被调用detached方法后的状态值*/
private static final int DETACHED = -3;
/**迭代器的初始化函数从takeIndex位置开始遍历*/
Itr() {
lastRet = NONE;
/**拿到当前阻塞队列的锁*/
final ReentrantLock lock = ArrayBlockingQueue.this.lock;
/**开始锁住*/
lock.lock();
try {
if (count == 0) {//当前阻塞队列中无数据
cursor = NONE;//下一次迭代的索引值:-1
nextIndex = NONE;//下一位元素的索引值:-1
prevTakeIndex = DETACHED;//上一次遍历使用的索引值:-3 失效
} else {//阻塞队列中有数据
/** 初始化Itr迭代器的属性值 */
final int takeIndex = ArrayBlockingQueue.this.takeIndex;//拿到当前阻塞队列下一个取到数据的索引值
prevTakeIndex = takeIndex;//下一位索引值=前一位取值索引值
nextItem = itemAt(nextIndex = takeIndex);
cursor = incCursor(takeIndex);//队列首元素后一个
if (itrs == null) {
itrs = new Itrs(this);
} else {
//注册到itrs,所有迭代器的集合,顺序注册的
itrs.register(this);
// 清理无用的迭代器
itrs.doSomeSweeping(false);
}
prevCycles = itrs.cycles;
}
} finally {
//解锁
lock.unlock();
}
}
/**当前迭代器是否失效: 负值意味着失效迭代器 */
boolean isDetached() {
return prevTakeIndex < 0;
}
/**初始化下一个要拿到的元素的索引值 */
private int incCursor(int index) {
if (++index == items.length){//如果下一个元素的索引值刚好等于阻塞队列元素个数(说明已经到了队列的尾部),迭代重头开始
index = 0;//返回第一个元素的索引值
}
if (index == putIndex){//下一个元素的索引值=putIndex:生产者放入对象的下一个对象的索引值
index = NONE;//NONE=-1 表示队列中无数据
}
return index;
} /**如果给定数量的索引无效,则返回true。*/
private boolean invalidated(int index, int prevTakeIndex,
long dequeues, int length) {
if (index < 0)//队列中无数据,失效
return false;
int distance = index - prevTakeIndex;//计算 下一位元素的索引值-前一位元素索引值
if (distance < 0)//如果差值小于0
distance += length;
return dequeues > distance;
}
private void incorporateDequeues() {
final int cycles = itrs.cycles;
final int takeIndex = ArrayBlockingQueue.this.takeIndex;
final int prevCycles = this.prevCycles;
final int prevTakeIndex = this.prevTakeIndex; if (cycles != prevCycles || takeIndex != prevTakeIndex) {
final int len = items.length;
long dequeues = (cycles - prevCycles) * len
+ (takeIndex - prevTakeIndex); if (invalidated(lastRet, prevTakeIndex, dequeues, len))
lastRet = REMOVED;
if (invalidated(nextIndex, prevTakeIndex, dequeues, len))
nextIndex = REMOVED;
if (invalidated(cursor, prevTakeIndex, dequeues, len))
cursor = takeIndex; if (cursor < 0 && nextIndex < 0 && lastRet < 0)
detach();
else {
this.prevCycles = cycles;
this.prevTakeIndex = takeIndex;
}
}
}
}
}
回顾一下;
我介绍了ArrayblockingQueue其实是包含了两个部分一个是标准阻塞队列接口的实现。另一个是jdk1.8增加的迭代器。上一个满大街博客都能找的到,我就把接口描述了一下,然后介绍了两个还算是复杂一点的接口。和整个一个工作原理,没有太多使用case。主要是就是生产者和消费者模型。一个锁应用,和其他的JUC框架不一样。它什么操作都加锁,并发变串行。所以它就没有用到原子类修饰的共享变量。
关于迭代器部分好像是只有我这里有写。如果有百度上有看到相关ArrayBlockingQueue迭代器文章的请留言。毕竟我一家之言,还是有可能会有理解上的偏差。我们总结一下这个迭代器。首先跟别的设计一样,谁用谁new。这个不一样的是会增加一个注册到堵塞队列对象里面itrs上面。然后呢用了一个软引用,那么就GC可以回收避免内存溢出。然后会有对无用的迭代器的清理,类似于threadLocal那样。那么什么是无用的迭代器呢。标识无用就一个条件,我的迭代器标识的结点被覆盖了,因为它空间就这么大,举个例子一个大小5的堵塞队列。然后我建了一个迭代器,那么这个迭代器的下标就是0.然后迭代器我没有马上用,然后进出队列10次,那么之前节点的值已经被替换了。队列里面还有值,但是迭代器的值已经在take方法中被干掉了,已经失效了。判断条件就是cycle的循环次数。有兴趣可以好好了解一下,这应该是我看过的最复杂的迭代器了。
留一些问题:
1.这个迭代器为什么会比arrayList复杂这么多?
2.其实作为堵塞队列来说无非就是数据交换,拿有什么场景是需要迭代器的?而且本身就全都锁控制,效率就不高。还加入这么复杂的迭代模块。会更慢一些的?
这篇文章会看起来比较碎。尽力了。。没有整块的时间去写。而且没想这个迭代器这么复杂。花费我很多时间去研究(没错,这就是我脱稿的原因)
还有就是风格和上一篇不一样了。我希望可以让看这篇文章的人不光是可以学习到之前不知道的知识。也可以触发大家更多的去主动的思考,去思考模块的设计,功能的实现。而不是被动接受这篇文章所传递出来的内容。
还有就是看这种源码。一定要先框架,功能。摸透再去看细节。如果你对这个代码块所要完成的功能不够了解。拿看起来费劲。框架,功能这些都摸透了。再钻到细节上面去。我们可能用到的框架很多,拿要读的源代码那就太多了。其实阅读源代码我觉得是培养一个阅读代码的能力。一个是学习处理这种场景的解决方案,一个是学习编程风格,编码模式。还有就是可能会培养对编程、对探究的兴趣。毕竟工作不能只是为了赚钱。
多线程学习-基础(十三)(学习参考·网摘) ArrayBlockingQueue源代碼解析(base jdk 1.8)的更多相关文章
- android开发学习---基础知识学习、如何导入已有项目和开发一个电话拨号器
一.基础知识点学习 1.Android体系结构 如图所示,android 架构分为三层: (1)最底层是linux内核,主要是各种硬件的驱动,如相机驱动(Camera Driver),闪存驱动(Fl ...
- C++学习基础十三——struct和class的区别
来自:http://blog.sina.com.cn/s/blog_48f587a80100k630.html C++中的struct是对C中struct进行了扩展,它不单是一个包含不同数据类型的数据 ...
- Java学习---基础知识学习
2016-07-23 周六 利用键盘输入的时候需要抛出异常 ,直接快捷键 ctrl + 1 ;定义数组 int score[] = new int[4] ; 只有4个数字BufferedRead ...
- HTML学习---基础知识学习
1.1. HTML 1.为什么要有HTML? "Hello" "<h1>Hello</h1>" - 浏览器渲染时使用一套HTML规则, ...
- Mysql学习---基础操作学习2
基本数据类型 Mysql基本数据类型:二进制,数值[整数,小数].字符串[定长,变长]. 二进制数据.时间和枚举集合 bit[(M)] 二进制位(101001),m表示二进制位的长度(1-64),默认 ...
- Mysql学习---基础操作学习
1.1. 基本操作 数据库引擎 Inodb:支持事务[原子性操作,完成一些列操作后才算完成操作,否则rollback] MyISAM: 支持全文索引,强调了快速读取操作,主要用于高负载的select ...
- Java并发包源码学习系列:阻塞队列实现之ArrayBlockingQueue源码解析
目录 ArrayBlockingQueue概述 类图结构及重要字段 构造器 出队和入队操作 入队enqueue 出队dequeue 阻塞式操作 E take() 阻塞式获取 void put(E e) ...
- 多线程高并发编程(12) -- 阻塞算法实现ArrayBlockingQueue源码分析(1)
一.前言 前文探究了非阻塞算法的实现ConcurrentLinkedQueue安全队列,也说明了阻塞算法实现的两种方式,使用一把锁(出队和入队同一把锁ArrayBlockingQueue)和两把锁(出 ...
- Java并发包源码学习系列:阻塞队列实现之LinkedBlockingQueue源码解析
目录 LinkedBlockingQueue概述 类图结构及重要字段 构造器 出队和入队操作 入队enqueue 出队dequeue 阻塞式操作 E take() 阻塞式获取 void put(E e ...
随机推荐
- shell变量扩展技巧
SHELL中有一些变量扩展的技巧,做下归纳总结 1.取字符串slice规则一:${变量名:位置起点}含义:由指定的位置起点开始,截取子字符串到字符串结束例如: var="/etc/passw ...
- linux下ioctl遇到的坑
在驱动编程里面经常会用到ioctl的系统调用,发现cmd = 2的时候,用户ioctl直接返回-1. 原因在于在linux-x.xx/fs/ioctl.c定义的do_vfs_ioctl函数 int d ...
- ADMEMS软件架构的4个阶段
业界软件架构设计的方法论很多,各有各自的应用场景和特点,下文结合ADMEMS(Architecture Design Method has been Extended to Method System ...
- Oracle导出导入
导出 exp 用户名/密码 file=文件名.dmp full=y; 导入 imp 用户名/密码 file=文件名.dmp full=y; 使用EXPDP和IMPDP时应该注意的事项: EXP和IMP ...
- Nginx解决错误413 Request Entity Too Large
最近一个项目当中,要求上传图片,并且限制图片大小,虽然在laravel当中已经添加了相关的表单验证来阻止文件过大的上传,然而当提交表单时,还没轮到laravel处理,nginx就先报错了.当你仔细看报 ...
- 推荐几个MySQL大牛的博客
1.淘宝丁奇 http://dinglin.iteye.com/ 2.周振兴@淘宝 花名:苏普 http://www.orczhou.com/ 3. 阿里云数据库高级专家彭立勋为 MariaDB Fo ...
- Admin.Admin/Login --- 后台项目中的管理员及登录模块
管理员模块: using System; using System.Collections.Generic; using System.Linq; using System.Web; using Sy ...
- 摘之知乎网友...PHYTIN学习
作者:东瓜王链接:https://www.zhihu.com/question/19593179/answer/23746083来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明 ...
- Hudson和Jenkins的关系
Jenkins is an open source continuous integration tool written in Java. The project was forked from H ...
- socket模型
Socket: "主机" + "端口" = 套接字/插座; 仅仅是一个通信模型,不属于七层协议(网络协议). 一台电脑(IP)的一个应用程序(端口) 和 另一台 ...