Git Product home page Git Product logo

cs-notes's Introduction

  • 👋 Hi, I’m @FxL2020
  • 👀 I’m interested in ...
  • 🌱 I’m currently learning ...
  • 💞️ I’m looking to collaborate on ...
  • 📫 How to reach me ...

cs-notes's People

Contributors

fxl2020 avatar

Stargazers

 avatar  avatar

Watchers

 avatar

cs-notes's Issues

用两个栈实现队列

问题:用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。
思路:
通过两个栈中元素之间的的复制交换去实现了队列的功能。
代码:

public class DoubleStack {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();   
    public void push(int node) {
        while(!stack2.isEmpty()){
			stack1.push(stack2.pop());
		}
		stack1.push(node);
    }    
    public int pop() {
        while(!stack1.isEmpty()){
			stack2.push(stack1.pop());
		}	
		return stack2.pop();	    
    }
}

Spring Data JPA

学习目标
一,JPA简介
二,Spring Data JPA用法介绍
三,Spring Data JPA,Hibernate与Spring Boot集成
四,数据持久层实战

一,JPA简介
JP是用于管理Java EE和Java SE环境中的持久化,以及对象/关系映射的Java API
实现:Hibernate
JPA核心概念
实体:表示关系型数据库中的表,每个实体的实例对应该表中的行,类必须用javax.persistence.Entity注解
类必须有一个public或者protected的无参数的构造函数
实体实例被当作值以分离对象方式进行传递(例如通过会话bean的远程业务接口),则该类必须实现serializable接口
唯一对象标识符:简单主键(javax.persistence.id),复合主键(javax.persistence.Embeddedid和javax.persistence.idClass)
关系:
一对一:@OnetoOne
一对多:@OneToMany
多对一:@manytoone
多对多:@manytomany
EntityManager:管理实体接口或类
定义用于与持久性上下文进行交互的方法
创建和删除持久性实体实例,通过实体的主键查找实体
允许在实体上运行查询

二,Spring Data JPA用法介绍
是更大的Spring Data家族的一部分
对基于JPA的数据访问层的增强支持
更容易构建基于使用Spring数据访问技术栈的应用程序
Spring Data JPA常用接口
CrudRepository:方便进行增删改查
方法:save(),findOne(),findAll(),count(),delete(),exists()
PagingAndSortingRepository:用于分页和排序
方法:findall(sort),findAll(Pageable)
Spring Data JPA自定义接口
根据方法名创建查询
集成repository或子接口
方法名要规范

三,Spring Data JPA,Hibernate与Spring Boot集成
配置环境
mysql community server 5.7.17
Spring Dara JPA 1.11.1.release
Hibernate 5.2.8.Final
MySQL Connector/J 6.0.5
修改build.gradle
dependencies{
compile(' ')
}

替换空格

题目:将一个字符串中的空格替换成 "%20"。
将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。
思路:
去遍历字符串,然后判断当前位置的字符是否为空格,如果为空格的话,就追加"%20",如果不为空格的话,那么就追加当前位置的字符
代码:

import java.util.Scanner;
public class ReplaceSpace {
	public static String replaceSpace(StringBuffer str){
		int len=str.length();
		String spe="%20";
		StringBuffer stb=new StringBuffer();
		for(int i=0;i<len;i++){
			/*if(str.charAt(i)==' '){
				stb.append(spe);
			}else{
				stb.append(str.charAt(i));
			}*/
			stb.append(str.charAt(i)==' '?spe:str.charAt(i));
		}
		return stb.toString();
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner sc=new Scanner(System.in);
		StringBuffer stb=new StringBuffer();
		stb.append(sc.nextLine());
		System.out.println(replaceSpace(stb));
	}
}

调整数组顺序使奇数位于偶数前面

题:输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。
**:
方法一:本质上就是开辟两个空间去存储奇数和偶数,最终将这两个空间中的值合并即可。时间复杂度是O(n),但是空间复杂度比方法一要大。

public class ReOrderArray {
    public void reOrderArray(int[] nums){
        ArrayList<Integer> list1=new ArrayList<>();
        ArrayList<Integer> list2=new ArrayList<>();
        for (int i=0;i<nums.length;i++){
            if(nums[i]%2==0){
                list1.add(nums[i]);
            }else {
                list2.add(nums[i]);
            }
        }
        int i=0;
        for(int x:list2){
            nums[i++]=x;
        }
        for(int x:list1){
            nums[i++]=x;
        }
    }
}

从尾到头打印链表

问题:输入一个链表,按链表从尾到头的顺序返回一个ArrayList。
思路:
通过Java中的Stack类去模拟栈的过程
代码:

import java.util.ArrayList;
import java.util.Stack;

public class ReverseList {
/**
 * 方法一:使用stack	
 * @param listNode
 * @return
 */
	public static  ArrayList<Integer> printReverseList(ListNode listNode){
		ArrayList<Integer> list=new ArrayList<>();
		Stack<Integer> stack=new Stack<>();
		while(listNode!=null){
			stack.add(listNode.val);
			listNode=listNode.next;			
		}
		while(!stack.isEmpty()){
			list.add(stack.pop());
		}		
		return list;		
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		/**
		 * 利用尾接法创建链表
		 */
		ListNode head=new ListNode(0);
		ListNode removeNode=head;
		for(int i=1;i<10;i++){
			ListNode x=new ListNode(i);
			x.next=null;
			removeNode.next=x;
			removeNode=x;		
		}
      System.out.println(printReverseList(head));
	}
}
class ListNode{
	int val;
	ListNode next=null;
	ListNode(int val){
		this.val=val;
	}	
}

并发

作者:冠状病毒biss
链接:https://www.nowcoder.com/discuss/447742?source_id=profile_create&channel=666
来源:牛客网

Java 线程的通信由 JMM 控制,JMM 的主要目的是定义程序中各种变量的访问规则。变量包括实例字段、静态字段,但不包括局部变量与方法参数,因为它们是线程私有的,不存在多线程竞争。JMM 遵循一个基本原则:只要不改变程序执行结果,编译器和处理器怎么优化都行。例如编译器分析某个锁只会单线程访问就消除锁,某个 volatile 变量只会单线程访问就把它当作普通变量。

JMM 规定所有变量都存储在主内存,每条线程有自己的工作内存,工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。不同线程间无法直接访问对方工作内存中的变量,线程通信必须经过主内存。

关于主内存与工作内存的交互,即变量如何从主内存拷贝到工作内存、从工作内存同步回主内存,JMM 定义了 8 种原子操作:

操作	作用变量范围	作用
lock	主内存	把变量标识为线程独占状态
unlock	主内存	释放处于锁定状态的变量
read	主内存	把变量值从主内存传到工作内存
load	工作内存	把 read 得到的值放入工作内存的变量副本
user	工作内存	把工作内存中的变量值传给执行引擎
assign	工作内存	把从执行引擎接收的值赋给工作内存变量
store	工作内存	把工作内存的变量值传到主内存
write	主内存	把 store 取到的变量值放入主内存变量中

Q2:as-if-serial 是什么?
不管怎么重排序,单线程程序的执行结果不能改变,编译器和处理器必须遵循 as-if-serial 语义。

为了遵循 as-if-serial,编译器和处理器不会对存在数据依赖关系的操作重排序,因为这种重排序会改变执行结果。但是如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

as-if-serial 把单线程程序保护起来,给程序员一种幻觉:单线程程序是按程序的顺序执行的。

Q3:happens-before 是什么?
先行发生原则,JMM 定义的两项操作间的偏序关系,是判断数据是否存在竞争的重要手段。

JMM 将 happens-before 要求禁止的重排序按是否会改变程序执行结果分为两类。对于会改变结果的重排序 JMM 要求编译器和处理器必须禁止,对于不会改变结果的重排序,JMM 不做要求。

JMM 存在一些天然的 happens-before 关系,无需任何同步器协助就已经存在。如果两个操作的关系不在此列,并且无法从这些规则推导出来,它们就没有顺序性保障,虚拟机可以对它们随意进行重排序。

程序次序规则:一个线程内写在前面的操作先行发生于后面的。
管程锁定规则: unlock 操作先行发生于后面对同一个锁的 lock 操作。
volatile 规则:对 volatile 变量的写操作先行发生于后面的读操作。
线程启动规则:线程的 start 方法先行发生于线程的每个动作。
线程终止规则:线程中所有操作先行发生于对线程的终止检测。
对象终结规则:对象的初始化先行发生于 finalize 方法。
传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C 。
Q4:as-if-serial 和 happens-before 有什么区别?
as-if-serial 保证单线程程序的执行结果不变,happens-before 保证正确同步的多线程程序的执行结果不变。

这两种语义的目的都是为了在不改变程序执行结果的前提下尽可能提高程序执行并行度。

Q5:什么是指令重排序?
为了提高性能,编译器和处理器通常会对指令进行重排序,重排序指从源代码到指令序列的重排序,分为三种:① 编译器优化的重排序,编译器在不改变单线程程序语义的前提下可以重排语句的执行顺序。② 指令级并行的重排序,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。③ 内存系统的重排序。

Q6:原子性、可见性、有序性分别是什么?
原子性

基本数据类型的访问都具备原子性,例外就是 long 和 double,虚拟机将没有被 volatile 修饰的 64 位数据操作划分为两次 32 位操作。

如果应用场景需要更大范围的原子性保证,JMM 还提供了 lock 和 unlock 操作满足需求,尽管 JVM 没有把这两种操作直接开放给用户使用,但是提供了更高层次的字节码指令 monitorenter 和 monitorexit,这两个字节码指令反映到 Java 代码中就是 synchronized。

可见性

可见性指当一个线程修改了共享变量时,其他线程能够立即得知修改。JMM 通过在变量修改后将值同步回主内存,在变量读取前从主内存刷新的方式实现可见性,无论普通变量还是 volatile 变量都是如此,区别是 volatile 保证新值能立即同步到主内存以及每次使用前立即从主内存刷新。

除了 volatile 外,synchronized 和 final 也可以保证可见性。同步块可见性由"对一个变量执行 unlock 前必须先把此变量同步回主内存,即先执行 store 和 write"这条规则获得。final 的可见性指:被 final 修饰的字段在构造方法中一旦初始化完成,并且构造方法没有把 this 引用传递出去,那么其他线程就能看到 final 字段的值。

有序性

有序性可以总结为:在本线程内观察所有操作是有序的,在一个线程内观察另一个线程,所有操作都是无序的。前半句指 as-if-serial 语义,后半句指指令重排序和工作内存与主内存延迟现象。

Java 提供 volatile 和 synchronized 保证有序性,volatile 本身就包含禁止指令重排序的语义,而 synchronized 保证一个变量在同一时刻只允许一条线程对其进行 lock 操作,确保持有同一个锁的两个同步块只能串行进入。

Q7:谈一谈 volatile
JMM 为 volatile 定义了一些特殊访问规则,当变量被定义为 volatile 后具备两种特性:

保证变量对所有线程可见

当一条线程修改了变量值,新值对于其他线程来说是立即可以得知的。volatile 变量在各个线程的工作内存中不存在一致性问题,但 Java 的运算操作符并非原子操作,导致 volatile 变量运算在并发下仍不安全。

禁止指令重排序优化

使用 volatile 变量进行写操作,汇编指令带有 lock 前缀,相当于一个内存屏障,后面的指令不能重排到内存屏障之前。

使用 lock 前缀引发两件事:① 将当前处理器缓存行的数据写回系统内存。②使其他处理器的缓存无效。相当于对缓存变量做了一次 store 和 write 操作,让 volatile 变量的修改对其他处理器立即可见。

静态变量 i 执行多线程 i++ 的不安全问题

自增语句由 4 条字节码指令构成的,依次为 getstatic、iconst_1、iadd、putstatic,当 getstatic 把 i 的值取到操作栈顶时,volatile 保证了 i 值在此刻正确,但在执行 iconst_1、iadd 时,其他线程可能已经改变了 i 值,操作栈顶的值就变成了过期数据,所以 putstatic 执行后就可能把较小的 i 值同步回了主内存。

适用场景

① 运算结果并不依赖变量的当前值。② 一写多读,只有单一的线程修改变量值。

内存语义

写一个 volatile 变量时,把该线程工作内存中的值刷新到主内存。

读一个 volatile 变量时,把该线程工作内存值置为无效,从主内存读取。

指令重排序特点

第二个操作是 volatile 写,不管第一个操作是什么都不能重排序,确保写之前的操作不会被重排序到写之后。

第一个操作是 volatile 读,不管第二个操作是什么都不能重排序,确保读之后的操作不会被重排序到读之前。

第一个操作是 volatile 写,第二个操作是 volatile 读不能重排序。

JSR-133 增强 volatile 语义的原因

在旧的内存模型中,虽然不允许 volatile 变量间重排序,但允许 volatile 变量与普通变量重排序,可能导致内存不可见问题。JSR-133 严格限制编译器和处理器对 volatile 变量与普通变量的重排序,确保 volatile 的写-读和锁的释放-获取具有相同的内存语义。

Q8:final 可以保证可见性吗?
final 可以保证可见性,被 final 修饰的字段在构造方法中一旦被初始化完成,并且构造方法没有把 this 引用传递出去,在其他线程中就能看见 final 字段值。

在旧的 JMM 中,一个严重缺陷是线程可能看到 final 值改变。比如一个线程看到一个 int 类型 final 值为 0,此时该值是未初始化前的零值,一段时间后该值被某线程初始化,再去读这个 final 值会发现值变为 1。

为修复该漏洞,JSR-133 为 final 域增加重排序规则:只要对象是正确构造的(被构造对象的引用在构造方法中没有逸出),那么不需要使用同步就可以保证任意线程都能看到这个 final 域初始化后的值。

写 final 域重排序规则

禁止把 final 域的写重排序到构造方法之外,编译器会在 final 域的写后,构造方法的 return 前,插入一个 Store Store 屏障。确保在对象引用为任意线程可见之前,对象的 final 域已经初始化过。

读 final 域重排序规则

在一个线程中,初次读对象引用和初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作。编译器在读 final 域操作的前面插入一个 Load Load 屏障,确保在读一个对象的 final 域前一定会先读包含这个 final 域的对象引用。

锁 17
Q1:谈一谈 synchronized
每个 Java 对象都有一个关联的 monitor,使用 synchronized 时 JVM 会根据使用环境找到对象的 monitor,根据 monitor 的状态进行加解锁的判断。如果成功加锁就成为该 monitor 的唯一持有者,monitor 在被释放前不能再被其他线程获取。

同步代码块使用 monitorenter 和 monitorexit 这两个字节码指令获取和释放 monitor。这两个字节码指令都需要一个引用类型的参数指明要锁定和解锁的对象,对于同步普通方法,锁是当前实例对象;对于静态同步方法,锁是当前类的 Class 对象;对于同步方法块,锁是 synchronized 括号里的对象。

执行 monitorenter 指令时,首先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执行 monitorexit 指令时会将锁计数器减 1。一旦计数器为 0 锁随即就被释放。

例如有两个线程 A、B 竞争 monitor,当 A 竞争到锁时会将 monitor 中的 owner 设置为 A,把 B 阻塞并放到等待资源的 ContentionList 队列。ContentionList 中的部分线程会进入 EntryList,EntryList 中的线程会被指定为 OnDeck 竞争候选者,如果获得了锁资源将进入 Owner 状态,释放锁后进入 !Owner 状态。被阻塞的线程会进入 WaitSet。

被 synchronized 修饰的同步块对一条线程来说是可重入的,并且同步块在持有锁的线程释放锁前会阻塞其他线程进入。从执行成本的角度看,持有锁是一个重量级的操作。Java 线程是映射到操作系统的内核线程上的,如果要阻塞或唤醒一条线程,需要操作系统帮忙完成,不可避免用户态到核心态的转换。

不公平的原因

所有收到锁请求的线程首先自旋,如果通过自旋也没有获取锁将被放入 ContentionList,该做法对于已经进入队列的线程不公平。

为了防止 ContentionList 尾部的元素被大量线程进行 CAS 访问影响性能,Owner 线程会在释放锁时将 ContentionList 的部分线程移动到 EntryList 并指定某个线程为 OnDeck 线程,该行为叫做竞争切换,牺牲了公平性但提高了性能。

Q2:锁优化有哪些策略?
JDK 6 对 synchronized 做了很多优化,引入了自适应自旋、锁消除、锁粗化、偏向锁和轻量级锁等提高锁的效率,锁一共有 4 个状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁,状态会随竞争情况升级。锁可以升级但不能降级,这种只能升级不能降级的锁策略是为了提高锁获得和释放的效率。

Q3:自旋锁是什么?
同步对性能最大的影响是阻塞,挂起和恢复线程的操作都需要转入内核态完成。许多应用上共享数据的锁定只会持续很短的时间,为了这段时间去挂起和恢复线程并不值得。如果机器有多个处理器核心,我们可以让后面请求锁的线程稍等一会,但不放弃处理器的执行时间,看看持有锁的线程是否很快会释放锁。为了让线程等待只需让线程执行一个忙循环,这项技术就是自旋锁。

自旋锁在 JDK1.4 就已引入,默认关闭,在 JDK6 中改为默认开启。自旋不能代替阻塞,虽然避免了线程切换开销,但要占用处理器时间,如果锁被占用的时间很短,自旋的效果就会非常好,反之只会白白消耗处理器资源。如果自旋超过了限定的次数仍然没有成功获得锁,就应挂起线程,自旋默认限定次数是 10。

Q4:什么是自适应自旋?
JDK6 对自旋锁进行了优化,自旋时间不再固定,而是由前一次的自旋时间及锁拥有者的状态决定。

如果在同一个锁上,自旋刚刚成功获得过锁且持有锁的线程正在运行,虚拟机会认为这次自旋也很可能成功,进而允许自旋持续更久。如果自旋很少成功,以后获取锁时将可能直接省略掉自旋,避免浪费处理器资源。

有了自适应自旋,随着程序运行时间的增长,虚拟机对程序锁的状况预测就会越来越精准。

Q5:锁消除是什么?
锁消除指即时编译器对检测到不可能存在共享数据竞争的锁进行消除。

主要判定依据来源于逃逸分析,如果判断一段代码中堆上的所有数据都只被一个线程访问,就可以当作栈上的数据对待,认为它们是线程私有的而无须同步。

Q6:锁粗化是什么?
原则需要将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中进行同步,这是为了使等待锁的线程尽快拿到锁。

但如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之外的,即使没有线程竞争也会导致不必要的性能消耗。因此如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把同步的范围扩展到整个操作序列的外部。

Q7:偏向锁是什么?
偏向锁是为了在没有竞争的情况下减少锁开销,锁会偏向于第一个获得它的线程,如果在执行过程中锁一直没有被其他线程获取,则持有偏向锁的线程将不需要进行同步。

当锁对象第一次被线程获取时,虚拟机会将对象头中的偏向模式设为 1,同时使用 CAS 把获取到锁的线程 ID 记录在对象的 Mark Word 中。如果 CAS 成功,持有偏向锁的线程以后每次进入锁相关的同步块都不再进行任何同步操作。

一旦有其他线程尝试获取锁,偏向模式立即结束,根据锁对象是否处于锁定状态决定是否撤销偏向,后续同步按照轻量级锁那样执行。

Q8:轻量级锁是什么?
轻量级锁是为了在没有竞争的前提下减少重量级锁使用操作系统互斥量产生的性能消耗。

在代码即将进入同步块时,如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前 Mark Word 的拷贝。然后虚拟机使用 CAS 尝试把对象的 Mark Word 更新为指向锁记录的指针,如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态。

如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的 Mark Word 是否指向当前线程的栈帧,如果是则说明当前线程已经拥有了锁,直接进入同步块继续执行,否则说明锁对象已经被其他线程抢占。如果出现两条以上线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为 10,此时Mark Word 存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。

解锁同样通过 CAS 进行,如果对象 Mark Word 仍然指向线程的锁记录,就用 CAS 把对象当前的 Mark Word 和线程复制的 Mark Word 替换回来。假如替换成功同步过程就顺利完成了,如果失败则说明有其他线程尝试过获取该锁,就要在释放锁的同时唤醒被挂起的线程。

Q9:偏向锁、轻量级锁和重量级锁的区别?
偏向锁的优点是加解锁不需要额外消耗,和执行非同步方法比仅存在纳秒级差距,缺点是如果存在锁竞争会带来额外锁撤销的消耗,适用只有一个线程访问同步代码块的场景。

轻量级锁的优点是竞争线程不阻塞,程序响应速度快,缺点是如果线程始终得不到锁会自旋消耗 CPU,适用追求响应时间、同步代码块执行快的场景。

重量级锁的优点是线程竞争不使用自旋不消耗CPU,缺点是线程会阻塞,响应时间慢,适应追求吞吐量、同步代码块执行慢的场景。

Q10:Lock 和 synchronized 有什么区别?
Lock 接是 juc 包的顶层接口,基于Lock 接口,用户能够以非块结构来实现互斥同步,摆脱了语言特性束缚,在类库层面实现同步。Lock 并未用到 synchronized,而是利用了 volatile 的可见性。

重入锁 ReentrantLock 是 Lock 最常见的实现,与 synchronized 一样可重入,不过它增加了一些高级功能:

*等待可中断: *持有锁的线程长期不释放锁时,正在等待的线程可以选择放弃等待而处理其他事情。
公平锁: 公平锁指多个线程在等待同一个锁时,必须按照申请锁的顺序来依次获得锁,而非公平锁不保证这一点,在锁被释放时,任何线程都有机会获得锁。synchronized 是非公平的,ReentrantLock 在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会急剧下降,影响吞吐量。
锁绑定多个条件: 一个 ReentrantLock 可以同时绑定多个 Condition。synchronized 中锁对象的 wait 跟 notify 可以实现一个隐含条件,如果要和多个条件关联就不得不额外添加锁,而 ReentrantLock 可以多次调用 newCondition 创建多个条件。
一般优先考虑使用 synchronized:① synchronized 是语法层面的同步,足够简单。② Lock 必须确保在 finally 中释放锁,否则一旦抛出异常有可能永远不会释放锁。使用 synchronized 可以由 JVM 来确保即使出现异常锁也能正常释放。③ 尽管 JDK5 时 ReentrantLock 的性能优于 synchronized,但在 JDK6 进行锁优化后二者的性能基本持平。从长远来看 JVM 更容易针对synchronized 优化,因为 JVM 可以在线程和对象的元数据中记录 synchronized 中锁的相关信息,而使用 Lock 的话 JVM 很难得知具体哪些锁对象是由特定线程持有的。

Q11:ReentrantLock 的可重入是怎么实现的?
以非公平锁为例,通过 nonfairTryAcquire 方法获取锁,该方法增加了再次获取同步状态的处理逻辑:判断当前线程是否为获取锁的线程来决定获取是否成功,如果是获取锁的线程再次请求则将同步状态值增加并返回 true,表示获取同步状态成功。

成功获取锁的线程再次获取锁将增加同步状态值,释放同步状态时将减少同步状态值。如果锁被获取了 n 次,那么前 n-1 次 tryRelease 方法必须都返回 fasle,只有同步状态完全释放才能返回 true,该方法将同步状态是否为 0 作为最终释放条件,释放时将占有线程设置为null 并返回 true。

对于非公平锁只要 CAS 设置同步状态成功则表示当前线程获取了锁,而公平锁则不同。公平锁使用 tryAcquire 方法,该方法与nonfairTryAcquire 的唯一区别就是判断条件中多了对同步队列中当前节点是否有前驱节点的判断,如果该方法返回 true 表示有线程比当前线程更早请求锁,因此需要等待前驱线程获取并释放锁后才能获取锁。

Q12:什么是读写锁?
ReentrantLock 是排他锁,同一时刻只允许一个线程访问,读写锁在同一时刻允许多个读线程访问,在写线程访问时,所有的读写线程均阻塞。读写锁维护了一个读锁和一个写锁,通过分离读写锁使并发性相比排他锁有了很大提升。

读写锁依赖 AQS 来实现同步功能,读写状态就是其同步器的同步状态。读写锁的自定义同步器需要在同步状态,即一个 int 变量上维护多个读线程和一个写线程的状态。读写锁将变量切分成了两个部分,高 16 位表示读,低 16 位表示写。

写锁是可重入排他锁,如果当前线程已经获得了写锁则增加写状态,如果当前线程在获取写锁时,读锁已经被获取或者该线程不是已经获得写锁的线程则进入等待。写锁的释放与 ReentrantLock 的释放类似,每次释放减少写状态,当写状态为 0 时表示写锁已被释放。

读锁是可重入共享锁,能够被多个线程同时获取,在没有其他写线程访问时,读锁总会被成功获取。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取则进入等待。读锁每次释放会减少读状态,减少的值是(1<<16),读锁的释放是线程安全的。

锁降级指把持住当前拥有的写锁,再获取读锁,随后释放先前拥有的写锁。

锁降级中读锁的获取是必要的,这是为了保证数据可见性,如果当前线程不获取读锁而直接释放写锁,假设此刻另一个线程 A 获取写锁修改了数据,当前线程无法感知线程 A 的数据更新。如果当前线程获取读锁,遵循锁降级的步骤,A 将被阻塞,直到当前线程使用数据并释放读锁之后,线程 A 才能获取写锁进行数据更新。

Q13:AQS 了解吗?
AQS 队列同步器是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,对同步状态进行更改需要使用同步器提供的 3个方法 getState、setState 和 compareAndSetState ,它们保证状态改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅定义若干同步状态获取和释放的方法,同步器既支持独占式也支持共享式。

同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。锁面向使用者,定义了使用者与锁交互的接口,隐藏实现细节;同步器面向锁的实现者,简化了锁的实现方式,屏蔽了同步状态管理、线程排队、等待与唤醒等底层操作。

每当有新线程请求资源时都会进入一个等待队列,只有当持有锁的线程释放锁资源后该线程才能持有资源。等待队列通过双向链表实现,线程被封装在链表的 Node 节点中,Node 的等待状态包括:CANCELLED(线程已取消)、SIGNAL(线程需要唤醒)、CONDITION (线程正在等待)、PROPAGATE(后继节点会传播唤醒操作,只在共享模式下起作用)。

Q14:AQS 有哪两种模式?
独占模式表示锁只会被一个线程占用,其他线程必须等到持有锁的线程释放锁后才能获取锁,同一时间只能有一个线程获取到锁。

共享模式表示多个线程获取同一个锁有可能成功,ReadLock 就采用共享模式。

