Java中线程并发同步的锁分三类,其一就是CAS,其二是sycronized的实现,其三是基于AQS(abstract queue sycronizer 抽象队列同步器)的Lock,reentrantLock,reentrantReadWriteLock实现等。JAVA中轻量级锁也就是CAS了,对于reentrantLock,Lock,reentrantLock,reentrantReadWriteLock是jdk封装自实现锁的公平与否,中断机制,还有锁分离,锁定时等(reentrantReadWriteLock读写锁分离)。对于关键字sycronized本身也是jdk自身设计封装,而且还加了锁的膨胀技术(从偏向锁,轻量级锁再到重量级锁)。
sycronized与Lock/reentrantLock等的相同点:
1.都是jdk自身封装的,都有自身相关的实现逻辑,但是底层都是调用操作系统方法pthread_mutex_lock来实现线程阻塞互斥的,对于何时调用该方法,那是jdk自己判断何时竞争了,且竞争到互斥区,所以jdk对互斥区有一个标记(jvm字节码指令 lock_lable标记的)。所以当线程被挂起的时候,就会被内核切换到用户空间。
2.在锁竞争的都有一个虚拟的等待队列,等待队列都是一个虚拟队列,不存在队列实例,仅存在节点之间的前后关系。
3.在锁竞争过程中被激活的线程都是CAS操作来获取锁,这就是竞争的机制。
sycronized与Lock/reentrantLock等的区别点:
1.sycronized 通过monitor监视器,等待队列,互斥区等组成的,sycronized的线程获取锁的机制有锁膨胀过程由jvm自身控制,Lock/reentrantLock等锁是直接通过代码xx.lock()来直接获取锁,且释放锁是由unLock()是释放。
2.Lock/reentrantLock等锁是通过jdkAQS的一套逻辑实现的,有一个等待队列,以及加锁的cas操作逻辑,当然还实现了公平锁的FIFO顺序获取锁,还有锁的分离,锁的中断,锁的定时等。
在java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLock、ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖java.util.concurrent.AbstractQueuedSynchronizer类,实现思路都大同小异,因此我们以ReentrantLock作为讲解切入点。
经过观察ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer:
static abstract class Sync extends AbstractQueuedSynchronizer
Sync又有两个子类:
final static class NonfairSync extends Sync
final static class FairSync extends Sync
显然是为了支持公平锁和非公平锁而定义,默认情况下为非公平锁。
先理一下Reentrant.lock()方法的调用过程(默认非公平锁):
这些讨厌的Template模式导致很难直观的看到整个调用过程,其实通过上面调用过程及AbstractQueuedSynchronizer的注释可以发现,AbstractQueuedSynchronizer中抽象了绝大多数Lock的功能,而只把tryAcquire方法延迟到子类中实现。tryAcquire方法的语义在于用具体子类判断请求线程是否可以获得锁,无论成功与否AbstractQueuedSynchronizer都将处理后面的流程。
简单说来,AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态,经过调查线程的显式阻塞是通过调用LockSupport.park()完成,而LockSupport.park()则调用sun.misc.Unsafe.park()本地方法,再进一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数把线程交给系统内核进行阻塞。
该队列如图:
与synchronized相同的是,这也是一个虚拟队列,不存在队列实例,仅存在节点之间的前后关系。令人疑惑的是为什么采用CLH队列呢?原生的CLH队列是用于自旋锁,但Doug Lea把其改造为阻塞锁。
当有线程竞争锁时,该线程会首先尝试获得锁,这对于那些已经在队列中排队的线程来说显得不公平,这也是非公平锁的由来,与synchronized实现类似,这样会极大提高吞吐量。
如果已经存在Running线程,则新的竞争线程会被追加到队尾,具体是采用基于CAS的Lock-Free算法,因为线程并发对Tail调用CAS可能会导致其他线程CAS失败,解决办法是循环CAS直至成功。AbstractQueuedSynchronizer的实现非常精巧,令人叹为观止,不入细节难以完全领会其精髓,下面详细说明实现过程:
2.1 Sync.nonfairTryAcquire
nonfairTryAcquire方法将是lock方法间接调用的第一个方法,每次请求锁时都会首先调用该方法。
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;
}
该方法会首先判断当前状态,如果c==0说明没有线程正在竞争该锁,如果不c !=0 说明有线程正拥有了该锁。 如果发现c==0,则通过CAS设置该状态值为acquires,acquires的初始调用值为1,每次线程重入该锁都会+1,每次unlock都会-1,但为0时释放锁。如果CAS设置成功,则可以预计其他任何线程调用CAS都不会再成功,也就认为当前线程得到了该锁,也作为Running线程,很显然这个Running线程并未进入等待队列。 如果c !=0 但发现自己已经拥有锁,只是简单地++acquires,并修改status值,但因为没有竞争,所以通过setStatus修改,而非CAS,也就是说这段代码实现了偏向锁的功能,并且实现的非常漂亮。
2.2 AbstractQueuedSynchronizer.addWaiter
addWaiter方法负责把当前无法获得锁的线程包装为一个Node添加到队尾:
private Node addWaiter(Node mode) {
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) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
其中参数mode是独占锁还是共享锁,默认为null,独占锁。追加到队尾的动作分两步:
如果当前队尾已经存在(tail!=null),则使用CAS把当前线程更新为Tail
如果当前Tail为null或则线程调用CAS设置队尾失败,则通过enq方法继续设置Tail
下面是enq方法:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
Node h = new Node(); // Dummy header
h.next = node;
node.prev = h;
if (compareAndSetHead(h)) {
tail = node;
return h;
}
}
else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
该方法就是循环调用CAS,即使有高并发的场景,无限循环将会最终成功把当前线程追加到队尾(或设置队头)。总而言之,addWaiter的目的就是通过CAS把当前线程追加到队尾,并返回包装后的Node实例。
把线程要包装为Node对象的主要原因,除了用Node构造供虚拟队列外,还用Node包装了各种线程状态,这些状态被精心设计为一些数字值:
- SIGNAL(-1) :线程的后继线程正/已被阻塞,当该线程release或cancel时要重新这个后继线程(unpark)
- CANCELLED(1):因为超时或中断,该线程已经被取消
- CONDITION(-2):表明该线程被处于条件队列,就是因为调用了Condition.await而被阻塞
- PROPAGATE(-3):传播共享锁
- 0:0代表无状态
2.3 AbstractQueuedSynchronizer.acquireQueued
acquireQueued的主要作用是把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (RuntimeException ex) {
cancelAcquire(node);
throw ex;
}
}
仔细看看这个方法是个无限循环,感觉如果p == head && tryAcquire(arg)条件不满足循环将永远无法结束,当然不会出现死循环,奥秘在于第12行的parkAndCheckInterrupt会把当前线程挂起,从而阻塞住线程的调用栈。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
如前面所述,LockSupport.park最终把线程交给系统(Linux)内核进行阻塞。当然也不是马上把请求不到锁的线程进行阻塞,还要检查该线程的状态,比如如果该线程处于Cancel状态则没有必要,具体的检查在shouldParkAfterFailedAcquire中:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 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;
}
检查原则在于:
规则1:如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将导致线程阻塞
规则2:如果前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,导致线程阻塞
规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与规则2同
总体看来,shouldParkAfterFailedAcquire就是靠前继节点判断当前线程是否应该被阻塞,如果前继节点处于CANCELLED状态,则顺便删除这些节点重新构造队列。
至此,锁住线程的逻辑已经完成,下面讨论解锁的过程。
综上,lock的实现其一是cas操作,其二是在Linux下是通过pthread_mutex_unlock完成。pthread_mutex_lock用户给mutex加锁,pthread_mutex_unlock用于给mutex解锁。synchronize 是由monitorenter监视器进行mutex lock的lock()和unluck(),所以系统层面上两者都是基于系统内核的锁。说到这里不得不讲下内核线程(轻量级进程)和jvm中线程对应关系(从这里可以预见是一一对应)
Linux从内核2.6开始使用NPTL (Native POSIX Thread Library)支持,但这时线程本质上还轻量级进程。
Java里的线程是由JVM来管理的,它如何对应到操作系统的线程是由JVM的实现来确定的。Linux 2.6上的HotSpot使用了NPTL机制,JVM线程跟内核轻量级进程有一一对应的关系。线程的调度完全交给了操作系统内核,当然jvm还保留一些策略足以影响到其内部的线程调度,举个例子,在linux下,只要一个Thread.run就会调用一个fork产生一个线程。
Java线程在Windows及Linux平台上的实现方式,现在看来,是内核线程的实现方式。这种方式实现的线程,是直接由操作系统内核支持的——由内核完成线程切换,内核通过操纵调度器(Thread Scheduler)实现线程调度,并将线程任务反映到各个处理器上。内核线程是内核的一个分身。程序一般不直接使用该内核线程,而是使用其高级接口,即轻量级进程(LWP),也即线程。这看起来可能很拗口。看图:
(说明:KLT即内核线程Kernel Thread,是“内核分身”。每一个KLT对应到进程P中的某一个轻量级进程LWP(也即线程),期间要经过用户态、内核态的切换,并在Thread Scheduler 下反应到处理器CPU上)
这种线程实现的方式也有它的缺陷:在程序面上使用内核线程,必然在操作系统上多次来回切换用户态及内核态;另外,因为是一对一的线程模型,LWP的支持数是有限的。
对于一个大型程序,我们可以开辟的线程数量至少等于运行机器的cpu内核数量。java程序里我们可以通过下面的一行代码得到这个数量:Runtime.getRuntime().availableProcessors();
所以最小线程数量即时cpu内核数量。如果所有的任务都是计算密集型的,这个最小线程数量就是我们需要的线程数。开辟更多的线程只会影响程序的性能,因为线程之间的切换工作,会消耗额外的资源。如果任务是IO密集型的任务,我们可以开辟更多的线程执行任务。当一个任务执行IO操作的时候,线程将会被阻塞,处理器立刻会切换到另外一个合适的线程去执行。如果我们只拥有与内核数量一样多的线程,即使我们有任务要执行,他们也不能执行,因为处理器没有可以用来调度的线程。
如果线程有50%的时间被阻塞,线程的数量就应该是内核数量的2倍。如果更少的比例被阻塞,那么它们就是计算密集型的,则需要开辟较少的线程。如果有更多的时间被阻塞,那么就是IO密集型的程序,则可以开辟更多的线程。于是我们可以得到下面的线程数量计算公式:线程数量=内核数量 / (1 - 阻塞率)
请求锁不成功的线程会被挂起在acquireQueued方法的第12行,12行以后的代码必须等线程被解锁锁才能执行,假如被阻塞的线程得到解锁,则执行第13行,即设置interrupted = true,之后又进入无限循环。
从无限循环的代码可以看出,并不是得到解锁的线程一定能获得锁,必须在第6行中调用tryAccquire重新竞争,因为锁是非公平的,有可能被新加入的线程获得,从而导致刚被唤醒的线程再次被阻塞,这个细节充分体现了“非公平”的精髓。通过之后将要介绍的解锁机制会看到,第一个被解锁的线程就是Head,因此p == head的判断基本都会成功。
至此可以看到,把tryAcquire方法延迟到子类中实现的做法非常精妙并具有极强的可扩展性,令人叹为观止!当然精妙的不是这个Template设计模式,而是Doug Lea对锁结构的精心布局。
解锁代码相对简单,主要体现在AbstractQueuedSynchronizer.release和Sync.tryRelease方法中:
class AbstractQueuedSynchronizer
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
class Sync
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;
}
tryRelease与tryAcquire语义相同,把如何释放的逻辑延迟到子类中。
tryRelease语义很明确:如果线程多次锁定,则进行多次释放,直至status==0则真正释放锁,所谓释放锁即设置status为0,因为无竞争所以没有使用CAS。
release的语义在于:如果可以释放锁,则唤醒队列第一个线程(Head),具体唤醒代码如下:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
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;
}
if (s != null)
LockSupport.unpark(s.thread);
}
这段代码的意思在于找出第一个可以unpark的线程,一般说来head.next == head,Head就是第一个线程,但Head.next可能被取消或被置为null,因此比较稳妥的办法是从后往前找第一个可用线程。貌似回溯会导致性能降低,其实这个发生的几率很小,所以不会有性能影响。之后便是通知系统内核继续该线程,在Linux下是通过pthread_mutex_unlock完成。之后,被解锁的线程进入上面所说的重新竞争状态。
AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。
synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。
当然Lock比synchronized更适合在应用层扩展,可以继承AbstractQueuedSynchronizer定义各种实现,比如实现读写锁(ReadWriteLock),公平或不公平锁;同时,Lock对应的Condition也比wait/notify要方便的多、灵活的多。
synchronized 原理:偏向锁、轻量级锁都是JVM引入的锁优化手段,目的是降低线程同步的开销。比如以下的同步代码块:
synchronized (lockObject) {
// do something
}
上述同步代码块中存在一个临界区,假设当前存在Thread#1和Thread#2这两个用户线程,分三种情况来讨论:
情况一:只有Thread#1会进入临界区;
情况二:Thread#1和Thread#2交替进入临界区;
情况三:Thread#1和Thread#2同时进入临界区。
上述的情况一是偏向锁的适用场景,此时当Thread#1进入临界区时,JVM会将lockObject的对象头Mark Word的锁标志位设为“01”,同时会用CAS操作把Thread#1的线程ID记录到Mark Word中,此时进入偏向模式。所谓“偏向”,指的是这个锁会偏向于Thread#1,若接下来没有其他线程进入临界区,则Thread#1再出入临界区无需再执行任何同步操作。也就是说,若只有Thread#1会进入临界区,实际上只有Thread#1初次进入临界区时需要执行CAS操作,以后再出入临界区都不会有同步操作带来的开销。
然而情况一是一个比较理想的情况,更多时候Thread#2也会尝试进入临界区。若Thread#2尝试进入时Thread#1已退出临界区,即此时lockObject处于未锁定状态,这时说明偏向锁上发生了竞争(对应情况二),此时会撤销偏向,Mark Word中不再存放偏向线程ID,而是存放hashCode和GC分代年龄,同时锁标识位变为“01”(表示未锁定),这时Thread#2会获取lockObject的轻量级锁。因为此时Thread#1和Thread#2交替进入临界区,所以偏向锁无法满足需求,需要膨胀到轻量级锁。
再说轻量级锁什么时候会膨胀到重量级锁。若一直是Thread#1和Thread#2交替进入临界区,那么没有问题,轻量锁hold住。一旦在轻量级锁上发生竞争,即出现“Thread#1和Thread#2同时进入临界区”的情况,轻量级锁就hold不住了。 (根本原因是轻量级锁没有足够的空间存储额外状态,此时若不膨胀为重量级锁,则所有等待轻量锁的线程只能自旋,可能会损失很多CPU时间)
MarkWord:
锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。
Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构如下:
在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储以下4种数据:
在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:
上面是最简单的说法或者理解,但是实际上有很多的切换细节设计性能优化问题。偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁。
一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。(偏向锁只进行一次cas操作)
一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象时偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。
如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。
为什么此时升级为重量级锁呢(这样会将所有竞争线程挂起);原因在于:假如很多线程都是处于轻量级上,都处于自旋状态,那么其中正在运行一个线程以及释放锁了,但是此时有很多线程在自旋,都在相互竞争。可能造成所有的线程都会一直自旋下去,cpu空转。
【题目描述】哥德巴赫猜想的命题之一是:大于6 的偶数等于两个素数之和。编程将6~100所有偶数表示成两个素数之和。【输入】(无)【输出】分行输出:例如:6=3+38=3+5…(每个数只拆开一次,请保证第一个加数最小)【输入样例】(无)【输出样例】(无)#include<iostream>#include<cmath>using namespace std;bool judge(int x);bool panduan(int y);int mai_c++编程实现从6到100之间哥德巴赫数输出
MyBatisPlus为什么要学它?MyBatisPlus可以节省我们大量的时间,所有CRUD代码都可以自动完成简介官网:https://baomidou.com/MyBatis的搭档2.特性无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各_mybatisplus creating a new sqlsession 日志屏蔽
a)程序硬件复位或者软件复位 b)判断mp/mc是否为0,微计算机模式(为1,当为微处理器模式时,2812内部的bootrom被禁止,通过zone7从外部调引导程序启动。) c)为0则从boot rom启动,否则从外部启动(0x3F FC00)
文章目录前言一、驱动二、JDBC三、ODBC四、其他驱动前言今天我们来看gaussDB 100的开发。GaussDB 数据库驱动一、驱动什么是驱动?数据库驱动是应用程序和数据库存储之间的一种接口,数据库厂商为了某一种开发语言环境(比如Java,C)能够实现数据库调用而开发的类似翻译员功能的程序,将复杂的数据库操作与通信抽象成为了当前开发语言的访问接口。GaussDB 100同时支持J..._高斯数据库驱动
国庆这几天,一直闲着没事干,偶然在硬盘的角落翻到了,之前汉化的r233,决定重新汉化最新版r247这次汉化,将C2的界面简单汉化,一些基本词汇并没有汉化,只要是英语2级的都能看懂,还有一些生僻词也咩有汉化,只是将解释文档汉化,方便之前用英文版的过渡,也有助于学习英语。整体汉化约60%,还有些behavior什么的内置的什么东东暂时没敢汉化-_-|| tip:还有一些词翻译的比较那什..._truice1.38
一、先去下载Tomcat8.0的包:github地址:https://github.com/nieyangyang712/Tomcat_8.0百度云盘地址:链接: https://pan.baidu.com/s/1kCZSwt-Nj4qla5BSCuIU3Q 提取码: k7at二、拿到Tomcat8.0包后解压后目录如下:三、修改配置文件,服务文件: 在conf文件夹里面的:server.xml我这里把打包后的文件放在了D盘 tcc 文件里面了:注意:看你的打包...
1.什么是域名 域名类似于互联网上的门牌号码,是用于识别和定位互联网上计算机的层次结构式字符标识,与该计算机的互联网协议(IP)地址相对应。但相对于IP地址而言,更便于使用者理解和记忆。域名属于互联网上的基础服务,基于域名可以提供WWW、EMAIL、FTP等应用服务。2、为什么要注册.CN域名?注册CN域名有如下优点:1、显著的标识作用,体现自身的价值和定位; 2、_成名停靠
八卦基本知识八卦基本知识 1) 天干分阴阳 甲、丙、戊、庚、壬属阳。乙、丁、己、辛、癸属阴。 (2) 十二地支阴阳及生肖寅 子、寅、辰、午、申、戌、属阳。丑、卯、巳、未、酉、亥属阴 子属鼠、丑属牛、寅属虎、卯属兔、辰属龙、巳属蛇、午属马、未属羊、申属猴、酉属鸡、戌属狗、亥属猪。 (3) 天干五行及方位 甲、乙属木,位东方。 丙、丁属火,位南方。 戊、己属土,位中央..._八卦基础
前言我朋友也是个写了四年Java代码的程序员,跟女友已经恋爱多年,最近突然结婚了。他结婚以前,换了一家公司,咱俩就好久没见过面了。刚好今天出门办事碰上了,找了一家店坐一起喝酒聊天。我聊天时打趣他:“亏得你小子运气好,还能娶到老婆。咱们这一行,不是加班就是加班,天天就是和代码过日子。”朋友说:“做这一行也就工资看着高点,确实是每天都累死累活。我还不是一样的加班,都没时间陪老婆!”跟着又聊到了工作,朋友听到我还在写业务代码非常意外:“你还在原来的公司做?我跳槽后就开始学习分布式架构、微服务、JVM调
_java高级知识图谱
直接上结论:(1)在release模式(O3)下,vector比list快十倍.(2)在debug模式(O0)下,两者相差不大,list还比vector快一点.分析: vector为随机访问容器,可直接通过下标访问数据,数据在内存连续排列,cache miss机率小。而list容器一般有头尾指针,遍历时需要跳转,内存分布不连接, cache miss机率大。但为什么debug模式下vector比list快,暂时不能解释,谁能回答一下万分感谢。..._vector list遍历效率对比
22.s,删除线效果(内联元素,常见标签但不被推荐使用)<!DOCTYPE HTML><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>s标签</title></head><..._禅道报