并发之AbstractQueuedLongSynchronize----AQS
一概述
谈论到并发,不得不谈论锁,而谈论到锁而言,又离不开ReentrantLock.ReentrantLock是锁锁的一种实现方式,对于锁而言,我们这里就需要讨论到AQS,即上面的AbstractQueuedLongSynchronize。我直接翻译过来就叫做抽象队列同步器。它规定了多线程访问并发资源的策略,或者提供了一种多线程访问资源的机制,也可以认为是规定多线程访问共享资源的框架。
二框架
AbstractOwnableSynchronizer:在谈论本文的主角之前,我们先来看看AQS的结构体系:
public abstract class AbstractQueuedLongSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
AQS继承AOS,我将AOS称之为抽象拥有同步器。哈哈,这样翻译过来可能不是太准确。我么看看AOS中都有什么。
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
/**序列化版本号*/
private static final long serialVersionUID = 3737899427754241961L;
/**无参数的构造器*/
protected AbstractOwnableSynchronizer() { }
/**独占模式、锁的持有者*/
private transient Thread exclusiveOwnerThread;
/**设置该线程为锁的持有者(独占模式)*/
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
/**获取独占模式下锁的持有者对象*/
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
从上面的AOS可以看出,主要有两个方法和一个成员变量。都是以独占模式下的关于线程获取锁和设置锁的一些操作。但是并没有实现,而是让子类自行去实现。看如下的示意图:
在上图中我们可以看到存在一个变量的状态。在AQS中维护了一个长型的变量状态以及一个FIFO(先进先出的队列)的队列,当多线程争共享资源时,收到阻塞会进入该队列。
private volatile long state;
关于状态值状态的访问存在三种形式:
- getState():
protected final long getState() {
return state;
}
- setState(long newState):
protected final void setState(long newState) {
state = newState;
}
- compareAndSetState(long expect,long update):
protected final boolean compareAndSetState(long expect, long update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapLong(this, stateOffset, expect, update);
}
这三个方法很容易理解,的getState()是获取当前可用资源的个数,的setState()为是当前可用资源重新设置值。左右的一个方法CAS方法是多线程在并发修改状态值时做的原子操作,允许某一时刻只有一个线程修改成功。从另外一个角度来看,也就是第一个和第二个方法是单个线程在做的操作,CAS方法是多线程操作,但是只有一个会修改成功,是一个轻量级的锁,关于CAS论述,在这里不再赘述。
AQS定义了两种访问资源的规则:
1 exclusive(独占锁),独占式访问,可以认为是在某某时刻只允许一个线程来操作。比如说写锁,在写的时候不允许其它线程写或者读.2 share(共享锁)。允许多个线程对同一个资源做操作。使得串行化的任务并行执行。在并发包下的Semaphore,CountDownLatch,以及ReentrantReadWriteLock都是可以共享执行的。独占锁中的ReentrantLock的则是一个典型的排它锁或者独占锁,和前面的几个恰好相反。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源状态的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法(这些方法是采用的模板方法来设计的,需要继承的类去实现):
- isHeldExclusively():该线程是否正在独占资源。只有用到条才才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败; 0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,失败则返回false。
以ReentrantLock为例,state初始化为0,表示未锁定状态.A线程lock()时,会调用tryAcquire()独占该锁并将状态+ 1。此后,其他线程再的tryAcquire()时就会失败,直到甲线程解锁()到状态= 0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(状态会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证状态是能回到零态的。再以CountDownLatch以例,任务分为Ñ个子线程去执行,状态也初始化为N(注意Ñ要与线程个数一致)。这Ñ个子线程是并行执行的,每个子线程执行完后COUNTDOWN()一次,状态会CAS减1.等到所有子线程都执行完后(即状态= 0),会取消驻留()主调用线程,然后主调用线程就会从AWAIT()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现的tryAcquire-tryRelease,tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如的ReentrantReadWriteLock。
三自定义同步器实现
实现原则:要想去实现一个同步器的实现,那么我么的类应该去继续AQS这个类,然后重写其中的方法,对于独占锁而言,要实现tryAcquire,tryRelease(),如果要实现共享锁,那么就要实现tryAcquireShared(),tryReleaseShared()这些方法。最后,在我们的组件中调用AQS中的模板方法就可以了,而这些模板方法是会调用到我们之前重写的那些方法的。也就是说,我们只需要很小的工作量就可以实现自己的同步组件,重写的那些方法,仅仅是一些简单的对于共享资源状态的获取和释放操作,至于像是获取资源失败,线程需要阻塞之类的操作,自然是AQS帮我们完成了。
设计思想:对于使用者来讲,我们无需关心获取资源失败,线程排队,线程阻塞/唤醒等一系列复杂的实现,这些都在AQS中为我们处理好了。我们只需要负责好自己的那个环节就好,也就是获取/释放共享资源状态 姿势T_T。很经典的模板方法设计模式的应用,AQS为我们定义好顶级逻辑的骨架,并提取出公用的线程入队列/出队列,阻塞/唤醒等一系列复杂逻辑的实现,将部分简单的可由使用者决定的操作逻辑延迟到子类中去实现即可。
public class Mutex implements Serializable { private static final long serialVersionUID = -7213470713366791047L;
//同步器对象
private final Sync sync = new Sync(); /**
* 自定义同步器实现
* @author gosaint
*
*/
private static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 1L; //线程是否持有锁,返回true表示锁定状态
@Override
protected boolean isHeldExclusively() {
return getState()==1;
} /**
* 以独占的方式获取锁
*/
@Override
protected boolean tryAcquire(int acquires) {
//当状态为0 的时候开始获取锁,CAS成功之后状态值修改为1
if(compareAndSetState(0, 1)){
//设置为当前线程为独占锁定状态
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
} @Override
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise unused 断言执行
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
//设置共享资源的状态为0,即释放锁的状态
setState(0);
return true;
} }
实现解读:对于上述的代码,定义了一个内部类同步,继承了类AQS,然后实现了其中的isHeldExclusively(),的tryAcquire(),tryRelease()下面的几个方法包括锁(),开锁()等方法是锁锁中的方法,在这里我没有实现锁定接口看下调用过程吧:sync.acquire(1),调用这个方法之后,看看这个方法都干了些什么,看如下的代码:
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// acquireQueued(addWaiter(Node.EXCLUSIVE),arg)指定了独占模式下的资源锁定。调用了tryAcquire()方法,我们接着看看这个方法干了些什么? protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
什么?这个方法里面竟然没有任何的实现,我是看错了吗?没有,其实这正是我第一次的内心的想法。我以为自己看错了。经过反复的阅读之后发现这样的设计简直是合情合理,回忆起来曾经观看任小龙老师的视频时讲的模板方法。在AQS中不做任何的实现,因为AQS定义了或者规定了多线程访问共享资源的策略。它有独占锁定和共享锁定。因此方法的设计并不是抽象的,而是受保护的。它让具体的实现自行选择对应的策略去实现具体的方法,这正是模板方法闪闪发光的地方。这位设计者正是大名鼎鼎的Doug Lea的
同步器代码测试:定义了30个线程,每个线程自增10000,正常情况下就是300000其实这种同步可以使用原子包下的AutomicInteger来完成的这里使用自定义同步器实现,为的是说明问题。
public class TestMutex { private static CyclicBarrier barrier = new CyclicBarrier(31);
private static int a = 0;
private static Mutex mutex = new Mutex(); public static void main(String []args) throws Exception {
//说明:我们启用30个线程,每个线程对i自加10000次,同步正常的话,最终结果应为300000;
//未加锁前
for(int i=0;i<30;i++){
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10000;i++){
increment1();//没有同步措施的a++;
}
try {
barrier.await();//等30个线程累加完毕启动线程
} catch (Exception e) {
e.printStackTrace();
}
}
});
t.start();
}
barrier.await();
System.out.println("加锁前,a="+a);
//加锁后
barrier.reset();//重置CyclicBarrier
a=0;
for(int i=0;i<30;i++){
new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<10000;i++){
increment2();//a++采用Mutex进行同步处理
}
try {
barrier.await();//等30个线程累加完毕
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
barrier.await();
System.out.println("加锁后,a="+a);
}
/**
* 没有同步措施的a++
* @return
*/
public static void increment1(){
a++;
}
/**
* 使用自定义的Mutex进行同步处理的a++
*/
public static void increment2(){
mutex.lock();
a++;
mutex.unlock();
}
}
看如下的结果::
加锁前,A = 283378
加锁后,A = 300000
四源码解析
CLH队列:我们先来简单描述下AQS的基本实现,前面我们提到过,AQS维护一个共享资源的状态,通过内置的FIFO来完成获取资源线程的排队工作(这个内置的同步队列称为” CLH “队列)。该队列由一个一个的节点结点组成,每个节点结点维护一个先前引用和下一个引用,分别指向自己的前驱和后继结点.AQS维护两个指针,分别指向队列头部的头和尾部尾这个在文章开始之前我就已经粘贴了一张图片下面的图片是我主要是针对这个线程排队的双向链表的示意图。:
实质其就是一个双向的链表
当线程通过的tryAcquire()设置状态失败的时候,就会进入CLH队列,当持有同步状态的线程释放同步状态时,就会唤醒后继节点,然后此节点就会继续假如。到同步状态的争夺当中在AQS中维持着一个节点的节点对象,来定义一个节点:
static final class Node { /** 等待状态的值,表示线程已经被中断或者取消 */
static final int CANCELLED = 1;
/** waitStatus值,表示后续线程需要被唤醒 */
static final int SIGNAL = -1;
/** waitStatus值,表示节点在condition上,当被signal后,会从等待队列转移到同步到队列中 */
static final int CONDITION = -2;
/**等待状态的值,初始值为0*/
volatile int waitStatus;
/**当前结点的前驱结点*/
volatile Node prev;
/** 当前结点的后继结点 */
volatile Node next;
/** 与当前结点关联的排队中的线程 */
volatile Thread thread;
}
独占式
取同步状态:
public final void acquire(long arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
一般的,我们的锁会直接调用获取()方法去获取锁,也就是同步状态。此时会调用的tryAcquire()方法,我们一般自定义同步器的时候会重写这个方法,去执行逻辑,获取同步状态。
流程::
1如果调用此时会调用的tryAcquire()方法获取锁的同步状态成功,那么直接返回真,真就是假的,后续的逻辑不再执行如果获取同步状态失败,那么进入2步骤!
2 acquireQueued (addWaiter(Node.EXCLUSIVE),arg)就是执行段代码,当获取同步状态失败之后,构造独占式同步节点,并通过addWaiter()添加到同步队列的尾部。(此时可能有多个线程需要添加至此队列,这里通过的是CAS保证了添加过程中的安全性)
3该结点以在队列中尝试获取同步状态,若获取不到,则阻塞结点线程,直到被前驱结点唤醒或者被中断。
addWaiter
获取同步状态失败的线程都要添加的CLH队列。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
流程::
1个节点node =新节点(Thread.currentThread(),模式);构造独占式节点
2节点预解码值=尾;将当前节点作为尾节点,并且判断原来的尾节点是否为空,不为空,
尝试CAS的快速插入3否则进入enq()方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
ENQ内部是个死循环,通过CAS设置尾结点,不成功就一直重试。很经典的CAS自旋的用法,我们在之前关于原子类的源码分析中也提到过。这是一种乐观的并发策略。
最后,看下acquireQueued方法
final boolean acquireQueued(final Node node, long arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
至此,关于获取的方法源码已经分析完毕,我们来简单总结下
一个首先的tryAcquire获取同步状态,成功则直接返回;否则,进入下一环节;
B线程获取同步状态失败,就构造一个结点,加入同步队列中,这个过程要保证线程安全;
加入队列中的结点线程进入自旋状态,若是老二结点(即前驱结点为头结点),才有机会尝试去获取同步状态;否则,当其前驱结点的状态为信号时,线程便可安心休息,进入阻塞状态,直到被中断或者被前驱结点唤醒。
释放同步状态-release(): :
当前线程执行完自己的逻辑之后,需要释放同步状态,来看看释放方法的逻辑
private void unparkSuccessor(Node node) {
//获取wait状态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);// 将等待状态waitStatus设置为初始值0
Node s = node.next;//后继结点
if (s == null || s.waitStatus > 0) {//若后继结点为空,或状态为CANCEL(已失效),则从后尾部往前遍历找到一个处于正常阻塞状态的结点 进行唤醒
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//使用LockSupprot唤醒结点对应的线程
}
发布的同步状态相对简单,需要找到头结点的后继结点进行唤醒,若后继结点为空或处于取消状态,从后向前遍历找寻一个正常的结点,唤醒其对应线程。
共享式
共享式:共享式地获取同步状态对于独占式同步组件来讲,同一时刻只有一个线程能获取到同步状态,其他线程都得去排队等待,其待重写的尝试获取同步状态的方法的tryAcquire返回值为布尔值,这很容易理解。对于共享式同步组件来讲,同一时刻可以有多个线程同时获取到同步状态,这也是“共享”的意义所在其待重写的尝试获取同步状态的方法tryAcquireShared返回值为int类型。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
1.当返回值大于0时,表示获取同步状态成功,同时还有剩余同步状态可供其他线程获取;
2.当返回值等于0时,表示获取同步状态成功,但没有可用同步状态了;
3 。当返回值小于0时,表示获取同步状态失败。
获取同步状态-acquireShared ::
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)//返回值小于0,获取同步状态失败,排队去;获取同步状态成功,直接返回去干自己的事儿。
doAcquireShared(arg);
}
doAcquireShared:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//构造一个共享结点,添加到同步队列尾部。若队列初始为空,先添加一个无意义的傀儡结点,再将新节点添加到队列尾部。
boolean failed = true;//是否获取成功
try {
boolean interrupted = false;//线程parking过程中是否被中断过
for (;;) {//死循环
final Node p = node.predecessor();//找到前驱结点
if (p == head) {//头结点持有同步状态,只有前驱是头结点,才有机会尝试获取同步状态
int r = tryAcquireShared(arg);//尝试获取同步装填
if (r >= 0) {//r>=0,获取成功
setHeadAndPropagate(node, r);//获取成功就将当前结点设置为头结点,若还有可用资源,传播下去,也就是继续唤醒后继结点
p.next = null; // 方便GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&//是否能安心进入parking状态
parkAndCheckInterrupt())//阻塞线程
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
释放同步状态releaseShared:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();//释放同步状态
return true;
}
return false;
}
doReleaseShared:
private void doReleaseShared() {
for (;;) {//死循环,共享模式,持有同步状态的线程可能有多个,采用循环CAS保证线程安全
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);//唤醒后继结点
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
代码逻辑比较容易理解,需要注意的是,共享模式,释放同步状态也是多线程的,此处采用了CAS自旋来保证。
总结:
关于AQS的介绍及源码分析到此为止了.AQS
是JUC 中很多同步组件的构建基础,简单来讲,它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,获取同步状态状态失败的线程,会被构造成一个结点(或共享式或独占式)加入到同步队列尾部(采用自旋CAS来保证此操作的线程安全),随后线程会阻塞;释放时唤醒头结点的后继结点,使其加入对同步状态的争夺中
AQS为我们定义好了顶层的处理实现逻辑,我们在使用AQS构建符合我们需求的同步组件时,只需重写的tryAcquire,tryAcquireShared,tryRelease,tryReleaseShared几个方法,来决定同步状态的释放和获取即可,至于背后复杂的线程排队,线程阻塞/唤醒,如何保证线程安全,都由AQS为我们完成了,这也是非常典型的模板方法的应用.AQS定义好顶级逻辑的骨 ,并提取出公用的线程入队列/出队列,阻塞/唤醒等一系列复杂逻辑的实现,将部分简单的可由使用者决定的操作逻辑延迟到子类中去实现。
并发之AbstractQueuedLongSynchronize----AQS的更多相关文章
- 并发之AQS原理(三) 如何保证并发
并发之AQS原理(三) 如何保证并发 1. 如何保证并发 AbstractQueuedSynchronizer 维护了一个state(代表了共享资源)和一个FIFO线程等待队列(多线程竞争资源被阻塞时 ...
- 并发之AQS原理(一) 原理介绍简单使用
并发之AQS原理(一) 如果说每一个同步的工具各有各的强大,那么这个强大背后是一个相同的动力,它就是AQS. AQS是什么 AQS是指java.util.concurrent.locks包里的Abst ...
- 并发之AQS原理(二) CLH队列与Node解析
并发之AQS原理(二) CLH队列与Node解析 1.CLH队列与Node节点 就像通常医院看病排队一样,医生一次能看的病人数量有限,那么超出医生看病速度之外的病人就要排队. 一条队列是队列中每一个人 ...
- Java并发之AQS原理解读(三)
上一篇:Java并发之AQS原理解读(二) 前言 本文从源码角度分析AQS共享锁工作原理,并介绍下使用共享锁的子类如何工作的. 共享锁工作原理 共享锁与独占锁的不同之处在于,获取锁和释放锁成功后,都会 ...
- Java并发之AQS原理解读(二)
上一篇: Java并发之AQS原理解读(一) 前言 本文从源码角度分析AQS独占锁工作原理,并介绍ReentranLock如何应用. 独占锁工作原理 独占锁即每次只有一个线程可以获得同一个锁资源. 获 ...
- Java并发之AQS原理解读(一)
前言 本文简要介绍AQS以及其中两个重要概念:state和Node. AQS 抽象队列同步器AQS是java.util.concurrent.locks包下比较核心的类之一,包括AbstractQue ...
- Java并发之AQS详解
一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同步器,AQ ...
- 并发之AQS
一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同步器,AQ ...
- Java并发之AQS同步器学习
AQS队列同步器学习 在学习并发的时候,我们一定会接触到 JUC 当中的工具,JUC 当中为我们准备了很多在并发中需要用到的东西,但是它们都是基于AQS(AbstractQueuedSynchroni ...
- Java并发之AQS详解(转)
一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronized(AQS)! 类如其名,抽象的队列式的同步器,AQ ...
随机推荐
- get传输时,会将加号+ 转换为空格
解决办法: 前端: 替换加号为 ‘%2B’, 后端: 直接接收即可.
- Python创建CRNN训练用的LMDB数据库文件
CRNN简介 CRNN由 Baoguang Shi, Xiang Bai, Cong Yao提出,2015年7月发表论文:"An End-to-End Trainable Neural Ne ...
- [python] 获得所有的最长公共子序列
两句闲话 得到两个序列的最长公共子序列(LCS)是个经典问题,使用动态规划,实现起来并不难. 一般来说,我们只是输出一个LCS.但是,老师布置的作业是输出所有的LCS. 解法 按照一般的方法,我们首先 ...
- HihoCoder1622 : 有趣的子区间(预处理+组合数)
有趣的子区间 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 如果一个区间[a, b]内恰好包含偶数个回文整数,我们就称[a, b]是有趣的区间. 例如[9, 12]包含 ...
- 大容量txt数据导入SQL Server助攻记
小伙伴们有个数据竞赛,提供的数据是944MB大小的TXT数据文档,导入SQL遇到一些麻烦.于是帮着解决,顺便也熟练了SQL Server的一些操作----- 打开如此大的txt需要的时间很长,而且不全 ...
- model里面字段choices的values值的选择
代码如下: Model: class Person(models.Model): name = models.CharField(max_length=200) CATEGORY_CHOICES = ...
- 7天学会HTML--HTML综述
一周学会HTML 1.HTML是什么? HTML 指的是超文本标记语言 (Hyper Text Markup Language) 2.HTML发展历程 HTML版本从1.0到4.0不断升级,其版本的规 ...
- Oracle中的存储过程简单例子
--创建表create table TESTTABLE( id1 VARCHAR2(12), name VARCHAR2(32))select t.id1,t.name from TESTTAB ...
- java实现一个最简单的tomcat服务
在了解tomcat的基本原理之前,首先要了解tomcatt最基本的运行原理. 1.如何启动? main方法是程序的入口,tomcat也不例外,查看tomcat源码,发现main是在Bootstrap ...
- git 基本操作 https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000/0013744142037508cf42e51debf49668810645e02887691000
1.创建版本库 (即仓库 repository)简单理解为一个目录,这个目录里的所有文件都可以被git管理起来,每个文件的修改删除,git都能跟踪,一边任何时刻都可以追踪历史,或者在将来某个时刻可以 ...