一、线程安全
1. 怎样让多线程下的类安全起来
无状态、加锁、让类不可变、栈封闭、安全的发布对象
2. 死锁
2.1 死锁概念及解决死锁的原则
一定发生在多个线程争夺多个资源里的情况下,发生的原因是每个线程拿到了某个(某些)资源不释放,同时等待着其他线程所持有的资源。
解决死锁的原则就是确保正确的获取资源的顺序,或者获取资源时使用定时尝试机制。
2.2 常见的死锁:
简单顺序死锁:
package com.study.deadlock.bank; /**
* 简单顺序死锁
* 解决办法:保证拿锁的顺序一致
* @author THINKPAD
*
*/
public class SimpleDeadLock { //左锁
private static Object left = new Object();
//右锁
private static Object right = new Object(); private static void leftToRight() throws InterruptedException {
synchronized (left){
System.out.println(Thread.currentThread().getName()+" get left");
Thread.sleep(100);
synchronized (right){
System.out.println(Thread.currentThread().getName()+" get right");
}
}
} private static void rightToLeft() throws InterruptedException {
synchronized (left){
System.out.println(Thread.currentThread().getName()+" get right-left");
Thread.sleep(100);
synchronized (right){
System.out.println(Thread.currentThread().getName()+" get left-right");
}
}
} private static class TestThread extends Thread{
private String name; public TestThread(String name) {
this.name = name;
} @Override
public void run(){
try {
rightToLeft();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} public static void main(String[] args) {
Thread.currentThread().setName("Main");
TestThread testThread = new TestThread("testThread");
testThread.start();
try {
leftToRight();
} catch (InterruptedException e) {
e.printStackTrace();
}
} }
动态顺序死锁:
A. 通过特殊手段保证拿锁的顺序一致
package com.study.deadlock.bank.serivice; import com.study.deadlock.bank.Account; /**
* 动态顺序死锁
* @author THINKPAD
* 解决办法:通过特殊手段保证拿锁的顺序一致,如获取要锁定对象的hash值,然后比较大小,先锁小的再锁大的
*/
public class SafeTransfer implements ITransfer { private static Object tieLock = new Object(); @Override
public void transfer(Account from, Account to, int amount)
throws InterruptedException { //获取要锁定对象的hash值,然后比较大小,先锁小的再锁大的
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to); if(fromHash<toHash){
//先锁小的
synchronized (from){
System.out.println(Thread.currentThread().getName()+" get "+from.getName());
Thread.sleep(100);
//再锁大的
synchronized (to){
System.out.println(Thread.currentThread().getName()
+" get "+to.getName());
from.flyMoney(amount);
to.addMoney(amount);
System.out.println(from);
System.out.println(to);
}
}
}
else if(toHash<fromHash){
//先锁小的
synchronized (to){
System.out.println(Thread.currentThread().getName()+" get "+to.getName());
Thread.sleep(100);
//再锁大的
synchronized (from){
System.out.println(Thread.currentThread().getName()
+" get "+from.getName());
from.flyMoney(amount);
to.addMoney(amount);
System.out.println(from);
System.out.println(to);
}
}
}
else{
//hash值相等时在前面再加一把锁
synchronized (tieLock){
synchronized (to){
System.out.println(Thread.currentThread().getName()+" get "+from.getName());
Thread.sleep(100);
synchronized (from){
System.out.println(Thread.currentThread().getName()
+" get "+to.getName());
from.flyMoney(amount);
to.addMoney(amount);
}
}
}
}
}
}
B. 定时轮询获取锁即定时尝试获取锁
package com.study.deadlock.bank; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; /**
* 账户信息
* @author THINKPAD
*
*/
public class Account {
private long number;
private final String name;
private int money;
private final Lock lock = new ReentrantLock(); public Lock getLock() {
return lock;
} public Account(String name, int amount) {
this.name = name;
this.money = amount;
} public String getName() {
return name;
} public int getAmount() {
return money;
} @Override
public String toString() {
return "Account{" +
"name='" + name + '\'' +
", money=" + money +
'}';
} public void addMoney(int amount){
money = money + amount;
} public void flyMoney(int amount){
money = money - amount;
}
}
package com.study.deadlock.bank.serivice; import com.study.deadlock.bank.Account; import java.util.Random; /**
* 动态顺序死锁
* @author THINKPAD
*
* 解决办法:
* 定时轮询获取锁即定时尝试获取锁
*/
public class TryLockTransfer implements ITransfer {
@Override
public void transfer(Account from, Account to, int amount)
throws InterruptedException {
Random r = new Random();
while(true){
if(from.getLock().tryLock()){
try{
System.out.println(Thread.currentThread().getName()
+" get from "+from.getName()); if(to.getLock().tryLock()){
try{
System.out.println(Thread.currentThread().getName()
+" get to "+to.getName());
from.flyMoney(amount);
to.addMoney(amount);
System.out.println(from);
System.out.println(to);
break;
}finally {
to.getLock().unlock();
}
} }finally {
from.getLock().unlock();
}
}
Thread.sleep(r.nextInt(5));//防止产生活锁
}
}
}
3. 活锁
概念:多个线程同时获取锁,当去拿另外的一把锁时发现被其他线程持有,觉得其他线程可能需要自己手中的锁就释放自己持有的锁,这样不断的循环就产生了活锁
4. 对性能的思考
4.1、 程序的安全性优于性能的提升
4.2、 使用多线程会带来额外的性能开销,滥用线程,有可能导致得不偿失。
4.3、 所谓性能,包含多个指标。例如“多快”:服务时间、等待时间、延迟时间;例如“多少”:吞吐量,例如可伸缩性等等。
4.4、 性能的各个指标方面,是完全独立的,有时候甚至是相互矛盾。
4.5、 所以性能的提升是个包括成本在内多方面权衡和妥协的结果。
性能优化的黄金原则:
首先保证程序正确,然后再提高运行速度(如果有确切的证据表明程序确实慢)。
5. Amdahl定律
F :程序中的串行部分,是个百分比(100%-1%),
N:cpu的个数
Speedup:指在增加cpu的情况下,程序的加速比
注意:任何程序都会有串行部分
6. 线程引入的开销
上下文的切换
内存同步
阻塞
7. 减少锁的竞争
快进快出,缩小锁的范围,将与锁无关的,有大量计算或者阻塞操作的代码移出同步范围。
减小锁的粒度,多个相互独立的状态变量可以使用多个锁来保护,每个锁只保护一个变量。
锁的分段,例如ConcurrentHashMap中的实现。
减少独占锁的使用,例如读多写少的情况下,用读写锁替换排他锁。
8. 安全的单例模式
懒汉式单例
package com.study.dcl; /**
* 懒汉式单例-双重检查
* @author THINKPAD
*
*/
public class SingleDcl { //双重检查不能保证线程安全,原因是第一个线程可能还没有初始化完,
//第二个线程就进来获取单例对象使用了,所以加一个volatile修饰保证可见性
private volatile static SingleDcl single;
private SingleDcl(){} public static SingleDcl getInstance(){
if(null==single){
synchronized (SingleDcl.class){
if(single==null){
single = new SingleDcl();
}
}
}
return single;
} }
饿汉式单例
package com.study.dcl; /**
* 饿汉式单例-线程安全
* @author THINKPAD
*
*/
public class SingleEHan {
public static SingleEHan singleEHan = new SingleEHan();
private SingleEHan(){};
}
延迟类占位符单例
package com.study.dcl; /**
* 延迟类占位符单例,利用JVM的类加载的时候会自动给加载的类加上锁的机制
* @author THINKPAD
*
*/
public class SingleClassInit {
private SingleClassInit(){} private static class InstanceHolder{
public static SingleClassInit instance = new SingleClassInit();
} public static SingleClassInit getInstance(){
return InstanceHolder.instance;
}
}
总结:单例建议使用延迟类占位符单例和枚举类型的单例
二、JMM
JMM: Java内存模型(Java Memory model)
由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。所谓的“内存模型”一词,可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
在JMM中如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。
1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。
三、底层实现原理
1、volatile的实现原理
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。
可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
volatile是如何来保证可见性的呢
是因为在编译的时候使用了一个Lock前缀的指令,Lock前缀的指令在多核处理器下会引发了两件事情,
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
2、synchronized的实现原理
synchronized一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着Java SE 1.6对synchronized进行了各种优化之后,有些情况下它就并不那么重了。
Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
·对于普通同步方法,锁是当前实例对象。
·对于静态同步方法,锁是当前类的Class对象。
·对于同步方法块,锁是Synchonized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁到底存在哪里呢?锁里面会存储什么信息呢?
synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节。
Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率
偏向锁
HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如Locking、Unlocking及对Mark Word的Update等)。当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态
轻量级锁
(1)轻量级锁加锁
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
(2)轻量级锁解锁
轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。
重量级锁
重量锁在JVM中又叫对象监视器(Monitor),除了具备Mutex(0|1)互斥的功能,它还负责实现了Semaphore(信号量)的功能,也就是说它至少包含一个竞争锁的队列,和一个信号阻塞队列(wait队列),前者负责做互斥,后一个用于做线程同步。
锁的优缺点对比
锁 |
优点 |
缺点 |
适用场景 |
偏向锁 |
加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距 |
如果线程间存在锁竞争,会带来额外的锁撤销的消耗 |
适用于只有一个线程访问同步块场景 |
轻量级锁 |
竞争的线程不会阻塞,提高了程序的响应速度 |
如果始终得不到锁竞争的线程使用自旋会消耗CPU |
追求响应时间,锁占用时间很短 |
重量级锁 |
线程竞争不使用自旋,不会消耗CPU |
线程阻塞,响应时间缓慢 |
追求吞吐量,锁占用时间较长 |
3、原子操作的实现原理
使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。但是复杂的内存操作处理器是不能自动保证其原子性的。处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
(1)使用总线锁保证原子性
第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写操作(i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
(2)使用缓存锁保证原子性
在同一时刻,我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。缓存锁定就是当某块CPU对缓存中的数据进行操作了之后,就通知其他CPU放弃储存在它们内部的缓存,或者从主内存中重新读取。
处理器提供了很多Lock前缀的指令来实现。例如,位测试和修改指令:BTS、BTR、BTC;交换指令XADD、CMPXCHG。JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。在Java中可以通过锁和循环CAS的方式来实现原子操作。
4、重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2)指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
不管怎么重排序(编译器和处理器为了提高并行度),程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
例如:
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。
5、happens-before
为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序,为此jvm中提出了happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happensbefore关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。
happens-before关系的定义如下。
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见。
2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见。
6、volatile的内存语义
理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。
volatile写的内存语义如下。
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
volatile读的内存语义如下。
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
7、锁的内存语义
锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
java并发编程系列七:volatile和sinchronized底层实现原理的更多相关文章
-
干货:Java并发编程系列之volatile(二)
接上一篇<Java并发编程系列之synchronized(一)>,这是第二篇,说的是关于并发编程的volatile元素. Java语言规范第三版中对volatile的定义如下:Java编程 ...
-
Java并发编程系列-(5) Java并发容器
5 并发容器 5.1 Hashtable.HashMap.TreeMap.HashSet.LinkedHashMap 在介绍并发容器之前,先分析下普通的容器,以及相应的实现,方便后续的对比. Hash ...
-
Java并发编程系列-(4) 显式锁与AQS
4 显示锁和AQS 4.1 Lock接口 核心方法 Java在java.util.concurrent.locks包中提供了一系列的显示锁类,其中最基础的就是Lock接口,该接口提供了几个常见的锁相关 ...
-
Java并发编程系列-(1) 并发编程基础
1.并发编程基础 1.1 基本概念 CPU核心与线程数关系 Java中通过多线程的手段来实现并发,对于单处理器机器上来讲,宏观上的多线程并行执行是通过CPU的调度来实现的,微观上CPU在某个时刻只会运 ...
-
Java并发编程系列-(7) Java线程安全
7. 线程安全 7.1 线程安全的定义 如果多线程下使用这个类,不过多线程如何使用和调度这个类,这个类总是表示出正确的行为,这个类就是线程安全的. 类的线程安全表现为: 操作的原子性 内存的可见性 不 ...
-
Java并发编程系列-(8) JMM和底层实现原理
8. JMM和底层实现原理 8.1 线程间的通信与同步 线程之间的通信 线程的通信是指线程之间以何种机制来交换信息.在编程中,线程之间的通信机制有两种,共享内存和消息传递. 在共享内存的并发模型里,线 ...
-
Java并发编程之验证volatile不能保证原子性
Java并发编程之验证volatile不能保证原子性 通过系列文章的学习,凯哥已经介绍了volatile的三大特性.1:保证可见性 2:不保证原子性 3:保证顺序.那么怎么来验证可见性呢?本文凯哥(凯 ...
-
Java并发编程之三:volatile关键字解析 转载
目录: <Java并发编程之三:volatile关键字解析 转载> <Synchronized之一:基本使用> volatile这个关键字可能很多朋友都听说过,或许也都用过 ...
-
Java并发编程系列-(3) 原子操作与CAS
3. 原子操作与CAS 3.1 原子操作 所谓原子操作是指不会被线程调度机制打断的操作:这种操作一旦开始,就一直运行到结束,中间不会有任何context switch,也就是切换到另一个线程. 为了实 ...
随机推荐
-
bgp多线
BGP(边界网关协议)主要用于互联网AS(自治系统)之间的互联,BGP的最主要功能在于控制路由的传播和选择最好的路由. 中国网通 .中国电信.中国铁通.教育网和一些大的民营IDC运营商都具有AS号,全 ...
-
Linux如何开机自动运行自己的脚本
博客分类: LINUX 脚本LinuxCentOSWindowsBash 记录这个事情是上次完成之后,今天要新加一个文件夹,一时之间忘记以前怎么做了,因为有几种方法,起码我知道三种方法,这里 ...
-
Queue and Message
#ifndef __QUEUE_H__ #define __QUEUE_H__ #include <stdint.h> #include <stdlib.h> #include ...
-
新手教程之使用Xib自定义UITableViewCell
新手教程之使用Xib自定义UITableViewCell 前言 首先:什么是UITableView?看图 其次:什么是cell? 然后:为什么要自定cell,UITableView不是自带的有cell ...
-
$response->;decoded_content; 和$response->;content; 乱码问题
centos6.5:/root/podinns/lib#cat t1.pl use LWP::UserAgent; use HTTP::Date qw(time2iso str2time time2i ...
-
$.ajax 提交数据到后台.
//AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML -- (Extensible Markup Language 可扩展标记语言 ...
-
殊途同归 - Church-Rosser and Consistency of Evaluation
在"解释语言的语言"中提到,设计一个新的语言,仅仅是使用meta-language给出其描述是不够的,我们还需要去验证一些性质. 考虑一下我们在"解释语言 ...
-
virsh命令详解
1.简介: virsh 有命令模式和交互模式如果直接在vrish后面添加参数是命令模式,如果直接写virsh,就会进入交互模式. 2.命令模式: virsh list 列出所有的虚拟机,虚拟机的 ...
-
django模板语言之Template
当前端的一些数据需要后端传送过来并展示时,用到了django的模板语言,模板语言的作用就是,在后端把一些处理好的数据,通过模板语言所规定的格式,通过render渲染,放到前端页面固定的位置展示.这之间 ...
-
Spring boot实现自定义拦截器
Sprintboot的拦截器提供了精细的控制:在request被响应之前.request被响应之后.request全部结束之后至视图渲染之前 三个时间点,我们都可以通过编写他们的函数来控制. 首先新建 ...