独占模式通过 acquire 和 release 方法获取和释放锁,共享模式通过 acquireShared 和 releaseShared 方法获取和释放锁。

Q15:AQS 独占式获取/释放锁的原理?
获取同步状态时,调用 acquire 方法,维护一个同步队列,使用 tryAcquire 方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过 addWaiter 方法加入到同步队列的尾部,在队列中自旋。之后调用 acquireQueued 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞,被阻塞线程的唤醒主要依靠前驱节点的出队或被中断实现,移出队列或停止自旋的条件是前驱节点是头结点且成功获取了同步状态。

释放同步状态时,同步器调用 tryRelease 方法释放同步状态,然后调用 unparkSuccessor 方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。

Q16:为什么只有前驱节点是头节点时才能尝试获取同步状态?
头节点是成功获取到同步状态的节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。

目的是维护同步队列的 FIFO 原则,节点和节点在循环检查的过程中基本不通信,而是简单判断自己的前驱是否为头节点,这样就使节点的释放规则符合 FIFO,并且也便于对过早通知的处理,过早通知指前驱节点不是头节点的线程由于中断被唤醒。

Q17:AQS 共享式式获取/释放锁的原理?
获取同步状态时,调用 acquireShared 方法,该方法调用 tryAcquireShared 方法尝试获取同步状态,返回值为 int 类型,返回值不小于于 0 表示能获取同步状态。因此在共享式获取锁的自旋过程中,成功获取同步状态并退出自旋的条件就是该方法的返回值不小于0。

释放同步状态时,调用 releaseShared 方法,释放后会唤醒后续处于等待状态的节点。它和独占式的区别在于 tryReleaseShared 方法必须确保同步状态安全释放,通过循环 CAS 保证,因为释放同步状态的操作会同时来自多个线程。

线程 13
Q1:线程的生命周期有哪些状态?
NEW:新建状态,线程被创建且未启动,此时还未调用 start 方法。

RUNNABLE:Java 将操作系统中的就绪和运行两种状态统称为 RUNNABLE,此时线程有可能在等待时间片,也有可能在执行。

BLOCKED:阻塞状态,可能由于锁被其他线程占用、调用了 sleep 或 join 方法、执行了 wait方法等。

WAITING:等待状态,该状态线程不会被分配 CPU 时间片,需要其他线程通知或中断。可能由于调用了无参的 wait 和 join 方法。

TIME_WAITING:限期等待状态,可以在指定时间内自行返回。导可能由于调用了带参的 wait 和 join 方法。

TERMINATED:终止状态,表示当前线程已执行完毕或异常退出。

Q2:线程的创建方式有哪些?
① 继承 Thread 类并重写 run 方法。实现简单,但不符合里氏替换原则,不可以继承其他类。

② 实现 Runnable 接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦。

③实现 Callable 接口并重写 call 方法。可以获取线程执行结果的返回值,并且可以抛出异常。

Q3:线程有哪些方法?
① sleep 方***导致当前线程进入休眠状态,与 wait 不同的是该方法不会释放锁资源,进入的是 TIMED-WAITING 状态。

② yiled 方法使当前线程让出 CPU 时间片给优先级相同或更高的线程,回到 RUNNABLE 状态,与其他线程一起重新竞争CPU时间片。

③ join 方法用于等待其他线程运行终止,如果当前线程调用了另一个线程的 join 方法,则当前线程进入阻塞状态,当另一个线程结束时当前线程才能从阻塞状态转为就绪态,等待获取CPU时间片。底层使用的是wait,也会释放锁。

Q4:什么是守护线程?
守护线程是一种支持型线程,可以通过 setDaemon(true) 将线程设置为守护线程,但必须在线程启动前设置。

守护线程被用于完成支持性工作,但在 JVM 退出时守护线程中的 finally 块不一定执行,因为 JVM 中没有非守护线程时需要立即退出,所有守护线程都将立即终止,不能靠在守护线程使用 finally 确保关闭资源。

Q5:线程通信的方式有哪些?
命令式编程中线程的通信机制有两种,共享内存和消息传递。在共享内存的并发模型里线程间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。在消息传递的并发模型里线程间没有公共状态,必须通过发送消息来显式通信。Java 并发采用共享内存模型,线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

volatile 告知程序任何对变量的读需要从主内存中获取,写必须同步刷新回主内存,保证所有线程对变量访问的可见性。

synchronized 确保多个线程在同一时刻只能有一个处于方法或同步块中,保证线程对变量访问的原子性、可见性和有序性。

等待通知机制指一个线程 A 调用了对象的 wait 方法进入等待状态,另一线程 B 调用了对象的 notify/notifyAll 方法,线程 A 收到通知后结束阻塞并执行后序操作。对象上的 wait 和 notify/notifyAll 如同开关信号,完成等待方和通知方的交互。

如果一个线程执行了某个线程的 join 方法,这个线程就会阻塞等待执行了 join 方法的线程终止,这里涉及等待/通知机制。join 底层通过 wait 实现,线程终止时会调用自身的 notifyAll 方法,通知所有等待在该线程对象上的线程。

管道 IO 流用于线程间数据传输,媒介为内存。PipedOutputStream 和 PipedWriter 是输出流,相当于生产者,PipedInputStream 和 PipedReader 是输入流,相当于消费者。管道流使用一个默认大小为 1KB 的循环缓冲数组。输入流从缓冲数组读数据,输出流往缓冲数组中写数据。当数组已满时,输出流所在线程阻塞;当数组首次为空时,输入流所在线程阻塞。

ThreadLocal 是线程共享变量,但它可以为每个线程创建单独的副本,副本值是线程私有的,互相之间不影响。

Q6:线程池有什么好处?
降低资源消耗,复用已创建的线程,降低开销、控制最大并发数。

隔离线程环境,可以配置独立线程池,将较慢的线程与较快的隔离开,避免相互影响。

实现任务线程队列缓冲策略和拒绝机制。

实现某些与时间相关的功能,如定时执行、周期执行等。

Q7:线程池处理任务的流程?
① 核心线程池未满,创建一个新的线程执行任务,此时 workCount < corePoolSize。

② 如果核心线程池已满,工作队列未满,将线程存储在工作队列,此时 workCount >= corePoolSize。

③ 如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务,此时 workCount < maximumPoolSize,这一步也需要获取全局锁。

④ 如果超过大小线程数,按照拒绝策略来处理任务,此时 workCount > maximumPoolSize。

线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。

Q8:有哪些创建线程池的方法?
可以通过 Executors 的静态工厂方法创建线程池:

① newFixedThreadPool,固定大小的线程池,核心线程数也是最大线程数,不存在空闲线程,keepAliveTime = 0。该线程池使用的工作队列是无界阻塞队列 LinkedBlockingQueue,适用于负载较重的服务器。

② newSingleThreadExecutor,使用单线程,相当于单线程串行执行所有任务,适用于需要保证顺序执行任务的场景。

③ newCachedThreadPool,maximumPoolSize 设置为 Integer 最大值,是高度可伸缩的线程池。该线程池使用的工作队列是没有容量的 SynchronousQueue,如果主线程提交任务的速度高于线程处理的速度,线程池会不断创建新线程,极端情况下会创建过多线程而耗尽CPU 和内存资源。适用于执行很多短期异步任务的小程序或负载较轻的服务器。

④ newScheduledThreadPool:线程数最大为 Integer 最大值,存在 OOM 风险。支持定期及周期性任务执行,适用需要多个后台线程执行周期任务,同时需要限制线程数量的场景。相比 Timer 更安全,功能更强,与 newCachedThreadPool 的区别是不回收工作线程。

⑤ newWorkStealingPool:JDK8 引入,创建持有足够线程的线程池支持给定的并行度,通过多个队列减少竞争。

Q9:创建线程池有哪些参数?
① corePoolSize:常驻核心线程数,如果为 0,当执行完任务没有任何请求时会消耗线程池;如果大于 0,即使本地任务执行完,核心线程也不会被销毁。该值设置过大会浪费资源,过小会导致线程的频繁创建与销毁。

② maximumPoolSize:线程池能够容纳同时执行的线程最大数,必须大于等于 1,如果与核心线程数设置相同代表固定大小线程池。

③ keepAliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存资源。

④ unit:keepAliveTime 的时间单位。

⑤ workQueue:工作队列,当线程请求数大于等于 corePoolSize 时线程会进入阻塞队列。

⑥ threadFactory:线程工厂,用来生产一组相同任务的线程。可以给线程命名,有利于分析错误。

⑦ handler:拒绝策略,默认使用 AbortPolicy 丢弃任务并抛出异常,CallerRunsPolicy 表示重新尝试提交该任务,DiscardOldestPolicy 表示抛弃队列里等待最久的任务并把当前任务加入队列,DiscardPolicy 表示直接抛弃当前任务但不抛出异常。

Q10:如何关闭线程池?
可以调用 shutdown 或 shutdownNow 方法关闭线程池,原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法中断线程,无法响应中断的任务可能永远无法终止。

区别是 shutdownNow 首先将线程池的状态设为 STOP,然后尝试停止正在执行或暂停任务的线程,并返回等待执行任务的列表。而 shutdown 只是将线程池的状态设为 SHUTDOWN,然后中断没有正在执行任务的线程。

通常调用 shutdown 来关闭线程池,如果任务不一定要执行完可调用 shutdownNow。

Q11:线程池的选择策略有什么?
可以从以下角度分析:①任务性质:CPU 密集型、IO 密集型和混合型。②任务优先级。③任务执行时间。④任务依赖性:是否依赖其他资源,如数据库连接。

性质不同的任务可用不同规模的线程池处理,CPU 密集型任务应配置尽可能小的线程,如配置 Ncpu+1 个线程的线程池。由于 IO 密集型任务线程并不是一直在执行任务,应配置尽可能多的线程,如 2*Ncpu。混合型的任务,如果可以拆分,将其拆分为一个 CPU 密集型任务和一个 IO 密集型任务,只要两个任务执行的时间相差不大那么分解后的吞吐量将高于串行执行的吞吐量,如果相差太大则没必要分解。

优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 处理。

执行时间不同的任务可以交给不同规模的线程池处理,或者使用优先级队列让执行时间短的任务先执行。

依赖数据库连接池的任务,由于线程提交 SQL 后需要等待数据库返回的结果,等待的时间越长 CPU 空闲的时间就越长,因此线程数应该尽可能地设置大一些,提高 CPU 的利用率。

建议使用有界队列,能增加系统的稳定性和预警能力,可以根据需要设置的稍微大一些。

Q12:阻塞队列有哪些选择?
阻塞队列支持阻塞插入和移除,当队列满时,阻塞插入元素的线程直到队列不满。当队列为空时,获取元素的线程会被阻塞直到队列非空。阻塞队列常用于生产者和消费者的场景,阻塞队列就是生产者用来存放元素,消费者用来获取元素的容器。

Java 中的阻塞队列

ArrayBlockingQueue,由数组组成的有界阻塞队列,默认情况下不保证线程公平,有可能先阻塞的线程最后才访问队列。

LinkedBlockingQueue,由链表结构组成的有界阻塞队列,队列的默认和最大长度为 Integer 最大值。

PriorityBlockingQueue,支持优先级的无界阻塞队列,默认情况下元素按照升序排序。可自定义 compareTo 方法指定排序规则,或者初始化时指定 Comparator 排序,不能保证同优先级元素的顺序。

DelayQueue,支持延时获取元素的无界阻塞队列,使用优先级队列实现。创建元素时可以指定多久才能从队列中获取当前元素,只有延迟期满时才能从队列中获取元素,适用于缓存和定时调度。

SynchronousQueue,不存储元素的阻塞队列,每一个 put 必须等待一个 take。默认使用非公平策略,也支持公平策略,适用于传递性场景,吞吐量高。

LinkedTransferQueue,链表组成的无界阻塞队列,相对于其他阻塞队列多了 tryTransfer 和 transfer 方法。transfer方法:如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者,否则会将元素放在队列的尾节点并等到该元素被消费者消费才返回。tryTransfer 方法用来试探生产者传入的元素能否直接传给消费者,如果没有消费者等待接收元素则返回 false,和 transfer 的区别是无论消费者是否消费都会立即返回。

LinkedBlockingDeque,链表组成的双向阻塞队列,可从队列的两端插入和移出元素,多线程同时入队时减少了竞争。

实现原理

使用通知模式实现,生产者往满的队列里添加元素时会阻塞,当消费者消费后,会通知生产者当前队列可用。当往队列里插入一个元素,如果队列不可用,阻塞生产者主要通过 LockSupport 的 park 方法实现,不同操作系统中实现方式不同,在 Linux 下使用的是系统方法 pthread_cond_wait 实现。

Q13:谈一谈 ThreadLocal
ThreadLoacl 是线程共享变量,主要用于一个线程内跨类、方法传递数据。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,Entry 中只有一个 Object 类的 vaule 值。ThreadLocal 是线程共享的,但 ThreadLocalMap 是每个线程私有的。ThreadLocal 主要有 set、get 和 remove 三个方法。

set 方法

首先获取当前线程,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就直接设置值,key 是当前的 ThreadLocal 对象,value 是传入的参数。

如果 map 不存在就通过 createMap 方法为当前线程创建一个 ThreadLocalMap 对象再设置值。

get 方法

首先获取当前线程,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就以当前 ThreadLocal 对象作为 key 获取 Entry 类型的对象 e,如果 e 存在就返回它的 value 属性。

如果 e 不存在或者 map 不存在,就调用 setInitialValue 方法先为当前线程创建一个 ThreadLocalMap 对象然后返回默认的初始值 null。

remove 方法

首先通过当前线程获取其对应的 ThreadLocalMap 类型的对象 m,如果 m 不为空,就解除 ThreadLocal 这个 key 及其对应的 value 值的联系。

存在的问题

线程复用会产生脏数据,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用。如果没有调用 remove 清理与线程相关的 ThreadLocal 信息,那么假如下一个线程没有调用 set 设置初始值就可能 get 到重用的线程信息。

ThreadLocal 还存在内存泄漏的问题,由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放。因此需要及时调用 remove 方法进行清理操作。

JUC 11
Q1:什么是 CAS?
CAS 表示 Compare And Swap,比较并交换,CAS 需要三个操作数,分别是内存位置 V、旧的预期值 A 和准备设置的新值 B。CAS 指令执行时,当且仅当 V 符合 A 时,处理器才会用 B 更新 V 的值,否则它就不执行更新。但不管是否更新都会返回 V 的旧值,这些处理过程是原子操作,执行期间不会被其他线程打断。

在 JDK 5 后,Java 类库中才开始使用 CAS 操作,该操作由 Unsafe 类里的 compareAndSwapInt 等几个方法包装提供。HotSpot 在内部对这些方法做了特殊处理,即时编译的结果是一条平台相关的处理器 CAS 指令。Unsafe 类不是给用户程序调用的类,因此 JDK9 前只有 Java 类库可以使用 CAS,譬如 juc 包里的 AtomicInteger类中 compareAndSet 等方法都使用了Unsafe 类的 CAS 操作实现。

Q2:CAS 有什么问题?
CAS 从语义上来说存在一个逻辑漏洞:如果 V 初次读取时是 A,并且在准备赋值时仍为 A,这依旧不能说明它没有被其他线程更改过,因为这段时间内假设它的值先改为 B 又改回 A,那么 CAS 操作就会误认为它从来没有被改变过。

这个漏洞称为 ABA 问题,juc 包提供了一个 AtomicStampedReference,原子更新带有版本号的引用类型,通过控制变量值的版本来解决 ABA 问题。大部分情况下 ABA 不会影响程序并发的正确性,如果需要解决,传统的互斥同步可能会比原子类更高效。

Q3:有哪些原子类?
JDK 5 提供了 java.util.concurrent.atomic 包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。到 JDK 8 该包共有17个类,依据作用分为四种:原子更新基本类型类、原子更新数组类、原子更新引用类以及原子更新字段类,atomic 包里的类基本都是使用 Unsafe 实现的包装类。

AtomicInteger 原子更新整形、 AtomicLong 原子更新长整型、AtomicBoolean 原子更新布尔类型。

AtomicIntegerArray,原子更新整形数组里的元素、 AtomicLongArray 原子更新长整型数组里的元素、 AtomicReferenceArray 原子更新引用类型数组里的元素。

AtomicReference 原子更新引用类型、AtomicMarkableReference 原子更新带有标记位的引用类型,可以绑定一个 boolean 标记、 AtomicStampedReference 原子更新带有版本号的引用类型,关联一个整数值作为版本号,解决 ABA 问题。

AtomicIntegerFieldUpdater 原子更新整形字段的更新器、 AtomicLongFieldUpdater 原子更新长整形字段的更新器AtomicReferenceFieldUpdater 原子更新引用类型字段的更新器。

Q4:AtomicIntger 实现原子更新的原理是什么?
AtomicInteger 原子更新整形、 AtomicLong 原子更新长整型、AtomicBoolean 原子更新布尔类型。

getAndIncrement 以原子方式将当前的值加 1,首先在 for 死循环中取得 AtomicInteger 里存储的数值,第二步对 AtomicInteger 当前的值加 1 ,第三步调用 compareAndSet 方法进行原子更新,先检查当前数值是否等于 expect,如果等于则说明当前值没有被其他线程修改,则将值更新为 next,否则会更新失败返回 false,程序会进入 for 循环重新进行 compareAndSet 操作。

atomic 包中只提供了三种基本类型的原子更新,atomic 包里的类基本都是使用 Unsafe 实现的,Unsafe 只提供三种 CAS 方法:compareAndSwapInt、compareAndSwapLong 和 compareAndSwapObject,例如原子更新 Boolean 是先转成整形再使用 compareAndSwapInt 。

Q5:CountDownLatch 是什么?
CountDownLatch 是基于执行时间的同步类,允许一个或多个线程等待其他线程完成操作,构造方法接收一个 int 参数作为计数器,如果要等待 n 个点就传入 n。每次调用 countDown 方法时计数器减 1,await 方***阻塞当前线程直到计数器变为0,由于 countDown 方法可用在任何地方,所以 n 个点既可以是 n 个线程也可以是一个线程里的 n 个执行步骤。

Q6: CyclicBarrier 是什么?
循环屏障是基于同步到达某个点的信号量触发机制,作用是让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障才会解除。构造方法中的参数表示拦截线程数量,每个线程调用 await 方法告诉 CyclicBarrier 自己已到达屏障,然后被阻塞。还支持在构造方法中传入一个 Runnable 任务,当线程到达屏障时会优先执行该任务。适用于多线程计算数据,最后合并计算结果的应用场景。

CountDownLacth 的计数器只能用一次,而 CyclicBarrier 的计数器可使用 reset 方法重置,所以 CyclicBarrier 能处理更为复杂的业务场景,例如计算错误时可用重置计数器重新计算。

Q7:Semaphore 是什么?
信号量用来控制同时访问特定资源的线程数量,通过协调各个线程以保证合理使用公共资源。信号量可以用于流量控制,特别是公共资源有限的应用场景,比如数据库连接。

Semaphore 的构造方法参数接收一个 int 值,表示可用的许可数量即最大并发数。使用 acquire 方法获得一个许可证,使用 release 方法归还许可,还可以用 tryAcquire 尝试获得许可。

Q8: Exchanger 是什么?
交换者是用于线程间协作的工具类,用于进行线程间的数据交换。它提供一个同步点,在这个同步点两个线程可以交换彼此的数据。

两个线程通过 exchange 方法交换数据,第一个线程执行 exchange 方法后会阻塞等待第二个线程执行该方法,当两个线程都到达同步点时这两个线程就可以交换数据,将本线程生产出的数据传递给对方。应用场景包括遗传算法、校对工作等。

P9:JDK7 的 ConcurrentHashMap 原理?
ConcurrentHashMap 用于解决 HashMap 的线程不安全和 HashTable 的并发效率低,HashTable 之所以效率低是因为所有线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器的部分数据,那么多线程访问容器不同数据段的数据时,线程间就不会存在锁竞争,从而有效提高并发效率,这就是 ConcurrentHashMap 的锁分段技术。首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。

get 实现简单高效,先经过一次再散列,再用这个散列值通过散列运算定位到 Segment,最后通过散列算法定位到元素。get 的高效在于不需要加锁,除非读到空值才会加锁重读。get 方法中将共享变量定义为 volatile,在 get 操作里只需要读所以不用加锁。

put 必须加锁,首先定位到 Segment,然后进行插入操作,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。

size 操作用于统计元素的数量,必须统计每个 Segment 的大小然后求和,在统计结果累加的过程中,之前累加过的 count 变化几率很小,因此先尝试两次通过不加锁的方式统计结果,如果统计过程中容器大小发生了变化,再加锁统计所有 Segment 大小。判断容器是否发生变化根据 modCount 确定。

P10:JDK8 的 ConcurrentHashMap 原理?
主要对 JDK7 做了三点改造:① 取消分段锁机制,进一步降低冲突概率。② 引入红黑树结构,同一个哈希槽上的元素个数超过一定阈值后,单向链表改为红黑树结构。③ 使用了更加优化的方式统计集合内的元素数量。具体优化表现在:在 put、resize 和 size 方法中设计元素总数的更新和计算都避免了锁,使用 CAS 代替。

get 同样不需要同步,put 操作时如果没有出现哈希冲突,就使用 CAS 添加元素,否则使用 synchronized 加锁添加元素。

当某个槽内的元素个数达到 7 且 table 容量不小于 64 时,链表转为红黑树。当某个槽内的元素减少到 6 时,由红黑树重新转为链表。在转化过程中,使用同步块锁住当前槽的首元素,防止其他线程对当前槽进行增删改操作,转化完成后利用 CAS 替换原有链表。由于 TreeNode 节点也存储了 next 引用,因此红黑树转为链表很简单,只需从 first 元素开始遍历所有节点,并把节点从 TreeNode 转为 Node 类型即可,当构造好新链表后同样用 CAS 替换红黑树。

P11:ArrayList 的线程安全集合是什么?
可以使用 CopyOnWriteArrayList 代替 ArrayList,它实现了读写分离。写操作复制一个新的集合,在新集合内添加或删除元素,修改完成后再将原集合的引用指向新集合。这样做的好处是可以高并发地进行读写操作而不需要加锁,因为当前集合不会添加任何元素。使用时注意尽量设置容量初始值,并且可以使用批量添加或删除,避免多次扩容,比如只增加一个元素却复制整个集合。

适合读多写少,单个添加时效率极低。CopyOnWriteArrayList 是 fail-safe 的,并发包的集合都是这种机制,fail-safe 在安全的副本上遍历,集合修改与副本遍历没有任何关系,缺点是无法读取最新数据。这也是 CAP 理论中 C 和 A 的矛盾,即一致性与可用性的矛盾。

JVM

作者:冠状病毒biss
链接:https://www.nowcoder.com/discuss/447742?source_id=profile_create&channel=666
来源:牛客网

内存区域划分 8
Q1:运行时数据区是什么?
虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干不同的数据区,这些区域有各自的用途、创建和销毁时间。

线程私有:程序计数器、Java 虚拟机栈、本地方法栈。

线程共享:Java 堆、方法区。

Q2:程序计数器是什么?
程序计数器是一块较小的内存空间,可以看作当前线程所执行字节码的行号指示器。字节码解释器工作时通过改变计数器的值选取下一条执行指令。分支、循环、跳转、线程恢复等功能都需要依赖计数器完成。是唯一在虚拟机规范中没有规定内存溢出情况的区域。

如果线程正在执行 Java 方法,计数器记录正在执行的虚拟机字节码指令地址。如果是本地方法,计数器值为 Undefined。

Q3:Java 虚拟机栈的作用?
Java 虚拟机栈来描述 Java 方法的内存模型。每当有新线程创建时就会分配一个栈空间,线程结束后栈空间被回收,栈与线程拥有相同的生命周期。栈中元素用于支持虚拟机进行方法调用,每个方法在执行时都会创建一个栈帧存储方法的局部变量表、操作栈、动态链接和方法出口等信息。每个方法从调用到执行完成,就是栈帧从入栈到出栈的过程。

有两类异常:① 线程请求的栈深度大于虚拟机允许的深度抛出 StackOverflowError。② 如果 JVM 栈容量可以动态扩展,栈扩展无法申请足够内存抛出 OutOfMemoryError(HotSpot 不可动态扩展,不存在此问题)。

Q4:本地方法栈的作用?
本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行 Java 方法服务,本地方法栈为虚本地方法服务。调用本地方法时虚拟机栈保持不变,动态链接并直接调用指定本地方法。

虚拟机规范对本地方法栈中方法的语言与数据结构无强制规定,虚拟机可自由实现,例如 HotSpot 将虚拟机栈和本地方法栈合二为一。

本地方法栈在栈深度异常和栈扩展失败时分别抛出 StackOverflowError 和 OutOfMemoryError。

Q5:堆的作用是什么?
堆是虚拟机所管理的内存中最大的一块,被所有线程共享的,在虚拟机启动时创建。堆用来存放对象实例,Java 里几乎所有对象实例都在堆分配内存。堆可以处于物理上不连续的内存空间,逻辑上应该连续,但对于例如数组这样的大对象,多数虚拟机实现出于简单、存储高效的考虑会要求连续的内存空间。

堆既可以被实现成固定大小,也可以是可扩展的,可通过 -Xms 和 -Xmx 设置堆的最小和最大容量,当前主流 JVM 都按照可扩展实现。如果堆没有内存完成实例分配也无法扩展,抛出 OutOfMemoryError。

Q6:方法区的作用是什么?
方法区用于存储被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

JDK8 之前使用永久代实现方法区,容易内存溢出,因为永久代有 -XX:MaxPermSize 上限,即使不设置也有默认大小。JDK7 把放在永久代的字符串常量池、静态变量等移出,JDK8 中永久代完全废弃,改用在本地内存中实现的元空间代替,把 JDK 7 中永久代剩余内容(主要是类型信息)全部移到元空间。

虚拟机规范对方法区的约束宽松,除和堆一样不需要连续内存和可选择固定大小/可扩展外,还可以不实现垃圾回收。垃圾回收在方法区出现较少,主要目标针对常量池和类型卸载。如果方法区无法满足新的内存分配需求,将抛出 OutOfMemoryError。

Q7:运行时常量池的作用是什么?
运行时常量池是方法区的一部分,Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容在类加载后存放到运行时常量池。一般除了保存 Class 文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池。

运行时常量池相对于 Class 文件常量池的一个重要特征是动态性,Java 不要求常量只有编译期才能产生,运行期间也可以将新的常量放入池中,这种特性利用较多的是 String 的 intern 方法。

