Java并发包学习--ReentrantLock
这个锁叫可重入锁。它其实语义上和synchronized差不多,但是添加了一些拓展的特性。
A reentrant mutual exclusion
Lock
with the same basic behavior and semantics as the implicit monitor lock accessed usingsynchronized
methods and statements, but with extended capabilities.
我们重点看看它的源代码实现。
在ReentrantLock类中很大一部分的函数是委托给这个类Sync来完成的。
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
Sync继承了AbstractQueuedSynchronizer,声明了lock()的抽象函数,还实现了nonfairTryAcquire的抽象函数。可以看到它是non-fair锁的默认实现方式。
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
static abstract class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L; /**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock(); /**
* Performs non-fair tryLock. tryAcquire is
* implemented in subclasses, but both need nonfair
* try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
} 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;
} protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
} final ConditionObject newCondition() {
return new ConditionObject();
} // Methods relayed from outer class final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
} final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
} final boolean isLocked() {
return getState() != 0;
} /**
* Reconstitutes this lock instance from a stream.
* @param s the stream
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
可以看出ReentrantLock是可重入锁的实现。而内部是委托java.util.concurrent.locks.ReentrantLock.Sync.lock()实现的。java.util.concurrent.locks.ReentrantLock.Sync是抽象类,有java.util.concurrent.locks.ReentrantLock.FairSync和java.util.concurrent.locks.ReentrantLock.NonfairSync两个实现,也就是常说的公平锁和不公平锁。
公平锁和非公平锁
如果获取一个锁是按照请求的顺序得到的,那么就是公平锁,否则就是非公平锁。
在没有深入了解内部机制及实现之前,先了解下为什么会存在公平锁和非公平锁。公平锁保证一个阻塞的线程最终能够获得锁,因为是有序的,所以总是可以按照请求的顺序获得锁。不公平锁意味着后请求锁的线程可能在其前面排列的休眠线程恢复前拿到锁,这样就有可能提高并发的性能。这是因为通常情况下挂起的线程重新开始与它真正开始运行,二者之间会产生严重的延时。因此非公平锁就可以利用这段时间完成操作。这是非公平锁在某些时候比公平锁性能要好的原因之一。
先看下公平锁FairSync的实现:
/**
* Sync object for fair locks
*/
final static class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L; final void lock() {
acquire(1); //调用的是AQS中的acquire()
} /**
* 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(); //也是AQS中的函数,记录当前有多少个线程拿了锁,对于重入锁,state不一定为1
if (c == 0) {
if (isFirst(current) && //判断是否是队列中的第一个,不是也拿不到,只能返回false,保证公平
compareAndSetState(0, acquires)) { //如果是就设置为1,刚刚acquire为1
setExclusiveOwnerThread(current); //并且设置当前线程为独占线程
return true;
}
} //即使当前state不为0,也不一定拿不到锁,只要是自己的线程独占了锁,就可以重进入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; //acquires为1,所以每进入一次,就加1
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; //不然就返回false,会执行后面的acquireQueued()函数
}
}
lock的话就直接用了AbstractQueuedSynchronizer的acquire()函数,并且实现了一个公平锁版本的tryAcquire函数。
我们可以看到acquire的参数是1,看看AbstractQueuedSynchronizer中acquire的实现:
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) && //tryAcquire(1)是在FairSync中自己实现的。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //被中断的话,acquireQueued会返回true,否则false
selfInterrupt();
}
我们先看看AbstractQueuedSynchronizer的tryAcquire(1),奇怪这里居然是直接抛出了异常。
/**
* Attempts to acquire in exclusive mode. This method should query
* if the state of the object permits it to be acquired in the
* exclusive mode, and if so to acquire it.
*
* <p>This method is always invoked by the thread performing
* acquire. If this method reports failure, the acquire method
* may queue the thread, if it is not already queued, until it is
* signalled by a release from some other thread. This can be used
* to implement method {@link Lock#tryLock()}.
*
* <p>The default
* implementation throws {@link UnsupportedOperationException}.
*
* @param arg the acquire argument. This value is always the one
* passed to an acquire method, or is the value saved on entry
* to a condition wait. The value is otherwise uninterpreted
* and can represent anything you like.
* @return {@code true} if successful. Upon success, this object has
* been acquired.
* @throws IllegalMonitorStateException if acquiring would place this
* synchronizer in an illegal state. This exception must be
* thrown in a consistent fashion for synchronization to work
* correctly.
* @throws UnsupportedOperationException if exclusive mode is not supported
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
其实不然,这里是一个protected的函数,利用了java的多态,在ReentrantLock中,有两个构造函数,默认是非公平锁NonfairSync,所以实际上对于公平锁而言,是调用了FairSync中自己的实现(代码在上面)。
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
} /**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = (fair)? new FairSync() : new NonfairSync();
}
前面说明对于AQS存在一个state来描述当前有多少线程持有锁。由于AQS支持共享锁(例如读写锁,后面会继续讲),所以这里state>=0,但是由于ReentrantLock是独占锁,所以这里不妨理解为0<=state,acquires=1。isFirst(current)是一个很复杂的逻辑,包括踢出无用的节点等复杂过程,这里暂且不提,大体上的意思是说判断AQS是否为空或者当前线程是否在队列头(为了区分公平与非公平锁)。
可以看到,在tryAcquire中,调用了c = getState()来判断当前有多少线程拿到了锁,
- 如果c ==0,说明这时锁没有人用。但是还要调用isFirst判断一下该线程是不是AQS队列的第一个。
- 如果第一个,那么就通过compareAndSetState把当前state设置为1(即acquires)。然后通过setExclusiveOwnerThread设置当前线程为这个锁的独占线程。
- 如果不是第一个,那么就不好意思,只能返回false,回去排队了(acquireQueued())。这就是公平锁和不公平锁的区别,公平锁必须永远保证队列的第一个拿到锁。所以这时即使没人拿到锁,那轮不到你,需要排队。
- 如果c !=0,也不一定拿不到锁,因为是可重入锁。那么调用getExclusiveOwnerThread()来判断当前这个线程是不是锁的独占线程。
- 如果这个锁的独占线程也是它,那么就把锁当前的数目加上1(即acquires)。这里之所以不是将当前状态位state设置为1,而是修改为旧值+1呢?这是因为ReentrantLock是可重入锁,同一个线程每持有一次state就+1。
- 如果不是,那就不好意思,那就只能返回false,也回去排队了(acquireQueued())。
setExclusiveOwnerThread,这个函数在AbstractOwnableSynchronizer类中,而AbstractQueuedSynchronizer又继承了他,这个变量就是用来控制说只有一个线程能访问这个锁,也就是这个独占锁所对应的独占线程。
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable { /** Use serial ID even though all fields transient. */
private static final long serialVersionUID = 3737899427754241961L; /**
* Empty constructor for use by subclasses.
*/
protected AbstractOwnableSynchronizer() { } /**
* The current owner of exclusive mode synchronization.
*/
private transient Thread exclusiveOwnerThread; /**
* Sets the thread that currently owns exclusive access. A
* <tt>null</tt> argument indicates that no thread owns access.
* This method does not otherwise impose any synchronization or
* <tt>volatile</tt> field accesses.
*/
protected final void setExclusiveOwnerThread(Thread t) {
exclusiveOwnerThread = t;
} /**
* Returns the thread last set by
* <tt>setExclusiveOwnerThread</tt>, or <tt>null</tt> if never
* set. This method does not otherwise impose any synchronization
* or <tt>volatile</tt> field accesses.
* @return the owner thread
*/
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
所以总结一下,FairSync版本的tryAcquire的功能就是保证如果是队列中的第一个的话就能够拿到锁,并且提供了可重入功能,即同一个线程获取这个锁多一次,state计数就加1。
接下来继续分析acquire()中的语句:
- 如果tryAcquire(1)成功,即(!tryAcquire(1))==false,由于&&的短路功能,那么就直接返回了,成功拿到锁。
- 如果tryAcquire(1)返回false,就意味要排队了。那么就会执行acquireQueued(addWaiter(Node.EXCLUSIVE),1),加入一个独占的Node。addWaiter(Node mode)函数的功能就是在AbstractQueuedSynchronizer中维护的双向队列的尾部加入一个节点。AQS支持独占锁和共享锁,而独占锁在Node中就意味着条件(Condition)队列为空。它调用Node中对应的构造函数,把这个node的nextWaiter设置为Node.EXCLUSIVE,即为null。addWaiter()函数返回的是这个新加入的node,并且把这个传给了acquireQueued函数。
我们只要记住,addWaiter会在AQS的双向队列的尾部加入一个新的Node,表示当前线程排队等待锁。
/**
* Creates and enqueues node for given thread and mode.
*
* @param current the thread
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
private Node addWaiter(Node mode) { //这里的mode是Node.EXCLUSIVE
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) { //判断tail是否为null
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); //enq函数会创建一个新的head,并且把node加入到tail。
return node;
}
Node的nextWaiter有两种值,一种是SHARED(一个new Node),一种是EXCLUSIVE(null),分别用来表示共享锁和互斥锁。
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode; //对于SHARED,这是一个node,对于ECLUSIVE,这是null
this.thread = thread;
}
再看看AbstractQueuedSynchronizer中的acquireQueued(),
引用一下别人说的
acquireQueued过程是这样的:
- 如果当前节点是AQS队列的头结点(如果第一个节点是DUMP节点也就是傀儡节点,那么第二个节点实际上就是头结点了),就尝试在此获取锁tryAcquire(arg)。如果成功就将头结点设置为当前节点(不管第一个结点是否是DUMP节点),返回中断位。否则进行2。
- 检测当前节点是否应该park(),如果应该park()就挂起当前线程并且返回当前线程中断位。进行操作1。
这里看到,其实acquireQueued中有一个for循环,所以它是没那么快返回的。引用一下:
自旋请求锁,如果可能的话挂起线程,直到得到锁,返回当前线程是否中断过(如果park()过并且中断过的话有一个interrupted中断位)。
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) { //一直循环,直到拿到锁
final Node p = node.predecessor(); //获取前一个node,如果为null,会抛出NullPointerException
if (p == head && tryAcquire(arg)) { //前面是head,那么再tryAcquire一下
setHead(node); //成功则设置当前节点为head,它会把thread和prev域设置为null
p.next = null; // help GC //此时原来的head就可以GC了
return interrupted; //成功则返回,如果此时被中断了,会调用selfInterrupt()
} //如果获取失败,才要判断是否要park了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //需要park才执行parkAndCheckInterrupt()
interrupted = true; //如果已经被interrupt,设置一下interrupted
}
} catch (RuntimeException ex) {
cancelAcquire(node);
throw ex;
}
}
可以顺便看下setHead()函数,它会把head的thread和prev设置为null,并用head指向它。
/**
* Sets head of queue to be node, thus dequeuing. Called only by
* acquire methods. Also nulls out unused fields for sake of GC
* and to suppress unnecessary signals and traversals.
*
* @param node the node
*/
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
如果前一个节点不是head,并且tryAcquire失败,那么就要调用shouldParkAfterFailedAcquire了。park的意思大概就是挂起一个线程,具体的语义参考LockSupport这个类。
要理解shouldParkAfterFailedAcquire,首先要理解Node中定义的四个状态位是什么意思:
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;//只有这个大于0,意味着Node对应的线程超时或中断,也就是我不要锁了,忽略我吧
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1; //就说当前node的thread释放锁的时候,要记得唤醒下一个node
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2; //暂时用不到
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3; //暂时用不到,ReentrantLock是EXCLUSIVE
分为3种情况:
- 如果前面节点是SIGNAL,那么直接返回true。意思是前面释放的时候会唤醒你的,你可以park了。
- 如果前面节点是CANCELLED,那么把所有waitStatus为CANCELLED的都去掉,然后返回false。
- 如果是0或PROPAGATE,那么调用compareAndSetWaitStatus,把前面的node的状态设置为SIGNAL,意思是前面线程释放的时候,要记得唤醒下一个node啊,我一直在等待啊。同样返回false,不park。
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; //获取前面节点的状态
if (ws == Node.SIGNAL) //说明前一个node已经释放啦,可以唤醒下一个了
/*
* This node has already set status asking a release
* to signal it, so it can safely park
*/
return true;
if (ws > 0) { //大于0表示我不要锁了,所以用while循环一直往前找到
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev; //pred和当前的node.prev都指向pred.prev
} while (pred.waitStatus > 0); //直到找到小于或等于0的
pred.next = node;
} else {
/* //这里说吧前面node的状态改为SIGNAL,但是依旧不park
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
再parkAndCheckInterrupt(),比较简单,依赖于LockSupport来挂起一个线程。
/**
* Convenience method to park and then check if interrupted
*
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
记得前面如果acquire函数中,如果tryAcquire()为false,acquireQueued()为真(被中断的情况下返回true,否则返回false),那么就调用selfInterrupt(),把自己中断
/**
* Convenience method to interrupt current thread.
*/
private static void selfInterrupt() {
Thread.currentThread().interrupt();
}
再来看看NonFairSync和FairSync的区别:
/**
* Sync object for non-fair locks
*/
final static 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); //否则才是acquire,acquire()函数中又会调用tryAcquire()和acquireQueued()
} protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires); //调用nonfairTryAcquire(),sync中是自己实现tryAcquire()
}
}
看一下Sync中nonfairTryAcquire的实现,这是默认的实现:
/**
* Performs non-fair tryLock. tryAcquire is
* implemented in subclasses, but both need nonfair
* try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
其实差别非常小,我们前面也说到,
在fair情况下,采用的是:if (isFirst(current) && compareAndSetState(0, acquires))
而在nonFair情况下,采用的是: if (compareAndSetState(0, acquires))
也就是说在nonFair情况下,我并不需要保证这个线程是队列的第一个线程才可以,其实就是那些还没有排队但是刚好要锁的线程,有可能比那些还在AQS队列的中线程还快速拿到锁。所以是不公平的,但是效率也明显有所提升。
参考资料:
Java并发包学习--ReentrantLock的更多相关文章
- [Java并发包学习七]解密ThreadLocal
概述 相信读者在网上也看了非常多关于ThreadLocal的资料,非常多博客都这样说:ThreadLocal为解决多线程程序的并发问题提供了一种新的思路:ThreadLocal的目的是为了解决多线程訪 ...
- [Java并发包学习八]深度剖析ConcurrentHashMap
转载自https://blog.csdn.net/WinWill2012/article/details/71626044 还记得大学快毕业的时候要准备找工作了,然后就看各种面试相关的书籍,还记得很多 ...
- Java并发包学习一 ThreadFactory介绍
ThreadFactory翻译过来是线程工厂,顾名思义,就是用来创建线程的,它用到了工厂模式的思想.它通常和线程池一起使用,主要用来控制创建新线程时的一些行为,比如设置线程的优先级,名字等等.它是一个 ...
- JAVA并发包学习
1)CyclicBarrier一个同步辅助类,允许一组线程相互等待,直到这组线程都到达某个公共屏障点.该barrier在释放等待线程后可以重用,因此称为循环的barrier 2)CountDownLa ...
- java并发包研究之-ConcurrentHashMap
概述 HashMap是非线程安全的,HashTable是线程安全的. 那个时候没怎么写Java代码,所以根本就没有听说过ConcurrentHashMap,只知道面试的时候就记住这句话就行了…至于为什 ...
- java并发编程学习: ThreadLocal使用及原理
多线程应用中,如果希望一个变量隔离在某个线程内,即:该变量只能由某个线程本身可见,其它线程无法访问,那么ThreadLocal可以很方便的帮你做到这一点. 先来看一下示例: package yjmyz ...
- Java并发包源码学习系列:ReentrantLock可重入独占锁详解
目录 基本用法介绍 继承体系 构造方法 state状态表示 获取锁 void lock()方法 NonfairSync FairSync 公平与非公平策略的差异 void lockInterrupti ...
- Java并发包源码学习之AQS框架(一)概述
AQS其实就是java.util.concurrent.locks.AbstractQueuedSynchronizer这个类. 阅读Java的并发包源码你会发现这个类是整个java.util.con ...
- Java并发包下锁学习第一篇:介绍及学习安排
Java并发包下锁学习第一篇:介绍及学习安排 在Java并发编程中,实现锁的方式有两种,分别是:可以使用同步锁(synchronized关键字的锁),还有lock接口下的锁.从今天起,凯哥将带领大家一 ...
随机推荐
- ASP.NET MVC3 系列教程 - Razor视图引擎基础语法
http://www.cnblogs.com/highend/archive/2011/04/09/aspnet_mvc3_razor_engine.html 4. 关于所有带"_" ...
- Getting Started(Google Cloud Storage Client Library)
在运行下面的步骤之前,请确保: 1.你的项目已经激活了Google Cloud Storage和App Engine,包括已经创建了至少一个Cloud Storage bucket. 2.你已经下载了 ...
- [Windows]VS2010如何以管理员权限启动?(转)
在某些项目进行开发的时候,需要提升应用程序本身的权限,这个是很容易的.但是如何让VS2010启动的时候就已管理员权限运行程序呢?为这个问题苦恼了好久,终于找到了办法. 找到VS2010的快捷方式:右击 ...
- -exec 与 xargs 的区别
实地在shell里执行下如下命令就知道区别了: $ find -type f -exec echo {} \; 很明显,exec是对每个找到的文件执行一次命令.从这里可以看出exec的缺点是每处理一个 ...
- cloud maintenance of OpenNebula
OpenNebula 4.4.1 maintenance release,官方建议当前的生产环境使用3.x or 4.x的其它版本; php调用curl工具伪造ip Upgrading from Op ...
- libevent的使用方法--回显服务器的简单实例
#include <event.h> #include <sys/types.h> #include <sys/socket.h> #include <net ...
- C#获取当前应用程序所在路径及环境变量
一.获取当前文件的路径 string str1=Process.GetCurrentProcess().MainModule.FileName;//可获得当前执行的exe的文件名. string st ...
- CSS开启硬件加速 hardware accelerated
作者:孙志勇 微博 日期:2016年12月6日 一.时效性 所有信息都具有时效性.文章的价值,往往跟时间有很大关联.特别是技术类文章,请注意本文创建时间,如果本文过于久远,请读者酌情考量,莫要浪费时间 ...
- Eclipse添加小工具_打开当前文件所在文件夹
CopyRight yuhuashi http://www.cnblogs.com/chuyuhuashi/archive/2012/05/06/2485831.html 默认情况下使用eclip ...
- PHP+MySQL多语句执行<转自wooyun>
发起这个帖子,估计就很多人看到题目就表示不屑了.一直以来PHP+MySQL环境下,无论是写程序或者是注入攻击,是无法多语句执行的,这么广为人知的常识,没理由会有人不知道.可权威就是用来被挑战的,常识也 ...