运行时常量池是方法区的一部分,受到方法区内存的限制,当常量池无法再申请到内存时会抛出 OutOfMemoryError。

Q8:直接内存是什么?
直接内存不属于运行时数据区,也不是虚拟机规范定义的内存区域,但这部分内存被频繁使用,而且可能导致内存溢出。

JDK1.4 中新加入了 NIO 这种基于通道与缓冲区的 IO,它可以使用 Native 函数库直接分配堆外内存,通过一个堆里的 DirectByteBuffer 对象作为内存的引用进行操作,避免了在 Java 堆和 Native堆来回复制数据。

直接内存的分配不受 Java 堆大小的限制,但还是会受到本机总内存及处理器寻址空间限制,一般配置虚拟机参数时会根据实际内存设置 -Xmx 等参数信息,但经常忽略直接内存,使内存区域总和大于物理内存限制,导致动态扩展时出现 OOM。

由直接内存导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显的异常,如果发现内存溢出后产生的 Dump 文件很小,而程序中又直接或间接使用了直接内存(典型的间接使用就是 NIO),那么就可以考虑检查直接内存方面的原因。

内存溢出 5
Q1:内存溢出和内存泄漏的区别?
内存溢出 OutOfMemory,指程序在申请内存时,没有足够的内存空间供其使用。

内存泄露 Memory Leak,指程序在申请内存后,无法释放已申请的内存空间,内存泄漏最终将导致内存溢出。

Q2:堆溢出的原因?
堆用于存储对象实例,只要不断创建对象并保证 GC Roots 到对象有可达路径避免垃圾回收,随着对象数量的增加,总容量触及最大堆容量后就会 OOM,例如在 while 死循环中一直 new 创建实例。

堆 OOM 是实际应用中最常见的 OOM,处理方法是通过内存映像分析工具对 Dump 出的堆转储快照分析,确认内存中导致 OOM 的对象是否必要,分清到底是内存泄漏还是内存溢出。

如果是内存泄漏,通过工具查看泄漏对象到 GC Roots 的引用链,找到泄露对象是通过怎样的引用路径、与哪些 GC Roots 关联才导致无法回收,一般可以准确定位到产生内存泄漏代码的具***置。

如果不是内存泄漏,即内存中对象都必须存活,应当检查 JVM 堆参数,与机器内存相比是否还有向上调整的空间。再从代码检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。

Q3:栈溢出的原因?
由于 HotSpot 不区分虚拟机和本地方法栈,设置本地方法栈大小的参数没有意义,栈容量只能由 -Xss 参数来设定,存在两种异常:

StackOverflowError: 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError,例如一个递归方法不断调用自己。该异常有明确错误堆栈可供分析,容易定位到问题所在。

OutOfMemoryError: 如果 JVM 栈可以动态扩展,当扩展无法申请到足够内存时会抛出 OutOfMemoryError。HotSpot 不支持虚拟机栈扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现 OOM,否则在线程运行时是不会因为扩展而导致溢出的。

Q4:运行时常量池溢出的原因?
String 的 intern 方法是一个本地方法,作用是如果字符串常量池中已包含一个等于此 String 对象的字符串,则返回池中这个字符串的 String 对象的引用,否则将此 String 对象包含的字符串添加到常量池并返回此 String 对象的引用。

在 JDK6 及之前常量池分配在永久代,因此可以通过 -XX:PermSize 和 -XX:MaxPermSize 限制永久代大小,间接限制常量池。在 while 死循环中调用 intern 方法导致运行时常量池溢出。在 JDK7 后不会出现该问题,因为存放在永久代的字符串常量池已经被移至堆中。

Q5:方法区溢出的原因?
方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。只要不断在运行时产生大量类,方法区就会溢出。例如使用 JDK 反射或 CGLib 直接操作字节码在运行时生成大量的类。很多框架如 Spring、Hibernate 等对类增强时都会使用 CGLib 这类字节码技术,增强的类越多就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出。

JDK8 使用元空间取代永久代,HotSpot 提供了一些参数作为元空间防御措施,例如 -XX:MetaspaceSize 指定元空间初始大小,达到该值会触发 GC 进行类型卸载,同时收集器会对该值进行调整,如果释放大量空间就适当降低该值,如果释放很少空间就适当提高。

创建对象 5
Q1:创建对象的过程是什么?
字节码角度

NEW: 如果找不到 Class 对象则进行类加载。加载成功后在堆中分配内存,从 Object 到本类路径上的所有属性都要分配。分配完毕后进行零值设置。最后将指向实例对象的引用变量压入虚拟机栈顶。
*DUP: * 在栈顶复制引用变量,这时栈顶有两个指向堆内实例的引用变量。两个引用变量的目的不同,栈底的引用用于赋值或保存局部变量表,栈顶的引用作为句柄调用相关方法。
INVOKESPECIAL: 通过栈顶的引用变量调用 init 方法。
执行角度

① 当 JVM 遇到字节码 new 指令时,首先将检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。

② 在类加载检查通过后虚拟机将为新生对象分配内存。

③ 内存分配完成后虚拟机将成员变量设为零值,保证对象的实例字段可以不赋初值就使用。

④ 设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。

⑤ 执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。

Q2:对象分配内存的方式有哪些?
对象所需内存大小在类加载完成后便可完全确定,分配空间的任务实际上等于把一块确定大小的内存块从 Java 堆中划分出来。

指针碰撞: 假设 Java 堆内存规整,被使用过的内存放在一边,空闲的放在另一边,中间放着一个指针作为分界指示器,分配内存就是把指针向空闲方向挪动一段与对象大小相等的距离。

空闲列表: 如果 Java 堆内存不规整,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。

选择哪种分配方式由堆是否规整决定,堆是否规整由垃圾收集器是否有空间压缩能力决定。使用 Serial、ParNew 等收集器时,系统采用指针碰撞;使用 CMS 这种基于清除算法的垃圾收集器时,采用空间列表。

Q3:对象分配内存是否线程安全?
对象创建十分频繁,即使修改一个指针的位置在并发下也不是线程安全的,可能正给对象 A 分配内存,指针还没来得及修改,对象 B 又使用了指针来分配内存。

解决方法:① CAS 加失败重试保证更新原子性。② 把内存分配按线程划分在不同空间,即每个线程在 Java 堆中预先分配一小块内存,叫做本地线程分配缓冲 TLAB,哪个线程要分配内存就在对应的 TLAB 分配,TLAB 用完了再进行同步。

Q4:对象的内存布局了解吗?
对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。

对象头占 12B,包括对象标记和类型指针。对象标记存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁标志、偏向线程 ID 等,这部分占 8B,称为 Mark Word。Mark Word 被设计为动态数据结构,以便在极小的空间存储更多数据,根据对象状态复用存储空间。

类型指针是对象指向它的类型元数据的指针,占 4B。JVM 通过该指针来确定对象是哪个类的实例。

实例数据是对象真正存储的有效信息,即本类对象的实例成员变量和所有可见的父类成员变量。存储顺序会受到虚拟机分配策略参数和字段在源码中定义顺序的影响。相同宽度的字段总是被分配到一起存放,在满足该前提条件的情况下父类中定义的变量会出现在子类之前。

对齐填充不是必然存在的,仅起占位符作用。虚拟机的自动内存管理系统要求任何对象的大小必须是 8B 的倍数,对象头已被设为 8B 的 1 或 2 倍,如果对象实例数据部分没有对齐,需要对齐填充补全。

Q5:对象的访问方式有哪些?
Java 程序会通过栈上的 reference 引用操作堆对象,访问方式由虚拟机决定,主流访问方式主要有句柄和直接指针。

句柄: 堆会划分出一块内存作为句柄池,reference 中存储对象的句柄地址,句柄包含对象实例数据与类型数据的地址信息。优点是 reference 中存储的是稳定句柄地址,在 GC 过程中对象被移动时只会改变句柄的实例数据指针,而 reference 本身不需要修改。

直接指针: 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 存储对象地址,如果只是访问对象本身就不需要多一次间接访问的开销。优点是速度更快,节省了一次指针定位的时间开销,HotSpot 主要使用直接指针进行对象访问。

垃圾回收 7
Q1:如何判断对象是否是垃圾?
引用计数:在对象中添加一个引用计数器,如果被引用计数器加 1,引用失效时计数器减 1,如果计数器为 0 则被标记为垃圾。原理简单,效率高,但是在 Java 中很少使用,因为存在对象间循环引用的问题,导致计数器无法清零。

可达性分析:主流语言的内存管理都使用可达性分析判断对象是否存活。基本思路是通过一系列称为 GC Roots 的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到 GC Roots 没有任何引用链相连,则会被标记为垃圾。可作为 GC Roots 的对象包括虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。

Q2:Java 的引用有哪些类型?
JDK1.2 后对引用进行了扩充,按强度分为四种:

强引用: 最常见的引用,例如 Object obj = new Object() 就属于强引用。只要对象有强引用指向且 GC Roots 可达,在内存回收时即使濒临内存耗尽也不会被回收。

软引用: 弱于强引用,描述非必需对象。在系统将发生内存溢出前,会把软引用关联的对象加入回收范围以获得更多内存空间。用来缓存服务器中间计算结果及不需要实时保存的用户行为等。

弱引用: 弱于软引用,描述非必需对象。弱引用关联的对象只能生存到下次 YGC 前,当垃圾收集器开始工作时无论当前内存是否足够都会回收只被弱引用关联的对象。由于 YGC 具有不确定性,因此弱引用何时被回收也不确定。

虚引用: 最弱的引用,定义完成后无法通过该引用获取对象。唯一目的就是为了能在对象被回收时收到一个系统通知。虚引用必须与引用队列联合使用,垃圾回收时如果出现虚引用,就会在回收对象前把这个虚引用加入引用队列。

Q3:有哪些 GC 算法?
标记-清除算法

分为标记和清除阶段,首先从每个 GC Roots 出发依次标记有引用关系的对象,最后清除没有标记的对象。

执行效率不稳定,如果堆包含大量对象且大部分需要回收,必须进行大量标记清除,导致效率随对象数量增长而降低。

存在内存空间碎片化问题,会产生大量不连续的内存碎片,导致以后需要分配大对象时容易触发 Full GC。

标记-复制算法

为了解决内存碎片问题,将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。主要用于进行新生代。

实现简单、运行高效,解决了内存碎片问题。 代价是可用内存缩小为原来的一半,浪费空间。

HotSpot 把新生代划分为一块较大的 Eden 和两块较小的 Survivor,每次分配内存只使用 Eden 和其中一块 Survivor。垃圾收集时将 Eden 和 Survivor 中仍然存活的对象一次性复制到另一块 Survivor 上,然后直接清理掉 Eden 和已用过的那块 Survivor。HotSpot 默认Eden 和 Survivor 的大小比例是 8:1,即每次新生代中可用空间为整个新生代的 90%。

标记-整理算法

标记-复制算法在对象存活率高时要进行较多复制操作,效率低。如果不想浪费空间,就需要有额外空间分配担保,应对被使用内存中所有对象都存活的极端情况,所以老年代一般不使用此算法。

老年代使用标记-整理算法,标记过程与标记-清除算法一样,但不直接清理可回收对象,而是让所有存活对象都向内存空间一端移动,然后清理掉边界以外的内存。

标记-清除与标记-整理的差异在于前者是一种非移动式算法而后者是移动式的。如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,是一种极为负重的操作,而且移动必须全程暂停用户线程。如果不移动对象就会导致空间碎片问题,只能依赖更复杂的内存分配器和访问器解决。

Q4:你知道哪些垃圾收集器?
Serial

最基础的收集器,使用复制算法、单线程工作,只用一个处理器或一条线程完成垃圾收集,进行垃圾收集时必须暂停其他所有工作线程。

Serial 是虚拟机在客户端模式的默认新生代收集器,简单高效,对于内存受限的环境它是所有收集器中额外内存消耗最小的,对于处理器核心较少的环境,Serial 由于没有线程交互开销,可获得最高的单线程收集效率。

ParNew

Serial 的多线程版本,除了使用多线程进行垃圾收集外其余行为完全一致。

ParNew 是虚拟机在服务端模式的默认新生代收集器,一个重要原因是除了 Serial 外只有它能与 CMS 配合。自从 JDK 9 开始,ParNew 加 CMS 不再是官方推荐的解决方案,官方希望它被 G1 取代。

Parallel Scavenge

新生代收集器,基于复制算法,是可并行的多线程收集器,与 ParNew 类似。

特点是它的关注点与其他收集器不同,Parallel Scavenge 的目标是达到一个可控制的吞吐量,吞吐量就是处理器用于运行用户代码的时间与处理器消耗总时间的比值。

Serial Old

Serial 的老年代版本,单线程工作,使用标记-整理算法。

Serial Old 是虚拟机在客户端模式的默认老年代收集器,用于服务端有两种用途:① JDK5 及之前与 Parallel Scavenge 搭配。② 作为CMS 失败预案。

Parellel Old

Parallel Scavenge 的老年代版本,支持多线程,基于标记-整理算法。JDK6 提供,注重吞吐量可考虑 Parallel Scavenge 加 Parallel Old。

CMS

以获取最短回收停顿时间为目标,基于标记-清除算法,过程相对复杂,分为四个步骤:初始标记、并发标记、重新标记、并发清除。

初始标记和重新标记需要 STW(Stop The World,系统停顿),初始标记仅是标记 GC Roots 能直接关联的对象,速度很快。并发标记从 GC Roots 的直接关联对象开始遍历整个对象图,耗时较长但不需要停顿用户线程。重新标记则是为了修正并发标记期间因用户程序运作而导致标记产生变动的那部分记录。并发清除清理标记阶段判断的已死亡对象,不需要移动存活对象,该阶段也可与用户线程并发。

缺点:① 对处理器资源敏感,并发阶段虽然不会导致用户线程暂停,但会降低吞吐量。② 无法处理浮动垃圾,有可能出现并发失败而导致 Full GC。③ 基于标记-清除算法,产生空间碎片。

G1

开创了收集器面向局部收集的设计思路和基于 Region 的内存布局,主要面向服务端,最初设计目标是替换 CMS。

G1 之前的收集器,垃圾收集目标要么是整个新生代,要么是整个老年代或整个堆。而 G1 可面向堆任何部分来组成回收集进行回收,衡量标准不再是分代,而是哪块内存中存放的垃圾数量最多,回收受益最大。

跟踪各 Region 里垃圾的价值,价值即回收所获空间大小以及回收所需时间的经验值,在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值最大的 Region。这种方式保证了 G1 在有限时间内获取尽可能高的收集效率。

G1 运作过程:

初始标记:标记 GC Roots 能直接关联到的对象,让下一阶段用户线程并发运行时能正确地在可用 Region 中分配新对象。需要 STW 但耗时很短,在 Minor GC 时同步完成。
并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆的对象图。耗时长但可与用户线程并发,扫描完成后要重新处理 SATB 记录的在并发时有变动的对象。
最终标记:对用户线程做短暂暂停,处理并发阶段结束后仍遗留下来的少量 SATB 记录。
筛选回收:对各 Region 的回收价值排序,根据用户期望停顿时间制定回收计划。必须暂停用户线程,由多条收集线程并行完成。
可由用户指定期望停顿时间是 G1 的一个强大功能,但该值不能设得太低,一般设置为100~300 ms。

Q5:ZGC 了解吗?
JDK11 中加入的具有实验性质的低延迟垃圾收集器,目标是尽可能在不影响吞吐量的前提下,实现在任意堆内存大小都可以把停顿时间限制在 10ms 以内的低延迟。

基于 Region 内存布局,不设分代,使用了读屏障、染色指针和内存多重映射等技术实现可并发的标记-整理,以低延迟为首要目标。

ZGC 的 Region 具有动态性,是动态创建和销毁的,并且容量大小也是动态变化的。

Q6:你知道哪些内存分配与回收策略?
对象优先在 Eden 区分配

大多数情况下对象在新生代 Eden 区分配,当 Eden 没有足够空间时将发起一次 Minor GC。

大对象直接进入老年代

大对象指需要大量连续内存空间的对象,典型是很长的字符串或数量庞大的数组。大对象容易导致内存还有不少空间就提前触发垃圾收集以获得足够的连续空间。

HotSpot 提供了 -XX:PretenureSizeThreshold 参数,大于该值的对象直接在老年代分配,避免在 Eden 和 Survivor 间来回复制。

长期存活对象进入老年代

虚拟机给每个对象定义了一个对象年龄计数器,存储在对象头。如果经历过第一次 Minor GC 仍然存活且能被 Survivor 容纳,该对象就会被移动到 Survivor 中并将年龄设置为 1。对象在 Survivor 中每熬过一次 Minor GC 年龄就加 1 ,当增加到一定程度(默认15)就会被晋升到老年代。对象晋升老年代的阈值可通过 -XX:MaxTenuringThreshold 设置。

动态对象年龄判定

为了适应不同内存状况,虚拟机不要求对象年龄达到阈值才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 的一半,年龄不小于该年龄的对象就可以直接进入老年代。

空间分配担保

MinorGC 前虚拟机必须检查老年代最大可用连续空间是否大于新生代对象总空间,如果满足则说明这次 Minor GC 确定安全。

如果不满足,虚拟机会查看 -XX:HandlePromotionFailure 参数是否允许担保失败,如果允许会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果满足将冒险尝试一次 Minor GC,否则改成一次 FullGC。

冒险是因为新生代使用复制算法,为了内存利用率只使用一个 Survivor,大量对象在 Minor GC 后仍然存活时,需要老年代进行分配担保,接收 Survivor 无法容纳的对象。

Q7:你知道哪些故障处理工具?
jps:虚拟机进程状况工具

功能和 ps 命令类似:可以列出正在运行的虚拟机进程,显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(LVMID)。LVMID 与操作系统的进程 ID(PID)一致,使用 Windows 的任务管理器或 UNIX 的 ps 命令也可以查询到虚拟机进程的 LVMID,但如果同时启动了多个虚拟机进程,必须依赖 jps 命令。

jstat:虚拟机统计信息监视工具

用于监视虚拟机各种运行状态信息。可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译器等运行时数据,在没有 GUI 界面的服务器上是运行期定位虚拟机性能问题的常用工具。

参数含义:S0 和 S1 表示两个 Survivor,E 表示新生代,O 表示老年代,YGC 表示 Young GC 次数,YGCT 表示 Young GC 耗时,FGC 表示 Full GC 次数,FGCT 表示 Full GC 耗时,GCT 表示 GC 总耗时。

jinfo:Java 配置信息工具

实时查看和调整虚拟机各项参数,使用 jps 的 -v 参数可以查看虚拟机启动时显式指定的参数,但如果想知道未显式指定的参数值只能使用 jinfo 的 -flag 查询。

jmap:Java 内存映像工具

用于生成堆转储快照,还可以查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率,当前使用的是哪种收集器等。和 jinfo 一样,部分功能在 Windows 受限,除了生成堆转储快照的 -dump 和查看每个类实例的 -histo 外,其余选项只能在 Linux 使用。

jhat:虚拟机堆转储快照分析工具

JDK 提供 jhat 与 jmap 搭配使用分析 jmap 生成的堆转储快照。jhat 内置了一个微型的 HTTP/Web 服务器,生成堆转储快照的分析结果后可以在浏览器查看。

jstack:Java 堆栈跟踪工具

用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等。线程出现停顿时通过 jstack 查看各个线程的调用堆栈,可以获知没有响应的线程在后台做什么或等什么资源。

类加载机制 7
Q1:Java 程序是怎样运行的?
首先通过 Javac 编译器将 .java 转为 JVM 可加载的 .class 字节码文件。

Javac 是由 Java 编写的程序,编译过程可以分为: ① 词法解析,通过空格分割出单词、操作符、控制符等信息,形成 token 信息流,传递给语法解析器。② 语法解析,把 token 信息流按照 Java 语法规则组装成语法树。③ 语义分析,检查关键字使用是否合理、类型是否匹配、作用域是否正确等。④ 字节码生成,将前面各个步骤的信息转换为字节码。

字节码必须通过类加载过程加载到 JVM 后才可以执行,执行有三种模式,解释执行、JIT 编译执行、JIT 编译与解释器混合执行(主流 JVM 默认执行的方式)。混合模式的优势在于解释器在启动时先解释执行,省去编译时间。

之后通过即时编译器 JIT 把字节码文件编译成本地机器码。

Java 程序最初都是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会认定其为"热点代码",热点代码的检测主要有基于采样和基于计数器两种方式,为了提高热点代码的执行效率,虚拟机会把它们编译成本地机器码,尽可能对代码优化,在运行时完成这个任务的后端编译器被称为即时编译器。

还可以通过静态的提前编译器 AOT 直接把程序编译成与目标机器指令集相关的二进制代码。

Q2:类加载是什么?
Class 文件中描述的各类信息都需要加载到虚拟机后才能使用。JVM 把描述类的数据从 Class 文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程称为虚拟机的类加载机制。

与编译时需要连接的语言不同,Java 中类型的加载、连接和初始化都是在运行期间完成的,这增加了性能开销,但却提供了极高的扩展性,Java 动态扩展的语言特性就是依赖运行期动态加载和连接实现的。

一个类型从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、解析和初始化三个部分称为连接。加载、验证、准备、初始化阶段的顺序是确定的,解析则不一定:可能在初始化之后再开始,这是为了支持 Java 的动态绑定。

Q3:类初始化的情况有哪些?
① 遇到 new、getstatic、putstatic 或 invokestatic 字节码指令时,还未初始化。典型场景包括 new 实例化对象、读取或设置静态字段、调用静态方法。

② 对类反射调用时,还未初始化。

③ 初始化类时,父类还未初始化。

④ 虚拟机启动时,会先初始化包含 main 方法的主类。

⑤ 使用 JDK7 的动态语言支持时,如果 MethodHandle 实例的解析结果为指定类型的方法句柄且句柄对应的类还未初始化。

⑥ 接口定义了默认方法,如果接口的实现类初始化,接口要在其之前初始化。

其余所有引用类型的方式都不会触发初始化,称为被动引用。被动引用实例:① 子类使用父类的静态字段时,只有父类被初始化。② 通过数组定义使用类。③ 常量在编译期会存入调用类的常量池,不会初始化定义常量的类。

接口和类加载过程的区别:初始化类时如果父类没有初始化需要初始化父类,但接口初始化时不要求父接口初始化,只有在真正使用父接口时(如引用接口中定义的常量)才会初始化。

Q4:类加载的过程是什么?
加载

该阶段虚拟机需要完成三件事:① 通过一个类的全限定类名获取定义类的二进制字节流。② 将字节流所代表的静态存储结构转化为方法区的运行时数据区。③ 在内存中生成对应该类的 Class 实例,作为方法区这个类的数据访问入口。

验证

确保 Class 文件的字节流符合约束。如果虚拟机不检查输入的字节流,可能因为载入有错误或恶意企图的字节流而导致系统受攻击。验证主要包含四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。

验证重要但非必需,因为只有通过与否的区别,通过后对程序运行期没有任何影响。如果代码已被反复使用和验证过,在生产环境就可以考虑关闭大部分验证缩短类加载时间。

准备

为类静态变量分配内存并设置零值,该阶段进行的内存分配仅包括类变量,不包括实例变量。如果变量被 final 修饰,编译时 Javac 会为变量生成 ConstantValue 属性,准备阶段虚拟机会将变量值设为代码值。

解析

将常量池内的符号引用替换为直接引用。

符号引用以一组符号描述引用目标,可以是任何形式的字面量,只要使用时能无歧义地定位目标即可。与虚拟机内存布局无关,引用目标不一定已经加载到虚拟机内存。

直接引用是可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。和虚拟机的内存布局相关,引用目标必须已在虚拟机的内存中存在。

初始化

直到该阶段 JVM 才开始执行类中编写的代码。准备阶段时变量赋过零值,初始化阶段会根据程序员的编码去初始化类变量和其他资源。初始化阶段就是执行类构造方法中的 方法,该方法是 Javac 自动生成的。

Q5:有哪些类加载器?
自 JDK1.2 起 Java 一直保持三层类加载器:

启动类加载器

在 JVM 启动时创建,负责加载最核心的类,例如 Object、System 等。无法被程序直接引用,如果需要把加载委派给启动类加载器,直接使用 null 代替即可,因为启动类加载器通常由操作系统实现,并不存在于 JVM 体系。

平台类加载器

从 JDK9 开始从扩展类加载器更换为平台类加载器,负载加载一些扩展的系统类,比如 XML、加密、压缩相关的功能类等。

应用类加载器

也称系统类加载器,负责加载用户类路径上的类库,可以直接在代码中使用。如果没有自定义类加载器,一般情况下应用类加载器就是默认的类加载器。自定义类加载器通过继承 ClassLoader 并重写 findClass 方法实现。

Q6:双亲委派模型是什么?
类加载器具有等级制度但非继承关系,以组合的方式复用父加载器的功能。双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应该有自己的父加载器。

一个类加载器收到了类加载请求,它不会自己去尝试加载,而将该请求委派给父加载器,每层的类加载器都是如此,因此所有加载请求最终都应该传送到启动类加载器,只有当父加载器反馈无法完成请求时,子加载器才会尝试。

类跟随它的加载器一起具备了有优先级的层次关系,确保某个类在各个类加载器环境中都是同一个,保证程序的稳定性。

Q7:如何判断两个类是否相等?
任意一个类都必须由类加载器和这个类本身共同确立其在虚拟机中的唯一性。

两个类只有由同一类加载器加载才有比较意义,否则即使两个类来源于同一个 Class 文件,被同一个 JVM 加载,只要类加载器不同,这两个类就必定不相等。

Thymeleaf

一,理解Thymeleaf的概念,用法
二,Thymeleaf与Spring Boot集成
三,Thymeleaf实战
一,
1,java模板引擎。能够处理HTML,XMLJAVAScript,CSSS甚至文本。类似JSP
2,自然模板。原型即页面
3,语法优雅,OGNL,springEL
4,遵守Web标准,支持HTML5

Thymeleaf标准方言


1,标准表达式
变量表达式
${}

消息表达式也称文本外部化,国际化
#{}
选择表达式
*{}
链接表达式
@{}
分段表达式
th:insert / th:replace
字面量 / 文本

2,设置属性值
th:attr

3,迭代器
th:each

  • 引用div th:insert th:replace th:include

    6,属性优先级
    insert ->each-> if->with->attr->value

    7,注释
    标准HTML/XML注释

    Thymeleaf解析器级注释块

    注释内容

    原型注释块

    8,内联
    内联表达式
    [[..]] [(..)]分别对应于th:text对特殊字符转义 th:utext不处理
    禁止内联
    th:inline="none"
    JavaScript内联
    CSS内联

    9,基本对象
    #ctx上下文对象
    #local
    request/session等属性
    Web上下文对象
    #request
    10,工具对象

    二,Thymeleaf与Spring Boot集成
    修改build.gradle
    dependencies{
    compile(‘org.springframework.boot:spring-boot-starter-thymeleaf')
    }
    三,Thymeleaf实战
    1 get/users -->list.html
    2 get/users/{id}-->view.html
    3 get/users/form -->form.html
    4 post/users list.html
    5 get /users/delete/{id}
    6 get /users/modify/{id}
    resources
    templates模板
    template.fragment
    header.html:共有头部页面
    footer.html:共用底部页面
    template.users
    form.html
    view.html

  • 项目常见问题

    @target 是Java的元注解(指修饰注解的注解)之一。用来指定注解修饰类的哪个成员。
    加大括号表示一个数组,指被修饰的注解能用于多个不同的类成员。
    举个栗子:
    @target(ElementType.FIELD)
    public @interface A{}
    表示注解A只能用来修饰类中的Field
    @target({ElementType.FIELD, ElementType.METHOD})
    public @interface A{}
    表示注解A能用来修饰类中的Field和Method

    四个元注解分别是:@target,@retention,@documented,@inherited ,再次强调下元注解是java API提供,是专门用来定义注解的注解,其作用分别如下:
          @target 表示该注解用于什么地方,可能的值在枚举类 ElemenetType 中,包括: 
              ElemenetType.CONSTRUCTOR----------------------------构造器声明 
              ElemenetType.FIELD --------------------------------------域声明(包括 enum 实例) 
              ElemenetType.LOCAL_VARIABLE------------------------- 局部变量声明 
              ElemenetType.METHOD ----------------------------------方法声明 
              ElemenetType.PACKAGE --------------------------------- 包声明 
              ElemenetType.PARAMETER ------------------------------参数声明 
              ElemenetType.TYPE--------------------------------------- 类,接口(包括注解类型)或enum声明 
               
         @retention 表示在什么级别保存该注解信息。可选的参数值在枚举类型 RetentionPolicy 中,包括: 
              RetentionPolicy.SOURCE ---------------------------------注解将被编译器丢弃 
              RetentionPolicy.CLASS -----------------------------------注解在class文件中可用,但会被VM丢弃 
              RetentionPolicy.RUNTIME VM-------将在运行期也保留注释,因此可以通过反射机制读取注解的信息。 
               
          @documented 将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同。相当与@see,@param 等。
           
          @inherited 允许子类继承父类中的注解。

    数值的整数次方

    题目:
    给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
    保证base和exponent不同时为0
    思路:
    对exponent进行分类讨论,主要是当exponent小于0的时候,我们需要求出base的-exponent次方的值,然后拿1除以这个结果即可
    代码:

    public double Power(double base, int exponent) {
           double ans=1.0;       
            if(exponent>=0){
            for(int i=1;i<=exponent;i++){
                ans=ans*base;
            }
            }else{
                for(int i=1;i<=-exponent;i++){
                    ans=ans*base;
                }
                ans=1/ans;
            }
            return ans;
      }
    }

    旋转数组的最小数字

    问题:
    把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
    输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。
    例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
    NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
    思路:
    方法一:遍历数组,不断去更新保存最小值的变量。时间复杂度是O(n)
    方法二:通过二分的方法,不断去更新存在于两个子数组(两个非递减排序子数组)中的下标。时间复杂度是O(log(n))
    代码:

    public class ArrayMinNuber {
    	/**
    	 * 方法一:遍历 时间复杂度O(n)
    	 * @param array
    	 * @return
    	 */
    	public static int find1(int[] array){
    		 int len=array.length;
    	        if(len==0){
    	            return 0;
    	        }
    			int m=array[0];
    			for(int i=1;i<len;i++){
    				m=Math.min(m,array[i]);
    			}		
    			return m;		    
    	    }
    	/**
    	 * 方法二:利用非递减 时间复杂度O(logN)
    	 * 3 4 5 1 2
    	 * 利用二分法
    	 * mid=(l+r)>>1
    	 * mid与array[l]比较mid>=array[l] l=mid
    	 * 跳出条件
    	 * while(l<r-1)
    	 */
    	public static int find2(int[] array){
    		int len=array.length;
    		if(len==0){
    			return 0;
    		}
    		int l=0;
    		int r=len-1;
    		while(l<r-1){
    			int mid=(l+r)/2;
    		if(array[mid]>=array[l]){
    			l=mid;
    		}else if(array[mid]<=array[r]){
    			r=mid;
    		}
    		}
    		return array[r];		
    	}
    	public static void main(String[] args){
    		int[] array=new int[]{3,2,2,2};
    		System.out.println(find2(array));		
    	}
    }

    Redis

    作者:冠状病毒biss
    链接:https://www.nowcoder.com/discuss/447742?source_id=profile_create&channel=666
    来源:牛客网

    架构 3
    Q1:Redis 有什么特点?
    基于键值对的数据结构服务器

    Redis 中的值不仅可以是字符串,还可以是具体的数据结构,这样不仅能应用于多种场景开发,也可以提高开发效率。它主要提供五种数据结构:字符串、哈希、列表、集合、有序集合,同时在字符串的基础上演变出了 Bitmaps 和 HyperLogLog 两种数据结构,Redis 3.2 还加入了有关 GEO 地理信息定位的功能。

    丰富的功能

    ① 提供了键过期功能,可以实现缓存。② 提供了发布订阅功能,可以实现消息系统。③ 支持 Lua 脚本,可以创造新的 Redis 命令。④ 提供了简单的事务功能,能在一定程度上保证事务特性。⑤ 提供了流水线功能,客户端能将一批命令一次性传到 Redis,减少网络开销。

    简单稳定

    Redis 的简单主要体现在三个方面:① 源码很少,早期只有 2 万行左右,在 3.0 版本由于添加了集群特性,增加到了 5 万行左右,相对于很多 NoSQL 数据库来说代码量要少很多。② 采用单线程模型,使得服务端处理模型更简单,也使客户端开发更简单。③ 不依赖底层操作系统的类库,自己实现了事件处理的相关功能。虽然 Redis 比较简单,但也很稳定。

    客户端语言多

    Redis 提供了简单的 TCP 通信协议,很多编程语言可以方便地接入 Redis,例如 Java、PHP、Python、C、C++ 等。

    持久化

    通常来说数据放在内存中是不安全的,一旦发生断电或故障数据就可能丢失,因此 Redis 提供了两种持久化方式 RDB 和 AOF 将内存的数据保存到硬盘中。

    高性能

    Redis 使用了单线程架构和 IO 多路复用模型来实现高性能的内存数据库服务。

    每次客户端调用都经历了发送命令、执行命令、返回结果三个过程,因为 Redis 是单线程处理命令的,所以一条命令从客户端到达服务器不会立即执行,所有命令都会进入一个队列中,然后逐个被执行。客户端的执行顺序可能不确定,但是可以确定不会有两条命令被同时执行,不存在并发问题。

    通常来说单线程处理能力要比多线程差,Redis 快的原因:① 纯内存访问,Redis 将所有数据放在内存中。② 非阻塞 IO,Redis 使用 epoll 作为 IO 多路复用技术的实现,再加上 Redis 本身的事件处理模型将 epoll 中的连接、读写、关闭都转换为时间,不在网络 IO 上浪费过多的时间。③ 单线程避免了线程切换和竞争产生的消耗。单线程的一个问题是对于每个命令的执行时间是有要求的,如果某个命令执行时间过长会造成其他命令的阻塞,对于 Redis 这种高性能服务来说是致命的,因此 Redis 是面向快速执行场景的数据库。

    Q2:Redis 的数据结构有哪些?
    可以使用 type 命令查看当前键的数据类型结构,它们分别是:string、hash、list、set、zset,但这些只是 Redis 对外的数据结构。实际上每种数据结构都有自己底层的内部编码实现,这样 Redis 会在合适的场景选择合适的内部编码,string 包括了 raw、int 和 embstr,hash 包括了 hashtable 和 ziplist,list 包括了 linkedlist 和 ziplist,set 包括了 hashtable 和 intset,zset 包括了 skiplist 和 ziplist。可以使用 object encoding 查看内部编码。

    Q3:Redis 为什么要使用内部编码?
    ① 可以改进内部编码,而对外的数据结构和命令没有影响。

    ② 多种内部编码实现可以在不同场景下发挥各自的优势,例如 ziplist 比较节省内存,但在列表元素较多的情况下性能有所下降,这时 Redis 会根据配置选项将列表类型的内部实现转换为 linkedlist。

    string 4
    Q1:简单说一说 string 类型
    字符串类型是 Redis 最基础的数据结构,键都是字符串类型,而且其他几种数据结构都是在字符串类型的基础上构建的。字符串类型的值可以实际可以是字符串(简单的字符串、复杂的字符串如 JSON、XML)、数字(整形、浮点数)、甚至二进制(图片、音频、视频),但是值最大不能超过 512 MB。

    Q2:你知道哪些 string 的命令?
    设置值

    set key value [ex seconds] [px millseconds] [nx|xx]

    ex seconds:为键设置秒级过期时间,跟 setex 效果一样
    px millseconds:为键设置毫秒级过期时间
    nx:键必须不存在才可以设置成功,用于添加,跟 setnx 效果一样。由于 Redis 的单线程命令处理机制,如果多个客户端同时执行,则只有一个客户端能设置成功,可以用作分布式锁的一种实现。
    xx:键必须存在才可以设置成功,用于更新
    获取值

    get key,如果不存在返回 nil

    批量设置值

    mset key value [key value...]

    批量获取值

    mget key [key...]

    批量操作命令可以有效提高开发效率,假如没有 mget,执行 n 次 get 命令需要 n 次网络时间 + n 次命令时间,使用 mget 只需要 1 次网络时间 + n 次命令时间。Redis 可以支持每秒数万的读写操作,但这指的是 Redis 服务端的处理能力,对于客户端来说一次命令处理命令时间还有网络时间。因为 Redis 的处理能力已足够高,对于开发者来说,网络可能会成为性能瓶颈。

    计数

    incr key

    incr 命令用于对值做自增操作,返回结果分为三种:① 值不是整数返回错误。② 值是整数,返回自增后的结果。③ 值不存在,按照值为 0 自增,返回结果 1。除了 incr 命令,还有自减 decr、自增指定数字 incrby、自减指定数组 decrby、自增浮点数 incrbyfloat。

    Q3:string 的内部编码是什么?
    int:8 个字节的长整形
    embstr:小于等于 39 个字节的字符串
    raw:大于 39 个字节的字符串
    Q4:string 的应用场景有什么?
    缓存功能

    Redis 作为缓存层,MySQL 作为存储层,首先从 Redis 获取数据,如果失败就从 MySQL 获取并将结果写回 Redis 并添加过期时间。

    计数

    Redis 可以实现快速计数功能,例如视频每播放一次就用 incy 把播放数加 1。

    共享 Session

    一个分布式 Web 服务将用户的 Session 信息保存在各自服务器,但会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问负载到不同服务器上,用户刷新一次可能会发现需要重新登陆。为解决该问题,可以使用 Redis 将用户的 Session 进行集中管理,在这种模式下只要保证 Redis 是高可用和扩展性的,每次用户更新或查询登录信息都直接从 Redis 集中获取。

    限速

    例如为了短信接口不被频繁访问会限制用户每分钟获取验证码的次数或者网站限制一个 IP 地址不能在一秒内访问超过 n 次。可以使用键过期策略和自增计数实现。
    hash 4
    Q1:简单说一说 hash 类型
    哈希类型指键值本身又是一个键值对结构,哈希类型中的映射关系叫 field-value,这里的 value 是指 field 对于的值而不是键对于的值。

    Q2:你知道哪些 hash 的命令?
    设置值

    hset key field value,如果设置成功会返回 1,反之会返回 0,此外还提供了 hsetnx 命令,作用和 setnx 类似,只是作用于由键变为 field。

    获取值

    hget key field,如果不存在会返回 nil。

    删除 field

    hdel key field [field...],会删除一个或多个 field,返回结果为删除成功 field 的个数。

    计算 field 个数

    hlen key

    批量设置或获取 field-value
    hmget key field [field...]
    hmset key field value [field value...]

    作者:冠状病毒biss
    链接:https://www.nowcoder.com/discuss/447742?source_id=profile_create&channel=666
    来源:牛客网

    判断 field 是否存在

    hexists key field,存在返回 1,否则返回 0。

    获取所有的 field

    hkeys key,返回指定哈希键的所有 field。

    获取所有 value

    hvals key,获取指定键的所有 value。

    获取所有的 field-value

    hgetall key,获取指定键的所有 field-value。

    Q3:hash 的内部编码是什么?
    ziplist 压缩列表:当哈希类型元素个数和值小于配置值(默认 512 个和 64 字节)时会使用 ziplist 作为内部实现,使用更紧凑的结构实现多个元素的连续存储,在节省内存方面比 hashtable 更优秀。

    hashtable 哈希表:当哈希类型无法满足 ziplist 的条件时会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度都为 O(1)。

    Q4:hash 的应用场景有什么?
    缓存用户信息,每个用户属性使用一对 field-value,但只用一个键保存。

    优点:简单直观,如果合理使用可以减少内存空间使用。

    缺点:要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,hashtable 会消耗更多内存。

    list 4
    Q1:简单说一说 list 类型
    list 是用来存储多个有序的字符串,列表中的每个字符串称为元素,一个列表最多可以存储 2^32^-1 个元素。可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发中有很多应用场景。

    list 有两个特点:① 列表中的元素是有序的,可以通过索引下标获取某个元素或者某个范围内的元素列表。② 列表中的元素可以重复。

    Q2:你知道哪些 list 的命令?
    添加

    从右边插入元素:rpush key value [value...]

    从左到右获取列表的所有元素:lrange 0 -1

    从左边插入元素:lpush key value [value...]

    向某个元素前或者后插入元素:linsert key before|after pivot value,会在列表中找到等于 pivot 的元素,在其前或后插入一个新的元素 value。

    查找

    获取指定范围内的元素列表:lrange key start end,索引从左到右的范围是 0N-1,从右到左是 -1-N,lrange 中的 end 包含了自身。

    获取列表指定索引下标的元素:lindex key index,获取最后一个元素可以使用 lindex key -1。

    获取列表长度:llen key

    删除

    从列表左侧弹出元素:lpop key

    从列表右侧弹出元素:rpop key

    删除指定元素:lrem key count value,如果 count 大于 0,从左到右删除最多 count 个元素,如果 count 小于 0,从右到左删除最多个 count 绝对值个元素,如果 count 等于 0,删除所有。

    按照索引范围修剪列表:ltrim key start end,只会保留 start ~ end 范围的元素。

    修改

    修改指定索引下标的元素:lset key index newValue。

    阻塞操作

    阻塞式弹出:blpop/brpop key [key...] timeout,timeout 表示阻塞时间。

    当列表为空时,如果 timeout = 0,客户端会一直阻塞,如果在此期间添加了元素,客户端会立即返回。

    如果是多个键,那么brpop会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回。

    如果多个客户端对同一个键执行 brpop,那么最先执行该命令的客户端可以获取弹出的值。

    Q3:list 的内部编码是什么?
    ziplist 压缩列表:跟哈希的 zipilist 相同,元素个数和大小小于配置值(默认 512 个和 64 字节)时使用。

    linkedlist 链表:当列表类型无法满足 ziplist 的条件时会使用linkedlist。

    Redis 3.2 提供了 quicklist 内部编码,它是以一个 ziplist 为节点的 linkedlist,它结合了两者的优势,为列表类提供了一种更为优秀的内部编码实现。

    作者:冠状病毒biss
    链接:https://www.nowcoder.com/discuss/447742?source_id=profile_create&channel=666
    来源:牛客网

    Q4:list 的应用场景有什么?
    消息队列

    Redis 的 lpush + brpop 即可实现阻塞队列,生产者客户端使用 lpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式地抢列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。

    文章列表

    每个用户有属于自己的文章列表,现在需要分页展示文章列表,就可以考虑使用列表。因为列表不但有序,同时支持按照索引范围获取元素。每篇文章使用哈希结构存储。

    lpush + lpop = 栈、lpush + rpop = 队列、lpush + ltrim = 优先集合、lpush + brpop = 消息队列。

    set 4
    Q1:简单说一说 set 类型
    集合类型也是用来保存多个字符串元素,和列表不同的是集合不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。一个集合最多可以存储 2^32^-1 个元素。Redis 除了支持集合内的增删改查,还支持多个集合取交集、并集、差集。

    Q2:你知道哪些 set 的命令?
    添加元素

    sadd key element [element...],返回结果为添加成功的元素个数。

    删除元素

    srem key element [element...],返回结果为成功删除的元素个数。

    计算元素个数

    scard key,时间复杂度为 O(1),会直接使用 Redis 内部的遍历。

    判断元素是否在集合中

    sismember key element,如果存在返回 1,否则返回 0。

    随机从集合返回指定个数个元素

    srandmember key [count],如果不指定 count 默认为 1。

    从集合随机弹出元素

    spop key,可以从集合中随机弹出一个元素。

    获取所有元素

    smembers key

    求多个集合的交集/并集/差集

    sinter key [key...]

    sunion key [key...]

    sdiff key [key...]

    保存交集、并集、差集的结果

    sinterstore/sunionstore/sdiffstore destination key [key...]

    集合间运算在元素较多情况下比较耗时,Redis 提供这三个指令将集合间交集、并集、差集的结果保存在 destination key 中。

    Q3:set 的内部编码是什么?
    intset 整数集合:当集合中的元素个数小于配置值(默认 512 个时),使用 intset。

    hashtable 哈希表:当集合类型无法满足 intset 条件时使用 hashtable。当某个元素不为整数时,也会使用 hashtable。

    Q4:set 的应用场景有什么?
    set 比较典型的使用场景是标签,例如一个用户可能与娱乐、体育比较感兴趣,另一个用户可能对例时、新闻比较感兴趣,这些兴趣点就是标签。这些数据对于用户体验以及增强用户黏度比较重要。

    sadd = 标签、spop/srandmember = 生成随机数,比如抽奖、sadd + sinter = 社交需求。

    zset 4
    Q1:简单说一说 zset 类型
    有序集合保留了集合不能有重复成员的特性,不同的是可以排序。但是它和列表使用索引下标作为排序依据不同的是,他给每个元素设置一个分数(score)作为排序的依据。有序集合提供了获取指定分数和元素查询范围、计算成员排名等功能。

    Q2:你知道哪些 zset 的命令?
    添加成员

    zadd key score member [score member...],返回结果是成功添加成员的个数

    Redis 3.2 为 zadd 命令添加了 nx、xx、ch、incr 四个选项:

    nx:member 必须不存在才可以设置成功,用于添加。
    xx:member 必须存在才能设置成功,用于更新。
    ch:返回此次操作后,有序集合元素和分数变化的个数。
    incr:对 score 做增加,相当于 zincrby。
    zadd 的时间复杂度为 O(logn),sadd 的时间复杂度为 O(1)。

    计算成员个数

    zcard key,时间复杂度为 O(1)。

    计算某个成员的分数

    zscore key member ,如果不存在则返回 nil。

    计算成员排名

    zrank key member,从低到高返回排名。

    zrevrank key member,从高到低返回排名。

    删除成员

    zrem key member [member...],返回结果是成功删除的个数。

    增加成员的分数

    zincrby key increment member

    返回指定排名范围的成员

    zrange key start end [withscores],从低到高返回

    zrevrange key start end [withscores], 从高到底返回

    返回指定分数范围的成员

    zrangebyscore key min max [withscores] [limit offset count],从低到高返回

    zrevrangebyscore key min max [withscores] [limit offset count], 从高到底返回

    返回指定分数范围成员个数

    zcount key min max

    删除指定分数范围内的成员

    zremrangebyscore key min max

    交集和并集

    zinterstore/zunionstore destination numkeys key [key...] [weights weight [weight...]] [aggregate sum|min|max]

    destination:交集结果保存到这个键

    numkeys:要做交集计算键的个数

    key:需要做交集计算的键

    weight:每个键的权重,默认 1

    aggregate sum|min|max:计算交集后,分值可以按和、最小值、最大值汇总,默认 sum。

    Q3:zset 的内部编码是什么?
    ziplist 压缩列表:当有序集合元素个数和值小于配置值(默认128 个和 64 字节)时会使用 ziplist 作为内部实现。

    skiplist 跳跃表:当 ziplist 不满足条件时使用,因为此时 ziplist 的读写效率会下降。

    Q4:zset 的应用场景有什么?
    有序集合的典型使用场景就是排行榜系统,例如用户上传了一个视频并获得了赞,可以使用 zadd 和 zincrby。如果需要将用户从榜单删除,可以使用 zrem。如果要展示获取赞数最多的十个用户,可以使用 zrange。

    键和数据库管理 5
    Q1:如何对键重命名?
    rename key newkey

    如果 rename 前键已经存在,那么它的值也会被覆盖。为了防止强行覆盖,Redis 提供了 renamenx 命令,确保只有 newkey 不存在时才被覆盖。由于重命名键期间会执行 del 命令删除旧的键,如果键对应值比较大会存在阻塞的可能。

    Q2:如何设置键过期?
    expire key seconds:键在 seconds 秒后过期。

    如果过期时间为负值,键会被立即删除,和 del 命令一样。persist 命令可以将键的过期时间清除。

    对于字符串类型键,执行 set 命令会去掉过期时间,set 命令对应的函数 setKey 最后执行了 removeExpire 函数去掉了过期时间。setex 命令作为 set + expire 的组合,不单是原子执行并且减少了一次网络通信的时间。

    Q3:如何进行键迁移?
    move

    move 命令用于在 Redis 内部进行数据迁移,move key db 把指定的键从源数据库移动到目标数据库中。

    dump + restore

    可以实现在不同的 Redis 实例之间进行数据迁移,分为两步:

    ① dump key ,在源 Redis 上,dump 命令会将键值序列化,格式采用 RDB 格式。

    ② restore key ttl value,在目标 Redis 上,restore 命令将序列化的值进行复原,ttl 代表过期时间, ttl = 0 则没有过期时间。

    整个迁移并非原子性的,而是通过客户端分步完成,并且需要两个客户端。

    migrate

    实际上 migrate 命令就是将 dump、restore、del 三个命令进行组合,从而简化操作流程。migrate 具有原子性,支持多个键的迁移,有效提高了迁移效率。实现过程和 dump + restore 类似,有三点不同:

    ① 整个过程是原子执行,不需要在多个 Redis 实例开启客户端。

    ② 数据传输直接在源 Redis 和目标 Redis 完成。

    ③ 目标 Redis 完成 restore 后会发送 OK 给源 Redis,源 Redis 接收后根据 migrate 对应选项来决定是否在源 Redis 上删除对应键。

    Q4:如何切换数据库?
    select dbIndex,Redis 中默认配置有 16 个数据库,例如 select 0 将切换到第一个数据库,数据库之间的数据是隔离的。

    Q5:如何清除数据库?
    用于清除数据库,flushdb 只清除当前数据库,flushall 会清除所有数据库。如果当前数据库键值数量比较多,flushdb/flushall 存在阻塞 Redis 的可能性。

    持久化 9
    Q1:RDB 持久化的原理?
    RDB 持久化是把当前进程数据生成快照保存到硬盘的过程,触发 RDB 持久化过程分为手动触发和自动触发。

    手动触发分别对应 save 和 bgsave 命令:

    save:阻塞当前 Redis 服务器,直到 RDB 过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。
    bgasve:Redis 进程执行 fork 操作创建子进程,RDB 持久化过程由子进程负责,完成后自动结束。阻塞只发生在 fork 阶段,一般时间很短。bgsave 是针对 save 阻塞问题做的优化,因此 Redis 内部所有涉及 RDB 的操作都采用 bgsave 的方式,而 save 方式已经废弃。
    除了手动触发外,Redis 内部还存在自动触发 RDB 的持久化机制,例如:

    使用 save 相关配置,如 save m n,表示 m 秒内数据集存在 n 次修改时,自动触发 bgsave。
    如果从节点执行全量复制操作,主节点自动执行 bgsave 生成 RDB 文件并发送给从节点。
    执行 debug reload 命令重新加载 Redis 时也会自动触发 save 操作。
    默认情况下执行 shutdown 命令时,如果没有开启 AOF 持久化功能则自动执行 bgsave。
    Q2:bgsave 的原理?
    ① 执行 bgsave 命令,Redis 父进程判断当前是否存在正在执行的子进程,如 RDB/AOF 子进程,如果存在 bgsave 命令直接返回。

    ② 父进程执行 fork 操作创建子进程,fork 操作过程中父进程会阻塞。

    ③ 父进程 fork 完成后,bgsave 命令返回并不再阻塞父进程,可以继续响应其他命令。

    ④ 子进程创建 RDB 文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。

    ⑤ 进程发送信号给父进程表示完成,父进程更新统计信息。

    Q3:RDB 持久化的优点?
    RDB 是一个紧凑压缩的二进制文件,代表 Redis 在某个时间点上的数据快照。非常适合于备份,全量复制等场景。例如每 6 个消时执行 bgsave 备份,并把 RDB 文件拷贝到远程机器或者文件系统中,用于灾难恢复。

    Redis 加载 RDB 恢复数据远远快于 AOF 的方式。

    Q4:RDB 持久化的缺点?
    RDB 方式数据无法做到实时持久化/秒级持久化,因为 bgsave 每次运行都要执行 fork 操作创建子进程,属于重量级操作,频繁执行成本过高。针对 RDB 不适合实时持久化的问题,Redis 提供了 AOF 持久化方式。

    RDB 文件使用特定二进制格式保存,Redis 版本演进过程中有多个格式的 RDB 版本,存在老版本 Redis 服务无法兼容新版 RDB 格式的问题。

    Q5:AOF 持久化的原理?
    AOF 持久化以独立日志的方式记录每次写命令,重启时再重新执行 AOF 文件中的命令达到恢复数据的目的。AOF 的主要作用是解决了数据持久化的实时性,目前是 Redis 持久化的主流方式。

    开启 AOF 功能需要设置:appendonly yes,默认不开启。保存路径同 RDB 方式一致,通过 dir 配置指定。

    AOF 的工作流程操作:命令写入 append、文件同步 sync、文件重写 rewrite、重启加载 load:

    所有的写入命令会追加到 aof_buf 缓冲区中。
    AOF 缓冲区根据对应的策略向硬盘做同步操作。
    随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
    当服务器重启时,可以加载 AOF 文件进行数据恢复。
    Q6:AOF 命令写入的原理?
    AOF 命令写入的内容直接是文本协议格式,采用文本协议格式的原因:

    文本协议具有很好的兼容性。
    开启 AOF 后所有写入命令都包含追加操作,直接采用协议格式避免了二次处理开销。
    文本协议具有可读性,方便直接修改和处理。
    AOF 把命令追加到缓冲区的原因:

    Redis 使用单线程响应命令,如果每次写 AOF 文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。先写入缓冲区中还有另一个好处,Redis 可以提供多种缓冲区同步硬盘策略,在性能和安全性方面做出平衡。

    Q7:AOF 文件同步的原理?
    Redis 提供了多种 AOF 缓冲区文件同步策略,由参数 appendfsync 控制,不同值的含义如下:

    always:命令写入缓冲区后调用系统 fsync 操作同步到 AOF 文件,fsync 完成后线程返回。每次写入都要同步 AOF,性能较低,不建议配置。

    everysec:命令写入缓冲区后调用系统 write 操作,write 完成后线程返回。fsync 同步文件操作由专门线程每秒调用一次。是建议的策略,也是默认配置,兼顾性能和数据安全。

    no:命令写入缓冲区后调用系统 write 操作,不对 AOF 文件做 fsync 同步,同步硬盘操作由操作系统负责,周期通常最长 30 秒。由于操作系统每次同步 AOF 文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但安全性无法保证。

    Q8:AOF 文件重写的原理?
    文件重写是把 Redis 进程内的数据转化为写命令同步到新 AOF 文件的过程,可以降低文件占用空间,更小的文件可以更快地被加载。

    重写后 AOF 文件变小的原因:

    进程内已经超时的数据不再写入文件。
    旧的 AOF 文件含有无效命令,重写使用进程内数据直接生成,这样新的 AOF 文件只保留最终数据写入命令。
    多条写命令可以合并为一个,为了防止单条命令过大造成客户端缓冲区溢出,对于 list、set、hash、zset 等类型操作,以 64 个元素为界拆分为多条。
    AOF 重写分为手动触发和自动触发,手动触发直接调用 bgrewriteaof 命令,自动触发根据 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 参数确定自动触发时机。

    重写流程:

    ① 执行 AOF 重写请求,如果当前进程正在执行 AOF 重写,请求不执行并返回,如果当前进程正在执行 bgsave 操作,重写命令延迟到 bgsave 完成之后再执行。

    ② 父进程执行 fork 创建子进程,开销等同于 bgsave 过程。

    ③ 父进程 fork 操作完成后继续响应其他命令,所有修改命令依然写入 AOF 缓冲区并同步到硬盘,保证原有 AOF 机制正确性。

    ④ 子进程根据内存快照,按命令合并规则写入到新的 AOF 文件。每次批量写入数据量默认为 32 MB,防止单次刷盘数据过多造成阻塞。

    ⑤ 新 AOF 文件写入完成后,子进程发送信号给父进程,父进程更新统计信息。

    ⑥ 父进程把 AOF 重写缓冲区的数据写入到新的 AOF 文件并替换旧文件,完成重写。

    Q9:AOF 重启加载的原理?
    AOF 和 RDB 文件都可以用于服务器重启时的数据恢复。Redis 持久化文件的加载流程:

    ① AOF 持久化开启且存在 AOF 文件时,优先加载 AOF 文件。

    ② AOF 关闭时且存在 RDB 文件时,记载 RDB 文件。

    ③ 加载 AOF/RDB 文件成功后,Redis 启动成功。

    ④ AOF/RDB 文件存在错误导致加载失败时,Redis 启动失败并打印错误信息。

    变态跳台阶

    问题:
    一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
    代码:

     public int jumpFloorII(int target) {
    		 if(target==1){
    			 return 1;
    		 }
    		 int[] a=new int[target+1];
    		 int sum=1;
    		 for(int i=2;i<=target;i++){
    			 a[i]=sum+1;//从起点到i-1在加上1
    			 sum=sum+a[i];				
    		 }
    		return a[target];	        
    	    }

    二维数组查找

    给定一个二维数组,其每一行从左到右递增排序,从上到下也是递增排序。给定一个数,判断这个数是否在该二维数组中。
    要求:
    要求时间复杂度 O(M + N),空间复杂度 O(1)。其中 M 为行数,N 为 列数。
    思路:
    方法一:通过遍历array数组,去查找array数组中有没有target的值。它的时间复杂度是(O(n * m))
    方法二:从矩阵的右上方开始找,设置一个i,j表示所找当前位置,如果说array[i][j] > target大的话,接着就往左找,反之往下找,直到找到array[i][j] == target为止。它的时间复杂度是:O(n + m)
    代码:

    import java.util.Scanner;
    public class ArraySearch {
    	/**
    	 * 方法一:时间复杂度O(n*m)不符合要求
    	 * @param target
    	 * @param array
    	 * @return
    	 */
    	/*public static boolean find(int target,int[][] array){
    		for(int i=0;i<array.length;i++){
    			for(int j=0;j<array[0].length;j++){
    				if(array[i][j]==target){
    					return true;
    				}
    			}
    		}
    		return false;		
    	}*/
    	/**
    	 * 方法二:时间复杂度O(n+m)
    	 * @param target
    	 * @param array
    	 * @return
    	 */
    	public static boolean find(int target,int[][] array){
    		int i=0;
    		int j=array[0].length-1;
    		while(i>=0&&i<array.length&&j>=0&&j<array[0].length){
    			if(array[i][j]==target){
    				return true;
    			}
    			else if(array[i][j]>target){
    				j--;
    			}else{
    				i++;
    			}			
    		}
    		return false;
    		
    	}
    	public static void main(String[] args) {
    		// TODO Auto-generated method stub
    		int n,m;
    		Scanner sc=new Scanner(System.in);
    		n=sc.nextInt();
    		m=sc.nextInt();
    		int[][] array=new int[n][m];
    		for(int i=0;i<n;i++)
    			for(int j=0;j<m;j++)
    				array[i][j]=sc.nextInt();		
    		int target=sc.nextInt();
    		System.out.println(find(target,array));		
    	}
    }

    合并两个排序的链表

    问题:
    输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。
    代码:
    方法一:递归

    public ListNode Merge(ListNode list1, ListNode list2) {
            if(list1==null){
                return list2;
            }
            if (list2==null){
                return list1;
            }
            if (list1.val<=list2.val){
               list1.next= Merge(list1.next,list2);
               return list1;
    
            }else{
                list2.next=Merge(list1,list2.next);
                return list2;
            }

    跳台阶

    问题:
    一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
    代码:

    public class JumpDemo {
    	/**
    	 * 递归
    	 * @param n
    	 * @return
    	 */
    	  public int jumpFloor1(int target) {
    	        if(target==1){
    	            return 1;
    	        }
    	        if(target==2){
    	            //return 2;
    	            return 1+jumpFloor1(target-1);
    	        }
    	        return jumpFloor1(target-1)+jumpFloor1(target-2);
    
    	    }
    	  /**
    	   * 递推
    	   */
    	  public int jumpFloor2(int target) {
    		  if(target==1){
    			  return 1;
    		  }
    		  if(target==2){
    			  return 2;
    		  }
    		  int[] a=new int[target+1];
    		  a[1]=1;
    		  a[2]=2;
    		  for(int i=3;i<=target;i++){
    			  a[i]=a[i-1]+a[i-2];
    		  }
    		return a[target];		  
    	  }
    }

    rocketMQ

    一,概念

    MQ是一个"先进先出"数据结构
    RPC耦合过高
    消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。
    场景
    1,应用解耦
    同步--》异步
    订单系统--MQ--支付系统/库存系统/物流系统
    2,流量削峰
    请求5k--MQ--系统从MQ中拉取2k--MYSQL
    3,数据分发
    系统--MQ--a系统/b系统/c系统
    优点:
    缺点:
    系统可用性降低如MQ宕机,系统复杂度增高 同步远程调用->MQ异步调用,一致性问题
    四大MQ比较:
    rocketMQ,activeMQ编写语言java
    rocketMQ,kafka单继吞吐量10万级,activeMQ,rabbitMq万级
    可用性:rocketMQ,kafka非常高,分布式activeMQ,rabbitMq高,主从架构
    rabbitMq二次开发困难rocketMQ功能完善,扩展性佳

    二,快速入门

    阿里巴巴2016开源中间件,支持双十一等高并发场景消息流转,能够处理万亿级别的消息
    安装:在linux安装rocketmq,jdk
    安装rocketmq
    bin:启动脚本包括shell脚本和CMD脚本
    config:实例配置文件,包括broker,logback配置文件
    lib:依赖jar包

    启动rocketmq

    1,启动NameServer
    输入指令:

    nohup sh bin/mqnamesrv &

    1.2,查看启动日志
    输入指令:

    tail -f ~/logs/rocketmqlogs/namesrv.log

    2,启动Broker
    输入指令:

    nohup sh bin/mqbroker -n localhost:9876 &

    2.2,查看启动日志
    输入指令:

    tail -f ~/logs/rocketmqlogs/broker.log

    RocketMQ默认的虚拟机内存较大,启动Broker如果因为内存不足失败,需要编辑如下两个配置文件,修改JVM内存大小

    # 编辑runbroker.sh和runserver.sh修改默认JVM大小
    vi runbroker.sh
    vi runserver.sh
    //参考设置
    JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m -XX:MetaspaceSize=128m  -XX:MaxMetaspaceSize=320m"

    rabbitMQ

    RabbitMQ
    一,异步和消息
    同步:
    异步:客户端的请求不会阻塞进程,服务器的响应可以是非即时的
    //http最常见方式是同步,但也支持异步调用
    异步的常见形态
    //常见一对一
    1,通知
    2,请求/异步响应
    //常见一对多
    3,消息
    二,MQ应用场景
    1,异步处理
    2,流量削峰//秒杀,应用前端加入消息队列,控制活动人数
    3,日志处理//比如kafka
    4,应用解耦//

    二,基本使用MQ
    安装rabbitMQ
    https://www.rabbitmq.com/download.html
    1,引入依赖spring-boot-starter-amqp
    2,配置MQ
    rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    3,写一个方法接受mq消息//接收方
    使用
    @RabbitListener(queues=”myQueue”)注解
    Public void process(String message){
    Log.info(“MqReceiver: {}”,message);}//打印出来
    4,发送方//Test
    AmqpTemplate
    convertAndSend(“myQueue”//队列名字, “now”+new Date());//发送内容
    缺点:
    需要在rabbitMQ控制面板创建消息队列
    改进:
    自动创建消息队列
    //方式一@RabbitListener(queues=”myQueue”)
    //方式二@RabbitListener(queuesToDeclare=@Queue(”myQueue”))

    5, 自动创建,Exchange和Queue消息队列绑定
    方式三@RabbitListener(bindings=@QueueBinding(
    value=@Queue(“myQueue”),
    exchange=@eXchange(“myExchange”)
    ))
    6,消息分组
    方式三@RabbitListener(bindings=@QueueBinding(
    value=@Queue(“myQueue”),
    key=”fruit”
    exchange=@eXchange(“myExchange”)
    4,发送方//Test
    AmqpTemplate
    convertAndSend(“myExchange”, “myQueue”//队列名字, “now”+new Date());//发送内容
    四,Spring Cloud Stream// Spring Cloud组件之一
    操作消息队列另一种方式
    应用程序通过inputs/outputs Binder与spring cloud交互,Binder与中间件交互
    Binder是应用和消息中间件联合剂
    Binder是stream抽象概念,对消息中间件进一步封装,可以做在代码方面做到无感知,甚至动态切换中间件
    缺点:
    只支持RabbitMQ,Kafka
    1,引入spring-cloud-starter-stream-rabbit
    2,配置mq

    五,业务中使用MQ//商品和订单服务中使用MQ
    订单<---库存变化---消息队列-库存变化---商品
    1,导入配置中心 spring-cloud-config-client
    引入依赖spring-boot-starter-amqp
    2,

    Spring

    Spring IoC

    Q1:IoC 是什么?

    IoC 即控制反转,把原来代码里需要实现的对象创建、依赖反转给容器来帮忙实现,需要创建一个容器并且需要一种描述让容器知道要创建的对象间的关系,在 Spring 中管理对象及其依赖关系是通过 Spring 的 IoC 容器实现的。

    IoC 的实现方式有依赖注入和依赖查找,由于依赖查找使用的很少,因此 IoC 也叫做依赖注入。依赖注入指对象被动地接受依赖类而不用自己主动去找,对象不是从容器中查找它依赖的类,而是在容器实例化对象时主动将它依赖的类注入给它。假设一个 Car 类需要一个 Engine 的对象,那么一般需要需要手动 new 一个 Engine,利用 IoC 就只需要定义一个私有的 Engine 类型的成员变量,容器会在运行时自动创建一个 Engine 的实例对象并将引用自动注入给成员变量。


    Q2:IoC 容器初始化过程?

    基于 XML 的容器初始化

    当创建一个 ClassPathXmlApplicationContext 时,构造方法做了两件事:① 调用父容器的构造方法为容器设置好 Bean 资源加载器。② 调用父类的 setConfigLocations 方法设置 Bean 配置信息的定位路径。

    ClassPathXmlApplicationContext 通过调用父类 AbstractApplicationContext 的 refresh 方法启动整个 IoC 容器对 Bean 定义的载入过程,refresh 是一个模板方法,规定了 IoC 容器的启动流程。在创建 IoC 容器前如果已有容器存在,需要把已有的容器销毁,保证在 refresh 方法后使用的是新创建的 IoC 容器。

    容器创建后通过 loadBeanDefinitions 方法加载 Bean 配置资源,该方法做两件事:① 调用资源加载器的方法获取要加载的资源。② 真正执行加载功能,由子类 XmlBeanDefinitionReader 实现。加载资源时首先解析配置文件路径,读取配置文件的内容,然后通过 XML 解析器将 Bean 配置信息转换成文档对象,之后按照 Spring Bean 的定义规则对文档对象进行解析。

    Spring IoC 容器中注册解析的 Bean 信息存放在一个 HashMap 集合中,key 是字符串,值是 BeanDefinition,注册过程中需要使用 synchronized 保证线程安全。当配置信息中配置的 Bean 被解析且被注册到 IoC 容器中后,初始化就算真正完成了,Bean 定义信息已经可以使用且可被检索。Spring IoC 容器的作用就是对这些注册的 Bean 定义信息进行处理和维护,注册的 Bean 定义信息是控制反转和依赖注入的基础。

    基于注解的容器初始化

    分为两种:① 直接将注解 Bean 注册到容器中,可以在初始化容器时注册,也可以在容器创建之后手动注册,然后刷新容器使其对注册的注解 Bean 进行处理。② 通过扫描指定的包及其子包的所有类处理,在初始化注解容器时指定要自动扫描的路径。


    Q3:依赖注入的实现方法有哪些?

    构造方法注入: IoC Service Provider 会检查被注入对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。这种方法的优点是在对象构造完成后就处于就绪状态,可以马上使用。缺点是当依赖对象较多时,构造方法的参数列表会比较长,构造方法无法被继承,无法设置默认值。对于非必需的依赖处理可能需要引入多个构造方法,参数数量的变动可能会造成维护的困难。

    setter 方法注入: 当前对象只需要为其依赖对象对应的属性添加 setter 方法,就可以通过 setter 方法将依赖对象注入到被依赖对象中。setter 方法注入在描述性上要比构造方法注入强,并且可以被继承,允许设置默认值。缺点是无法在对象构造完成后马上进入就绪状态。

    接口注入: 必须实现某个接口,接口提供方法来为其注入依赖对象。使用少,因为它强制要求被注入对象实现不必要接口,侵入性强。


    Q4:依赖注入的相关注解?

    @Autowired:自动按类型注入,如果有多个匹配则按照指定 Bean 的 id 查找,查找不到会报错。

    @Qualifier:在自动按照类型注入的基础上再按照 Bean 的 id 注入,给变量注入时必须搭配 @Autowired,给方法注入时可单独使用。

    @Resource :直接按照 Bean 的 id 注入,只能注入 Bean 类型。

    @Value :用于注入基本数据类型和 String 类型。


    Q5:依赖注入的过程?

    getBean 方法获取 Bean 实例,该方***调用 doGetBeandoGetBean 真正实现从 IoC 容器获取 Bean 的功能,也是触发依赖注入的地方。

    具体创建 Bean 对象的过程由 ObjectFactory 的 createBean 完成,该方法主要通过 createBeanInstance 方法生成 Bean 包含的 Java 对象实例和 populateBean 方法对 Bean 属性的依赖注入进行处理。

    populateBean方法中,注入过程主要分为两种情况:① 属性值类型不需要强制转换时,不需要解析属性值,直接进行依赖注入。② 属性值类型需要强制转换时,首先解析属性值,然后对解析后的属性值进行依赖注入。依赖注入的过程就是将 Bean 对象实例设置到它所依赖的 Bean 对象属性上,真正的依赖注入是通过 setPropertyValues 方法实现的,该方法使用了委派模式。

    BeanWrapperImpl 类负责对完成初始化的 Bean 对象进行依赖注入,对于非集合类型属性,使用 JDK 反射,通过属性的 setter 方法为属性设置注入后的值。对于集合类型的属性,将属性值解析为目标类型的集合后直接赋值给属性。

    当容器对 Bean 的定位、载入、解析和依赖注入全部完成后就不再需要手动创建对象,IoC 容器会自动为我们创建对象并且注入依赖。


    Q6:Bean 的生命周期?

    在 IoC 容器的初始化过程中会对 Bean 定义完成资源定位,加载读取配置并解析,最后将解析的 Bean 信息放在一个 HashMap 集合中。当 IoC 容器初始化完成后,会进行对 Bean 实例的创建和依赖注入过程,注入对象依赖的各种属性值,在初始化时可以指定自定义的初始化方法。经过这一系列初始化操作后 Bean 达到可用状态,接下来就可以使用 Bean 了,当使用完成后会调用 destroy 方法进行销毁,此时也可以指定自定义的销毁方法,最终 Bean 被销毁且从容器中移除。

    XML 方式通过配置 bean 标签中的 init-Method 和 destory-Method 指定自定义初始化和销毁方法。

    注解方式通过 @PreConstruct@PostConstruct 注解指定自定义初始化和销毁方法。


    Q7:Bean 的作用范围?

    通过 scope 属性指定 bean 的作用范围,包括:

    ① singleton:单例模式,是默认作用域,不管收到多少 Bean 请求每个容器中只有一个唯一的 Bean 实例。

    ② prototype:原型模式,和 singleton 相反,每次 Bean 请求都会创建一个新的实例。

    ③ request:每次 HTTP 请求都会创建一个新的 Bean 并把它放到 request 域中,在请求完成后 Bean 会失效并被垃圾收集器回收。

    ④ session:和 request 类似,确保每个 session 中有一个 Bean 实例,session 过期后 bean 会随之失效。

    ⑤ global session:当应用部署在 Portlet 容器时,如果想让所有 Portlet 共用全局存储变量,那么该变量需要存储在 global session 中。


    Q8:如何通过 XML 方式创建 Bean?

    默认无参构造方法,只需要指明 bean 标签中的 id 和 class 属性,如果没有无参构造方***报错。

    静态工厂方法,通过 bean 标签中的 class 属性指明静态工厂,factory-method 属性指明静态工厂方法。

    实例工厂方法,通过 bean 标签中的 factory-bean 属性指明实例工厂,factory-method 属性指明实例工厂方法。


    Q9:如何通过注解创建 Bean?

    @Component 把当前类对象存入 Spring 容器中,相当于在 xml 中配置一个 bean 标签。value 属性指定 bean 的 id,默认使用当前类的首字母小写的类名。

    @Controller@Service@Repository 三个注解都是 @Component 的衍生注解,作用及属性都是一模一样的。只是提供了更加明确语义,@Controller 用于表现层,@Service用于业务层,@Repository用于持久层。如果注解中有且只有一个 value 属性要赋值时可以省略 value。

    如果想将第三方的类变成组件又没有源代码,也就没办法使用 @Component 进行自动配置,这种时候就要使用 @Bean 注解。被 @Bean 注解的方法返回值是一个对象,将会实例化,配置和初始化一个新对象并返回,这个对象由 Spring 的 IoC 容器管理。name 属性用于给当前 @Bean 注解方法创建的对象指定一个名称,即 bean 的 id。当使用注解配置方法时,如果方法有参数,Spring 会去容器查找是否有可用 bean对象,查找方式和 @Autowired 一样。


    Q10:如何通过注解配置文件?

    @Configuration 用于指定当前类是一个 spring 配置类,当创建容器时会从该类上加载注解,value 属性用于指定配置类的字节码。

    @ComponentScan 用于指定 Spring 在初始化容器时要扫描的包。basePackages 属性用于指定要扫描的包。

    @PropertySource 用于加载 .properties 文件中的配置。value 属性用于指定文件位置,如果是在类路径下需要加上 classpath。

    @Import 用于导入其他配置类,在引入其他配置类时可以不用再写 @Configuration 注解。有 @Import 的是父配置类,引入的是子配置类。value 属性用于指定其他配置类的字节码。


    Q11:BeanFactory、FactoryBean 和 ApplicationContext 的区别?

    BeanFactory 是一个 Bean 工厂,使用简单工厂模式,是 Spring IoC 容器顶级接口,可以理解为含有 Bean 集合的工厂类,作用是管理 Bean,包括实例化、定位、配置对象及建立这些对象间的依赖。BeanFactory 实例化后并不会自动实例化 Bean,只有当 Bean 被使用时才实例化与装配依赖关系,属于延迟加载,适合多例模式。

    FactoryBean 是一个工厂 Bean,使用了工厂方法模式,作用是生产其他 Bean 实例,可以通过实现该接口,提供一个工厂方法来自定义实例化 Bean 的逻辑。FactoryBean 接口由 BeanFactory 中配置的对象实现,这些对象本身就是用于创建对象的工厂,如果一个 Bean 实现了这个接口,那么它就是创建对象的工厂 Bean,而不是 Bean 实例本身。

    ApplicationConext 是 BeanFactory 的子接口,扩展了 BeanFactory 的功能,提供了支持国际化的文本消息,统一的资源文件读取方式,事件传播以及应用层的特别配置等。容器会在初始化时对配置的 Bean 进行预实例化,Bean 的依赖注入在容器初始化时就已经完成,属于立即加载,适合单例模式,一般推荐使用。


    Spring AOP 4

    Q1:AOP 是什么?

    AOP 即面向切面编程,简单地说就是将代码中重复的部分抽取出来,在需要执行的时候使用动态代理技术,在不修改源码的基础上对方法进行增强。

    Spring 根据类是否实现接口来判断动态代理方式,如果实现接口会使用 JDK 的动态代理,核心是 InvocationHandler 接口和 Proxy 类,如果没有实现接口会使用 CGLib 动态代理,CGLib 是在运行时动态生成某个类的子类,如果某个类被标记为 final,不能使用 CGLib 。

    JDK 动态代理主要通过重组字节码实现,首先获得被代理对象的引用和所有接口,生成新的类必须实现被代理类的所有接口,动态生成Java 代码后编译新生成的 .class 文件并重新加载到 JVM 运行。JDK 代理直接写 Class 字节码,CGLib 是采用 ASM 框架写字节码,生成代理类的效率低。但是 CGLib 调用方法的效率高,因为 JDK 使用反射调用方法,CGLib 使用 FastClass 机制为代理类和被代理类各生成一个类,这个类会为代理类或被代理类的方法生成一个 index,这个 index 可以作为参数直接定位要调用的方法。

    常用场景包括权限认证、自动缓存、错误处理、日志、调试和事务等。


    Q2:AOP 的相关注解有哪些?

    @Aspect:声明被注解的类是一个切面 Bean。

    @Before:前置通知,指在某个连接点之前执行的通知。

    @After:后置通知,指某个连接点退出时执行的通知(不论正常返回还是异常退出)。

    @AfterReturning:返回后通知,指某连接点正常完成之后执行的通知,返回值使用returning属性接收。

    @AfterThrowing:异常通知,指方法抛出异常导致退出时执行的通知,和@AfterReturning只会有一个执行,异常使用throwing属性接收。


    Q3:AOP 的相关术语有什么?

    Aspect:切面,一个关注点的模块化,这个关注点可能会横切多个对象。

    Joinpoint:连接点,程序执行过程中的某一行为,即业务层中的所有方法。。

    Advice:通知,指切面对于某个连接点所产生的动作,包括前置通知、后置通知、返回后通知、异常通知和环绕通知。

    Pointcut:切入点,指被拦截的连接点,切入点一定是连接点,但连接点不一定是切入点。

    Proxy:代理,Spring AOP 中有 JDK 动态代理和 CGLib 代理,目标对象实现了接口时采用 JDK 动态代理,反之采用 CGLib 代理。

    Target:代理的目标对象,指一个或多个切面所通知的对象。

    Weaving :织入,指把增强应用到目标对象来创建代理对象的过程。


    Q4:AOP 的过程?

    Spring AOP 由 BeanPostProcessor 后置处理器开始,这个后置处理器是一个***,可以监听容器触发的 Bean 生命周期事件,向容器注册后置处理器以后,容器中管理的 Bean 就具备了接收 IoC 容器回调事件的能力。BeanPostProcessor 的调用发生在 Spring IoC 容器完成 Bean 实例对象的创建和属性的依赖注入后,为 Bean 对象添加后置处理器的入口是 initializeBean 方法。

    Spring 中 JDK 动态代理通过 JdkDynamicAopProxy 调用 Proxy 的 newInstance 方法来生成代理类,JdkDynamicAopProxy 也实现了 InvocationHandler 接口,invoke 方法的具体逻辑是先获取应用到此方法上的拦截器链,如果有拦截器则创建 MethodInvocation 并调用其 proceed 方法,否则直接反射调用目标方法。因此 Spring AOP 对目标对象的增强是通过拦截器实现的。

    二进制中1的个数

    问题:输入一个整数二进制数,输出该数二进制表示中1的个数。其中负数用补码表示。
    思路:
    n&n-1
    该位运算去除 n 的位级表示中最低的那一位
    方法:本质上对n的二进制表示中的1的位置的判断。
    eg: 5 -》 101 & 100(101 - 1) = 100 -》 100 & 011(100 - 1) = 000 -》 000
    代码:

    n       : 10110100
    n-1     : 10110011
    n&(n-1) : 10110000

    时间复杂度O(m) m表示1的个数

    如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1(如果最右边的1后面还有0的话)。其余所有位将不会受到影响。
    举个例子:一个二进制数1100,从右边数起第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成了1,而前面的1保持不变,因此得到的结果是1011.我们发现减1的结果是把最右边的一个1开始的所有位都取反了。这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。如1100&1011=1000.也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。

    public class NumberOf1 {
        public static int numberOf1(int n){
            int count=0;
            while(n!=0){
                n=n&n-1;
                count++;
            }
            return count;
        }
        public static void main(String[] args) {
            int n=NumberOf1.numberOf1(11);
            System.out.println(n);
        }
    }

    vue

    2020最新版前端学习路线图
    https://www.bilibili.com/read/cv5650633/?spm_id_from=333.788.b_636f6d6d656e74.6

    vue $router 路由传参的4种方法详解
    https://www.jianshu.com/p/ed6f2d4b2d0e

    javascript及vue中 this全面指南
    https://blog.csdn.net/daijiguo/article/details/82991217

    2019前端面试题汇总(主要为Vue)
    https://segmentfault.com/a/1190000018225708

    关于Vue中props的详解
    https://blog.csdn.net/jingtian678/article/details/81160995

    https://www.jianshu.com/p/89bd18e44e73

    js声明变量的三种方式
    https://www.cnblogs.com/amujoe/p/8874343.html

    学习一个概念最好去维基百科

    关于JSON.parse(JSON.stringify(obj))实现深拷贝应该注意的坑
    https://www.jianshu.com/p/b084dfaad501

    MVVM
    view div
    viewModel new Vue()
    model data
    创建一个vue实例,传入一个对象options
    el:挂载要管理的元素,决定实例管理哪一个dom
    data:vue实例对应数据对象 组件之中data必须是一个函数

    github
    dev 开发版本
    tags 稳定版本
    下载tags版本的

    debug/release:测试,稳定版本

    树的子结构

    问题:
    输入两棵二叉树A,B,判断B是不是A的子结构。(ps:我们约定空树不是任意一个树的子结构)
    思路:
    比较二叉树A中的每个结点和B的root结点比较,A的当前节点的val与B的root结点一致,就抽离出

    反转链表

    输入一个链表,反转链表后,输出新链表的表头。
    代码
    方法一:

    class ListNode{
        int val;
        ListNode next;
    }
    public class ReverseList {
        public ListNode reverseList(ListNode head) {
            if(head==null){
                return null;
            }
            ListNode frontNode=head;
            ListNode removeNode=head.next;
            while(removeNode!=null){
                ListNode tempNode=removeNode.next;//保存移动结点的下一个结点
                removeNode.next=frontNode;//实现链表反置
                //实现两个结点向右平移
                frontNode=removeNode;
                removeNode=tempNode;
            }
            head.next=null;
            return frontNode;
        }
    }

    方法二:

    javascript

    javascript 脚本语言 编程语言
    运行在客户端的脚本语言,(script是脚本的意思)
    脚本语言:不需要编译,js解释器js引擎逐行解释执行
    也可以基于node.js技术进行服务器端编程

    1bit(位) 可以保存一个0或1
    1B(字节)=8bit
    1kB=1024bit
    CPU执行内存里的数据,把数据从硬板里读到内存

    浏览器执行js
    浏览器分为渲染引擎和js引擎
    渲染引擎:用来解析html,css,俗称内核 谷歌浏览器 blink
    js引擎:js解释器,读取网页中js代码,对其处理后运行 谷歌浏览器V8

    js的组成部分
    1 ECMAScript js语法
    2 DOM 页面文档对象模型
    标准编程接口,通过DOM提供的接口对页面上的各种元素进行处理,(大小,位置,颜色)
    3 BOM 浏览器对象模型
    通过BOM可以操作浏览器窗口,比如弹窗,获取分辨率

    js位置
    行内,内嵌,外部
    单行注释 // 快捷键 ctrl+ /
    多行 /* */ shift+ alt+a

    输入输出
    alert(msg)
    console.log(smg) 浏览器控制台输出打印信息
    prompt(info) 浏览器弹出输入框,用户可以输入

    变量
    内存中存放数据的空间
    var age;
    var age,name,sex;
    未赋值,undefined未定义
    var myname=prompt('');取过来的值是字符串
    变量未声明,未赋值使用会报错
    未定义,直接赋值可以使用不推荐
    严格区分大小写
    js是一种弱类型或动态语言,不用提前申明变量类型,程序运行根据等号右边的值会自动确定
    数据类型
    简单类型:Number,String,Boolean,null,undefined 默认值 0 “” false null undefined
    复杂数据类型:object
    进制
    八进制:数字前面加0表示八进制 010 表示十进制8
    十六进制:数字前面加0x表示十六进制,oxa 表示十进制10
    number范围 Number.max_value Number.min_value
    NAN:代表一个非数值
    isNaN(12):判断是否为数值,不是返回true,是返回false

    字符串
    js推荐使用单引号
    转义字符都是 \开头
    \n:换行
    \b:空格
    1 检测获取字符串长度 length str.length
    2 字符串拼接 字符串+任意类型=新的字符串
    3 字符串+变量
    boolean
    true+1=2
    false+1=1
    4 typeof 判断数据类型

    数据类型转换
    转为字符串:
    1 toString()
    2 String()
    3 和字符串拼接

    转为数值型
    1 parseInt(‘23’)
    2 parseFloat(‘23.34’)

    把其他类型转为boolean类型
    boolean()
    代表空,否定的词被转为false,其他被转为true

    解释型语言和编译型语言
    解释型语言:解释一行执行一行
    编译型语言:生成中间文件

    前置自增:先加一然后 21
    后置自增:先返回原值,在加1 20
    单独使用效果一样
    var i=10
    console.log(++i +10)

    比较运算符

    =
    ==:默认转化数据类型,会把字符串转化为数字
    ===:全等 类型和值都相同

    逻辑运算符
    && 逻辑与 and
    || 逻辑或 or
    !逻辑非
    逻辑与短路运算
    如果表达式1为真,返回表达式2.如果表达式1为假,则返回表达式1
    console.log(123&&345) //345
    0 ' ' undefined null NAN为假
    逻辑或短路运算
    如果表达式1为真,返回表达式1.如果表达式1为假,则返回表达式2
    console.log(123||345) //123
    逻辑中断会影响程序运行结果
    console.log(123 || num++) //num++不会执行

    运算符优先级
    1 小括号() 2 一元运算符++,-- ! 3 算术运算符* 4 关系>= 5 相等 == 6 逻辑运算符 && 7 赋值=

    流程控制
    三种结构:顺序,分支(条件判断),循环
    js中分支
    if
    switch
    var num=12;
    switch(表达式num)
    {
    case value1:
    执行语句1;
    break;
    default:
    执行最后的语句;
    注意:1必须是全等才能匹配 2如果没有break,则会继续执行下一个case
    switch分支多效率比if else if高

    三元表达式 //针对变量设置一些列特定值
    条件表达式 ? 表达式1:表达式2

    循环
    1 for 2 while 3 do while

    断点调试
    可以帮我们认识程序运行过程
    浏览器f12---source--找到需要调试文件--在程序某一行前面设置断点--刷新一次

    双重循环
    var str=' ';
    for(var i=1;i<=10;i++){
    for(var j=i;j<=10;j++){
    str=str+ '*";
    }
    str+='\n';
    }
    console.log(str);
    打印99乘法表
    var str=' ';
    for(var i=1;i<=9;i++)//外层循环控制层数{
    for(var j=1;j<=i;i++){//内层循环控制每行的个数
    str+=i+'x'+j+'=' i * j +'\t';
    }
    str+='\n';
    }
    console.log(str);

    continue:立即跳出本次循环,继续下一次循环//计算1-100除了7的倍数的和
    break:用户跳出整个循环

    js数组:一组数据的集合
    创建数组
    1 new var arr=new Array();
    2 利用数组字面量创建数组 var arr= [];
    可以存放不同数据类型的元素
    遍历
    for(var i=0;i<arr.length;i++)

    数组转化为分割字符串

    增加数组长度
    var arr=[1,2,3]
    arr.length=5;
    没赋值默认undefined
    不能直接给数组名赋值,否则会覆盖之前的内容
    undefined——表示变量声明过但并未赋过值。
    null——表示一个变量将来可能指向一个对象。
    一般用于主动释放指向对象的引用

    js函数
    封装了一段可被重复调用执行的代码块,通过此代码块可以实现大量代码的重复使用
    函数
    1 声明函数
    function 函数名(){
    函数体
    }
    函数名一般用动词
    2 调用函数
    函数名()

    函数的参数
    1 形参
    2 实参
    function 函数名(形参1,形参2..){
    return 返回的结果;
    }
    函数名(实参1,实参2...)=返回的结果
    //形参接受实参的,形参可以看做不用声明的变量

    js实参和形参个数不匹配问题
    实参个数大于形参,会取到形参的个数
    实参个数小于形参个数,数据+undefined(没定义的变量) -->NAN

    return 终止函数(renturn之后的代码不会执行了)
    return只能返回一个值,最后一个:num1,num2会返回num2/也可以返回一个数组
    函数没有return,返回undefined

    不确定有多少参数传递的时候,可以使用arguments来获取,js中,arguments就是函数的一个内置对象,存储了传递的所以实参。
    使用:在方法中console.log(arguments/arguments.length)

    利用函数求任意个数的最大值
    function getMax(){
    var max=arguments[0];
    for(var i=1;i<arguments.length;i++){
    if(max<arguments[i])
    max=arguments[i]
    }
    return max;
    }

    函数可以调用另外一个函数

    函数两种命名方式
    1 命名函数 function fu(){}
    2 表达式函数 var num=function(){}:匿名表达式也可以传参 num(1)

    js作用域:局部,全局
    es6新增快级作用域{}

    js引擎运行js分为两步,预解析和代码执行
    预解析:会把js里面var function提升到当前作用域的最前面
    代码执行:从上到下执行
    变量提升:提升声明,不提升赋值
    函数提升:

    js创建对象
    1 字面量创建对象{} var obj={
    name: ‘张三’,
    sayHi: function(){},
    };
    2 new object
    var obj=new Object()
    3 构造方法
    function 构造方法名(){
    this.属性=值,
    this.方法=function(){}
    }
    new 构造方法名();
    不需要return 就可以返回值

    函数单独声明,调用 函数名()
    方法在对象中使用 对象.方法名()

    new关键字的执行过程
    1在内存中创建一个·新的对象
    2this就会指向刚创建的空对象
    3指向构造函数里面的代码
    4返回这个对象

    遍历对象
    for in 对象
    for(var i in obj){
    console.log(i);:得到属性名
    console.log(obj[i]):得到属性值
    }

    js对象分为三种,自定义对象,内置对象,浏览器对象
    内置对象:js自带的对象
    Math,Date,String,Array

    webapi 是浏览器提供的一套操作浏览器功能和页面元素的api(BOM和BOM)
    api是为程序员提供的一个接口,帮助我们实现某个功能

    dom document object model可处理可扩展标记语言html/xml的标准编程接口
    通过dom接口可以改变网页的内容,结构和样式
    dom树
    页面中的所以标签都是元素,dom中用element表示
    网页中的所以内容都可以看做节点,node

    获取网页元素
    1 通过id获取
    2 通过标签名获取
    3 通过h5新增的方法获取

    4 特殊元素获取
    ...<script>
    var timer=document.getElementById('id');//得到一个对象
    comsole.dir(timer)//打印返回的对象
    ...</script>

    斐波那契数列

    问题:
    大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。
    n<=39
    代码:

    public class FiboncciDemo {
    	/**
    	 * 方法一:采用递推
    	 * @param n
    	 * @return
    	 */
    	public int fiboncci1(int n){
    		if(n==0){
    			return 0;
    		}
            int[] a=new int[n+1];
    		if(n==1 || n==2){
    			return 1;
    		}
            a[1]=1;
            a[2]=1;
    		for(int i=3;i<=n;i++){
    			a[i]=a[i-1]+a[i-2];
    		}
    		return a[n];		      
        }
    	/**
    	 * 方法二,采用递归
    	 * @param n
    	 * @return
    	 */
    	public int fiboncci2(int n){
    		if(n==0){
    			return 0;//终止递归条件
    		}
    		if(n==1||n==2){
    			return 1;//终止递归条件
    		}
    		return fiboncci2(n-1)+fiboncci2(n-2);		     
        }	
    }

    链表中倒数第k个结点

    问题:输入一个链表,输出该链表中倒数第k个结点。
    思路:
    通过初始化两个移动节点的位置距离为k,然后同时移动两个节点,知道第二个节点移动到链表的末尾时,移动节点1的位置就是链表倒数第k个节点。
    代码:

     public ListNode FindKthToTail(ListNode head,int k) {        
            ListNode removeNode=head;
           while(k!=0){
               if(removeNode==null){
                   return null;
               }
               removeNode=removeNode.next;
               k--;
            }
           while(removeNode!=null){
               removeNode=removeNode.next;
               head=head.next;
           }
            return head;
        }

    linux基础

    一,帮助命令

    在linux终端,有详细的内置帮助文档

    命令使用

    whatis info man which whereis
    简要说明命令作用

    whatis command

    正则匹配

    whatis -w "local*"

    详细说明文档

    info command

    查询命令的说明文档

    man command

    根据命令中部分关键字来查询命令

    man -k keyword

    查看路径
    查看程序的二进制文件路径

    which command

    查看make程序的安装路径

    which make

    查看程序搜索路径
    当系统安装了同一软件不同版本的时候,不确定使用哪个版本的时候

    whereis command

    二,文件及目录管理

    2.1创建和删除

    创建:mkdir
    删除:rm
    删除非空目录:rm -rf file目录
    删除日志:rm *log
    移动:mv
    复制:cp 复制目录 cp -r
    查看当前目录下文件个数:

    find ./ | wc -l

    复制目录

    cp -r dir1 dir2
    cp -r dir1./ dir2

    2.2目录切换

    找到文件/文件目录:cd
    切换到上一个目录:cd ..
    切换到home目录:cd/cd ~
    显示当前路径:pwd
    更改当前工作路径:path:$cd path

    2.3列出目录项

    显示当前目录下的文件ls
    以时间为序,列表显示ls -lrt
    ls -a:列出全部文件,连同隐藏文件.开头file一起列出来(常用)
    ls -l:列出长数据串,包含文件的属性与权限数据等
    ls -d:仅列出目录本身,而不列出目录的文件数据
    ls -r:递归列出所有目录包括子目录

    2.4查找目录以及文件

    find locate
    搜索文件或目录

    find ./ -name "core*"

    查找目标文件夹中是否obj文件

    fing ./ -name "*.obj"

    find是实时查询,locate更快,会为文件系统建立索引数据库,如果有文件更新,则需要定期执行更新索引库。
    查找包含有String的路径

    locate String

    更新索引库

    updatedb

    2.5查看文件内容

    cat vi vim head tail more
    显示时同时显示行号

    cat -n filename

    按页显示列表内容

    ls -al | more filename

    显示文件前1行,2行

    head -1 filename  head -2 filename

    显示文件后1,2行

    tail -1 filename tail -2 filename

    查看两个文件的差别

    diff file1 file2

    动态显示文本最新内容

    tail -f file.log

    2.6文件与目录权限修改

    改变文件的拥有者:chown
    改变文件读,写,执行等属性:chmod
    递归子目录修改:chown -r tuxapp source/
    增加脚本可执行权限:chmod a+x myscirpt

    2.7给文件增加别名

    创建符合链接(软连接)和硬链接

    ln cc ccagain:硬链接,删除一个,仍然能找到
    ln  -s cc ccagain:软链接,删除源文件,另一个无法使用ccagain新键文件

    2.8管道和重定向

    批处理命令连接执行:|
    串联使用 ;分号
    前面成功则执行后一条,否则,不执行:&&
    前面失败,则执行后一条:||
    能够提示命名是否执行成功or失败

    ls /proc  && echo suss!  || echo failed
    或者
    if ls /proc ;then echo suss!;else echo failed;fi

    查找record.log中包含AAA,却不包含BBB的记录总数

    cat -v record.log | grep AAA | grep -v BBB | wc -l

    2.9设置环境变量

    启动账号后自动执行的文件是.profie,然后通过这个文件可以设置自己的环境变量,安装的软件路径一般要加入path中

    PATH=$APPDIR:/opt/app/soft/bin:$PATH:/usr/local/bin:$TUXDIR/bin:$ORACLE_HOME/bin;export PATH

    三,文本处理

    3.1 find文件查找

    找到.log文件后,并删除

    find -name "*.log"  | xargs rm -f
    find -name "*.log"  | xargs chmod 755 查找文件并修改权限为755
    find -iname   忽略大小写

    3.2 grep文本搜索

    grep pattern file
    grep -o 只输出匹配的文本行
    grep -v 只输出不匹配的文本行
    grep -c 统计文件中包含文本的次数

    grep -c "text" filename

    -n 打印匹配的行号
    -i 搜索时忽略大小写
    -l 只打印文件名

    匹配多个模式

    grep -e "class" -e "we" file

    3.3 wc统计行和字符的工具

    wc -l file 统计行数
    wc -c file 统计字符数
    wc -w file 统计单词书

    3.4 sed文本替换

    首处替换

    sed 's/test/replacetest/' file  //s 替换 test匹配被替换 replace替换后 替换每一行匹配的test

    全局替换

    sed 's/test/replacetest/g' test

    替换后默认输出替换后得内容

    sed -i 's/test/replacetest/g' file

    移除空白行

    sed '/^$/d' file

    特殊字符

    ^表示行首
    $ 在引用中表示行尾,在引用外表示末行

    双引号求值
    一般用单引号,也可以使用双引号,双引号会对表达式求值

    sed 's/$val/home/' file

    3.5 awk数据流处理工具

    四 磁盘管理

    4.1 查看磁盘空间

    查看当前磁盘使用情况,查看当前目录所占大小,以及打包压缩以及解压缩
    查看当前磁盘使用情况

    df -h  //-h 带单位M/G df不带显示的数字以B为单位

    查看当前目录所占大小

    du -sh   s 递归整个目录

    4.2 打包/压缩

    在linux中打包和压缩分为两个步骤
    打包压缩
    将多个文件归并到一个文件

    tar -zcvf  目标文件 源文件/文件夹1 源文件/文件夹2
    目标文件.tar.gz 打包压缩
    目标文件.tar 只打包

    解压

    tar -zxvf  目标文件  //解压到当前目录
    tar -zxvf  目标文件  目标目录  //解压到目标目录

    五 进程管理工具

    使用进程管理工具,我们可以查询程序当前的运行状态,或终止一个进程;
    任何进程都与文件关联;lsof工具(list opened files),作用是列举系统中已经被打开的文件。在linux环境中,任何事物都是文件,设备是文件,目录是文件,甚至sockets也是文件。用好lsof命令,对日常的linux管理非常有帮助。

    5.1 查询进程

    查询正在运行的进程

    ps -ef
    jps

    查询归属用户colin115的进程

    ps -ef | grep colin115

    显示进程信息并实时更新

    top

    查看端口占用的进程状态

    lsof -i:3306

    查看用户username的进程所打开的文件

    lsof -u username

    5.2 终止进程

    杀死指定pid的进程

    kill pid

    杀死相关进程

    kill -9 pid

    5.3 进程监控

    查看使用cpu,内存最多的进程
    top

    5.4 分析线程栈

    ps -ef | grep redis
    pmap pid

    六 性能监控

    6.1 监控cpu

    MySQL

    作者:冠状病毒biss
    链接:https://www.nowcoder.com/discuss/447742?source_id=profile_create&channel=666
    来源:牛客网

    逻辑架构 13
    Q1:MySQL 的逻辑架构了解吗?
    第一层是服务器层,主要提供连接处理、授权认证、安全等功能。

    第二层实现了 MySQL 核心服务功能,包括查询解析、分析、优化、缓存以及日期和时间等所有内置函数,所有跨存储引擎的功能都在这一层实现,例如存储过程、触发器、视图等。

    第三层是存储引擎层,存储引擎负责 MySQL 中数据的存储和提取。服务器通过 API 与存储引擎通信,这些接口屏蔽了不同存储引擎的差异,使得差异对上层查询过程透明。除了会解析外键定义的 InnoDB 外,存储引擎不会解析 SQL,不同存储引擎之间也不会相互通信,只是简单响应上层服务器请求。

    Q2:谈一谈 MySQL 的读写锁
    在处理并发读或写时,可以通过实现一个由两种类型组成的锁系统来解决问题。这两种类型的锁通常被称为共享锁和排它锁,也叫读锁和写锁。读锁是共享的,相互不阻塞,多个客户在同一时刻可以同时读取同一个资源而不相互干扰。写锁则是排他的,也就是说一个写锁会阻塞其他的写锁和读锁,确保在给定时间内只有一个用户能执行写入并防止其他用户读取正在写入的同一资源。

    在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改某一部分数据时,MySQL 会通过锁定防止其他用户读取同一数据。写锁比读锁有更高的优先级,一个写锁请求可能会被插入到读锁队列的前面,但是读锁不能插入到写锁前面。

    Q3:MySQL 的锁策略有什么?
    表锁是MySQL中最基本的锁策略,并且是开销最小的策略。表锁会锁定整张表,一个用户在对表进行写操作前需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获取读锁,读锁之间不相互阻塞。

    行锁可以最大程度地支持并发,同时也带来了最大开销。InnoDB 和 XtraDB 以及一些其他存储引擎实现了行锁。行锁只在存储引擎层实现,而服务器层没有实现。

    Q4:数据库死锁如何解决?
    死锁是指多个事务在同一资源上相互占用并请求锁定对方占用的资源而导致恶性循环的现象。当多个事务试图以不同顺序锁定资源时就可能会产生死锁,多个事务同时锁定同一个资源时也会产生死锁。

    为了解决死锁问题,数据库系统实现了各种死锁检测和死锁超时机制。越复杂的系统,例如InnoDB 存储引擎,越能检测到死锁的循环依赖,并立即返回一个错误。这种解决方式很有效,否则死锁会导致出现非常慢的查询。还有一种解决方法,就是当查询的时间达到锁等待超时的设定后放弃锁请求,这种方式通常来说不太好。InnoDB 目前处理死锁的方法是将持有最少行级排它锁的事务进行回滚。

    死锁发生之后,只有部分或者完全回滚其中一个事务,才能打破死锁。对于事务型系统这是无法避免的,所以应用程序在设计时必须考虑如何处理死锁。大多数情况下只需要重新执行因死锁回滚的事务即可。

    Q5:事务是什么?
    事务是一组原子性的 SQL 查询,或者说一个独立的工作单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。也就是说事务内的语句要么全部执行成功,要么全部执行失败。

    Q6:事务有什么特性?
    原子性 atomicity

    一个事务在逻辑上是必须不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说不可能只执行其中的一部分。

    一致性 consistency

    数据库总是从一个一致性的状态转换到另一个一致性的状态。

    隔离性 isolation

    针对并发事务而言,隔离性就是要隔离并发运行的多个事务之间的相互影响,一般来说一个事务所做的修改在最终提交以前,对其他事务是不可见的。

    持久性 durability

    一旦事务提交成功,其修改就会永久保存到数据库中,此时即使系统崩溃,修改的数据也不会丢失。

    Q7:MySQL 的隔离级别有哪些?
    未提交读 READ UNCOMMITTED

    在该级别事务中的修改即使没有被提交,对其他事务也是可见的。事务可以读取其他事务修改完但未提交的数据,这种问题称为脏读。这个级别还会导致不可重复读和幻读,性能没有比其他级别好很多,很少使用。

    提交读 READ COMMITTED

    多数数据库系统默认的隔离级别。提交读满足了隔离性的简单定义:一个事务开始时只能"看见"已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前的任何修改对其他事务都是不可见的。也叫不可重复读,因为两次执行同样的查询可能会得到不同结果。

    可重复读 REPEATABLE READ(MySQL默认的隔离级别)

    可重复读解决了不可重复读的问题,保证了在同一个事务中多次读取同样的记录结果一致。但还是无法解决幻读,所谓幻读指的是当某个事务在读取某个范围内的记录时,会产生幻行。InnoDB 存储引擎通过多版本并发控制MVCC 解决幻读的问题。

    可串行化 SERIALIZABLE

    最高的隔离级别,通过强制事务串行执行,避免幻读。可串行化会在读取的每一行数据上都加锁,可能导致大量的超时和锁争用的问题。实际应用中很少用到这个隔离级别,只有非常需要确保数据一致性且可以接受没有并发的情况下才考虑该级别。

    Q8:MVCC 是什么?
    MVCC 是多版本并发控制,在很多情况下避免加锁,大都实现了非阻塞的读操作,写操作也只锁定必要的行。

    InnoDB 的MVCC 通过在每行记录后面保存两个隐藏的列来实现,这两个列一个保存了行的创建时间,一个保存行的过期时间间。不过存储的不是实际的时间值而是系统版本号,每开始一个新的事务系统版本号都会自动递增,事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。

    MVCC 只能在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作,因为 READ UNCOMMITTED 总是读取最新的数据行,而不是符合当前事务版本的数据行,而 SERIALIZABLE 则会对所有读取的行都加锁。

    Q9:谈一谈 InnoDB
    InnoDB 是 MySQL 的默认事务型引擎,用来处理大量短期事务。InnoDB 的性能和自动崩溃恢复特性使得它在非事务型存储需求中也很流行,除非有特别原因否则应该优先考虑 InnoDB。

    InnoDB 的数据存储在表空间中,表空间由一系列数据文件组成。MySQL4.1 后 InnoDB 可以将每个表的数据和索引放在单独的文件中。

    InnoDB 采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别。其默认级别是 REPEATABLE READ,并通过间隙锁策略防止幻读,间隙锁使 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定防止幻行的插入。

    InnoDB 表是基于聚簇索引建立的,InnoDB 的索引结构和其他存储引擎有很大不同,聚簇索引对主键查询有很高的性能,不过它的二级索引中必须包含主键列,所以如果主键很大的话其他所有索引都会很大,因此如果表上索引较多的话主键应当尽可能小。

    InnoDB 的存储格式是平***立的,可以将数据和索引文件从一个平台复制到另一个平台。

    InnoDB 内部做了很多优化,包括从磁盘读取数据时采用的可预测性预读,能够自动在内存中创建加速读操作的自适应哈希索引,以及能够加速插入操作的插入缓冲区等。

    Q10:谈一谈 MyISAM
    MySQL5.1及之前,MyISAM 是默认存储引擎,MyISAM 提供了大量的特性,包括全文索引、压缩、空间函数等,但不支持事务和行锁,最大的缺陷就是崩溃后无法安全恢复。对于只读的数据或者表比较小、可以忍受修复操作的情况仍然可以使用 MyISAM。

    MyISAM 将表存储在数据文件和索引文件中,分别以 .MYD 和 .MYI 作为扩展名。MyISAM 表可以包含动态或者静态行,MySQL 会根据表的定义决定行格式。MyISAM 表可以存储的行记录数一般受限于可用磁盘空间或者操作系统中单个文件的最大尺寸。

    MyISAM 对整张表进行加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但是在表有读取查询的同时,也支持并发往表中插入新的记录。

    对于MyISAM 表,MySQL 可以手动或自动执行检查和修复操作,这里的修复和事务恢复以及崩溃恢复的概念不同。执行表的修复可能导致一些数据丢失,而且修复操作很慢。

    对于 MyISAM 表,即使是 BLOB 和 TEXT 等长字段,也可以基于其前 500 个字符创建索引。MyISAM 也支持全文索引,这是一种基于分词创建的索引,可以支持复杂的查询。

    MyISAM 设计简单,数据以紧密格式存储,所以在某些场景下性能很好。MyISAM 最典型的性能问题还是表锁问题,如果所有的查询长期处于 Locked 状态,那么原因毫无疑问就是表锁。

    Q12:谈一谈 Memory
    如果需要快速访问数据且这些数据不会被修改,重启以后丢失也没有关系,那么使用 Memory 表是非常有用的。Memory 表至少要比 MyISAM 表快一个数量级,因为所有数据都保存在内存,不需要磁盘 IO,Memory 表的结构在重启后会保留,但数据会丢失。

    Memory 表适合的场景:查找或者映射表、缓存周期性聚合数据的结果、保存数据分析中产生的中间数据。

    Memory 表支持哈希索引,因此查找速度极快。虽然速度很快但还是无法取代传统的基于磁盘的表,Memory 表使用表级锁,因此并发写入的性能较低。它不支持 BLOB 和 TEXT 类型的列,并且每行的长度是固定的,所以即使指定了 VARCHAR 列,实际存储时也会转换成CHAR,这可能导致部分内存的浪费。

    如果 MySQL 在执行查询的过程中需要使用临时表来保持中间结果,内部使用的临时表就是 Memory 表。如果中间结果太大超出了Memory 表的限制,或者含有 BLOB 或 TEXT 字段,临时表会转换成 MyISAM 表。

    Q13:查询执行流程是什么?
    简单来说分为五步:① 客户端发送一条查询给服务器。② 服务器先检查查询缓存,如果命中了缓存则立刻返回存储在缓存中的结果,否则进入下一阶段。③ 服务器端进行 SQL 解析、预处理,再由优化器生成对应的执行计划。④ MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询。⑤ 将结果返回给客户端。

    数据类型 3
    Q1:VARCHAR 和 CHAR 的区别?
    VARCHAR 用于存储可变字符串,是最常见的字符串数据类型。它比 CHAR 更节省空间,因为它仅使用必要的空间。VARCHAR 需要 1 或 2 个额外字节记录字符串长度,如果列的最大长度不大于 255 字节则只需要 1 字节。VARCHAR 不会删除末尾空格。

    VARCHAR 适用场景:字符串列的最大长度比平均长度大很多、列的更新很少、使用了 UTF8 这种复杂字符集,每个字符都使用不同的字节数存储。

    CHAR 是定长的,根据定义的字符串长度分配足够的空间。CHAR 会删除末尾空格。

    CHAR 适合存储很短的字符串,或所有值都接近同一个长度,例如存储密码的 MD5 值。对于经常变更的数据,CHAR 也比 VARCHAR更好,因为定长的 CHAR 不容易产生碎片。对于非常短的列,CHAR 在存储空间上也更有效率,例如用 CHAR 来存储只有 Y 和 N 的值只需要一个字节,但是 VARCHAR 需要两个字节,因为还有一个记录长度的额外字节。

    Q2:DATETIME 和 TIMESTAMP 的区别?
    DATETIME 能保存大范围的值,从 1001~9999 年,精度为秒。把日期和时间封装到了一个整数中,与时区无关,使用 8 字节存储空间。

    TIMESTAMP 和 UNIX 时间戳相同,只使用 4 字节的存储空间,范围比 DATETIME 小得多,只能表示 1970 ~2038 年,并且依赖于时区。

    Q3:数据类型有哪些优化策略?
    更小的通常更好

    一般情况下尽量使用可以正确存储数据的最小数据类型,更小的数据类型通常也更快,因为它们占用更少的磁盘、内存和 CPU 缓存。

    尽可能简单

    简单数据类型的操作通常需要更少的 CPU 周期,例如整数比字符操作代价更低,因为字符集和校对规则使字符相比整形更复杂。应该使用 MySQL 的内建类型 date、time 和 datetime 而不是字符串来存储日期和时间,另一点是应该使用整形存储 IP 地址。

    尽量避免 NULL

    通常情况下最好指定列为 NOT NULL,除非需要存储 NULL值。因为如果查询中包含可为 NULL 的列对 MySQL 来说更难优化,可为 NULL 的列使索引、索引统计和值比较都更复杂,并且会使用更多存储空间。当可为 NULL 的列被索引时,每个索引记录需要一个额外字节,在MyISAM 中还可能导致固定大小的索引变成可变大小的索引。

    如果计划在列上建索引,就应该尽量避免设计成可为 NULL 的列。

    索引 10
    Q1:索引有什么作用?
    索引也叫键,是存储引擎用于快速找到记录的一种数据结构。索引对于良好的性能很关键,尤其是当表中数据量越来越大时,索引对性能的影响愈发重要。在数据量较小且负载较低时,不恰当的索引对性能的影响可能还不明显,但数据量逐渐增大时,性能会急剧下降。

    索引大大减少了服务器需要扫描的数据量、可以帮助服务器避免排序和临时表、可以将随机 IO 变成顺序 IO。但索引并不总是最好的工具,对于非常小的表,大部分情况下会采用全表扫描。对于中到大型的表,索引就非常有效。但对于特大型的表,建立和使用索引的代价也随之增长,这种情况下应该使用分区技术。

    在MySQL中,首先在索引中找到对应的值,然后根据匹配的索引记录找到对应的数据行。索引可以包括一个或多个列的值,如果索引包含多个列,那么列的顺序也十分重要,因为 MySQL 只能使用索引的最左前缀。

    Q2:谈一谈 MySQL 的 B-Tree 索引
    大多数 MySQL 引擎都支持这种索引,但底层的存储引擎可能使用不同的存储结构,例如 NDB 使用 T-Tree,而 InnoDB 使用 B+ Tree。

    B-Tree 通常意味着所有的值都是按顺序存储的,并且每个叶子页到根的距离相同。B-Tree 索引能够加快访问数据的速度,因为存储引擎不再需要进行全表扫描来获取需要的数据,取而代之的是从索引的根节点开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限。最终存储引擎要么找到对应的值,要么该记录不存在。叶子节点的指针指向的是被索引的数据,而不是其他的节点页。

    B-Tree索引的限制:

    如果不是按照索引的最左列开始查找,则无法使用索引。
    不能跳过索引中的列,例如索引为 (id,name,sex),不能只使用 id 和 sex 而跳过 name。
    如果查询中有某个列的范围查询,则其右边的所有列都无法使用索引。
    Q3:了解 Hash 索引吗?
    哈希索引基于哈希表实现,只有精确匹配索引所有列的查询才有效。对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码,哈希码是一个较小的值,并且不同键值的行计算出的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。

    只有 Memory 引擎显式支持哈希索引,这也是 Memory 引擎的默认索引类型。

    因为索引自身只需存储对应的哈希值,所以索引的结构十分紧凑,这让哈希索引的速度非常快,但它也有一些限制:

    哈希索引数据不是按照索引值顺序存储的,无法用于排序。
    哈希索引不支持部分索引列匹配查找,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。例如在数据列(a,b)上建立哈希索引,如果查询的列只有a就无法使用该索引。
    哈希索引只支持等值比较查询,不支持任何范围查询。
    Q4:什么是自适应哈希索引?
    自适应哈希索引是 InnoDB 引擎的一个特殊功能,当它注意到某些索引值被使用的非常频繁时,会在内存中基于 B-Tree 索引之上再创键一个哈希索引,这样就让 B-Tree 索引也具有哈希索引的一些优点,比如快速哈希查找。这是一个完全自动的内部行为,用户无法控制或配置,但如果有必要可以关闭该功能。

    Q5 :什么是空间索引?
    MyISAM 表支持空间索引,可以用作地理数据存储。和 B-Tree 索引不同,这类索引无需前缀查询。空间索引会从所有维度来索引数据,查询时可以有效地使用任意维度来组合查询。必须使用 MySQL 的 GIS 即地理信息系统的相关函数来维护数据,但 MySQL 对 GIS 的支持并不完善,因此大部分人都不会使用这个特性。

    Q6:什么是全文索引?
    通过数值比较、范围过滤等就可以完成绝大多数需要的查询,但如果希望通过关键字匹配进行查询,就需要基于相似度的查询,而不是精确的数值比较,全文索引就是为这种场景设计的。

    MyISAM 的全文索引是一种特殊的 B-Tree 索引,一共有两层。第一层是所有关键字,然后对于每一个关键字的第二层,包含的是一组相关的"文档指针"。全文索引不会索引文档对象中的所有词语,它会根据规则过滤掉一些词语,例如停用词列表中的词都不会被索引。

    Q7:什么是聚簇索引?
    聚簇索引不是一种索引类型,而是一种数据存储方式。InnoDB 的聚簇索引实际上在同一个结构中保存了 B-Tree 索引和数据行。当表有聚餐索引时,它的行数据实际上存放在索引的叶子页中,因为无法同时把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。

    优点:① 可以把相关数据保存在一起。② 数据访问更快,聚簇索引将索引和数据保存在同一个 B-Tree 中,因此获取数据比非聚簇索引要更快。③ 使用覆盖索引扫描的查询可以直接使用页节点中的主键值。

    缺点:① 聚簇索引最大限度提高了 IO 密集型应用的性能,如果数据全部在内存中将会失去优势。② 更新聚簇索引列的代价很高,因为会强制每个被更新的行移动到新位置。③ 基于聚簇索引的表插入新行或主键被更新导致行移动时,可能导致页分裂,表会占用更多磁盘空间。④ 当行稀疏或由于页分裂导致数据存储不连续时,全表扫描可能很慢。

    Q8:什么是覆盖索引?
    覆盖索引指一个索引包含或覆盖了所有需要查询的字段的值,不再需要根据索引回表查询数据。覆盖索引必须要存储索引列的值,因此 MySQL 只能使用 B-Tree 索引做覆盖索引。

    优点:① 索引条目通常远小于数据行大小,可以极大减少数据访问量。② 因为索引按照列值顺序存储,所以对于 IO 密集型防伪查询回避随机从磁盘读取每一行数据的 IO 少得多。③ 由于 InnoDB 使用聚簇索引,覆盖索引对 InnoDB 很有帮助。InnoDB 的二级索引在叶子节点保存了行的主键值,如果二级主键能覆盖查询那么可以避免对主键索引的二次查询。

    Q9:你知道哪些索引使用原则?
    建立索引

    对查询频次较高且数据量比较大的表建立索引。索引字段的选择,最佳候选列应当从 WHERE 子句的条件中提取,如果 WHERE 子句中的组合比较多,应当挑选最常用、过滤效果最好的列的组合。业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。

    使用前缀索引

    索引列开始的部分字符,索引创建后也是使用硬盘来存储的,因此短索引可以提升索引访问的 IO 效率。对于 BLOB、TEXT 或很长的 VARCHAR 列必须使用前缀索引,MySQL 不允许索引这些列的完整长度。前缀索引是一种能使索引更小更快的有效方法,但缺点是 MySQL 无法使用前缀索引做 ORDER BY 和 GROUP BY,也无法使用前缀索引做覆盖扫描。

    选择合适的索引顺序

    当不需要考虑排序和分组时,将选择性最高的列放在前面。索引的选择性是指不重复的索引值和数据表的记录总数之比,索引的选择性越高则查询效率越高,唯一索引的选择性是 1,因此也可以使用唯一索引提升查询效率。

    删除无用索引

    MySQL 允许在相同列上创建多个索引,重复的索引需要单独维护,并且优化器在优化查询时也需要逐个考虑,这会影响性能。重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引,应该避免创建重复索引。如果创建了索引 (A,B) 再创建索引 (A) 就是冗余索引,因为这只是前一个索引的前缀索引,对于 B-Tree 索引来说是冗余的。解决重复索引和冗余索引的方法就是删除这些索引。除了重复索引和冗余索引,可能还会有一些服务器永远不用的索引,也应该考虑删除。

    Q10:索引失效的情况有哪些?
    如果索引列出现了隐式类型转换,则 MySQL 不会使用索引。常见的情况是在 SQL 的 WHERE 条件中字段类型为字符串,其值为数值,如果没有加引号那么 MySQL 不会使用索引。

    如果 WHERE 条件中含有 OR,除非 OR 前使用了索引列而 OR 之后是非索引列,索引会失效。

    MySQL 不能在索引中执行 LIKE 操作,这是底层存储引擎 API 的限制,最左匹配的 LIKE 比较会被转换为简单的比较操作,但如果是以通配符开头的 LIKE 查询,存储引擎就无法做比较。这种情况下 MySQL 只能提取数据行的值而不是索引值来做比较。

    如果查询中的列不是独立的,则 MySQL 不会使用索引。独立的列是指索引列不能是表达式的一部分,也不能是函数的参数。

    对于多个范围条件查询,MySQL 无法使用第一个范围列后面的其他索引列,对于多个等值查询则没有这种限制。

    如果 MySQL 判断全表扫描比使用索引查询更快,则不会使用索引。

    索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。

    优化 5
    Q1:如何定位低效 SQL?
    可以通过两种方式来定位执行效率较低的 SQL 语句。一种是通过慢查询日志定位,可以通过慢查询日志定位那些已经执行完毕的 SQL 语句。另一种是使用 SHOW PROCESSLIST 查询,慢查询日志在查询结束以后才记录,所以在应用反应执行效率出现问题的时候查询慢查询日志不能定位问题,此时可以使用 SHOW PROCESSLIST 命令查看当前 MySQL 正在进行的线程,包括线程的状态、是否锁表等,可以实时查看 SQL 的执行情况,同时对一些锁表操作进行优化。找到执行效率低的 SQL 语句后,就可以通过 SHOW PROFILE、EXPLAIN 或 trace 等丰富来继续优化语句。

    Q2:SHOW PROFILE 的作用?
    通过 SHOW PROFILE 可以分析 SQL 语句性能消耗,例如查询到 SQL 会执行多少时间,并显示 CPU、内存使用量,执行过程中系统锁及表锁的花费时间等信息。例如 SHOW PROFILE CPU/MEMORY/BLOCK IO FOR QUERY N 分别查询 id 为 N 的 SQL 语句的 CPU、内存以及 IO 的消耗情况。

    Q3:trace 是干什么的?
    从 MySQL5.6 开始,可以通过 trace 文件进一步获取优化器是是如何选择执行计划的,在使用时需要先打开设置,然后执行一次 SQL,最后查看 information_schema.optimizer_trace 表而都内容,该表为联合i表,只能在当前会话进行查询,每次查询后返回的都是最近一次执行的 SQL 语句。

    Q4:EXPLAIN 的字段有哪些,具有什么含义?
    执行计划是 SQL 调优的一个重要依据,可以通过 EXPLAIN 命令查看 SQL 语句的执行计划,如果作用在表上,那么该命令相当于 DESC。EXPLAIN 的指标及含义如下:

    指标名	含义
    id	表示 SELECT 子句或操作表的顺序,执行顺序从大到小执行,当 id 样时,执行顺序从上往下。
    select_type	表示查询中每个 SELECT 子句的类型,例如 SIMPLE 表示不包含子查询、表连接或其他复杂语法的简单查询,PRIMARY 表示复杂查询的最外层查询,SUBQUERY 表示在 SELECTWHERE 列表中包含了子查询。
    type	表示访问类型,性能由差到好为:ALL 全表扫描、index 索引全扫描、range 索引范围扫描、ref 返回匹配某个单独值得所有行,常见于使用非唯索引或唯索引的非唯前缀进行的查找,也经常出现在 join 操作中、eq_ref性索引扫描,对于每个索引键只有条记录与之匹配、constMySQL 对查询某部分进行优化,并转为个常量时,使用这些访问类型,例如将主键或唯索引置于 WHERE 列表就能将该查询转为constsystem 表中只有行数据或空表,只能用于 MyISAMMemory 表、NULL 执行时不用访问表或索引就能得到结果。SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是consts 最好。
    possible_keys	表示查询时可能用到的索引,但不定使用。列出大量可能索引时意味着备选索引数量太多了。
    key	显示 MySQL 在查询时实际使用的索引,如果没有使用则显示为 NULLkey_len	表示使用到索引字段的长度,可通过该列计算查询中使用的索引的长度,对于确认索引有效性以及多列索引中用到的列数目很重要。
    ref	表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值。
    rows	表示 MySQL 根据表统计信息及索引选用情况,估算找到所需记录所需要读取的行数。
    Extra	表示额外信息,例如 Using temporary 表示需要使用临时表存储结果集,常见于排序和分组查询。Using filesort 表示无法利用索引完成的文件排序,这是 ORDER BY 的结果,可以通过合适的索引改进性能。Using index 表示只需要使用索引就可以满足查询表得要求,说明表正在使用覆盖索引。

    Q5:有哪些优化 SQL 的策略?
    优化 COUNT 查询

    COUNT 是一个特殊的函数,它可以统计某个列值的数量,在统计列值时要求列值是非空的,不会统计 NULL 值。如果在 COUNT 中指定了列或列的表达式,则统计的就是这个表达式有值的结果数,而不是 NULL。

    COUNT 的另一个作用是统计结果集的行数,当 MySQL 确定括号内的表达式不可能为 NULL 时,实际上就是在统计行数。当使用 COUNT() 时,\ 不会扩展成所有列,它会忽略所有的列而直接统计所有的行数。

    某些业务场景并不要求完全精确的 COUNT 值,此时可以使用近似值来代替,EXPLAIN 出来的优化器估算的行数就是一个不错的近似值,因为执行 EXPLAIN 并不需要真正地执行查询。

    通常来说 COUNT 都需要扫描大量的行才能获取精确的结果,因此很难优化。在 MySQL 层还能做的就只有覆盖扫描了,如果还不够就需要修改应用的架构,可以增加汇总表或者外部缓存系统。

    优化关联查询

    确保 ON 或 USING 子句中的列上有索引,在创建索引时就要考虑到关联的顺序。

    确保任何 GROUP BY 和 ORDER BY 的表达式只涉及到一个表中的列,这样 MySQL 才有可能使用索引来优化这个过程。

    在 MySQL 5.5 及以下版本尽量避免子查询,可以用关联查询代替,因为执行器会先执行外部的 SQL 再执行内部的 SQL。

    优化 GROUP BY

    如果没有通过 ORDER BY 子句显式指定要排序的列,当查询使用 GROUP BY 时,结果***自动按照分组的字段进行排序,如果不关心结果集的顺序,可以使用 ORDER BY NULL 禁止排序。

    优化 LIMIT 分页

    在偏移量非常大的时候,需要查询很多条数据再舍弃,这样的代价非常高。要优化这种查询,要么是在页面中限制分页的数量,要么是优化大偏移量的性能。最简单的办法是尽可能地使用覆盖索引扫描,而不是查询所有的列,然后根据需要做一次关联操作再返回所需的列。

    还有一种方法是从上一次取数据的位置开始扫描,这样就可以避免使用 OFFSET。其他优化方法还包括使用预先计算的汇总表,或者关联到一个冗余表,冗余表只包含主键列和需要做排序的数据列。

    优化 UNION 查询

    MySQL 通过创建并填充临时表的方式来执行 UNION 查询,除非确实需要服务器消除重复的行,否则一定要使用 UNION ALL,如果没有 ALL 关键字,MySQL 会给临时表加上 DISTINCT 选项,这会导致对整个临时表的数据做唯一性检查,这样做的代价非常高。

    使用用户自定义变量

    在查询中混合使用过程化和关系化逻辑的时候,自定义变量可能会非常有用。用户自定义变量是一个用来存储内容的临时容器,在连接 MySQL 的整个过程中都存在,可以在任何可以使用表达式的地方使用自定义变量。例如可以使用变量来避免重复查询刚刚更新过的数据、统计更新和插入的数量等。

    优化 INSERT

    需要对一张表插入很多行数据时,应该尽量使用一次性插入多个值的 INSERT 语句,这种方式将缩减客户端与数据库之间的连接、关闭等消耗,效率比多条插入单个值的 INSERT 语句高。也可以关闭事务的自动提交,在插入完数据后提交。当插入的数据是按主键的顺序插入时,效率更高。

    复制 2
    Q1:MySQL 主从复制的作用?
    复制解决的基本问题是让一台服务器的数据与其他服务器保持同步,一台主库的数据可以同步到多台备库上,备库本身也可以被配置成另外一台服务器的主库。主库和备库之间可以有多种不同的组合方式。

    MySQL 支持两种复制方式:基于行的复制和基于语句的复制,基于语句的复制也称为逻辑复制,从 MySQL 3.23 版本就已存在,基于行的复制方式在 5.1 版本才被加进来。这两种方式都是通过在主库上记录二进制日志、在备库重放日志的方式来实现异步的数据复制。因此同一时刻备库的数据可能与主库存在不一致,并且无法包装主备之间的延迟。

    MySQL 复制大部分是向后兼容的,新版本的服务器可以作为老版本服务器的备库,但是老版本不能作为新版本服务器的备库,因为它可能无法解析新版本所用的新特性或语法,另外所使用的二进制文件格式也可能不同。

    复制解决的问题:数据分布、负载均衡、备份、高可用性和故障切换、MySQL 升级测试。

    Q2:MySQL 主从复制的步骤?
    ① 在主库上把数据更改记录到二进制日志中。② 备库将主库的日志复制到自己的中继日志中。 ③ 备库读取中继日志中的事件,将其重放到备库数据之上。

    第一步是在主库上记录二进制日志,每次准备提交事务完成数据更新前,主库将数据更新的事件记录到二进制日志中。MySQL 会按事务提交的顺序而非每条语句的执行顺序来记录二进制日志,在记录二进制日志后,主库会告诉存储引擎可以提交事务了。

    下一步,备库将主库的二进制日志复制到其本地的中继日志中。备库首先会启动一个工作的 IO 线程,IO 线程跟主库建立一个普通的客户端连接,然后在主库上启动一个特殊的二进制转储线程,这个线程会读取主库上二进制日志中的事件。它不会对事件进行轮询。如果该线程追赶上了主库将进入睡眠状态,直到主库发送信号量通知其有新的事件产生时才会被唤醒,备库 IO 线程会将接收到的事件记录到中继日志中。

    备库的 SQL 线程执行最后一步,该线程从中继日志中读取事件并在备库执行,从而实现备库数据的更新。当 SQL 线程追赶上 IO 线程时,中继日志通常已经在系统缓存中,所以中继日志的开销很低。SQL 线程执行的时间也可以通过配置选项来决定是否写入其自己的二进制日志中。

    二进制中1的个数

    问题:输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。
    思路:
    n&(n-1)
    该位运算去除 n 的位级表示中最低的那一位。
    n : 10110100
    n-1 : 10110011
    n&(n-1) : 10110000
    方法:本质上对n的二进制表示中的1的位置的判断。
    eg: 5 -》 101 & 100(101 - 1) = 100 -》 100 & 011(100 - 1) = 000 -》 000
    代码:

    public int NumberOf1(int n) {
            int sum = 0; /// 记录1的个数
            while (n != 0) {  /// 说明当前n的二进制表示中肯定有1
                sum++;
                n = n & (n - 1); /// 本质上就是消除从右往左数的第一个位置的1。
            }
            return sum;
        }

    git&github

    Git 结构

    工作区--git add-->暂存区-->git commit-->git库

    Git 和代码托管中心

    代码托管中心的任务:维护远程库
    局域网环境下
    GitLab 服务器
    外网环境下
    GitHub
    码云

    本地库和远程库

    团队内部协作
    员工1 --从远程库--clone克隆-->本地库1--push-->远程库 --员工2-->pull-本地库2->push--远程库
    跨团队协作
    远程库1--fork->远程库2-->clone-本地库2-push--远程库2-->pull request--审核--merge--远程库1--pull--本地库1

    Git 命令行操作

    本地库初始化

    mkdir chat //创建项目
    git init   //初始化 生成了.git文件
    ll .git/

    设置签名

    形式
    用户名:tom
    Email地址:[email protected]
    作用:区分不同开发人员的身份
    命令:
    项目级别/仓库级别:仅在当前本地库范围内有效

    git config user.name tom_pro
    git config user.email [email protected]
    信息保存位置:./.git/config 文件

    系统用户级别:登录当前操作系统的用户范围

    git config --global user.name tom_glb
    git config --global user.email [email protected]
    信息保存位置:~/.gitconfig 文件

    就近原则:项目级别优先于系统用户级别,如果只有系统用户级别的签名,就以系统用户级别的签名为准,二者都没有不允许

    基本操作

    状态查看

    查看工作区、暂存区状态

    git status

    添加
    将工作区的“新建/修改”添加到暂存区

    git add file

    提交
    将暂存区的内容提交到本地库 -m 带上提交说明信息

    git commit -m "my first commit" [filename]

    查看历史记录

    git log   //显示太详细
    git log --pretty=oneline  //显示简洁些
    git log --oneline
    git reflog  //HEAD@{移动到当前版本需要多少步}

    多屏显示控制方式:
    空格向下翻页
    b 向上翻页
    q 退出

    历史版本前进后退

    基于索引值操作[推荐]

    git reset --hard [局部索引值]
    git reset --hard a6ace91

    使用^符号:只能后退

    git reset --hard HEAD^^  注:一个^表示后退一步,n 个表示后退 n 步

    使用~符号:只能后退

    git reset --hard HEAD~n //注:表示后退 n 步1 2 3..

    删除文件并找到

    前提:删除前,文件存在时的状态提交到了本地库。

    操作:git reset --hard [指针位置]

    删除操作已经提交到本地库:指针位置指向历史记录
    删除操作尚未提交到本地库:指针位置使用 HEAD

    比较文件差异

    比较文件差异
    git diff [文件名]
    将工作区中的文件和暂存区进行比较
    git diff [本地库中历史版本] [文件名]
    将工作区中的文件和本地库历史记录比较
    不带文件名比较多个文件

    分支管理

    什么是分支

    在版本控制过程中,使用多条线同时推进多个任务

     热修复分支                                         |hot_fix------|
    file                     master----- -   | ------|      --------  |-------|-------------
    file 复制1份       feature_biue-- |                                          |
    file 复制2份     feature_game--------------------------------|

    分支的好处

    同时并行推进多个功能开发,提高开发效率
    各个分支在开发过程中,如果某一个分支开发失败,不会对其他分支有任
    何影响。失败的分支删除重新开始即可

    分支操作

    查看所有分支

    git branch -v
    git branch -al //查看本地和远程的所有分支。
    这样就可以看到所有的分支,
    其中master是本地分支,
    前面的星号*表示正在使用的分支
    前面带有remotes的分支都是远程分支

    创建分支

    git branch hot-fix(分支名)

    切换分支

    git checkout hot_fix(分支名)

    合并分支
    切换到被合并的分支上

    git checkout 被合并分支名
    git merge hot-fix  //把hot_fix那个分支合并到被合并分支上

    git pull
    :取回远程主机某个分支的更新,再与本地的指定分支合并

    git pull = git fetch + git merge

    git fetch不会进行合并执行后需要手动执行git merge合并分支,而git pull拉取远程分之后直接与本地分支进行合并。更准确地说,git pull使用给定的参数运行git fetch,并调用git merge将检索到的分支头合并到当前分支中。

    git pull <远程主机名> <远程分支名>:<本地分支名>
    git pull origin master:brantest
    git pull origin master //与当前分支合并
    
    git fetch origin master:brantest 
    git merge brantest

    git fetch更安全一些,因为在merge前,我们可以查看更新情况,然后再决定是否合并

    解决冲突
    冲突的解决
    第一步:编辑文件,删除特殊符号
    第二步:把文件修改到满意的程度,保存退出
    第三步:git add [文件名]
    第四步:git commit -m "日志信息"  注意:此时 commit 一定不能带具体文件名

    Git 保存版本的机制

    集中式版本控制工具的文件管理机制
    以文件变更列表的方式存储信息
    Git 的文件管理机制
    Git 把数据看作是小型文件系统的一组快照。每次提交更新时 Git 都会对当前
    的全部文件制作一个快照并保存这个快照的索引。为了高效,如果文件没有修改,
    Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。所以 Git 的
    工作方式可以称之为快照流。

    创建远程库

    库名和本地库一样
    创建远程库地址别名

    git remote -v   //查看当前所有远程地址别名
    git remote add 别名  远程地址

    https://blog.csdn.net/xqhys/article/details/98113227
    git add . 添加全部已经修改的文件,准备commit 提交
    该命令效果等同于 git add -A

    推送

    git push 别名 分支名

    克隆

    新键一个本地仓库

    git clone 

    效果
    完整的把远程库下载到本地
    创建 origin 远程地址别名
    初始化本地库

    从暂存区撤销到工作区
    git rm --cached good.txt
    window 10 凭据
    想切换别的账号可以清除掉

    两字符串旋转

    问题:如果对于一个字符串A,将A的前面任意一部分挪到后边去形成的字符串称为A的旋转词。比如A="12345",A的旋转词有"12345","23451","34512","45123"和"51234"。对于两个字符串A和B,请判断A和B是否互为旋转词。

    给定两个字符串A和B及他们的长度lena,lenb,请返回一个bool值,代表他们是否互为旋转词。
    测试样例:
    "cdab",4,"abcd",4
    返回:true
    思路:
    最优时间复杂度O(n)
    1先判断str1与str2长度是否相等
    2如果长度相等,生产str1+str1大字符串
    3用kmp算法判断大字符串是否包含str2
    str1+str1大字符串包含str1所有的旋转词
    代码:

    import java.util.*;
    public class Rotation {
        public boolean chkRotation(String A, int lena, String B, int lenb) {
            // write code here
            if(lena!=lenb){
                return false;
            }
            String str=A+A;
            return str.contains(B);        
        }
    }

    前端开发工作技巧总结

    1 模块设置内边距padding 上下右左 高度定位设置relative 高度自适应

    2 一行文字在div中上下左右居中 text-aglin:center line-height:div的高度

    3 absolute top/left/
    relative margin-top/margin-left

    4 cursor:default箭头/pointer指示链接一只手

    API与SDK

    https://www.jianshu.com/p/dd2eff92e8fc
    API是接口的一种,在程序交互中具有重要的作用,而SDK与API有着密不可分的关系。
    只需要根据他提供好的接口,也就是调用他的方法,传入他规定的参数,然后这个函数就会帮你实现这些功能。
    SDK
    SDK即“软体开发工具包
    第三方服务商提供的实现软件产品某项功能的工具包。
    利用SDK开发
    https://www.jianshu.com/p/71482f325e82
    3.使用Java SDK

    调用阿里云Java SDK的3个主要步骤:
    创建DefaultAcsClient实例并初始化。
    创建API请求并设置参数。
    发起请求并处理应答或异常。

    IO

    一,概述
    java io大致分为以下几类
    磁盘操作:File
    字节操作:inputStream,outputStream
    字符操作:Reader,Writer
    对象操作:Serializable
    网络操作:Socket

    故障处理工具

    作者:冠状病毒biss
    链接:https://www.nowcoder.com/discuss/447742?source_id=profile_create&channel=666
    来源:牛客网

    jps:虚拟机进程状况工具

    功能和 ps 命令类似:可以列出正在运行的虚拟机进程,显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(LVMID)。LVMID 与操作系统的进程 ID(PID)一致,使用 Windows 的任务管理器或 UNIX 的 ps 命令也可以查询到虚拟机进程的 LVMID,但如果同时启动了多个虚拟机进程,必须依赖 jps 命令。

    jstat:虚拟机统计信息监视工具

    用于监视虚拟机各种运行状态信息。可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、即时编译器等运行时数据,在没有 GUI 界面的服务器上是运行期定位虚拟机性能问题的常用工具。

    参数含义:S0 和 S1 表示两个 Survivor,E 表示新生代,O 表示老年代,YGC 表示 Young GC 次数,YGCT 表示 Young GC 耗时,FGC 表示 Full GC 次数,FGCT 表示 Full GC 耗时,GCT 表示 GC 总耗时。

    jinfo:Java 配置信息工具

    实时查看和调整虚拟机各项参数,使用 jps 的 -v 参数可以查看虚拟机启动时显式指定的参数,但如果想知道未显式指定的参数值只能使用 jinfo 的 -flag 查询。

    jmap:Java 内存映像工具

    用于生成堆转储快照,还可以查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率,当前使用的是哪种收集器等。和 jinfo 一样,部分功能在 Windows 受限,除了生成堆转储快照的 -dump 和查看每个类实例的 -histo 外,其余选项只能在 Linux 使用。

    jhat:虚拟机堆转储快照分析工具

    JDK 提供 jhat 与 jmap 搭配使用分析 jmap 生成的堆转储快照。jhat 内置了一个微型的 HTTP/Web 服务器,生成堆转储快照的分析结果后可以在浏览器查看。

    jstack:Java 堆栈跟踪工具

    用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等。线程出现停顿时通过 jstack 查看各个线程的调用堆栈,可以获知没有响应的线程在后台做什么或等什么资源。

    排序

    时间复杂度O(n^2)
    一,冒泡排序
    思路:0->n-1
    左边第一个和第二个比较,如果第一个比第二个大,交换他俩位置,接着比较第二个和第三个....
    代码:

    public class BubbleSort {
    public int[] bubbleSort(int[] A, int n) {
    // write code here
    for(int i=0;i<n;i++){
    for(int j=0;j<n-i-1;j++){
    if(A[j+1]<A[j])
    {
    int temp=A[j+1];
    A[j+1]=A[j];
    A[j]=temp;
    }
    }
    }
    return A;
    }

    二,选择排序
    思路:0->n-1 1->n-1
    首先找到数组里最小的那个元素,将它和数组的第一个元素交换位置,其次,在剩余元素中找到最小的和数组第二个元素交换位置...
    代码:

    public class SelectionSort {
        public int[] selectionSort(int[] A, int n) {
            // write code here
            for(int i=0;i<n-1;i++){
                int min=i;
                for(int j=i+1;j<n;j++){
                    if(A[j]<A[min]){
                        min=j;
                    }
                }
                int temp=A[min];
                A[min]=A[i];
                A[i]=temp;
            }
            return A;
        }
    }

    三,插入排序
    思路:往前比较插
    从数组第二个元素开始与左边第一个元素比较,如果左边的元素比它大,则继续与左边第二个元素比,直至遇到不比他大的元素,然后插到这个元素的右边...
    代码:

    public class InsertionSort {
        public int[] insertionSort(int[] A, int n) {
            // write code here
            for(int i=0;i<n-1;i++){
                for(int j=i+1;j>0;j--){
                    if(A[j]<A[j-1]){
                        int temp=A[j];
                        A[j]=A[j-1];
                        A[j-1]=temp;
                    }else
                        break;
                }
            }
               return A; 
        }
    }

    时间复杂度O(nlogn)
    四,归并排序
    思路:1->2 2->4 最后成为一个最大有序数组
    把两个有序的数组合并成一个有序的数组,很快,通过递归的方式将大的数组一直分割,直到数组的大小为1,此时只有一个元素,之后再合并
    1 1->2 2 2->4
    代码:
    递归方式

    public class MergeSort {
         // 归并排序
         public static int[] mergeSort(int[] arr, int left, int right) {
             // 如果 left == right,表示数组只有一个元素,则不用递归排序
             if (left < right) {
                 // 把大的数组分隔成两个数组
                 int mid = (left + right) / 2;
                 // 对左半部分进行排序
                 arr = mergeSort(arr, left, mid);
                // 对右半部分进行排序
                arr = mergeSort(arr, mid + 1, right);
                //进行合并
                merge(arr, left, mid, right);
            }
           return arr;
        }
    
        // 合并函数,把两个有序的数组合并起来
        // arr[left..mif]表示一个数组,arr[mid+1 .. right]表示一个数组
        private static void merge(int[] arr, int left, int mid, int right) {
            //先用一个临时数组把他们合并汇总起来
            int[] a = new int[right - left + 1];
           int i = left;
           int j = mid + 1;
            int k = 0;
            while (i <= mid && j <= right) {
                if (arr[i] < arr[j]) {
                    a[k++] = arr[i++];
               } else {
                    a[k++] = arr[j++];
                }
            }
           while(i <= mid) a[k++] = arr[i++];
            while(j <= right) a[k++] = arr[j++];
            // 把临时数组复制到原数组
           for (i = 0; i < k; i++) {
                arr[left++] = a[i];
            }
        }
    }

    五,快速排序
    随机选一个数,小于等于放它前面,大于放后面,递归前后

    public class QuickSort {
        public static void quickSort(int[] nums,int start,int end){
            if(start<end){
                int pivotIndex=getPivotIndex(nums,start,end);
                quickSort(nums,start,pivotIndex-1);
                quickSort(nums,pivotIndex+1,end);
            }
        }
        private static int getPivotIndex(int[] nums,int start,int end){
            int pivot=nums[start];
            int left=start;
            int right=end;
            while(left<right) {
                while (left <= right && nums[left] <= pivot)
                    left++;
                while (left <= right && nums[right] >= pivot)
                    right--;
                if (left < right) {
                    int temp = nums[left];
                    nums[left] = nums[right];
                    nums[right] = temp;
                }
            }
            int temp=nums[start];
            nums[start]=nums[right];
            nums[right]=temp;
            return right;
        }
        public static void main(String[] args) {
            int[] nums=new int[]{4,3,3,1,7,7,2,8};
            QuickSort.quickSort(nums,0,nums.length-1);
            for (int i=0;i<nums.length;i++) {
                System.out.println(nums[i]);
            }
        }
    }

    六,堆排序
    先把n个数建成大小为n的大根堆,把堆顶和堆的最后一个数交换,把堆顶放入数组最后一个位置,调整n-1大顶堆,继续同上,对大小为1时
    七,希尔排序
    插入排序的改良
    ->步长为1
    时间复杂度趋近为O(N)的排序算法
    不是基于比较的排序算法
    **来自桶排序
    八,计数排序
    放入桶里,倒出的顺序就是大小的顺序
    九,基数排序
    空间复杂度
    O(1)
    插入排序,选择排序,冒泡排序,堆排序,希尔排序
    O(logn)->O(N)
    快速排序
    O(n)
    归并排序
    O(m)
    计数,基数m桶的数量
    稳定性
    相同关键字的记录,经过排序,这些记录的相对次序保持不变
    不稳定排序
    快速排序,选择排序,希尔排序,堆排序
    https://www.nowcoder.com/discuss/438905?source_id=profile_create&channel=666

    设计模式

    作者:冠状病毒biss
    链接:https://www.nowcoder.com/discuss/447742?source_id=profile_create&channel=666
    来源:牛客网

    Q1:设计模式有哪些原则?
    开闭原则:OOP 中最基础的原则,指一个软件实体(类、模块、方法等)应该对扩展开放,对修改关闭。强调用抽象构建框架,用实现扩展细节,提高代码的可复用性和可维护性。

    单一职责原则:一个类、接口或方法只负责一个职责,降低代码复杂度以及变更引起的风险。

    依赖倒置原则:程序应该依赖于抽象类或接口,而不是具体的实现类。

    接口隔离原则:将不同功能定义在不同接口中实现接口隔离,避免了类依赖它不需要的接口,减少了接口之间依赖的冗余性和复杂性。

    里氏替换原则:开闭原则的补充,规定了任何父类可以出现的地方子类都一定可以出现,可以约束继承泛滥,加强程序健壮性。

    迪米特原则:也叫最少知道原则,每个模块对其他模块都要尽可能少地了解和依赖,降低代码耦合度。

    合成/聚合原则:尽量使用组合(has-a)/聚合(contains-a)而不是继承(is-a)达到软件复用的目的,避免滥用继承带来的方法污染和方法爆炸,方法污染指父类的行为通过继承传递给子类,但子类并不具备执行此行为的能力;方法爆炸指继承树不断扩大,底层类拥有的方法过于繁杂,导致很容易选择错误。

    Q2:设计模式的分类,你知道哪些设计模式?
    创建型: 在创建对象的同时隐藏创建逻辑,不使用 new 直接实例化对象,程序在判断需要创建哪些对象时更灵活。包括工厂/抽象工厂/单例/建造者/原型模式。

    *结构型: *通过类和接口间的继承和引用实现创建复杂结构的对象。包括适配器/桥接模式/过滤器/组合/装饰器/外观/享元/代理模式。

    *行为型: *通过类之间不同通信方式实现不同行为。包括责任链/命名/解释器/迭代器/中介者/备忘录/观察者/状态/策略/模板/访问者模式。

    Q3:说一说简单工厂模式
    简单工厂模式指由一个工厂对象来创建实例,客户端不需要关注创建逻辑,只需提供传入工厂的参数。

    适用于工厂类负责创建对象较少的情况,缺点是如果要增加新产品,就需要修改工厂类的判断逻辑,违背开闭原则,且产品多的话会使工厂类比较复杂。

    Calendar 抽象类的 getInstance 方法,调用 createCalendar 方法根据不同的地区参数创建不同的日历对象。

    Spring 中的 BeanFactory 使用简单工厂模式,根据传入一个唯一的标识来获得 Bean 对象。

    Q4:说一说工厂方法模式
    工厂方法模式指定义一个创建对象的接口,让接口的实现类决定创建哪种对象,让类的实例化推迟到子类中进行。

    客户端只需关心对应工厂而无需关心创建细节,主要解决了产品扩展的问题,在简单工厂模式中如果产品种类变多,工厂的职责会越来越多,不便于维护。

    Collection 接口这个抽象工厂中定义了一个抽象的 iterator 工厂方法,返回一个 Iterator 类的抽象产品。该方法通过 ArrayList 、HashMap 等具体工厂实现,返回 Itr、KeyIterator 等具体产品。

    Spring 的 FactoryBean 接口的 getObject 方法也是工厂方法。

    Q5:抽象工厂模式了解吗?
    抽象工厂模式指提供一个创建一系列相关或相互依赖对象的接口,无需指定它们的具体类。

    客户端不依赖于产品类实例如何被创建和实现的细节,主要用于系统的产品有多于一个的产品族,而系统只消费其中某一个产品族产品的情况。抽象工厂模式的缺点是不方便扩展产品族,并且增加了系统的抽象性和理解难度。

    java.sql.Connection 接口就是一个抽象工厂,其中包括很多抽象产品如 Statement、Blob、Savepoint 等。

    Q6:单例模式的特点是什么?
    单例模式属于创建型模式,一个单例类在任何情况下都只存在一个实例,构造方法必须是私有的、由自己创建一个静态变量存储实例,对外提供一个静态公有方法获取实例。

    优点是内存中只有一个实例,减少了开销,尤其是频繁创建和销毁实例的情况下并且可以避免对资源的多重占用。缺点是没有抽象层,难以扩展,与单一职责原则冲突。

    Spring 的 ApplicationContext 创建的 Bean 实例都是单例对象,还有 ServletContext、数据库连接池等也都是单例模式。

    Q7:单例模式有哪些实现?
    饿汉式:在类加载时就初始化创建单例对象,线程安全,但不管是否使用都创建对象可能会浪费内存。

    public class HungrySingleton {
        private HungrySingleton(){}
     
        private static HungrySingleton instance = new HungrySingleton();
     
        public static HungrySingleton getInstance() {
            return instance;
        }
    }

    懒汉式:在外部调用时才会加载,线程不安全,可以加锁保证线程安全但效率低。

    public class LazySingleton {
        private LazySingleton(){}
     
        private static LazySingleton instance;
     
        public static LazySingleton getInstance() {
            if(instance == null) {
                instance = new LazySingleton();
            }
            return instance;
        }
    }

    双重检查锁:使用 volatile 以及多重检查来减小锁范围,提升效率。

    public class DoubleCheckSingleton {
        private DoubleCheckSingleton(){}
     
        private volatile static DoubleCheckSingleton instance;
     
        public static DoubleCheckSingleton getInstance() {
            if(instance == null) {
                synchronized (DoubleCheckSingleton.class) {
                    if (instance == null) {
                        instance = new DoubleCheckSingleton();
                    }
                }
            }
            return instance;
        }
    }

    静态内部类:同时解决饿汉式的内存浪费问题和懒汉式的线程安全问题。

    public class StaticSingleton {
        private StaticSingleton(){}
     
        public static StaticSingleton getInstance() {
            return StaticClass.instance;
        }
     
        private static class StaticClass {
            private static final StaticSingleton instance = new StaticSingleton();
        }
    }

    枚举:《Effective Java》提倡的方式,不仅能避免线程安全问题,还能防止反序列化重新创建新的对象,绝对防止多次实例化,也能防止反射破解单例的问题。

    public enum EnumSingleton {
        INSTANCE;
    }

    Q8:讲一讲代理模式
    代理模式属于结构型模式,为其他对象提供一种代理以控制对这个对象的访问。优点是可以增强目标对象的功能,降低代码耦合度,扩展性好。缺点是在客户端和目标对象之间增加代理对象会导致请求处理速度变慢,增加系统复杂度。

    Spring 利用动态代理实现 AOP,如果 Bean 实现了接口就使用 JDK 代理,否则使用 CGLib 代理。

    静态代理:代理对象持有被代理对象的引用,调用代理对象方法时也会调用被代理对象的方法,但是会在被代理对象方法的前后增加其他逻辑。需要手动完成,在程序运行前就已经存在代理类的字节码文件,代理类和被代理类的关系在运行前就已经确定了。 缺点是一个代理类只能为一个目标服务,如果要服务多种类型会增加工作量。

    动态代理:动态代理在程序运行时通过反射创建具体的代理类,代理类和被代理类的关系在运行前是不确定的。动态代理的适用性更强,主要分为 JDK 动态代理和 CGLib 动态代理。

    JDK 动态代理:通过 Proxy 类的 newInstance 方法获取一个动态代理对象,需要传入三个参数,被代理对象的类加载器、被代理对象实现的接口,以及一个 InvocationHandler 调用处理器来指明具体的逻辑,相比静态代理的优势是接口中声明的所有方法都被转移到 InvocationHandler 的 invoke 方法集中处理。
    CGLib 动态代理:JDK 动态代理要求实现被代理对象的接口,而 CGLib 要求继承被代理对象,如果一个类是 final 类则不能使用 CGLib 代理。两种代理都在运行期生成字节码,JDK 动态代理直接写字节码,而 CGLib 动态代理使用 ASM 框架写字节码,ASM 的目的是生成、转换和分析以字节数组表示的已编译 Java 类。 JDK 动态代理调用代理方法通过反射机制实现,而 GCLib 动态代理通过 FastClass 机制直接调用方法,它为代理类和被代理类各生成一个类,该类为代理类和被代理类的方法分配一个 int 参数,调用方法时可以直接定位,因此调用效率更高。

    Q9:讲一讲装饰器模式
    装饰器模式属于结构型模式,在不改变原有对象的基础上将功能附加到对象,相比继承可以更加灵活地扩展原有对象的功能。

    装饰器模式适合的场景:在不想增加很多子类的前提下扩展一个类的功能。

    java.io 包中,InputStream 字节输入流通过装饰器 BufferedInputStream 增强为缓冲字节输入流。

    Q10:装饰器模式和动态代理的区别?
    装饰器模式的关注点在于给对象动态添加方法,而动态代理更注重对象的访问控制。动态代理通常会在代理类中创建被代理对象的实例,而装饰器模式会将装饰者作为构造方法的参数。

    Q11:讲一讲适配器模式
    适配器模式属于结构型模式,它作为两个不兼容接口之间的桥梁,结合了两个独立接口的功能,将一个类的接口转换成另外一个接口使得原本由于接口不兼容而不能一起工作的类可以一起工作。

    缺点是过多使用适配器会让系统非常混乱,不易整体把握。

    java.io 包中,InputStream 字节输入流通过适配器 InputStreamReader 转换为 Reader 字符输入流。

    Spring MVC 中的 HandlerAdapter,由于 handler 有很多种形式,包括 Controller、HttpRequestHandler、Servlet 等,但调用方式又是确定的,因此需要适配器来进行处理,根据适配规则调用 handle 方法。

    Arrays.asList 方法,将数组转换为对应的集合(注意不能使用修改集合的方法,因为返回的 ArrayList 是 Arrays 的一个内部类)。

    Q12:适配器模式和和装饰器模式以及代理模式的区别?
    适配器模式没有层级关系,适配器和被适配者没有必然连续,满足 has-a 的关系,解决不兼容的问题,是一种后置考虑。

    装饰器模式具有层级关系,装饰器与被装饰者实现同一个接口,满足 is-a 的关系,注重覆盖和扩展,是一种前置考虑。

    适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。

    Q13:讲一讲策略模式
    策略模式属于行为型模式,定义了一系列算法并封装起来,之间可以互相替换。策略模式主要解决在有多种算法相似的情况下,使用 if/else 所带来的难以维护。

    优点是算法可以自由切换,可以避免使用多重条件判断并且扩展性良好,缺点是策略类会增多并且所有策略类都需要对外暴露。

    在集合框架中,经常需要通过构造方法传入一个比较器 Comparator 进行比较排序。Comparator 就是一个抽象策略,一个类通过实现该接口并重写 compare 方法成为具体策略类。

    创建线程池时,需要传入拒绝策略,当创建新线程使当前运行的线程数超过 maximumPoolSize 时会使用相应的拒绝策略处理。

    Q14:讲一讲模板模式
    模板模式属于行为型模式,使子类可以在不改变算法结构的情况下重新定义算法的某些步骤,适用于抽取子类重复代码到公共父类。

    优点是可以封装固定不变的部分,扩展可变的部分。缺点是每一个不同实现都需要一个子类维护,会增加类的数量。

    为防止恶意操作,一般模板方法都以 final 修饰。

    HttpServlet 定义了一套处理 HTTP 请求的模板,service 方法为模板方法,定义了处理HTTP请求的基本流程,doXXX 等方法为基本方法,根据请求方法的类型做相应的处理,子类可重写这些方法。

    Q15:讲一讲观察者模式
    观察者模式属于行为型模式,也叫发布订阅模式,定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。主要解决一个对象状态改变给其他对象通知的问题,缺点是如果被观察者对象有很多的直接和间接观察者的话通知很耗时, 如果存在循环依赖的话可能导致系统崩溃,另外观察者无法知道目标对象具体是怎么发生变化的。

    ServletContextListener 能够监听 ServletContext 对象的生命周期,实际上就是监听 Web 应用。当 Servlet 容器启动 Web 应用时调用 contextInitialized 方法,终止时调用 contextDestroyed 方法。

    Recommend Projects

    • React photo React

      A declarative, efficient, and flexible JavaScript library for building user interfaces.

    • Vue.js photo Vue.js

      🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

    • Typescript photo Typescript

      TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

    • TensorFlow photo TensorFlow

      An Open Source Machine Learning Framework for Everyone

    • Django photo Django

      The Web framework for perfectionists with deadlines.

    • D3 photo D3

      Bring data to life with SVG, Canvas and HTML. 📊📈🎉

    Recommend Topics

    • javascript

      JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

    • web

      Some thing interesting about web. New door for the world.

    • server

      A server is a program made to process requests and deliver data to clients.

    • Machine learning

      Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

    • Game

      Some thing interesting about game, make everyone happy.

    Recommend Org

    • Facebook photo Facebook

      We are working to build community through open source technology. NB: members must have two-factor auth.

    • Microsoft photo Microsoft

      Open source projects and samples from Microsoft.

    • Google photo Google

      Google ❤️ Open Source for everyone.

    • D3 photo D3

      Data-Driven Documents codes.