diff --git a/.gitignore b/.gitignore index c5217a32..48b1d15d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,8 @@ hs_err_pid* out # iml -*.iml \ No newline at end of file +*.iml + +.DS_Store +.aider* +.env diff --git "a/Basics/Java\351\235\242\350\257\225\345\237\272\347\241\200\345\270\270\350\247\201\351\227\256\351\242\230.md" "b/Basics/Java\351\235\242\350\257\225\345\237\272\347\241\200\345\270\270\350\247\201\351\227\256\351\242\230.md" deleted file mode 100644 index 112f0e99..00000000 --- "a/Basics/Java\351\235\242\350\257\225\345\237\272\347\241\200\345\270\270\350\247\201\351\227\256\351\242\230.md" +++ /dev/null @@ -1,67 +0,0 @@ -- [简述线程、程序、进程的基本概念。以及他们之间关系是什么?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E7%AE%80%E8%BF%B0%E7%BA%BF%E7%A8%8B%E7%A8%8B%E5%BA%8F%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5%E4%BB%A5%E5%8F%8A%E4%BB%96%E4%BB%AC%E4%B9%8B%E9%97%B4%E5%85%B3%E7%B3%BB%E6%98%AF%E4%BB%80%E4%B9%88) -- [Java 语言有哪些特点?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#java-%E8%AF%AD%E8%A8%80%E6%9C%89%E5%93%AA%E4%BA%9B%E7%89%B9%E7%82%B9) -- [面向对象和面向过程的区别](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E5%92%8C%E9%9D%A2%E5%90%91%E8%BF%87%E7%A8%8B%E7%9A%84%E5%8C%BA%E5%88%AB) -- **==、hashcode和equals** - - [==与equals](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#-%E4%B8%8E-equals%E9%87%8D%E8%A6%81) - - [hashcode和equlas](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#hashcode-%E4%B8%8E-equals-%E9%87%8D%E8%A6%81) - - [hashcode和equlas代码小例子](/Basics/src/com/equal/Student.java) -- **关于 JVM JDK 和 JRE 最详细通俗的解答** - - [JVM](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#jvm) - - [JDK 和 JRE](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#jdk-%E5%92%8C-jre) -- [Java和C++的区别?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#java%E5%92%8Cc%E7%9A%84%E5%8C%BA%E5%88%AB) -- **基本类型** - - [自动装箱与拆箱](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E8%87%AA%E5%8A%A8%E8%A3%85%E7%AE%B1%E4%B8%8E%E6%8B%86%E7%AE%B1) - - [字符型常量和字符串常量的区别?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E5%AD%97%E7%AC%A6%E5%9E%8B%E5%B8%B8%E9%87%8F%E5%92%8C%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%B8%B8%E9%87%8F%E7%9A%84%E5%8C%BA%E5%88%AB) - - [说说&和&&的区别](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E8%AF%B4%E8%AF%B4%E5%92%8C%E7%9A%84%E5%8C%BA%E5%88%AB) - - [short s1 = 1; s1 = s1 + 1;有什么错? short s1 = 1; s1 += 1; 有什么错?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#short-s1--1-s1--s1--1%E6%9C%89%E4%BB%80%E4%B9%88%E9%94%99-short-s1--1-s1--1-%E6%9C%89%E4%BB%80%E4%B9%88%E9%94%99) [demo](/Basics/src/com/type/TypeConvert.java) - - [char 型变量中能不能存贮一个中文汉字?为什么?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#char-%E5%9E%8B%E5%8F%98%E9%87%8F%E4%B8%AD%E8%83%BD%E4%B8%8D%E8%83%BD%E5%AD%98%E8%B4%AE%E4%B8%80%E4%B8%AA%E4%B8%AD%E6%96%87%E6%B1%89%E5%AD%97%E4%B8%BA%E4%BB%80%E4%B9%88) - - [整形包装缓存池例子vlaueof](/Basics/src/com/pack/IntegerPackDemo.java) -- **构造器** - - [构造器 Constructor 是否可被 override?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E6%9E%84%E9%80%A0%E5%99%A8-constructor-%E6%98%AF%E5%90%A6%E5%8F%AF%E8%A2%AB-override) - - [构造方法有哪些特性?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E6%9E%84%E9%80%A0%E6%96%B9%E6%B3%95%E6%9C%89%E5%93%AA%E4%BA%9B%E7%89%B9%E6%80%A7) -- **String** - - [String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#string-stringbuffer-%E5%92%8C-stringbuilder-%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88-string-%E4%B8%BA%E4%BB%80%E4%B9%88%E6%98%AF%E4%B8%8D%E5%8F%AF%E5%8F%98%E7%9A%84) - - [String StringBuffer 和 StringBuilder的代码例子](/Basics/src/com/strings/SbDemo.java) - - [String A = "123"; String B = new String("123");生成几个对象?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#string-a--123-string-b--new-string123%E7%94%9F%E6%88%90%E5%87%A0%E4%B8%AA%E5%AF%B9%E8%B1%A1) - - [String.intern与缓存池](/Basics/src/com/strings/SIntern.java) -- **对象** - - [Java 面向对象编程三大特性: 封装 继承 多态](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E9%87%8D%E8%BD%BD%E5%92%8C%E9%87%8D%E5%86%99%E7%9A%84%E5%8C%BA%E5%88%AB) - - [接口和抽象类的区别是什么?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E6%8E%A5%E5%8F%A3%E5%92%8C%E6%8A%BD%E8%B1%A1%E7%B1%BB%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88) - - [成员变量与局部变量的区别有哪些?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E6%88%90%E5%91%98%E5%8F%98%E9%87%8F%E4%B8%8E%E5%B1%80%E9%83%A8%E5%8F%98%E9%87%8F%E7%9A%84%E5%8C%BA%E5%88%AB%E6%9C%89%E5%93%AA%E4%BA%9B) - - [重载和重写的区别](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E9%87%8D%E8%BD%BD%E5%92%8C%E9%87%8D%E5%86%99%E7%9A%84%E5%8C%BA%E5%88%AB) - - [创建一个对象用什么运算符?对象实体与对象引用有何不同?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%B1%A1%E7%94%A8%E4%BB%80%E4%B9%88%E8%BF%90%E7%AE%97%E7%AC%A6%E5%AF%B9%E8%B1%A1%E5%AE%9E%E4%BD%93%E4%B8%8E%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%E6%9C%89%E4%BD%95%E4%B8%8D%E5%90%8C) - - [对象的相等与指向他们的引用相等,两者有什么不同?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E5%AF%B9%E8%B1%A1%E7%9A%84%E7%9B%B8%E7%AD%89%E4%B8%8E%E6%8C%87%E5%90%91%E4%BB%96%E4%BB%AC%E7%9A%84%E5%BC%95%E7%94%A8%E7%9B%B8%E7%AD%89%E4%B8%A4%E8%80%85%E6%9C%89%E4%BB%80%E4%B9%88%E4%B8%8D%E5%90%8C) -- [为什么Java中只有值传递?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E4%B8%BA%E4%BB%80%E4%B9%88java%E4%B8%AD%E5%8F%AA%E6%9C%89%E5%80%BC%E4%BC%A0%E9%80%92) - - [参考这篇文章](https://github.com/Snailclimb/JavaGuide/blob/master/docs/essential-content-for-interview/PreparingForInterview/%E5%BA%94%E5%B1%8A%E7%94%9F%E9%9D%A2%E8%AF%95%E6%9C%80%E7%88%B1%E9%97%AE%E7%9A%84%E5%87%A0%E9%81%93Java%E5%9F%BA%E7%A1%80%E9%97%AE%E9%A2%98.md#%E4%B8%80-%E4%B8%BA%E4%BB%80%E4%B9%88-java-%E4%B8%AD%E5%8F%AA%E6%9C%89%E5%80%BC%E4%BC%A0%E9%80%92) - - [基本类型传递代码例子](/Basics/src/com/transfer/TransferDemo.java) - - [数组类型传递代码例子](/Basics/src/com/transfer/TransferDemo2.java) - - [对象引用类型传递代码例子](/Basics/src/com/transfer/TransferDemo3.java) -- **关键字** - - [关于 final 关键字的一些总结](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E5%85%B3%E4%BA%8E-final-%E5%85%B3%E9%94%AE%E5%AD%97%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%BB%E7%BB%93) **一般关键字的面试,答的时候按照变量、方法和类去总结** - - [static 关键字](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#static-%E5%85%B3%E9%94%AE%E5%AD%97) - - [this 关键字](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#this-%E5%85%B3%E9%94%AE%E5%AD%97) - - [super 关键字](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#super-%E5%85%B3%E9%94%AE%E5%AD%97) - - [final, finally, finalize 的区别](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#final-finally-finalize-%E7%9A%84%E5%8C%BA%E5%88%AB) - - [请说出作用域 public,private,protected,以及不写时的区别](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E8%AF%B7%E8%AF%B4%E5%87%BA%E4%BD%9C%E7%94%A8%E5%9F%9F-publicprivateprotected%E4%BB%A5%E5%8F%8A%E4%B8%8D%E5%86%99%E6%97%B6-%E7%9A%84%E5%8C%BA%E5%88%AB) -- **异常** - - [Java 中的异常处理](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#java-%E4%B8%AD%E7%9A%84%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86) - - [请写出你最常见到的 5 个 runtime exception](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E8%AF%B7%E5%86%99%E5%87%BA%E4%BD%A0%E6%9C%80%E5%B8%B8%E8%A7%81%E5%88%B0%E7%9A%84-5-%E4%B8%AA-runtime-exception) -- **IO** - - [获取用键盘输入常用的两种方法](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E8%8E%B7%E5%8F%96%E7%94%A8%E9%94%AE%E7%9B%98%E8%BE%93%E5%85%A5%E5%B8%B8%E7%94%A8%E7%9A%84%E4%B8%A4%E7%A7%8D%E6%96%B9%E6%B3%95) - - [Java 中 IO 流分为几种?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#java-%E4%B8%AD-io-%E6%B5%81%E5%88%86%E4%B8%BA%E5%87%A0%E7%A7%8D) - - [既然有了字节流,为什么还要有字符流?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E6%97%A2%E7%84%B6%E6%9C%89%E4%BA%86%E5%AD%97%E8%8A%82%E6%B5%81%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%98%E8%A6%81%E6%9C%89%E5%AD%97%E7%AC%A6%E6%B5%81) - - [BIO,NIO,AIO 有什么区别?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#bionioaio-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) -- **反射** - - [反射是什么?](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6%E4%BB%8B%E7%BB%8D) - - [静态编译和动态编译](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E9%9D%99%E6%80%81%E7%BC%96%E8%AF%91%E5%92%8C%E5%8A%A8%E6%80%81%E7%BC%96%E8%AF%91) - - [反射机制优缺点](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E5%8F%8D%E5%B0%84%E6%9C%BA%E5%88%B6%E4%BC%98%E7%BC%BA%E7%82%B9) - - [反射的应用场景](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E5%8F%8D%E5%B0%84%E7%9A%84%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF) - - [反射得到Class对象的三种方式代码例子](/Basics/src/com/reflect/ReflectDemo.java) - - [反射访问并调用构造方法的代码例子](/Basics/src/com/reflect/ConstructorsDemo.java) - - [反射访问并调用成员变量的代码例子](/Basics/src/com/reflect/FieldDemo.java) - - [反射访问并调用成员方法的代码例子](/Basics/src/com/reflect/MethodDemo.java) -- **拷贝** - - [深拷贝 vs 浅拷贝](https://github.com/DreamCats/JavaBooks/blob/master/Basics/Java%E9%9D%A2%E8%AF%95%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md#%E6%B7%B1%E6%8B%B7%E8%B4%9D-vs-%E6%B5%85%E6%8B%B7%E8%B4%9D) - - [深浅拷贝参考](https://juejin.im/post/5c988a7ef265da6116246d11) - - [浅拷贝代码例子](/Basics/src/com/copy/ShallowCopyDemo.java) - - [深拷贝代码例子](/Basics/src/com/copy/Student.java) diff --git "a/Basics/Java\351\235\242\350\257\225\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/Basics/Java\351\235\242\350\257\225\345\237\272\347\241\200\347\237\245\350\257\206.md" deleted file mode 100644 index be380cfb..00000000 --- "a/Basics/Java\351\235\242\350\257\225\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ /dev/null @@ -1,555 +0,0 @@ -## 引言 - -> [JavaGuide](https://github.com/Snailclimb/JavaGuide) :一份涵盖大部分Java程序员所需要掌握的核心知识。**star:45159**,替他宣传一下子 -> -> 这位大佬,总结的真好!!!我引用这位大佬的文章,因为方便自己学习和打印... - - - -## 面向对象和面向过程的区别 - -- **面向过程** :**面向过程性能比面向对象高。** 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发。但是,**面向过程没有面向对象易维护、易复用、易扩展。** -- **面向对象** :**面向对象易维护、易复用、易扩展。** 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,**面向对象性能比面向过程低**。 - -> 这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java性能差的主要原因并不是因为它是面向对象语言,而是Java是半编译语言,最终的执行代码并不是可以直接被CPU执行的二进制机械码。 -> -> 而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比Java好。 - -## Java 语言有哪些特点? - -1. 简单易学; -2. 面向对象(封装,继承,多态); -3. 平台无关性( Java 虚拟机实现平台无关性); -4. 可靠性; -5. 安全性; -6. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持); -7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便); -8. 编译与解释并存; - -## 关于 JVM JDK 和 JRE 最详细通俗的解答 - -### JVM - -Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。 - -**什么是字节码?采用字节码的好处是什么?** - -> 在 Java 中,JVM可以理解的代码就叫做`字节码`(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同操作系统的计算机上运行。 - -**Java 程序从源代码到运行一般有下面3步:** - -![Java程序运行过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E8%BF%87%E7%A8%8B.png) - -我们需要格外注意的是 .class->机器码 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT 编译器,而JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。 - -> HotSpot采用了惰性评估(Lazy Evaluation)的做法,根据二八定律,消耗大部分系统资源的只有那一小部分的代码(热点代码),而这也就是JIT所需要编译的部分。JVM会根据代码每次被执行的情况收集信息并相应地做出一些优化,因此执行的次数越多,它的速度就越快。JDK 9引入了一种新的编译模式AOT(Ahead of Time Compilation),它是直接将字节码编译成机器码,这样就避免了JIT预热等各方面的开销。JDK支持分层编译和AOT协作使用。但是 ,AOT 编译器的编译质量是肯定比不上 JIT 编译器的。 - -**总结:** - -Java虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。 - -### JDK 和 JRE - -JDK是Java Development Kit,它是功能齐全的Java SDK。它拥有JRE所拥有的一切,还有编译器(javac)和工具(如javadoc和jdb)。它能够创建和编译程序。 - -JRE 是 Java运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java虚拟机(JVM),Java类库,java命令和其他的一些基础构件。但是,它不能用于创建新程序。 - -如果你只是为了运行一下 Java 程序的话,那么你只需要安装 JRE 就可以了。如果你需要进行一些 Java 编程方面的工作,那么你就需要安装JDK了。但是,这不是绝对的。有时,即使您不打算在计算机上进行任何Java开发,仍然需要安装JDK。例如,如果要使用JSP部署Web应用程序,那么从技术上讲,您只是在应用程序服务器中运行Java程序。那你为什么需要JDK呢?因为应用程序服务器会将 JSP 转换为 Java servlet,并且需要使用 JDK 来编译 servlet。 - -## Java和C++的区别? - -我知道很多人没学过 C++,但是面试官就是没事喜欢拿咱们 Java 和 C++ 比呀!没办法!!!就算没学过C++,也要记下来! - -- 都是面向对象的语言,都支持封装、继承和多态 -- Java 不提供指针来直接访问内存,程序内存更加安全 -- Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。 -- Java 有自动内存管理机制,不需要程序员手动释放无用内存 - -## 什么是 Java 程序的主类 应用程序和小程序的主类有何不同? - -一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 main()方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。 - -## Java 应用程序与小程序之间有哪些差别? - -简单说应用程序是从主线程启动(也就是 `main()` 方法)。applet 小程序没有 `main()` 方法,主要是嵌在浏览器页面上运行(调用`init()`或者`run()`来启动),嵌入浏览器这点跟 flash 的小游戏类似。 - -## 字符型常量和字符串常量的区别? - -1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符 -2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置) -3. 占内存大小 字符常量只占2个字节; 字符串常量占若干个字节(至少一个字符结束标志) (**注意: char在Java中占两个字节**) - -![参考-JavaGuide](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) - -## 构造器 Constructor 是否可被 override? - -在讲继承的时候我们就知道父类的私有属性和构造方法并不能被继承,所以 Constructor 也就不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。 - -## 重载和重写的区别 - -### 重载 - -发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 - -### 重写 - - 重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。** - -## Java 面向对象编程三大特性: 封装 继承 多态 - -### 封装 - -封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 - -### 继承 - -继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。 - -**关于继承如下 3 点请记住:** - -1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,**只是拥有**。 -2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 -3. 子类可以用自己的方式实现父类的方法。(以后介绍)。 - -### 多态 - -所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 - -在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。 - -## String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? - -**可变性** - -简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串`char[]value` 但是没有用 final 关键字修饰,所以这两种对象都是可变的。 - -**线程安全性** - -String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。  - -**性能** - -每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 - -**对于三者使用的总结:** - -1. 操作少量的数据: 适用String -2. 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder -3. 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer - -## 自动装箱与拆箱 - -- **装箱**:将基本类型用它们对应的引用类型包装起来; -- **拆箱**:将包装类型转换为基本数据类型; - -## 在一个静态方法内调用一个非静态成员为什么是非法的? - -由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。 - -## 在 Java 中定义一个不做事且没有参数的构造方法的作用 - -Java 程序在执行子类的构造方法之前,如果没有用 `super() `来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用 `super() `来调用父类中特定的构造方法,则编译时将发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。 - -## import java和javax有什么区别? - -刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准API的一部分。 - -## 接口和抽象类的区别是什么? - -1. 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 -2. 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。 -3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。 -4. 接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。 -5. 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。 - -备注:在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写,不然会报错。 - -## 成员变量与局部变量的区别有哪些? - -1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。 -2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 -3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 -4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 - -## 创建一个对象用什么运算符?对象实体与对象引用有何不同? - -new运算符,new创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。一个对象引用可以指向0个或1个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有n个引用指向它(可以用n条绳子系住一个气球)。 - -## 什么是方法的返回值?返回值在类的方法里的作用是什么? - -方法的返回值是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用:接收出结果,使得它可以用于其他的操作! - -## 一个类的构造方法的作用是什么? 若一个类没有声明构造方法,该程序能正确执行吗? 为什么? - -主要作用是完成对类对象的初始化工作。可以执行。因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。 - -## 构造方法有哪些特性? - -1. 名字与类名相同。 -2. 没有返回值,但不能用void声明构造函数。 -3. 生成类的对象时自动执行,无需调用。 - -## 静态方法和实例方法有何不同 - -1. 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。 -2. 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。 - -## 对象的相等与指向他们的引用相等,两者有什么不同? - -对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。 - -## 在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是? - -帮助子类做初始化工作。 - -## == 与 equals(重要) - -**==** : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。 - -**equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: - -- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。 -- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 - -**举个例子:** - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -**说明:** - -- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -## hashCode 与 equals (重要) - -面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?” - -### hashCode()介绍 - -hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。 - -散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - -### 为什么要有 hashCode - -**我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:** 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 `equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -通过我们可以看出:`hashCode()` 的作用就是**获取哈希码**,也称为散列码;它实际上是返回一个int整数。这个**哈希码的作用**是确定该对象在哈希表中的索引位置。**`hashCode() `在散列表中才有用,在其它情况下没用**。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。 - -### hashCode()与equals()的相关规定 - -1. 如果两个对象相等,则hashcode一定也是相同的 -2. 两个对象相等,对两个对象分别调用equals方法都返回true -3. 两个对象有相同的hashcode值,它们也不一定是相等的 -4. **因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖** -5. hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据) - -## 为什么Java中只有值传递? - -- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。 -- 一个方法可以改变一个对象参数的状态。 -- 一个方法不能让对象参数引用一个新的对象。 - -## 简述线程、程序、进程的基本概念。以及他们之间关系是什么? - -**线程**与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。 - -**程序**是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。 - -**进程**是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 -线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。从另一角度来说,进程属于操作系统的范畴,主要是同一段时间内,可以同时执行一个以上的程序,而线程则是在同一程序内几乎同时执行一个以上的程序段。 - -## 关于 final 关键字的一些总结 - -final关键字主要用在三个地方:变量、方法、类。 - -1. 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。 -2. 当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。 -3. 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 - -**Final修饰有啥好处** - -- final的关键字提高了性能,JVM和java应用会缓存final变量; -- final变量可以在多线程环境下保持线程安全; -- 使用final的关键字提高了性能,JVM会对方法变量类进行优化; - -## Java 中的异常处理 - -在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 **Throwable类**。Throwable: 有两个重要的子类:**Exception(异常)** 和 **Error(错误)** ,二者都是 Java 异常处理的重要子类,各自都包含大量子类。 - -**Error(错误):是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。 - -这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。 - -**Exception(异常):是程序本身可以处理的异常**。Exception 类有一个重要的子类 **RuntimeException**。RuntimeException 异常由Java虚拟机抛出。**NullPointerException**(要访问的变量没有引用任何对象时,抛出该异常)、**ArithmeticException**(算术运算异常,一个整数除以0时,抛出该异常)和 **ArrayIndexOutOfBoundsException** (下标越界异常)。 - -**注意:异常和错误的区别:异常能被程序本身处理,错误是无法处理。** - -### Throwable类常用方法 - -- **public string getMessage()**:返回异常发生时的简要描述 -- **public string toString()**:返回异常发生时的详细信息 -- **public string getLocalizedMessage()**:返回异常对象的本地化信息。使用Throwable的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与getMessage()返回的结果相同 -- **public void printStackTrace()**:在控制台上打印Throwable对象封装的异常信息 - -### 异常处理总结 - -- **try 块:** 用于捕获异常。其后可接零个或多个catch块,如果没有catch块,则必须跟一个finally块。 -- **catch 块:** 用于处理try捕获到的异常。 -- **finally 块:** 无论是否捕获或处理异常,finally块里的语句都会被执行。当在try块或catch块中遇到return - 语句时,finally语句块将在方法返回之前被执行。 - -**在以下4种特殊情况下,finally块不会被执行:** - -1. 在finally语句块第一行发生了异常。 因为在其他行,finally块还是会得到执行 -2. 在前面的代码中用了System.exit(int)已退出程序。 exit是带参函数 ;若该语句在异常语句之后,finally会执行 -3. 程序所在的线程死亡。 -4. 关闭CPU。 - -**注意:** 当try语句和finally语句中都有return语句时,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值将会覆盖原始的返回值。如下: - -```java - public static int f(int value) { - try { - return value * value; - } finally { - if (value == 2) { - return 0; - } - } - } -``` - -如果调用 `f(2)`,返回值将是0,因为finally语句的返回值覆盖了try语句块的返回值。 - -## Java序列化中如果有些字段不想进行序列化,怎么办? - -对于不想进行序列化的变量,使用transient关键字修饰。 - -transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。 - -## 获取用键盘输入常用的两种方法 - -方法1:通过 Scanner - -```java -Scanner input = new Scanner(System.in); -String s = input.nextLine(); -input.close(); -``` - -方法2:通过 BufferedReader - -```java -BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); -String s = input.readLine(); -``` - -## Java 中 IO 流 - -### Java 中 IO 流分为几种? - - - 按照流的流向分,可以分为输入流和输出流; - - 按照操作单元划分,可以划分为字节流和字符流; - - 按照流的角色划分为节点流和处理流。 - -Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。 - - - InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。 - - OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。 - -### 既然有了字节流,为什么还要有字符流? - -问题本质想问:**不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?** - -回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。 - -### BIO,NIO,AIO 有什么区别? - -- **BIO (Blocking I/O):** 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 -- **NIO (New I/O):** NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。NIO中的N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发 -- **AIO (Asynchronous I/O):** AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 - -## static 关键字 - -**static 关键字主要有以下四种使用场景:** - -1. **修饰成员变量和成员方法:** 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:`类名.静态变量名` `类名.静态方法名()` -2. **静态代码块:** 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次. -3. **静态内部类(static修饰类的话只能修饰内部类):** 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。 -4. **静态导包(用来导入类中的静态资源,1.5之后的新特性):** 格式为:`import static` 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。 - -## this 关键字 - -```java -class Manager { - Employees[] employees; - - void manageEmployees() { - int totalEmp = this.employees.length; - System.out.println("Total employees: " + totalEmp); - this.report(); - } - - void report() { } -} -``` - -在上面的示例中,this关键字用于两个地方: - -- this.employees.length:访问类Manager的当前实例的变量。 -- this.report():调用类Manager的当前实例的方法。 - -此关键字是可选的,这意味着如果上面的示例在不使用此关键字的情况下表现相同。 但是,使用此关键字可能会使代码更易读或易懂。 - -## super 关键字 - -super关键字用于从子类访问父类的变量和方法。 例如: - -```java -public class Super { - protected int number; - - protected showNumber() { - System.out.println("number = " + number); - } -} - -public class Sub extends Super { - void bar() { - super.number = 10; - super.showNumber(); - } -} -``` - -在上面的例子中,Sub 类访问父类成员变量 number 并调用其其父类 Super 的 `showNumber()` 方法。 - -**使用 this 和 super 要注意的问题:** - -- 在构造器中使用 `super()` 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。 -- this、super不能用在static方法中。 - -**简单解释一下:** - -被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享。而 this 代表对本类对象的引用,指向本类对象;而 super 代表对父类对象的引用,指向父类对象;所以, **this和super是属于对象范畴的东西,而静态方法是属于类范畴的东西**。 - -## 深拷贝 vs 浅拷贝 - -1. **浅拷贝**:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。 -2. **深拷贝**:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。 - -![deep and shallow copy](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/java-deep-and-shallow-copy.jpg) - -## String A = "123"; String B = new String("123");生成几个对象? - -我说如果常量池中,原来没有“123”那么就是生成了2个对象,如果常量池中有“123”那么只要1个对象生成 - -## hashTable吗,和hashmap有啥区别? - -- Hashtable的方法是安全的有synchronized修饰,Hashmap是不安全的; -- hashtable不可以有null值;HashMap则可以有空值; -- Hashtable中数组默认大小是11,而Hashmap默认大小是16,;扩容的时候hashtable是乘以2加1,而hashmap是乘以2. - -## ArrayList和LinkedList的区别? - -- ArrayList底层是数组,ArrayLIst查找数据快 -- LinkedList底层是链表,LinkedList插入删除快; - -## linkedList可以用for循环遍历吗? - -- 能不用尽量不要用,linkedList底层是链表,它使用for进行遍历,访问每一个元素都是从头开始访问然后直到找到这个元素, -- 比如说找第三个节点,需要先找到第一个节点然后找到第二个节点; -- 继续找第4个节点,不是从第三个节点开始找的,还是从第一个节点开始,所以非常的慢,不推荐,可以用迭代器进行遍历。 - -## 说说&和&&的区别 - -- &和&&都可以用作逻辑与的运算符,表示逻辑与(and) -- 当运算符两边的表达式的结果都为 true 时, 整个运算结果才为 true,否则,只要有一方为 false,则结果为 false。 -- &&还具有短路的功能,即如果第一个表达式为 false,则不再计算第二个表达式 -- &还可以用作位运算符,当&操作符两边的表达式不是 boolean 类型时,&表示按位与操作,我们通常 使用 0x0f 来与一个整数进行&运算,来获取该整数的最低 4 个 bit 位 - -## switch语句能否作用在byte上,能否作用在long上,能否作用在String上? - -- switch的condition只能是一个整数表达式或者枚举常量 -- 整数表达式则是int或者integet包装类型,由于,byte,short,char都可以隐式转换为int,则作用。 -- long 和 String 类型都不符合 switch 的语法规定,并且不能被隐式转 换成 int 类型,所以,它们不能作用于 swtich 语句中。 - -## short s1 = 1; s1 = s1 + 1;有什么错? short s1 = 1; s1 += 1; 有什么错? - -- 对于 short s1 = 1; s1 = s1 + 1; 由于 s1+1 运算时会自动提升表达式的类型,所以结果是 int 型,再赋值 给 short 类型 s1 时,**编译器将报告需要强制转换类型的错误。** -- 对于 short s1 = 1; s1 += 1;由于 **+= 是 java 语言规定的运算符,java 编译器会对它进行特殊处理**,因此 可以正确编译。 - -## char 型变量中能不能存贮一个中文汉字?为什么? - -- char 型变量是用来存储 Unicode 编码的字符的,unicode 编码字符集中包含了汉字,所以,char 型变量 中当然可以存储汉字啦。 -- 如果某个特殊的汉字没有被包含在 unicode 编码字符集中,那么,这个 char 型变量中就不能存储这个特殊汉字。 -- unicode 编码占用两个字节,所以,char 类型的变量也是占 用两个字节。 - -## 请说出作用域 public,private,protected,以及不写时 的区别 - -| 作用域 | 当前类 | 同package | 子孙类 | 其他package | -| :-------: | :----: | :-------: | :----: | :---------: | -| public | √ | √ | √ | √ | -| protected | √ | √ | √ | × | -| friednly | √ | √ | × | × | -| private | √ | × | × | × | - -## final, finally, finalize 的区别。 - -- final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。 内部类要访问局部变量,局部变量必须定义成 final 类型 -- finally 是异常处理语句结构的一部分,表示总是执行。 -- finalize 是 Object 类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法, 可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。JVM 不保证此方法总被调用 - -## 请写出你最常见到的 5 个 runtime exception。 - -- RuntimeException 的子类 -- NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException。 - -## 反射 - -### 反射机制介绍 - -**Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能成为java语言的反射机制。** - -### 静态编译和动态编译 - -- 静态编译:在编译时确定类型,绑定对象 -- 动态编译:运行时确定类型,绑定对象 - -### 反射机制优缺点 - -- 优点:运行期间类型的判断,动态加载类,提高代码的灵活度。 -- 缺点:性能瓶颈:反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的java代码要慢很多。 - -### 反射的应用场景 - -**反射是框架设计的灵魂** - -在我们平时的项目开发过程中,基本上很少会直接使用的反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如**模块化**的开发,通过反射去调用对应的字节码;动态代理设计模型也采用了反射机制,还有我们日常使用的**Spring / Hibernate**等框架也大量使用到了反射机制。 - -- 我们在使用JDBC连接数据库时使用`Class.forName()`通过反射加载数据看的驱动程序; -- Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring通过XML配置模式装载Bean的过程; - - 将程序内所有XML或Properties配置文件加载入内存中; - - Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; - - 使用反射机制,根据这个字符串获得某个类的Class实例 - - 动态配置实例的属性 -- [反射参考(重要)](https://blog.csdn.net/sinat_38259539/article/details/71799078) \ No newline at end of file diff --git a/Basics/src/com/copy/ShallowCopyDemo.java b/Basics/src/com/copy/ShallowCopyDemo.java deleted file mode 100644 index a5987f44..00000000 --- a/Basics/src/com/copy/ShallowCopyDemo.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @program JavaBooks - * @description: ShalldowCopy - * @author: mf - * @create: 2020/02/10 21:21 - */ - -package com.copy; - -public class ShallowCopyDemo { - public static void main(String[] args) throws CloneNotSupportedException { - // 原始对象 - Student student = new Student(new Subject("code"), "dream"); - System.out.println("原始对象: " + student.getName() + " - " + student.getSubject().getName()); - - // 拷贝对象 - Student cloneStu = (Student) student.clone(); - System.out.println("拷贝对象: " + cloneStu.getName() + " - " + cloneStu.getSubject().getName()); - - // 原始对象和拷贝对象是否一样: - System.out.println("原始对象和拷贝对象是否一样: " + (student == cloneStu)); - - // 原始对象和拷贝对象的name属性是否一样 - System.out.println("原始对象和拷贝对象的name属性是否一样: " + (student.getName() == cloneStu.getName())); - - // 原始对象和拷贝对象的subj属性是否一样 - System.out.println("原始对象和拷贝对象的subj属性是否一样: " + (student.getSubject() == cloneStu.getSubject())); - - student.setName("cat"); - student.getSubject().setName("eat"); - System.out.println("更新后的原始对象: " + student.getName() + " - " + student.getSubject().getName()); - System.out.println("更新原始对象后的克隆对象: " + cloneStu.getName() + " - " + cloneStu.getSubject().getName()); - - // 在这个例子中,让要拷贝的类Student实现了Clonable接口并重写Object类的clone()方法,然后在方法内部调用super.clone()方法。 - // 从输出结果中我们可以看到,对原始对象stud的"name"属性所做的改变并没有影响到拷贝对象clonedStud; - // 但是对引用对象subj的"name"属性所做的改变影响到了拷贝对象clonedStud。 - } -} diff --git a/Basics/src/com/copy/Student.java b/Basics/src/com/copy/Student.java deleted file mode 100644 index 64453203..00000000 --- a/Basics/src/com/copy/Student.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * @program JavaBooks - * @description: Student - * @author: mf - * @create: 2020/02/10 21:22 - */ - -package com.copy; - -public class Student implements Cloneable{ - - // 对象的引用 - private Subject subject; - - private String name; - - public Student(Subject s, String name) { - this.subject = s; - this.name = name; - } - - public Subject getSubject() { - return subject; - } - - public String getName() { - return name; - } - - public void setSubject(Subject subject) { - this.subject = subject; - } - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Student{" + - "subject=" + subject + - ", name='" + name + '\'' + - '}'; - } - - @Override - protected Object clone() throws CloneNotSupportedException { - // 浅拷贝 -// return super.clone(); - - // 深拷贝 - Student student = new Student(new Subject(subject.getName()), name); - return student; - // 因为它是深拷贝,所以你需要创建拷贝类的一个对象。 - // 因为在Student类中有对象引用,所以需要在Student类中实现Cloneable接口并且重写clone方法。 - } -} diff --git a/Basics/src/com/copy/Subject.java b/Basics/src/com/copy/Subject.java deleted file mode 100644 index 92982084..00000000 --- a/Basics/src/com/copy/Subject.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @program JavaBooks - * @description: Subject - * @author: mf - * @create: 2020/02/10 21:22 - */ - -package com.copy; - -public class Subject { - - private String name; - - public Subject(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Subject{" + - "name='" + name + '\'' + - '}'; - } -} diff --git a/Basics/src/com/equal/Student.java b/Basics/src/com/equal/Student.java deleted file mode 100644 index 4736b8c5..00000000 --- a/Basics/src/com/equal/Student.java +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @program JavaBooks - * @description: Student - * @author: mf - * @create: 2020/02/07 02:07 - */ - -package com.equal; - -import java.util.HashSet; -import java.util.Objects; - -/** - * HashCode、equals和== - */ -public class Student { - - private Long id; - - private String name; - - public Student(Long id, String name) { - this.id = id; - this.name = name; - } - - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Student{" + - "id=" + id + - ", name='" + name + '\'' + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Student student = (Student) o; - System.out.println("name:" + this.getName() + "-->equals:" + - (Objects.equals(id, student.id) && Objects.equals(name, student.name)) + " " + student.getName()); - return Objects.equals(id, student.id) && - Objects.equals(name, student.name); - } - - @Override - public int hashCode() { - System.out.println("name:" + this.getName() + "-->hashcode:" + Objects.hash(id, name)); - return Objects.hash(id, name); - } - - - public static void main(String[] args) { - Student student = new Student(1L, "Mai"); - Student student1 = new Student(1L, "Mai"); -// System.out.println(student == student1); -// System.out.println(student.equals(student1)); - Student student2 = new Student(2L, "Liu"); - HashSet set = new HashSet<>(); - set.add(student); - set.add(student1); - set.add(student2); - System.out.println(set.toString()); - } -} diff --git a/Basics/src/com/java8/LambdaDemo.java b/Basics/src/com/java8/LambdaDemo.java deleted file mode 100644 index 6c159b00..00000000 --- a/Basics/src/com/java8/LambdaDemo.java +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @program JavaBooks - * @description: Lambda表达式 - * @author: mf - * @create: 2020/01/23 16:20 - */ - -package com.java8; - -import java.util.*; - -public class LambdaDemo { - public static void main(String[] args) { - // 老版本的排序 - List names = Arrays.asList("peter", "anna", "mike", "xenia"); - System.out.println(names); -// Collections.sort(names, new Comparator() { -// @Override -// public int compare(String a, String b) { -// return b.compareTo(a); -// } -// }); - - // 新版本 lambda表达式: - -// Collections.sort(names, (String a, String b) -> { -// return b.compareTo(a); -// }); -// Collections.sort(names, (String a, String b) -> b.compareTo(a)); - names.sort((String a, String b) -> b.compareTo(a)); - System.out.println(names); - } -} diff --git a/Basics/src/com/java8/MapsDemo.java b/Basics/src/com/java8/MapsDemo.java deleted file mode 100644 index eaca1551..00000000 --- a/Basics/src/com/java8/MapsDemo.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @program JavaBooks - * @description: map - * @author: mf - * @create: 2020/01/25 12:48 - */ - -package com.java8; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class MapsDemo { - public static void main(String[] args) { -// Map map = new HashMap<>(); -// for (int i = 0; i < 10; i++) { -// map.put(i, "val" + i); -// } -// map.forEach((id, val) -> System.out.println(val)); - - Student s1 = new Student(1L, "肖战", 15); - Student s2 = new Student(2L, "王一博", 15); - Student s3 = new Student(3L, "杨紫", 17); - Student s4 = new Student(4L, "李现", 17); - List students = new ArrayList<>(); - students.add(s1); - students.add(s2); - students.add(s3); - students.add(s4); - - List names = students.stream().map(s -> "名字:" + s.getName()).collect(Collectors.toList()); - names.forEach(a -> System.out.println(a)); - } -} diff --git a/Basics/src/com/java8/ParallerStreamDemo.java b/Basics/src/com/java8/ParallerStreamDemo.java deleted file mode 100644 index 8fbce412..00000000 --- a/Basics/src/com/java8/ParallerStreamDemo.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @program JavaBooks - * @description: 并行计算 - * @author: mf - * @create: 2020/01/25 12:40 - */ - -package com.java8; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -public class ParallerStreamDemo { - - public static void main(String[] args) { - int max = 1000000; - List values = new ArrayList<>(max); - for (int i = 0; i < max; i++) { - UUID uuid = UUID.randomUUID(); - values.add(uuid.toString()); - } - - // Sequential Sort(串行排序) - long t0 = System.nanoTime(); -// long t0 = System.currentTimeMillis(); - long count = values.stream().sorted().count(); - System.out.println(count); - long t1 = System.nanoTime(); -// long t1 = System.currentTimeMillis(); - long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0); -// long millis = t1 - t0; - System.out.println(String.format("sequential sort took: %d ms", millis)); - - // paraller sorted - - long t2 = System.nanoTime(); - long count1 = values.parallelStream().sorted().count(); - System.out.println(count1); - long t3 = System.nanoTime(); - long millis1 = TimeUnit.NANOSECONDS.toMillis(t3 - t2); - System.out.println(String.format("paraller sort took: %d ms", millis1)); - } -} diff --git a/Basics/src/com/java8/Person.java b/Basics/src/com/java8/Person.java deleted file mode 100644 index f199e208..00000000 --- a/Basics/src/com/java8/Person.java +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @program JavaBooks - * @description: 方法和函数式引用 - * @author: mf - * @create: 2020/01/23 16:36 - */ - -package com.java8; - -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Supplier; - -public class Person { - - String name; - - Integer age; - - public Person() { - } - - public Person(String name) { - this.name = name; - } - - public Person(String name, Integer age) { - this.name = name; - this.age = age; - } - - - @Override - public String toString() { - return "Person{" + - "name='" + name + '\'' + - ", age=" + age + - '}'; - } - - public static void main(String[] args) { - // 当只有1个参数时 - Supplier supplier = Person::new; // 构造函数引用指向默认的Apple()构造函数 - Person person = supplier.get(); // new 一个对象 - System.out.println(person); - - Supplier supplier1 = () -> new Person(); // 和上面等价 - Person person1 = supplier.get(); - System.out.println(person1); - - // 当只有一个参数时 - Function function = Person::new; - Person person2 = function.apply("mai"); - System.out.println(person2); - - // 当有两个参数时 - BiFunction function1 = Person::new; - Person person3 = function1.apply("feng", 25); - System.out.println(person3); - } -} diff --git a/Basics/src/com/java8/Student.java b/Basics/src/com/java8/Student.java deleted file mode 100644 index 75f255e8..00000000 --- a/Basics/src/com/java8/Student.java +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @program JavaBooks - * @description: Student - * @author: mf - * @create: 2020/02/07 02:07 - */ - -package com.java8; - -import java.util.HashSet; -import java.util.Objects; - -/** - * HashCode、equals和== - */ -public class Student { - - private Long id; - - private String name; - - private Integer age; - - public Student(Long id, String name, Integer age) { - this.id = id; - this.name = name; - this.age = age; - } - - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Integer getAge() { - return age; - } - - public void setAge(Integer age) { - this.age = age; - } - - @Override - public String toString() { - return "Student{" + - "id=" + id + - ", name='" + name + '\'' + - ", age=" + age + - '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Student student = (Student) o; - System.out.println("name:" + this.getName() + "-->equals:" + - (Objects.equals(id, student.id) && Objects.equals(name, student.name)) + " " + student.getName()); - return Objects.equals(id, student.id) && - Objects.equals(name, student.name) && - Objects.equals(age, student.age); - } - - @Override - public int hashCode() { - System.out.println("name:" + this.getName() + "-->hashcode:" + Objects.hash(id, name)); - return Objects.hash(id, name, age); - } - - - public static void main(String[] args) { - Student student = new Student(1L, "Mai", 16); - Student student1 = new Student(1L, "Mai", 16); -// System.out.println(student == student1); -// System.out.println(student.equals(student1)); - Student student2 = new Student(2L, "Liu", 18); - HashSet set = new HashSet<>(); - set.add(student); - set.add(student1); - set.add(student2); - System.out.println(set.toString()); - } -} diff --git a/Basics/src/com/java8/streamDemo.java b/Basics/src/com/java8/streamDemo.java deleted file mode 100644 index 0b18d0d6..00000000 --- a/Basics/src/com/java8/streamDemo.java +++ /dev/null @@ -1,94 +0,0 @@ -/** - * @program JavaBooks - * @description: 流的一些例子 - * @author: mf - * @create: 2020/01/23 17:39 - */ - -package com.java8; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; - -public class streamDemo { - public static void main(String[] args) { - List strings = Arrays.asList("ddd3", "bbb", "ccc", "www", "eee", "ddd2", "bbb"); - - System.out.println("过滤:"); - // 过滤 - strings - .stream() - .filter((s) -> s.startsWith("d")) - .forEach(System.out::println); - - System.out.println("排序:"); - // 排序 - strings - .stream() - .sorted() - .filter((s) -> s.startsWith("d")) - .forEach(System.out::println); - - System.out.println("映射:"); - strings - .stream() - .map(String::toUpperCase) - .sorted() - .forEach(System.out::println); - - // match - System.out.println("匹配:"); - boolean anyStartsWithA = - strings - .stream() - .anyMatch((s) -> s.startsWith("d")); - System.out.println(anyStartsWithA); - boolean allStartsWithA = - strings - .stream() - .allMatch((s) -> s.startsWith("d")); - System.out.println(allStartsWithA); - boolean noneStartsWithA = - strings - .stream() - .noneMatch((s) -> s.startsWith("a")); - System.out.println(noneStartsWithA); - - System.out.println("计数:"); - long count = - strings - .stream() - .filter((s) -> s.startsWith("d")) - .count(); - System.out.println(count); - - //规约 - System.out.println("规约"); - Optional reduce = strings - .stream() - .sorted() - .reduce((s1, s2) -> s1 + "#" + s2); - reduce.ifPresent(System.out::println); - - // 去重 - System.out.println("去重"); - strings.stream().distinct().forEach(s -> System.out.println(s)); - - // 字符串拼接 - String reduce1 = Stream.of("A", "B", "C").reduce("", String::concat); - System.out.println(reduce1); - // 求最小值 - Double reduce2 = Stream.of(-1.5, 1.0, -3.0).reduce(Double.MAX_VALUE, Double::min); - System.out.println(reduce2); - // 求和 - Integer reduce3 = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum); - System.out.println(reduce3); - // 求和 - Optional reduce4 = Stream.of(1, 2, 3, 4).reduce(Integer::sum); - System.out.println(reduce4.get()); - - - } -} diff --git a/Basics/src/com/pack/IntegerPackDemo.java b/Basics/src/com/pack/IntegerPackDemo.java deleted file mode 100644 index a6ce9659..00000000 --- a/Basics/src/com/pack/IntegerPackDemo.java +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @program JavaBooks - * @description: IntegerPackDemo - * @author: mf - * @create: 2020/02/12 00:08 - */ - -package com.pack; - -/** - * 所有整型包装类对象值的比较必须使用equals方法。 - */ -public class IntegerPackDemo { - public static void main(String[] args) { - Integer x = 3; // 装箱 - int z = x; // 拆箱 - Integer y = 3; - System.out.println(x == y); // true - // 当使用自动装箱方式创建一个Integer对象时,当数值在-128 ~127时, - // 会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的Integer对象。 - // 所以上述代码中,x和y引用的是相同的Integer对象。 - Integer a = new Integer(3); - Integer b = new Integer(3); - System.out.println(a == b); // false - System.out.println(a.equals(b)); // true - - // 可以看一下整形包装类的equals的源码 - /** - * public boolean equals(Object obj) { - * if (obj instanceof Integer) { - * // 返回参数中的Integer的value,所以最后的比的是内容内容 - * return value == ((Integer)obj).intValue(); - * } - * return false; - * } - */ - - // 缓存池 - // new Integer(123) 与 Integer.valueOf(123) - // new Integer(123) 每次都会新建一个对象 - // Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。前提注意范围 - Integer ab = new Integer(123); - Integer ba = new Integer(123); - System.out.println(ab == ba); // false - // 从缓存中拿123 - Integer aa = Integer.valueOf(123); - Integer bb = Integer.valueOf(123); - System.out.println(aa == bb); // true - /** - * - * public static Integer valueOf(int i) { - * // 判断是否在Integer的范围内 - * if (i >= IntegerCache.low && i <= IntegerCache.high) - * return IntegerCache.cache[i + (-IntegerCache.low)]; - * return new Integer(i); - * } - */ - } -} diff --git a/Basics/src/com/reflect/ConstructorsDemo.java b/Basics/src/com/reflect/ConstructorsDemo.java deleted file mode 100644 index 7cd02529..00000000 --- a/Basics/src/com/reflect/ConstructorsDemo.java +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @program JavaBooks - * @description: ConstructorsDemo - * @author: mf - * @create: 2020/02/09 17:28 - */ - -package com.reflect; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - -/** - * 通过Class对象可以获取某个类中的:构造方法、成员变量、成员方法;并访问成员; - * - */ -public class ConstructorsDemo { - - public static void main(String[] args) throws Exception { - // 1. 加载Class对象 - Class clazz = Class.forName("com.reflect.Student"); - - // 2. 获取所有公有构造方法 - System.out.println("**********************所有公有构造方法*********************************"); - Constructor[] constructors = clazz.getConstructors(); - for (Constructor constructor : constructors) { - System.out.println(constructor); - } - - // 3. - System.out.println("************所有的构造方法(包括:私有、受保护、默认、公有)***************"); - Constructor[] declaredConstructors = clazz.getDeclaredConstructors(); - for (Constructor declaredConstructor : declaredConstructors) { - System.out.println(declaredConstructor); - } - - // 4. - System.out.println("*****************获取公有、无参的构造方法*******************************"); - Constructor constructor = clazz.getConstructor(); - System.out.println(constructor); - - // 调用构造方法 - Object object = constructor.newInstance(); - System.out.println(object); - - // - System.out.println("******************获取私有构造方法,并调用*******************************"); - Constructor constructor1 = clazz.getDeclaredConstructor(char.class); - System.out.println(constructor1); - // 调用构造方法 - constructor1.setAccessible(true); // 暴力访问 - Object object2 = constructor1.newInstance('买'); - System.out.println(object2); - } -} diff --git a/Basics/src/com/reflect/FieldDemo.java b/Basics/src/com/reflect/FieldDemo.java deleted file mode 100644 index 5c182c38..00000000 --- a/Basics/src/com/reflect/FieldDemo.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @program JavaBooks - * @description: FieldDemo - * @author: mf - * @create: 2020/02/09 20:08 - */ - -package com.reflect; - -import java.lang.reflect.Field; - -public class FieldDemo { - public static void main(String[] args) throws Exception { - // 1. 获取class对象 - Class clazz = Class.forName("com.reflect.Student"); - - // 2. 获取所有字段 - System.out.println("************获取所有公有的字段********************"); - Field[] fields = clazz.getFields(); - for (Field field : fields) { - System.out.println(field); - } - // - System.out.println("************获取所有的字段(包括私有、受保护、默认的)********************"); - Field[] fields1 = clazz.getDeclaredFields(); - for (Field field : fields1) { - System.out.println(field); - } - // - System.out.println("*************获取公有字段**并调用***********************************"); - Field gender = clazz.getField("gender"); - System.out.println(gender); - // 获取一个对象 - Object o = clazz.getConstructor().newInstance(); - gender.set(o, "男"); - Student stu = (Student) o; - System.out.println("验证性别:" + stu.getGender()); - // - System.out.println("*************获取公有字段**并调用***********************************"); - Field name = clazz.getDeclaredField("name"); - System.out.println(name); - name.setAccessible(true); //暴力反射,解除私有限定 - name.set(o, "买"); - System.out.println("验证姓名:" + stu); - - - } -} diff --git a/Basics/src/com/reflect/MethodDemo.java b/Basics/src/com/reflect/MethodDemo.java deleted file mode 100644 index 835c90a5..00000000 --- a/Basics/src/com/reflect/MethodDemo.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @program JavaBooks - * @description: MethodDemo - * @author: mf - * @create: 2020/02/09 23:45 - */ - -package com.reflect; - -import java.lang.reflect.Method; - -public class MethodDemo { - public static void main(String[] args) throws Exception { - // 1. 获取对象 - Class clazz = Class.forName("com.reflect.Student"); - // 2. 获取所有公有方法 - System.out.println("***************获取所有的”公有“方法*******************"); - Method[] methods = clazz.getMethods(); - for (Method method : methods) { - System.out.println(method); - } - System.out.println("***************获取所有的方法,包括私有的*******************"); - Method[] declaredMethods = clazz.getDeclaredMethods(); - for (Method declaredMethod : declaredMethods) { - System.out.println(declaredMethod); - } - System.out.println("***************获取公有的show1()方法*******************"); - Method m = clazz.getMethod("show1", String.class); - System.out.println(m); - // 实例化对象 - Object o = clazz.getConstructor().newInstance(); - m.invoke(o, "买"); - - System.out.println("***************获取私有的show4()方法******************"); - m = clazz.getDeclaredMethod("show4", int.class); - System.out.println(m); - m.setAccessible(true); // 暴力解除 私有 - Object result = m.invoke(o, 20);//需要两个参数,一个是要调用的对象(获取有反射),一个是实参 - System.out.println("返回值:" + result); - } -} diff --git a/Basics/src/com/reflect/ReflectDemo.java b/Basics/src/com/reflect/ReflectDemo.java deleted file mode 100644 index 024f0a2e..00000000 --- a/Basics/src/com/reflect/ReflectDemo.java +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @program JavaBooks - * @description: ReflectDemo - * @author: mf - * @create: 2020/02/09 16:55 - */ - -package com.reflect; - -/** - * 获取Class对象的三种方式 - * Object ——> getClass(); - * 任何数据类型(包括基本数据类型)都有一个“静态”的class属性 - * 通过Class类的静态方法:forName(String className)(常用) - */ -public class ReflectDemo { - public static void main(String[] args) throws ClassNotFoundException { - // 第一种方式获取Class对象 - Student student = new Student(); // 这一new 产生一个Student对象,一个Class对象。 - Class studentClass = student.getClass(); - System.out.println(studentClass.getName()); // com.reflect.Student - - // 第二种方式获取Class对象 - Class studentClass2 = Student.class; - System.out.println(studentClass == studentClass2); //判断第一种方式获取的Class对象和第二种方式获取的是否是同一个 - - //第三种方式获取Class对象 - Class studentClass3 = Class.forName("com.reflect.Student"); //注意此字符串必须是真实路径,就是带包名的类路径,包名.类名 - System.out.println(studentClass3 == studentClass2); // //判断三种方式是否获取的是同一个Class对象 - - // 三种方式常用第三种,第一种对象都有了还要反射干什么。 - // 第二种需要导入类的包,依赖太强,不导包就抛编译错误。 - // 一般都第三种,一个字符串可以传入也可写在配置文件中等多种方法。 - } -} diff --git a/Basics/src/com/reflect/Student.java b/Basics/src/com/reflect/Student.java deleted file mode 100644 index e9c374a1..00000000 --- a/Basics/src/com/reflect/Student.java +++ /dev/null @@ -1,97 +0,0 @@ -/** - * @program JavaBooks - * @description: Student - * @author: mf - * @create: 2020/02/09 16:55 - */ - -package com.reflect; - -public class Student { - - private String name; - - private Integer age; - - public String gender; - - // 无参数的构造方法 - public Student() { - System.out.println("调用了公有、无参构造方法执行了。。。"); - } - - // 默认的构造方法 - public Student(String str) { - System.out.println("(默认)的构造方法 s = " + str); - } - - // 有一个参数的构造方法 - public Student(char name) { - System.out.println("姓名:" + name); - } - - // 有多个参数的构造方法 - public Student(String name, Integer age) { - this.name = name; - this.age = age; - } - - //受保护的构造方法 - protected Student(boolean n){ - System.out.println("受保护的构造方法 n = " + n); - } - - //私有构造方法 - private Student(int age){ - System.out.println("私有的构造方法 年龄:"+ age); - } - - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Integer getAge() { - return age; - } - - public void setAge(Integer age) { - this.age = age; - } - - public String getGender() { - return gender; - } - - public void setGender(String gender) { - this.gender = gender; - } - - //**************成员方法***************// - public void show1(String s){ - System.out.println("调用了:公有的,String参数的show1(): s = " + s); - } - protected void show2(){ - System.out.println("调用了:受保护的,无参的show2()"); - } - void show3(){ - System.out.println("调用了:默认的,无参的show3()"); - } - private String show4(int age){ - System.out.println("调用了,私有的,并且有返回值的,int参数的show4(): age = " + age); - return "abcd"; - } - - @Override - public String toString() { - return "Student{" + - "name='" + name + '\'' + - ", age=" + age + - ", gender='" + gender + '\'' + - '}'; - } -} diff --git a/Basics/src/com/strings/SIntern.java b/Basics/src/com/strings/SIntern.java deleted file mode 100644 index 3bc3fb8a..00000000 --- a/Basics/src/com/strings/SIntern.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @program JavaBooks - * @description: String.Intern - * @author: mf - * @create: 2020/02/12 01:35 - */ - -package com.strings; - -/** - * 使用 String.intern() 可以保证相同内容的字符串变量引用同一的内存对象。 - */ -public class SIntern { - public static void main(String[] args) { - // 采用new String创建对象 - String s1 = new String("aaa"); - String s2 = new String("aaa"); - System.out.println(s1 == s2); // false - - // 将s1的对象通过intern放到String Pool中 - String s3 = s1.intern(); - System.out.println(s1.intern() == s3); // true - System.out.println(s1.intern() == s2.intern()); // true - System.out.println(s3 == "aaa"); // true - - - // 如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。 - String s4 = "bbb"; - String s5 = "bbb"; - System.out.println(s4 == s5); // true - } -} diff --git a/Basics/src/com/strings/SbDemo.java b/Basics/src/com/strings/SbDemo.java deleted file mode 100644 index 7b2b2db8..00000000 --- a/Basics/src/com/strings/SbDemo.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @program JavaBooks - * @description: String StringBuffer StringBuilder - * @author: mf - * @create: 2020/02/07 19:13 - */ - -package com.strings; - -public class SbDemo { - public static void main(String[] args) { - // String - String str = "hello"; - long start = System.currentTimeMillis(); - for (int i = 0; i < 100000; i++) { - str += i; // 创建多少个对象,, - } - System.out.println("String: " + (System.currentTimeMillis() - start)); - - // StringBuffer - StringBuffer sb = new StringBuffer("hello"); - long start1 = System.currentTimeMillis(); - for (int i = 0; i < 1000000; i++) { - sb.append(i); - } - System.out.println("StringBuffer: " + (System.currentTimeMillis() - start1)); - - - // StringBuilder - StringBuilder stringBuilder = new StringBuilder("hello"); - long start2 = System.currentTimeMillis(); - for (int i = 0; i < 1000000; i++) { - stringBuilder.append(i); - } - System.out.println("StringBuilder: " + (System.currentTimeMillis() - start2)); - } -} diff --git a/Basics/src/com/transfer/Student.java b/Basics/src/com/transfer/Student.java deleted file mode 100644 index 114f7af9..00000000 --- a/Basics/src/com/transfer/Student.java +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @program JavaBooks - * @description: Student - * @author: mf - * @create: 2020/02/07 22:01 - */ - -package com.transfer; - -public class Student { - - private String name; - - public Student(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Student{" + - "name='" + name + '\'' + - '}'; - } -} diff --git a/Basics/src/com/transfer/TransferDemo.java b/Basics/src/com/transfer/TransferDemo.java deleted file mode 100644 index 4a940e73..00000000 --- a/Basics/src/com/transfer/TransferDemo.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @program JavaBooks - * @description: TransferDemo - * @author: mf - * @create: 2020/02/07 21:22 - */ - -package com.transfer; - -/** - * Java 程序设计语言总是采用按值调用。 - * 也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。 - */ -public class TransferDemo { - public static void main(String[] args) { - int num1 = 10; - int num2 = 20; - - swap(num1, num2); - System.out.println("num1 = " + num1); - System.out.println("num2 = " + num2); - // 通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 - - - - - } - - - - private static void swap(int a, int b) { - int temp = a; - a = b; - b = temp; - System.out.println("a = " + a); - System.out.println("b = " + b); - } -} - - diff --git a/Basics/src/com/transfer/TransferDemo2.java b/Basics/src/com/transfer/TransferDemo2.java deleted file mode 100644 index a5733624..00000000 --- a/Basics/src/com/transfer/TransferDemo2.java +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @program JavaBooks - * @description: TransferDemo2 - * @author: mf - * @create: 2020/02/07 22:04 - */ - -package com.transfer; - -public class TransferDemo2 { - - public static void main(String[] args) { - int[] arr = { 1, 2, 3, 4, 5 }; - System.out.println(arr[0]); - change(arr); - System.out.println(arr[0]); - // 法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。 - } - - private static void change(int[] array) { - // 修改数组中的一个元素 - array[0] = 0; - } -} diff --git a/Basics/src/com/transfer/TransferDemo3.java b/Basics/src/com/transfer/TransferDemo3.java deleted file mode 100644 index 4417f4ed..00000000 --- a/Basics/src/com/transfer/TransferDemo3.java +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @program JavaBooks - * @description: TransferDemo3 - * @author: mf - * @create: 2020/02/07 22:05 - */ - -package com.transfer; - -public class TransferDemo3 { - public static void main(String[] args) { - // 有些程序员(甚至本书的作者)认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。 - Student s1 = new Student("Mai"); - Student s2 = new Student("Feng"); - swap2(s1, s2); - System.out.println("s1:" + s1.getName()); - System.out.println("s2:" + s2.getName()); - // 方法并没有改变存储在变量 s1 和 s2 中的对象引用。 - // swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝 - } - - private static void swap2(Student x, Student y) { - Student temp = x; - x = y; - y = temp; - System.out.println("x:" + x.getName()); - System.out.println("y:" + y.getName()); - } -} diff --git a/Basics/src/com/type/IntegerTest.java b/Basics/src/com/type/IntegerTest.java deleted file mode 100644 index 90c4423b..00000000 --- a/Basics/src/com/type/IntegerTest.java +++ /dev/null @@ -1,14 +0,0 @@ -/** - * @program JavaBooks - * @description: 模拟变量在内存的布局 - * @author: mf - * @create: 2020/03/20 13:30 - */ - -package com.type; - -public class IntegerTest { - int value = 1; - String s = "买"; - int a = 257; -} diff --git a/Basics/src/com/type/TypeConvert.java b/Basics/src/com/type/TypeConvert.java deleted file mode 100644 index 2407ca7a..00000000 --- a/Basics/src/com/type/TypeConvert.java +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @program JavaBooks - * @description: TypeConvert - * @author: mf - * @create: 2020/02/12 01:46 - */ - -package com.type; - -/** - * 隐士转换 - */ -public class TypeConvert { - public static void main(String[] args) { - // 字面量属于 double 类型 - // 不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型 - // Java 不能隐式执行向下转型,因为这会使得精度降低。 - // float f = 1.1; - float f = 1.1f; - - // 因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。 - short s1 = 1; - // s1 = s1 + 1; - - // 但是使用 += 运算符可以执行隐式类型转换。 - s1 += 1; - // 上面的语句相当于将 s1 + 1 的计算结果进行了向下转型: - s1 = (short) (s1 + 1); - - } -} diff --git "a/Collections/HashMap-ConcurrentHashMap\351\235\242\350\257\225\345\277\205\351\227\256.md" "b/Collections/HashMap-ConcurrentHashMap\351\235\242\350\257\225\345\277\205\351\227\256.md" deleted file mode 100644 index 74a4c212..00000000 --- "a/Collections/HashMap-ConcurrentHashMap\351\235\242\350\257\225\345\277\205\351\227\256.md" +++ /dev/null @@ -1,477 +0,0 @@ -## 引言 - -**HashMap 和 ConcurrentHashMap面试常问,务必理解和掌握** - -## HashMap - -众所周知,HashMap的底层结构是**数组和链表**组成的,不过在jdk1.7和jdk1.8中具体实现略有不同。 - - - -### jdk1.7 - -先看图![](https://i.loli.net/2019/05/08/5cd1d2be77958.jpg) - -再看看1.7的实现![](https://i.loli.net/2019/05/08/5cd1d2bfd6aba.jpg) - -介绍成员变量: - -- 初始化桶大小,因为底层是数组,所以这是数组默认的大小。 -- 桶最大值。 -- 默认的负载因子(0.75) -- table真正存放数据的数组。 -- map存放数量的大小 -- 桶大小,可在构造函数时显式指定。 -- 负载因子,可在构造函数时显式指定。 - -#### 负载因子 - -源代码 - -```java -public HashMap() { - this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); // 桶和负载因子 -} -public HashMap(int initialCapacity, float loadFactor) { - if (initialCapacity < 0) - throw new IllegalArgumentException("Illegal initial capacity: " + - initialCapacity); - if (initialCapacity > MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - if (loadFactor <= 0 || Float.isNaN(loadFactor)) - throw new IllegalArgumentException("Illegal load factor: " + - loadFactor); - this.loadFactor = loadFactor; - threshold = initialCapacity; - init(); -} -``` - -- 给定的默认容量为16,负载因子为0.75. -- Map在使用过程中不断的往里面存放数据,当数量达到了`16 * 0.75 = 12`就需要将当前16的容量进行扩容,而扩容这个过程涉及到rehash(重新哈希)、复制数据等操作,所有非常消耗性能。 -- 因此通常建议能提前预估HashMap的大小最好,尽量的减少扩容带来的额外性能损耗。 - -#### Entry - -`transient Entry[] table = (Entry[]) EMPTY_TABLE;` - -如何定义呢?![](https://i.loli.net/2019/05/08/5cd1d2c08e693.jpg) - -Entry是Hashmap中的一个内部类,从他的成员变量很容易看出: - -- key就是写入时的键 -- value自然就是值 -- 开始的时候就提到HashMap是由数组和链表组成,所以这个next就是用于实现链表结构 -- hash存放的是当前key的hashcode - -#### put - -```java -public V put(K key, V value) { - if (table == EMPTY_TABLE) { - inflateTable(threshold); // 判断数组是否需要初始化 - } - if (key == null) - return putForNullKey(value); // 判断key是否为空 - int hash = hash(key); // 计算hashcode - int i = indexFor(hash, table.length); // 计算桶 - for (Entry e = table[i]; e != null; e = e.next) { - Object k; - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 遍历判断链表中的key和hashcode是否相等,等就替换 - V oldValue = e.value; - e.value = value; - e.recordAccess(this); - return oldValue; - } - } - modCount++; - addEntry(hash, key, value, i); // 没有就添加新的呗 - return null; -} -``` - -- 判断当前数组是否需要初始化 -- 如果key为空,则put一个空值进去 -- 根据key计算hashcode -- 根据计算的hashcode定位index的桶 -- 如果桶是一个链表,则需要遍历判断里面的hashcode、key是否和传入的key相等,如果相等则进行覆盖,并返回原来的值 -- 如果桶是空的,说明当前位置没有数据存入,此时新增一个Entry对象写入当前位置。 - -```java -void addEntry(int hash, K key, V value, int bucketIndex) { - if ((size >= threshold) && (null != table[bucketIndex])) {// 是否扩容 - resize(2 * table.length); - hash = (null != key) ? hash(key) : 0; - bucketIndex = indexFor(hash, table.length); - } - createEntry(hash, key, value, bucketIndex); -} -void createEntry(int hash, K key, V value, int bucketIndex) { - Entry e = table[bucketIndex]; - table[bucketIndex] = new Entry<>(hash, key, value, e); - size++; -} -``` - -- 当调用addEntry写入Entry时需要判断是否需要扩容 -- 如果需要就进行两倍扩充,并将当前的key重新hash并定位。 -- 而在createEntry中会将当前位置的桶传入到新建的桶中,如果当前桶油值就会在位置形成链表。 - -#### get - -```java -public V get(Object key) { - if (key == null) // 判断key是否为空 - return getForNullKey(); // 为空,就返回空值 - Entry entry = getEntry(key); // get entry - return null == entry ? null : entry.getValue(); -} -final Entry getEntry(Object key) { - if (size == 0) { - return null; - } - int hash = (key == null) ? 0 : hash(key); //根据key和hashcode - for (Entry e = table[indexFor(hash, table.length)]; - e != null; - e = e.next) { - Object k; - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } - return null; -} -``` - -- 首先根据key计算hashcode,然后定位具体的桶 -- 判断该位置是否为链表 -- 不是链接就根据key和hashcode是否相等来返回值 -- 为链表则需要遍历直到key和hashcode相等就返回值 -- 啥都没得,就返回null - -### jdk1.8 - -不知道 1.7 的实现大家看出需要优化的点没有? - -其实一个很明显的地方就是链表 - -**当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 `O(N)`。** - -![](https://i.loli.net/2019/05/08/5cd1d2c1c1cd7.jpg) - -看看成员变量 - -```java -static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 -/** - * The maximum capacity, used if a higher value is implicitly specified - * by either of the constructors with arguments. - * MUST be a power of two <= 1<<30. - */ -static final int MAXIMUM_CAPACITY = 1 << 30; -/** - * The load factor used when none specified in constructor. - */ -static final float DEFAULT_LOAD_FACTOR = 0.75f; -static final int TREEIFY_THRESHOLD = 8; -transient Node[] table; -/** - * Holds cached entrySet(). Note that AbstractMap fields are used - * for keySet() and values(). - */ -transient Set> entrySet; -/** - * The number of key-value mappings contained in this map. - */ -transient int size; -``` - -- `TREEIFY_THRESHOLD` 用于判断是否需要将链表转换为红黑树的阈值。 -- HashEntry 修改为 Node。 -- Node 的核心组成其实也是和 1.7 中的 HashEntry 一样,存放的都是 `key value hashcode next` 等数据。 - -#### put - -![](https://i.loli.net/2019/05/08/5cd1d2c378090.jpg) - -- 判断当前桶是否为空,空的就需要初始化(resize中会判断是否进行初始化) -- 根据当前key的hashcode定位到具体的桶中并判断是否为空,为空则表明没有Hash冲突,就直接在当前位置创建一个新桶 -- 如果当前桶油值(Hash冲突),那么就要比较当前桶中的key、key的hashcode与写入的key是否相等,相等就赋值给e,在第8步的时候会统一进行赋值及返回 -- 如果当前桶为红黑树,那就要按照红黑树的方式写入数据 -- 如果是个链表,就需要将当前的key、value封装称一个新节点写入到当前桶的后面形成链表。 -- 接着判断当前链表的大小是否大于预设的阈值,大于就要转换成为红黑树 -- 如果在遍历过程中找到key相同时直接退出遍历。 -- 如果`e != null`就相当于存在相同的key,那就需要将值覆盖。 -- 最后判断是否需要进行扩容。 - -#### get - -```java -public V get(Object key) { - Node e; - return (e = getNode(hash(key), key)) == null ? null : e.value; -} -final Node getNode(int hash, Object key) { - Node[] tab; Node first, e; int n; K k; - if ((tab = table) != null && (n = tab.length) > 0 && - (first = tab[(n - 1) & hash]) != null) { - if (first.hash == hash && // always check first node - ((k = first.key) == key || (key != null && key.equals(k)))) - return first; - if ((e = first.next) != null) { - if (first instanceof TreeNode) - return ((TreeNode)first).getTreeNode(hash, key); - do { - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } while ((e = e.next) != null); - } - } - return null; -} -``` - -- 首先将key hash之后取得所定位的桶 -- 如果桶为空,则直接返回null -- 否则判断桶的第一个位置(有可能是链表、红黑树)的key是否为查询的key,是就直接返回value -- 如果第一个不匹配,则判断它的下一个是红黑树还是链表 -- 红黑树就按照树的查找方式返回值 -- 不然就按照链表的方式遍历匹配返回值 - -**从这两个核心方法(get/put)可以看出 1.8 中对大链表做了优化,修改为红黑树之后查询效率直接提高到了 `O(logn)`。** - -## 问题 - -但是 HashMap 原有的问题也都存在,比如在并发场景下使用时容易出现死循环。 - -```java -final HashMap map = new HashMap(); -for (int i = 0; i < 1000; i++) { - new Thread(new Runnable() { - @Override - public void run() { - map.put(UUID.randomUUID().toString(), ""); - } - }).start(); -} -``` - -- HashMap扩容的时候会调用resize()方法,就是这里的并发操作容易在一个桶上形成环形链表 -- 这样当获取一个不存在的key时,计算出的index正好是环形链表的下标就会出现死循环。 -- 但是1.7的头插法造成的问题,1.8改变了插入顺序,就解决了这个问题,但是为了内存可见性等安全性,还是需要ConCurrentHashMap - -![](https://i.loli.net/2019/05/08/5cd1d2c4ede54.jpg) - -## 遍历方式 - -还有一个值得注意的是 HashMap 的遍历方式,通常有以下几种: - -```java -Iterator> entryIterator = map.entrySet().iterator(); - while (entryIterator.hasNext()) { - Map.Entry next = entryIterator.next(); - System.out.println("key=" + next.getKey() + " value=" + next.getValue()); - } - -Iterator iterator = map.keySet().iterator(); - while (iterator.hasNext()){ - String key = iterator.next(); - System.out.println("key=" + key + " value=" + map.get(key)); - } -``` - -- 建议使用第一种,同时可以把key value取出。 -- 第二种还需要通过key取一次key,效率较低。 - -## ConCurrentHashMap - -### jdk1.7 - -![](https://i.loli.net/2019/05/08/5cd1d2c5ce95c.jpg) - -- Segment数组 -- HashEntry组成 -- 和HashMap一样,仍然是数组加链表 - -```java -/** - * Segment 数组,存放数据时首先需要定位到具体的 Segment 中。 - */ -final Segment[] segments; -transient Set keySet; -transient Set> entrySet; -``` - -Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下: - -```java -static final class Segment extends ReentrantLock implements Serializable { - private static final long serialVersionUID = 2249069246763182397L; - - // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶 - transient volatile HashEntry[] table; - transient int count; - transient int modCount; - transient int threshold; - final float loadFactor; - -} -``` - -![](https://i.loli.net/2019/05/08/5cd1d2c635c69.jpg) - -- 唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。 -- ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。 -- 不会像HashTable那样不管是put还是get操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。 -- 每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。 - -#### put - -```java -public V put(K key, V value) { - Segment s; - if (value == null) - throw new NullPointerException(); - int hash = hash(key); - int j = (hash >>> segmentShift) & segmentMask; - if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck - (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment - s = ensureSegment(j); - return s.put(key, hash, value, false); -} -``` - -- 通过key定位到Segment,之后在对应的Segment中进行具体的put - -```java -final V put(K key, int hash, V value, boolean onlyIfAbsent) { - HashEntry node = tryLock() ? null : - scanAndLockForPut(key, hash, value); - V oldValue; - try { - HashEntry[] tab = table; - int index = (tab.length - 1) & hash; - HashEntry first = entryAt(tab, index); - for (HashEntry e = first;;) { - if (e != null) { - K k; - if ((k = e.key) == key || - (e.hash == hash && key.equals(k))) { - oldValue = e.value; - if (!onlyIfAbsent) { - e.value = value; - ++modCount; - } - break; - } - e = e.next; - } - else { - if (node != null) - node.setNext(first); - else - node = new HashEntry(hash, key, value, first); - int c = count + 1; - if (c > threshold && tab.length < MAXIMUM_CAPACITY) - rehash(node); - else - setEntryAt(tab, index, node); - ++modCount; - count = c; - oldValue = null; - break; - } - } - } finally { - unlock(); - } - return oldValue; -} -``` - -- 虽然HashEntry中的value是用volatile关键字修饰的,但是并不能保证并发的原子性,所以put操作仍然需要加锁处理。 -- 首先第一步的时候会尝试获取锁,如果获取失败肯定就是其他线程存在竞争,则利用 `scanAndLockForPut()` 自旋获取锁。 - -![](https://i.loli.net/2019/05/08/5cd1d2cc3c982.jpg) - -- 尝试获取自旋锁 -- 如果重试的次数达到了`MAX_SCAN_RETRIES` 则改为阻塞锁获取,保证能获取成功。 - -![](https://i.loli.net/2019/05/08/5cd1d2cd25c37.jpg) - -- 将当前的Segment中的table通过key的hashcode定位到HashEntry -- 遍历该HashEntry,如果不为空则判断传入的key和当前遍历的key是否相等,相等则覆盖旧的value -- 不为空则需要新建一个HashEntry并加入到Segment中,同时会先判断是否需要扩容 -- 最后会解除在1中所获取当前Segment的所。 - -#### get方法 - -```java -public V get(Object key) { - Segment s; // manually integrate access methods to reduce overhead - HashEntry[] tab; - int h = hash(key); - long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; - if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && - (tab = s.table) != null) { - for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile - (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); - e != null; e = e.next) { - K k; - if ((k = e.key) == key || (e.hash == h && key.equals(k))) - return e.value; - } - } - return null; -} -``` - -- 只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。 -- 由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。 -- ConcurrentHashMap 的 get 方法是非常高效的,**因为整个过程都不需要加锁**。 - -### jdk1.8 - -**那就是查询遍历链表效率太低。** - -![](https://i.loli.net/2019/05/08/5cd1d2ce33795.jpg) - -**其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性** - -![](https://i.loli.net/2019/05/08/5cd1d2ceebe02.jpg) - -- 也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。 -- 其中的 `val next` 都用了 volatile 修饰,保证了可见性。 - -#### put - -![](https://i.loli.net/2019/05/08/5cd1d2cfc3293.jpg) - -- 根据key计算出hashcode -- 判断是否需要进行初始化 -- f即为当前key定位出的Node,如果为空表示当前位置可以写入数据,利用CAS尝试写入,失败则自旋保证成功。 -- 如果当前位置的`hashcode == MOVED == -1`,则需要进行扩容 -- 如果都不满足,则利用synchronized锁写入数据 -- 如果数量大于`TREEIFY_THRESHOLD` 则要转换为红黑树。 - -#### get - -![](https://i.loli.net/2019/05/08/5cd1d2d22c6cb.jpg) - -- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 -- 如果是红黑树那就按照树的方式获取值。 -- 就不满足那就按照链表的方式遍历获取值。 - -1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(`O(logn)`),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。 - -## 总结 - -套路: - -- 谈谈你理解的 HashMap,讲讲其中的 get put 过程。 -- 1.8 做了什么优化? -- 是线程安全的嘛? -- 不安全会导致哪些问题? -- 如何解决?有没有线程安全的并发容器? -- ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做? \ No newline at end of file diff --git "a/Collections/Java\351\235\242\347\273\217-Java\351\233\206\345\220\210\346\241\206\346\236\266.md" "b/Collections/Java\351\235\242\347\273\217-Java\351\233\206\345\220\210\346\241\206\346\236\266.md" deleted file mode 100644 index c6c1337c..00000000 --- "a/Collections/Java\351\235\242\347\273\217-Java\351\233\206\345\220\210\346\241\206\346\236\266.md" +++ /dev/null @@ -1,82 +0,0 @@ -## 引言 - -**Java的集合框架,包括常见的面试源码解析等...** - - - -## Collection类关系图 - -![类关系图](https://www.pdai.tech/_images/java_collections_overview.png) - -### 介绍 - -容器,就是可以容纳其他Java对象的对象。其始于JDK 1.2。 优点: - -- 降低编程难度 -- 提高程序性能 -- 提高API间的互操作性 -- 降低学习难度 -- 降低设计和实现相关API的难度 -- 增加程序的重用性 - -### Collection - -容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 - -#### Set - -##### TreeSet - -基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如HashSet,HashSet查找的时间复杂为O(1),TreeSet则为O(logN)。 - -##### HashSet - -基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用Iterator遍历HashSet得到结果是不确定的。 - -##### LinkedHashSet - -具有HashSet的查找效率,且内部使用双向链表维护元素的插入顺序。 - -#### List - -##### ArrayList - -基于动态数组实现,支持随机访问。 - -##### Vector - -和ArrayList类似,但它是线程安全的。 - -##### LinkedList - -基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList还可以用作栈、队列和双向队列。 - -#### Queue - -##### LinkedList - -可以用来实现双向队列。 - -##### PriorityQueue - -基于堆结构实现,可以用它实现优先队列。 - -#### Map - -##### TreeMap - -基于红黑树实现 - -##### HashMap - -基于哈希表实现 - -##### HashTable - -- 和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。 -- 它是遗留类,不应该去使用它。 -- 现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。 - -##### LinkedHashMap - -使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 \ No newline at end of file diff --git "a/Interview/codes/basics/IO\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Interview/codes/basics/IO\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index b1a1d0d6..00000000 --- "a/Interview/codes/basics/IO\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,30 +0,0 @@ -## IO的一些问题 - -### 键盘输入 - -> 在牛客网撕代码的时候,经常获取键盘输入 - -如: -```java -Scanner sc = new Scanner(System.in); -int a = sc.nextInt() -String s = sc.nextLine(); -``` - -### IO流 -- **按照流的流向**:可以分为**输入流**和**输出流**; -- **按照操作单元**:可以划分为**字节流**和**字符流**; -- **按照流的角色**:可以分为**节点流**和**处理流**。 - -Java IO流共涉及40多个类,这些类看上去很杂乱,哥们我也记不住,就是咱一直不用,就是记不住,那咋办捏。害,烧脑... - -- `InputSream/Reader`:所有的输入流的基类,前者是字节输入流,后者是字符输入流。 -- `OutputSream/Writer`:所有的输出流的基类,前者是字节输出流,后者是字符输出流。 - -### 一直问BIO、NIO和AIO... -> 问我都有点烦了... 但是又没办法呀... -- **BIO (Blocking I/O)**:**同步阻塞I/O模式**,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 -- **NIO (New I/O)**:NIO是一种**同步非阻塞的I/O模型**,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 `Channel` , `Selector`,`Buffer`等抽象。NIO中的N可以理解为`Non-blocking`,不单纯是New。它支持**面向缓冲**的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。 -> 有兴趣的,可以学学Netty,这哥们的源码倒是挺难的... - -- **AIO (Asynchronous I/O)**: AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是**异步非阻塞的IO模型**。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 diff --git "a/Interview/codes/basics/String\347\232\204\351\227\256\351\242\230.md" "b/Interview/codes/basics/String\347\232\204\351\227\256\351\242\230.md" deleted file mode 100644 index 03b6ec62..00000000 --- "a/Interview/codes/basics/String\347\232\204\351\227\256\351\242\230.md" +++ /dev/null @@ -1,134 +0,0 @@ -## String的一些问题 - -### String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? - -先简单聊一下它们仨 - -#### 可变性 -> 简单的来说:`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 `String` 对象是不可变的。 -> `StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串char[]value 但是没有用 `final` 关键字修饰,所以这两种对象都是可变的。 - -#### 线程安全 -> `String` 中的对象是不可变的,也就可以理解为常量,线程安全。 -> `AbstractStringBuilder` 是 `StringBuilder` 与 `StringBuffer` 的公共父类,定义了一些字符串的基本操作,如 `expandCapacity`、`append`、`insert`、`indexOf` 等公共方法。`StringBuffer` 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。`StringBuilder` 并没有对方法进行加同步锁,所以是非线程安全的。 - - -#### 性能 -> 每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。 -> `StringBuffer` 每次都会对 `StringBuffer` 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 - -#### 总结一波 -- 操作少量的数据: 适用String -- 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder -- 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer - -#### 举个例子 -```java -public static void main(String[] args) { - // String - String str = "hello"; - long start = System.currentTimeMillis(); - for (int i = 0; i < 100000; i++) { - str += i; // 创建多少个对象,, - } - System.out.println("String: " + (System.currentTimeMillis() - start)); - - // StringBuffer - StringBuffer sb = new StringBuffer("hello"); - long start1 = System.currentTimeMillis(); - for (int i = 0; i < 1000000; i++) { - sb.append(i); - } - System.out.println("StringBuffer: " + (System.currentTimeMillis() - start1)); - - - // StringBuilder - StringBuilder stringBuilder = new StringBuilder("hello"); - long start2 = System.currentTimeMillis(); - for (int i = 0; i < 1000000; i++) { - stringBuilder.append(i); - } - System.out.println("StringBuilder: " + (System.currentTimeMillis() - start2)); -} -``` - -### String A = "123"; String B = new String("123");生成几个对象? -> 如果常量池中,原来没有“123”那么就是生成了2个对象,如果常量池中有“123”那么只要1个对象生成 - -这个问题,简单。 但是接下来这个可以看看`String.intern()`方法 - -举个例子: -```java -public class StringTest { - public static void main(String[] args) { - String str1 = "todo"; // 常量池 - String str2 = "todo"; // 从常量池找了str1 - String str3 = "to"; // 常量池 - String str4 = "do"; // 常量池 - String str5 = str3 + str4; // 内部用StringBuilder拼接了一波。 因此, 并非常量池 - String str6 = new String(str1); // 创建对象了, 那还能是常量池的引用? - - System.out.println("------普通String测试结果------"); - System.out.print("str1 == str2 ? "); - System.out.println( str1 == str2); // true - System.out.print("str1 == str5 ? "); - System.out.println(str1 == str5); // false - System.out.print("str1 == str6 ? "); - System.out.print(str1 == str6); // false - System.out.println(); - - System.out.println("---------intern测试结果---------"); // intern 直奔常量池上找 - System.out.print("str1.intern() == str2.intern() ? "); - System.out.println(str1.intern() == str2.intern()); // true - System.out.print("str1.intern() == str5.intern() ? "); - System.out.println(str1.intern() == str5.intern()); // true - System.out.print("str1.intern() == str6.intern() ? "); - System.out.println(str1.intern() == str6.intern()); // true - System.out.print("str1 == str6.intern() ? "); - System.out.println(str1 == str6.intern()); // true - } - -``` -结果一目了然, 但需要知道原理: -``` -------普通String测试结果------ -str1 == str2 ? true -str1 == str5 ? false -str1 == str6 ? false ----------intern测试结果--------- -str1.intern() == str2.intern() ? true -str1.intern() == str5.intern() ? true -str1.intern() == str6.intern() ? true -str1 == str6.intern() ? true -``` - -待我分析一波: -> jvm你可要知道哇。 -> Java语言会使用常量池保存那些在编译期就已确定的已编译的class文件中的一份数据。主要有类、接口、方法中的常量,以及一些以文本形式出现的符号引用,如类和接口的全限定名、字段的名称和描述符、方法和名称和描述符等。 - -- 因此在编译完Intern类后,生成的class文件中会在常量池中保存“todo”、“to”和“do”三个String常量。 -- 变量str1和str2均保存的是常量池中“todo”的引用,所以str1==str2成立; -- 在执行 str5 = str3 + str4这句时,JVM会先创建一个StringBuilder对象,通过StringBuilder.append()方法将str3与str4的值拼接,然后通过StringBuilder.toString()返回一个堆中的String对象的引用,赋值给str5,因此str1和str5指向的不是同一个String对象,str1 == str5不成立; -- String str6 = new String(str1)一句显式创建了一个新的String对象,因此str1 == str6不成立便是显而易见的事了。 - -重要的一点就是intern了: - -> String.intern()是一个Native方法,底层调用C++的 StringTable::intern方法实现。当通过语句str.intern()调用intern()方法后,JVM 就会在当前类的常量池中查找是否存在与str等值的String, - -- 若存在则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用。 -- 因此,只要是等值的String对象,使用intern()方法返回的都是常量池中同一个String引用,所以,这些等值的String对象通过intern()后使用==是可以匹配的。 -- 由此就可以理解上面代码中------intern------部分的结果了。因为str1、str5和str6是三个等值的String,所以通过intern()方法,**他们均会指向常量池中的同一个String引用**,因此str1.intern() == str5.intern() == str6.intern()均为true。 - -额外补充一哈: - -#### JDK6 -> Jdk6中常量池位于PermGen(永久代)中,PermGen是一块主要用于存放已加载的类信息和字符串池的大小固定的区域。执行intern()方法时,**若常量池中不存在等值的字符串,JVM就会在常量池中创建一个等值的字符串,然后返回该字符串的引用**。除此以外,JVM 会自动在常量池中保存一份之前已使用过的字符串集合。Jdk6中使用intern()方法的主要问题就在于常量池被保存在PermGen中: -> 首先,PermGen是一块大小固定的区域,一般不同的平台PermGen的默认大小也不相同,大致在32M到96M之间。所以不能对不受控制的运行时字符串(如用户输入信息等)使用intern()方法,否则很有可能会引发PermGen内存溢出;其次String对象保存在Java堆区,Java堆区与PermGen是物理隔离的,因此如果对多个不等值的字符串对象执行intern操作,则会导致内存中存在许多重复的字符串,会造成性能损失。 - -#### JDK7 -> Jdk7将常量池从PermGen区移到了Java堆区,执行intern操作时,如果常量池已经存在该字符串,则直接返回字符串引用,否则**复制该字符串对象的引用到常量池中并返回**。堆区的大小一般不受限,所以将常量池从PremGen区移到堆区使得常量池的使用不再受限于固定大小。 -> 除此之外,位于堆区的常量池中的对象可以被垃圾回收。当常量池中的字符串不再存在指向它的引用时,JVM就会回收该字符串。可以使用 -XX:StringTableSize 虚拟机参数设置字符串池的map大小。字符串池内部实现为一个HashMap,所以当能够确定程序中需要intern的字符串数目时,可以将该map的size设置为所需数目*2(减少hash冲突),这样就可以使得String.intern()每次都只需要常量时间和相当小的内存就能够将一个String存入字符串池中。 - - -#### 适应场景 -Jdk6中常量池位于PermGen区,大小受限,所以不建议适用intern()方法,当需要字符串池时,需要自己使用HashMap实现。Jdk7、8中,常量池由PermGen区移到了堆区,还可以通过-XX:StringTableSize参数设置StringTable的大小,常量池的使用不再受限,由此可以重新考虑使用intern()方法。intern()方法优点:执行速度非常快,直接使用==进行比较要比使用equals()方法快很多;内存占用少。虽然intern()方法的优点看上去很诱人,但若不是在恰当的场合中使用该方法的话,便非但不能获得如此好处,反而还可能会有性能损失。 diff --git "a/Interview/codes/basics/equals\345\222\214hashcode.md" "b/Interview/codes/basics/equals\345\222\214hashcode.md" deleted file mode 100644 index aee59082..00000000 --- "a/Interview/codes/basics/equals\345\222\214hashcode.md" +++ /dev/null @@ -1,115 +0,0 @@ -## equals和hashcode - -### == -它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象: -- 基本数据类型==比较的是值 -- 引用数据类型==比较的是内存地址 - -### equals -它的作用也是判断两个对象是否相等。但它一般有两种使用情况: -- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过`==`比较这两个对象。 -- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 - -```java -public class test1 { - public static void main(String[] args) { - String a = new String("ab"); // a 为一个引用 - String b = new String("ab"); // b为另一个引用,对象的内容一样 - String aa = "ab"; // 放在常量池中 - String bb = "ab"; // 从常量池中查找 - if (aa == bb) // true - System.out.println("aa==bb"); - if (a == b) // false,非同一对象 - System.out.println("a==b"); - if (a.equals(b)) // true - System.out.println("aEQb"); - if (42 == 42.0) { // true - System.out.println("true"); - } - } -} -``` - -注意:这里String类重写了equals方法。 -- String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。 -- 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。 - -```java -public boolean equals(Object anObject) { - if (this == anObject) { // 比较两个对象的地址 - return true; - } - if (anObject instanceof String) { // 地址不相同,开始比较内容 - String anotherString = (String)anObject; - int n = value.length; - if (n == anotherString.value.length) { - char v1[] = value; - char v2[] = anotherString.value; - int i = 0; - while (n-- != 0) { - if (v1[i] != v2[i]) - return false; - i++; - } - return true; - } - } - return false; - } -``` - -### hashcode -> hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。 -> 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象) - -#### 比如 -> 我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置 -> 这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -```java -public class Demo { - public static void main(String[] args) { - HashSet set = new HashSet<>(); - set.add("1"); - set.add("2"); - set.add("1"); - System.out.println(set.toString()); - } -} -``` -可以看一下HashSet源码中的add,实际上是HashMap的put方法。 - -```java -public boolean add(E e) { - return map.put(e, PRESENT)==null; -} -``` - -接着看HashMap的put方法 -```java -public V put(K key, V value) { - return putVal(hash(key), key, value, false, true); -} -``` -其中,hash的这个方法的源码,你可看 -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` -也就是你传入这个键的对象的hashcode,本例是String,可以看:也是重写了hashcode的。所以,像使用集合,判断对象是否相等,还是务必重写hashcode和equals。put具体细节源码就不贴了 -```java -public int hashCode() { - int h = hash; - if (h == 0 && value.length > 0) { - char val[] = value; - - for (int i = 0; i < value.length; i++) { - h = 31 * h + val[i]; - } - hash = h; - } - return h; -} -``` \ No newline at end of file diff --git "a/Interview/codes/basics/\345\217\215\345\260\204\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Interview/codes/basics/\345\217\215\345\260\204\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index ed29af72..00000000 --- "a/Interview/codes/basics/\345\217\215\345\260\204\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,208 +0,0 @@ -# 反射 -> Java反射机制是在**运行状态**中,对于任意一个类,都能够知道**这个类的所有属性和方法**;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动**态获取的信息以及动态调用对象的方法的功能**成为java语言的反射机制。 - -## 静态编译和动态编译 -- 静态编译:在编译时确定类型,绑定对象 -- 动态编译:运行时确定类型,绑定对象 - -## 反射机制优缺点 -- 优点:运行期间类型的判断,动态加载类,提高代码的灵活度 -- 缺点:性能瓶颈:反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的java代码要慢很多 - -## 反射的应用场景 -> 在我们平时的项目开发过程中,基本上很少会直接使用的反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如**模块化的开发**,通过反射去调用对应的字节码;**动态代理设计模型也采用了反射机制**,还有我们日常使用的`Spring` / `Hibernate`等框架也大量使用到了反射机制。 - -## 反射得到的Class对象的三种方式 -首先是Student类 -```java -public class Student { - - private String name; - - private Integer age; - - public String gender; - - // 无参数的构造方法 - public Student() { - System.out.println("调用了公有、无参构造方法执行了。。。"); - } - - // 默认的构造方法 - public Student(String str) { - System.out.println("(默认)的构造方法 s = " + str); - } - - // 有一个参数的构造方法 - public Student(char name) { - System.out.println("姓名:" + name); - } - - // 有多个参数的构造方法 - public Student(String name, Integer age) { - this.name = name; - this.age = age; - } - - //受保护的构造方法 - protected Student(boolean n){ - System.out.println("受保护的构造方法 n = " + n); - } - - //私有构造方法 - private Student(int age){ - System.out.println("私有的构造方法 年龄:"+ age); - } - - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Integer getAge() { - return age; - } - - public void setAge(Integer age) { - this.age = age; - } - - public String getGender() { - return gender; - } - - public void setGender(String gender) { - this.gender = gender; - } - - //**************成员方法***************// - public void show1(String s){ - System.out.println("调用了:公有的,String参数的show1(): s = " + s); - } - protected void show2(){ - System.out.println("调用了:受保护的,无参的show2()"); - } - void show3(){ - System.out.println("调用了:默认的,无参的show3()"); - } - private String show4(int age){ - System.out.println("调用了,私有的,并且有返回值的,int参数的show4(): age = " + age); - return "abcd"; - } - - @Override - public String toString() { - return "Student{" + - "name='" + name + '\'' + - ", age=" + age + - ", gender='" + gender + '\'' + - '}'; - } -} -``` -测试: -```java -public class ReflectDemo { - public static void main(String[] args) throws ClassNotFoundException { - // 第一种方式获取Class对象 - Student student = new Student(); // 这一new 产生一个Student对象,一个Class对象。 - Class studentClass = student.getClass(); - System.out.println(studentClass.getName()); // com.reflect.Student - - // 第二种方式获取Class对象 - Class studentClass2 = Student.class; - System.out.println(studentClass == studentClass2); //判断第一种方式获取的Class对象和第二种方式获取的是否是同一个 - - //第三种方式获取Class对象 - Class studentClass3 = Class.forName("com.reflect.Student"); //注意此字符串必须是真实路径,就是带包名的类路径,包名.类名 - System.out.println(studentClass3 == studentClass2); // //判断三种方式是否获取的是同一个Class对象 - - // 三种方式常用第三种,第一种对象都有了还要反射干什么。 - // 第二种需要导入类的包,依赖太强,不导包就抛编译错误。 - // 一般都第三种,一个字符串可以传入也可写在配置文件中等多种方法。 - } -} -``` -## 反射访问并调用构造方法 -```java -public class ConstructorsDemo { - public static void main(String[] args) throws Exception { - // 1. 加载Class对象 - Class clazz = Class.forName("com.reflect.Student"); - - // 2. 获取所有公有构造方法 - System.out.println("**********************所有公有构造方法*********************************"); - Constructor[] constructors = clazz.getConstructors(); - for (Constructor constructor : constructors) { - System.out.println(constructor); - } - - // 3. - System.out.println("************所有的构造方法(包括:私有、受保护、默认、公有)***************"); - Constructor[] declaredConstructors = clazz.getDeclaredConstructors(); - for (Constructor declaredConstructor : declaredConstructors) { - System.out.println(declaredConstructor); - } - - // 4. - System.out.println("*****************获取公有、无参的构造方法*******************************"); - Constructor constructor = clazz.getConstructor(); - System.out.println(constructor); - - // 调用构造方法 - Object object = constructor.newInstance(); - System.out.println(object); - - // - System.out.println("******************获取私有构造方法,并调用*******************************"); - Constructor constructor1 = clazz.getDeclaredConstructor(char.class); - System.out.println(constructor1); - // 调用构造方法 - constructor1.setAccessible(true); // 暴力访问 - Object object2 = constructor1.newInstance('买'); - System.out.println(object2); - } -} -``` - -## 反射访问并调用成员变量 -```java -public class FieldDemo { - public static void main(String[] args) throws Exception { - // 1. 获取class对象 - Class clazz = Class.forName("com.reflect.Student"); - // 2. 获取所有字段 - System.out.println("************获取所有公有的字段********************"); - Field[] fields = clazz.getFields(); - for (Field field : fields) { - System.out.println(field); - } - // - System.out.println("************获取所有的字段(包括私有、受保护、默认的)********************"); - Field[] fields1 = clazz.getDeclaredFields(); - for (Field field : fields1) { - System.out.println(field); - } - // - System.out.println("*************获取公有字段**并调用***********************************"); - Field gender = clazz.getField("gender"); - System.out.println(gender); - // 获取一个对象 - Object o = clazz.getConstructor().newInstance(); - gender.set(o, "男"); - Student stu = (Student) o; - System.out.println("验证性别:" + stu.getGender()); - // - System.out.println("*************获取公有字段**并调用***********************************"); - Field name = clazz.getDeclaredField("name"); - System.out.println(name); - name.setAccessible(true); //暴力反射,解除私有限定 - name.set(o, "买"); - System.out.println("验证姓名:" + stu); - } -} -``` diff --git "a/Interview/codes/basics/\345\270\270\350\247\201\345\205\263\351\224\256\345\255\227\351\227\256\351\242\230.md" "b/Interview/codes/basics/\345\270\270\350\247\201\345\205\263\351\224\256\345\255\227\351\227\256\351\242\230.md" deleted file mode 100644 index 7e061e4b..00000000 --- "a/Interview/codes/basics/\345\270\270\350\247\201\345\205\263\351\224\256\345\255\227\351\227\256\351\242\230.md" +++ /dev/null @@ -1,18 +0,0 @@ -## 常见的关键字问题 -### final - -> final关键字主要用在三个地方:变量、方法、类。 - -1. **对于一个final变量**,如果是基本数据类型的变量,则其**数值一旦在初始化之后便不能更改**;如果是引用类型的变量,则在对其**初始化之后便不能再让其指向另一个对象**。 -2. **使用final方法的原因**有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。 -3. **当用final修饰一个类时**,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。 - -好处: -1. final的关键字提高了性能,JVM和java应用会缓存final变量; -2. final变量可以在多线程环境下保持线程安全; -3. 使用final的关键字提高了性能,JVM会对方法变量类进行优化; - -### static -1. **修饰成员变量和成员方法**: 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。调用格式:类名.静态变量名 类名.静态方法名() -2. **静态代码块**: 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块**只执行一次**. -3. **静态内部类**(static修饰类的话只能修饰内部类):静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。 diff --git "a/Interview/codes/basics/\345\274\202\345\270\270\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Interview/codes/basics/\345\274\202\345\270\270\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index 7f0d1583..00000000 --- "a/Interview/codes/basics/\345\274\202\345\270\270\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,67 +0,0 @@ -# 异常的一些问题 -> 在 Java 中,所有的异常都有一个共同的祖先java.lang包中的 Throwable类。 -- **Exception(异常)** -- **Error(错误)** - -## 思维导图 -![Throwable](http://media.dreamcat.ink/uPic/%E5%BC%82%E5%B8%B8%E5%88%86%E7%B1%BB.png) - -## Error -**是程序无法处理的错误**,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。比如:`OutOfMemoryError` - -## Exception -**是程序本身可以处理的异常。** 比如: - -- `RuntimeException` 异常由Java虚拟机抛出。 -- `NullPointerException`(要访问的变量没有引用任何对象时,抛出该异常) -- `ArithmeticException`(算术运算异常,一个整数除以0时,抛出该异常) -- `ArrayIndexOutOfBoundsException` (下标越界异常)。 - -## 处理 - -- `try` 块: 用于捕获异常。其后可接零个或多个`catch`块,如果没有`catch`块,则必须跟一个`finally`块。 -- `catch` 块: 用于处理`try`捕获到的异常。 -- `finally` 块: 无论是否捕获或处理异常,`finally`块里的语句都会被执行。当在`try`块或`catch`块中遇到`return`语句时,`finally`语句块将在方法返回之前被执行。 - -> 注意:当try语句和finally语句中都有return语句时,在方法返回之前,finally语句的内容将被执行,并且finally语句的返回值将会**覆盖原始的返回值**。如下: - -```java - public static int f(int value) { - try { - return value * value; - } finally { - if (value == 2) { - return 0; - } - } - } -``` - -## throw和throws -> 抛出异常 - -throw的例子: - -```java -public class Test1 { - public static void main(String[] args) { - if(true) { - throw new NumberFormatException(); // 抛出一个异常 - } else { - ... - } - } -} -``` - -throws是方法可能抛出异常的声明。(用在声明方法时,表示该方法可能要抛出异常),throws例子: - -```java -public class Test2 { - public void fun() throws NumberFormatException { - if(true) { - throw new NumberFormatException(); - } - } -} -``` \ No newline at end of file diff --git "a/Interview/codes/basics/\346\265\205\346\213\267\350\264\235\345\222\214\346\267\261\346\213\267\350\264\235\344\276\213\345\255\220.md" "b/Interview/codes/basics/\346\265\205\346\213\267\350\264\235\345\222\214\346\267\261\346\213\267\350\264\235\344\276\213\345\255\220.md" deleted file mode 100644 index 1f4f2078..00000000 --- "a/Interview/codes/basics/\346\265\205\346\213\267\350\264\235\345\222\214\346\267\261\346\213\267\350\264\235\344\276\213\345\255\220.md" +++ /dev/null @@ -1,145 +0,0 @@ -## 深拷贝和浅拷贝 - -> 1. **浅拷贝**:对**基本数据类型进行值传递**,对**引用数据类型进行引用传递般的拷贝**,此为浅拷贝。 -> 2. **深拷贝**:对**基本数据类型进行值传递**,对**引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝** -> 3. 也就二者对引用数据类型有区别 - -### 举例子 -1. 首先看浅拷贝: - -Subject类: -```java -public class Subject { - - private String name; - - public Subject(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Subject{" + - "name='" + name + '\'' + - '}'; - } -} -``` - -Student类: -```java -public class Student implements Cloneable{ - - // 对象的引用 - private Subject subject; - - private String name; - - public Student(Subject s, String name) { - this.subject = s; - this.name = name; - } - - public Subject getSubject() { - return subject; - } - - public String getName() { - return name; - } - - public void setSubject(Subject subject) { - this.subject = subject; - } - public void setName(String name) { - this.name = name; - } - - @Override - public String toString() { - return "Student{" + - "subject=" + subject + - ", name='" + name + '\'' + - '}'; - } - - @Override - protected Object clone() throws CloneNotSupportedException { - // 浅拷贝 -// return super.clone(); - } -} -``` -测试: -```java -public class ShallowCopyDemo { - public static void main(String[] args) throws CloneNotSupportedException { - // 原始对象 - Student student = new Student(new Subject("code"), "dream"); - System.out.println("原始对象: " + student.getName() + " - " + student.getSubject().getName()); // dream-code - - // 拷贝对象 - Student cloneStu = (Student) student.clone(); - System.out.println("拷贝对象: " + cloneStu.getName() + " - " + cloneStu.getSubject().getName()); // dream-code - - // 原始对象和拷贝对象是否一样: - System.out.println("原始对象和拷贝对象是否一样: " + (student == cloneStu)); // false - - // 原始对象和拷贝对象的name属性是否一样 - System.out.println("原始对象和拷贝对象的name属性是否一样: " + (student.getName() == cloneStu.getName())); // true - - // 原始对象和拷贝对象的subj属性是否一样 - System.out.println("原始对象和拷贝对象的subj属性是否一样: " + (student.getSubject() == cloneStu.getSubject())); // true - - student.setName("cat"); - student.getSubject().setName("eat"); - System.out.println("更新后的原始对象: " + student.getName() + " - " + student.getSubject().getName()); // cat-eat - System.out.println("更新原始对象后的克隆对象: " + cloneStu.getName() + " - " + cloneStu.getSubject().getName()); // dream-eat - // 在这个例子中,让要拷贝的类Student实现了Clonable接口并重写Object类的clone()方法,然后在方法内部调用super.clone()方法。 - // 从输出结果中我们可以看到,对原始对象stud的"name"属性所做的改变并没有影响到拷贝对象clonedStud; - // 但是对引用对象subj的"name"属性所做的改变影响到了拷贝对象clonedStud。 - } -} -``` -运行结果: -```shell -原始对象: dream - code -拷贝对象: dream - code -原始对象和拷贝对象是否一样: false -原始对象和拷贝对象的name属性是否一样: true -原始对象和拷贝对象的subj属性是否一样: true -更新后的原始对象: cat - eat -更新原始对象后的克隆对象: dream - eat -``` - -2. 深拷贝 - -只需要重写Student的clone方法 -```java - @Override - protected Object clone() throws CloneNotSupportedException { - Student student = new Student(new Subject(subject.getName()), name); // 新建对象,拷贝内容 - return student; - // 因为它是深拷贝,所以你需要创建拷贝类的一个对象。 - // 因为在Student类中有对象引用,所以需要在Student类中实现Cloneable接口并且重写clone方法。 - } -``` -测试结果: -```shell -原始对象: dream - code -拷贝对象: dream - code -原始对象和拷贝对象是否一样: false -原始对象和拷贝对象的name属性是否一样: true -原始对象和拷贝对象的subj属性是否一样: false -更新后的原始对象: cat - eat -更新原始对象后的克隆对象: dream - code -``` -晓得了吧? \ No newline at end of file diff --git "a/Interview/codes/basics/\347\261\273\345\236\213\350\275\254\346\215\242\347\232\204\351\227\256\351\242\230.md" "b/Interview/codes/basics/\347\261\273\345\236\213\350\275\254\346\215\242\347\232\204\351\227\256\351\242\230.md" deleted file mode 100644 index 39423d20..00000000 --- "a/Interview/codes/basics/\347\261\273\345\236\213\350\275\254\346\215\242\347\232\204\351\227\256\351\242\230.md" +++ /dev/null @@ -1,94 +0,0 @@ -## 类型转换的问题 - - -### 比如 - -```java -short s1 = 1; -s1 = s1 + 1;有什么错? -short s1 = 1; -s1 += 1; 有什么错? -``` - - -- 对于 `short s1 = 1; s1 = s1 + 1;` 由于 s1+1 运算时会自动提升表达式的类型,所以结果是 int 型,再赋值给 short 类型 s1 时,编译器将报告需要强制转换类型的错误。 -- 对于 `short s1 = 1; s1 += 1;`由于 += 是 **java 语言规定的运算符**,java 编译器会对它进行特殊处理,因此 可以正确编译。 - -```java -public class TypeConvert { - public static void main(String[] args) { - // 字面量属于 double 类型 - // 不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型 - // Java 不能隐式执行向下转型,因为这会使得精度降低。 - // float f = 1.1; - float f = 1.1f; - - // 因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。 - short s1 = 1; - // s1 = s1 + 1; - - // 但是使用 += 运算符可以执行隐式类型转换。 - s1 += 1; - // 上面的语句相当于将 s1 + 1 的计算结果进行了向下转型 如下: - s1 = (short) (s1 + 1); - - } -} - -``` - -### 聊聊整形包装类的缓存池 -举个例子: -```java -public class IntegerPackDemo { - public static void main(String[] args) { - Integer x = 3; // 装箱,3自动包装为Integer - int z = x; // 拆箱 x 拆箱为基本类型 - Integer y = 3; - System.out.println(x == y); // true 基本类型比较可用== - // ------------------------- - Integer a = new Integer(3); - Integer b = new Integer(3); - System.out.println(a == b); // false 老生常谈了,就不说为什么了 - System.out.println(a.equals.(b)); // true // 这里是用重写了equals方法,比较的是值,而不是对象的地址 - // ------------------------ - // 缓存池 - Integer aa = Integer.valueOf(123); - Integer bb = Integer.valueOf(123); - System.out.println(aa == bb); // true - /** - * valueOf的源码 - * public static Integer valueOf(int i) { - * // 判断是否在Integer的范围内 - * if (i >= IntegerCache.low && i <= IntegerCache.high) - * return IntegerCache.cache[i + (-IntegerCache.low)]; - * return new Integer(i); - * } - */ - } -} - -``` -个人还是喜欢贴Integer的equals源码: -```java -public boolean equals(Object obj) { - if (obj instanceof Integer) { - return value == ((Integer)obj).intValue(); // 这里比的就是值了 - } - return false; -} -``` -继续看valueOf方法的源码: - -```java -// 源码描述: -// This method will always cache values in the range -128 to 127, -public static Integer valueOf(int i) { - if (i >= IntegerCache.low && i <= IntegerCache.high) // 条件 - return IntegerCache.cache[i + (-IntegerCache.low)];// 取缓存, - // Integeer的源码中: - // static final int low = -128; IntegerCache.low = -128 - return new Integer(i); -} -``` -> 当使用自动装箱方式创建一个Integer对象时,当数值在-128 ~127时,会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的Integer对象。所以上述代码中,x和y引用的是相同的Integer对象。 diff --git "a/Interview/codes/basics/\351\235\242\345\220\221\345\257\271\350\261\241\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Interview/codes/basics/\351\235\242\345\220\221\345\257\271\350\261\241\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index a42fb42c..00000000 --- "a/Interview/codes/basics/\351\235\242\345\220\221\345\257\271\350\261\241\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,141 +0,0 @@ -## 三大特性 -### 封装 -> 封装把一个对象的**属性私有化**,同时提供一些可以**被外界访问的属性的方法**,**如果属性不想被外界访问,我们大可不必提供方法给外界访问**。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。 - -代码举个例子也妥: -```java -class Person{ - private String name; // 属性私有,并不想暴漏 - private Integer age; // 属性私有 - - // 给外界一个访问方法,给name赋值 - public void setName(String name) { - this.name = name; - } - - // 给外界一个访问方法,获取name的值 - public String getName() { - return this.name; - } -} -``` - -### 继承 -> 继承是使用**已存在的类**的定义作为基础建立新类的技术,新类的定义可以增加**新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类**。通过使用继承我们能够非常方便地复用以前的代码。 - -代码举个例子撒: -```java -import java.util.*; - -public class Person { - private String name; - - private Integer age; - - public void setName(String name) { - this.name = name; - } - - public void setAge(Integer age) { - this.age = age; - } - - public String getName() { - return this.name; - } - - public Integer getAge() { - return this.age; - } - - public static void main(String[] args) { - Student s = new Student(); - s.setName("Dream"); - s.setAge(18); - s.setSchool("家里蹲"); - s.say(); - } -} - -class Student extends Person{ // 集成父类 - private String school; - - public void setSchool(String school) { - this.school = school; - } - - public String getSchool() { - return this.school; - } - - public void say(){ - System.out.println(this.getName() + " say: 我tm" + this.getAge() + "岁,不服?不服就来" + this.getSchool()); - } -} - -``` - -注意: -- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。 -- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。 -- 子类可以用自己的方式实现父类的方法。(以后介绍)。 - -### 多态 -> 所谓多态就是指程序中定义的**引用变量**所指向的**具体类型**和通过**该引用变量发出的方法调用**在编程时并不确定,而是在**程序运行期间才确定**,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。 -> 说白了,就是任何事物的多个姿态,多个形态。比如,你说一个猫在吃东西,同样的,你也能说一个动物在吃东西。 - -一般两种方式: -- 继承(多个子类对同一方法的重写) -- 接口(实现接口并覆盖接口中同一方法) - -举个例子吧: -定义一个动物类 -```java -class Animal { - public void eat() { - System.out.println("动物在吃东西..."); - } -} -``` - -定义一个猫 -```java -class Cat extends Animal{ - @Override - public void eat() { - System.out.println("猫在吃东西..."); - } -} -``` -测试 -```java -public class Test { - public static void main(String[] args){ - Animal animal = new Cat(); - animal.eat() // 猫也会吃饭 - // 你看到了一只猫,同样它也是动物 - // 比如有很多其他种类继承了动物哈, - // 当编译期间的animal引用变量,到底指的哪个实例对象, - // 或者该引用调用的eat方法,到底是哪个实例对象的eat,编译期间恐怕不知道哦 - // 只有运行期间,哦哦, 原来是猫的eat方法哇... - } -} -``` - -### 重载和重写的区别 - -- 重载 -> 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 -> 例子的话,你看源码的时候,那里面的方法,你晓得伐? - -- 重写 -> 重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。也就是说方法提供的行为改变,而方法的外貌并没有改变。 -> 当你看源码出现继承的时候, 你晓得伐? - -### 接口和抽象类的区别是什么? -1. 方法:接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。 -2. 变量:接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。 -3. 实现:一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过implement关键字扩展多个接口。 -4. 修饰符:接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。 - - diff --git "a/Interview/codes/collection/ArrayList\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Interview/codes/collection/ArrayList\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index 7a8d9fa1..00000000 --- "a/Interview/codes/collection/ArrayList\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,123 +0,0 @@ -# ArrayList -## 一些特点 - -- ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入`null`元素,底层通过**数组**实现。 -- 每个ArrayList都有一个容量(capacity),表示**底层数组的实际大小**,容器内存储元素的个数不能多于当前容量。 -- 当向容器中添加元素时,如果容量不足,**容器会自动增大底层数组的大小**。 -- Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。 - -## 底层数据结构 -```java -private static final int DEFAULT_CAPACITY = 10; // 默认容量 -transient Object[] elementData; // Object 数组 -private int size; // 大小 -``` - -## 构造函数 - -### 有参 -```java -public ArrayList(int initialCapacity) { - if (initialCapacity > 0) { - this.elementData = new Object[initialCapacity]; - } else if (initialCapacity == 0) { - this.elementData = EMPTY_ELEMENTDATA; // 默认10 - } else { - throw new IllegalArgumentException("Illegal Capacity: "+ - initialCapacity); - } -} -``` - -### 无参 -```java -public ArrayList() { - this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 默认10 -} -``` - -## 扩容 -常问,看一下源码吧 - -```java -public void ensureCapacity(int minCapacity) { - int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) - // any size if not default element table - ? 0 - // larger than default for default empty table. It's already - // supposed to be at default size. - : DEFAULT_CAPACITY; - - if (minCapacity > minExpand) { - ensureExplicitCapacity(minCapacity); - } -} -``` -继续看ensureExplicitCapacity -```java -private void ensureExplicitCapacity(int minCapacity) { - modCount++; - - // overflow-conscious code - if (minCapacity - elementData.length > 0) - grow(minCapacity); // 扩容最关键的方法 -} -``` -其实可以之和看grow(jdk1.8) - -```java - private void grow(int minCapacity) { - // overflow-conscious code - int oldCapacity = elementData.length; - int newCapacity = oldCapacity + (oldCapacity >> 1); // old + old * 2 就在这... - if (newCapacity - minCapacity < 0) - newCapacity = minCapacity; - if (newCapacity - MAX_ARRAY_SIZE > 0) - newCapacity = hugeCapacity(minCapacity); - // minCapacity is usually close to size, so this is a win: - elementData = Arrays.copyOf(elementData, newCapacity); -} -``` - -## add -还是看直接看源码方便一些 -```java -public boolean add(E e) { - // 扩容判断 - ensureCapacityInternal(size + 1); // Increments modCount!! 这个参数,起到并发异常作用。 - elementData[size++] = e; // 这一步非原子性,并发容易出错,好几种情况。 下次分析 - return true; -} -``` - -## get -```java -一般获取元素,第一步都要判断索引是否越界 -public E get(int index) { - rangeCheck(index); // 判断给定索引是否越界 - return elementData(index); -} -``` - -## 聊一下并发容易出现的问题 -### 首先是数组越界,举个例子 -1. 列表大小为9,即size=9 -2. 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。 -3. 线程B此时也进入add方法,它和获取的size的值也为9,也开始调用ensureCapacityInternal方法。 -4. 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。 -5. 线程B也发现需要大小为10,也可以容纳,返回。 -6. 好了,**问题来了哈** -7. 线程A开始进行设置值操作,elementData[size++] = e操作。此时size变为10。 -8. 线程B也开始进行设置值操作,它尝试设置elementData[10] = e, 而elementData没有进行过扩容,它的下标最大为9,于是此时会报出一个数组越界的异常`ArrayIndexOutOfBoundsException`。 - -### null值,举个例子 -1. 列表大小为10,即size=0 -2. 线程A开始添加元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。也就是说,线程挂在了`element[0] = e`上。 -3. 接着线程B刚好也要开始天极爱一个值为B的元素,且走到了第一条的操作。此时线程B获取的size的值依然为0,于是它将B也放在了elementData下标为0的位置上。 -4. **问题来了**,其实上面也是问题,覆盖了。。。 -5. 线程A将size的值增加为1 -6. 线程B开始将size的值增加为2 -7. 当你获取1索引的时候,那不就是null了? - -## Fail-Fast机制 -ArrayList也采用了快速失败的机制,**通过记录modCount参数来实现**。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。 \ No newline at end of file diff --git "a/Interview/codes/collection/ConcurrentHashMap\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Interview/codes/collection/ConcurrentHashMap\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index 608032e8..00000000 --- "a/Interview/codes/collection/ConcurrentHashMap\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,238 +0,0 @@ -# ConcurrentHashMap -> 1.7和1.8的源码还不太一样,也就是说加锁的方式不一样。 - -## 1.7 -> 分段锁 - -![图示意图](https://i.loli.net/2019/05/08/5cd1d2c5ce95c.jpg) - -### Segment的数据结构 -```java -/** - * Segment 数组,存放数据时首先需要定位到具体的 Segment 中。 - */ -final Segment[] segments; -transient Set keySet; -transient Set> entrySet; -``` - -Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下: - -```java -static final class Segment extends ReentrantLock implements Serializable { - private static final long serialVersionUID = 2249069246763182397L; - - // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶 - transient volatile HashEntry[] table; // volatile修饰 - transient int count; - transient int modCount; - transient int threshold; - final float loadFactor; - -} -``` -- 唯一的区别就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。 -- ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 `ReentrantLock`。 -- 不会像HashTable那样不管是put还是get操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。 -- **每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。** - -### put -```java -public V put(K key, V value) { - Segment s; - if (value == null) - throw new NullPointerException(); - int hash = hash(key); - int j = (hash >>> segmentShift) & segmentMask; - if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck - (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment - s = ensureSegment(j); - return s.put(key, hash, value, false); -} -``` -通过key定位到Segment,之后在对应的Segment中进行具体的put -```java -final V put(K key, int hash, V value, boolean onlyIfAbsent) { - HashEntry node = tryLock() ? null : - scanAndLockForPut(key, hash, value); // 1. 加锁处理 - V oldValue; - try { - HashEntry[] tab = table; - int index = (tab.length - 1) & hash; - HashEntry first = entryAt(tab, index); - for (HashEntry e = first;;) { - if (e != null) { - K k; - if ((k = e.key) == key || // 2. 遍历该HashEntry,如果不为空则判断传入的key和当前遍历的key是否相等,相等则覆盖旧的value - (e.hash == hash && key.equals(k))) { - oldValue = e.value; - if (!onlyIfAbsent) { - e.value = value; - ++modCount; - } - break; - } - e = e.next; - } - else { // 3. 不为空则需要新建一个HashEntry并加入到Segment中,同时会先判断是否需要扩容 - if (node != null) - node.setNext(first); - else - node = new HashEntry(hash, key, value, first); - int c = count + 1; - if (c > threshold && tab.length < MAXIMUM_CAPACITY) - rehash(node); - else - setEntryAt(tab, index, node); - ++modCount; - count = c; - oldValue = null; - break; - } - } - } finally { - unlock(); // 4. 解锁 - } - return oldValue; -} -``` -- 虽然HashEntry中的value是用volatile关键字修饰的,但是并不能保证并发的原子性,所以put操作仍然需要加锁处理。 -- 首先第一步的时候会尝试获取锁,如果获取失败肯定就是其他线程存在竞争,则利用 `scanAndLockForPut()` 自旋获取锁。 - - 尝试获取自旋锁 - - 如果重试的次数达到了`MAX_SCAN_RETRIES` 则改为阻塞锁获取,保证能获取成功。 - -总的来说: -- 将当前的Segment中的table通过key的hashcode定位到HashEntry -- 遍历该HashEntry,如果不为空则判断传入的key和当前遍历的key是否相等,相等则覆盖旧的value -- 不为空则需要新建一个HashEntry并加入到Segment中,同时会先判断是否需要扩容 -- 最后会解除在1中所获取当前Segment的锁。 - -### get -```java -public V get(Object key) { - Segment s; // manually integrate access methods to reduce overhead - HashEntry[] tab; - int h = hash(key); - long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; - if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && - (tab = s.table) != null) { - for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile - (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); - e != null; e = e.next) { - K k; - if ((k = e.key) == key || (e.hash == h && key.equals(k))) - return e.value; - } - } - return null; -} -``` -- 只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。 -- 由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。 -- ConcurrentHashMap 的 get 方法是非常高效的,**因为整个过程都不需要加锁。** - -## 1.8 -> 1.7 查询遍历链表效率太低。其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性 - -### put -```java - final V putVal(K key, V value, boolean onlyIfAbsent) { - if (key == null || value == null) throw new NullPointerException(); - int hash = spread(key.hashCode()); - int binCount = 0; - for (Node[] tab = table;;) { // 1. 根据key计算出hashcode - Node f; int n, i, fh; - if (tab == null || (n = tab.length) == 0) // 2. 判断是否需要进行初始化 - tab = initTable(); - else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 3. f即为当前key定位出的Node,如果为空表示当前位置可以写入数据,利用CAS尝试写入,失败则自旋保证成功。 - if (casTabAt(tab, i, null, - new Node(hash, key, value, null))) - break; // no lock when adding to empty bin - } - else if ((fh = f.hash) == MOVED) // 4. 如果当前位置的`hashcode == MOVED == -1`,则需要进行扩容 - tab = helpTransfer(tab, f); - else { - V oldVal = null; - synchronized (f) { // 5. 如果都不满足,则利用synchronized锁写入数据 - if (tabAt(tab, i) == f) { - if (fh >= 0) { - binCount = 1; - for (Node e = f;; ++binCount) { - K ek; - if (e.hash == hash && - ((ek = e.key) == key || - (ek != null && key.equals(ek)))) { - oldVal = e.val; - if (!onlyIfAbsent) - e.val = value; - break; - } - Node pred = e; - if ((e = e.next) == null) { - pred.next = new Node(hash, key, - value, null); - break; - } - } - } - else if (f instanceof TreeBin) { - Node p; - binCount = 2; - if ((p = ((TreeBin)f).putTreeVal(hash, key, - value)) != null) { - oldVal = p.val; - if (!onlyIfAbsent) - p.val = value; - } - } - } - } - if (binCount != 0) { - if (binCount >= TREEIFY_THRESHOLD) // 6. 如果数量大于`TREEIFY_THRESHOLD` 则要转换为红黑树。 - treeifyBin(tab, i); - if (oldVal != null) - return oldVal; - break; - } - } - } - addCount(1L, binCount); - return null; - } - -``` -- 根据key计算出hashcode -- 判断是否需要进行初始化 -- f即为当前key定位出的Node,如果为空表示当前位置可以写入数据,**利用CAS尝试写入,失败则自旋保证成功。** -- 如果当前位置的`hashcode == MOVED == -1`,则需要进行扩容 -- 如果都不满足,则利用`synchronized`锁写入数据 -- 如果数量大于TREEIFY_THRESHOLD 则要转换为红黑树。 - -### get -```java - public V get(Object key) { - Node[] tab; Node e, p; int n, eh; K ek; - int h = spread(key.hashCode()); - if ((tab = table) != null && (n = tab.length) > 0 && - (e = tabAt(tab, (n - 1) & h)) != null) { - if ((eh = e.hash) == h) { - if ((ek = e.key) == key || (ek != null && key.equals(ek))) - return e.val; - } - else if (eh < 0) - return (p = e.find(h, key)) != null ? p.val : null; - while ((e = e.next) != null) { - if (e.hash == h && - ((ek = e.key) == key || (ek != null && key.equals(ek)))) - return e.val; - } - } - return null; - } -``` -- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 -- 如果是红黑树那就按照树的方式获取值。 -- 就不满足那就按照链表的方式遍历获取值。 - -## 总结 -1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。 \ No newline at end of file diff --git "a/Interview/codes/collection/HashMap\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Interview/codes/collection/HashMap\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index a1905e14..00000000 --- "a/Interview/codes/collection/HashMap\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,173 +0,0 @@ -# HashMap -> 谈到这个,真的是面试高频呀,处处问,关键HashMap在jdk1.7和1.8的源码是我不一样的我这边就只放1.8的源码。 - -## 底层结构 - -> HashMap的底层结构是是**数组+链表**。关于为什么是链表,那是因为哈希冲突,采取链表一种方式。 - -## 常见参数 -```java -static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 初始容量 -static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子0.75 -// 给定的默认容量为16,负载因子为0.75. -// Map在使用过程中不断的往里面存放数据,当数量达到了16 * 0.75 = 12就需要将当前16的容量进行扩容, -// 而扩容这个过程涉及到rehash(重新哈希)、复制数据等操作,所有非常消耗性能。 -static final int TREEIFY_THRESHOLD = 8;// 成为红黑树的阈值,为什么是8? -static final int UNTREEIFY_THRESHOLD = 6; // 红黑树转链表阈值6 -static final int MIN_TREEIFY_CAPACITY = 64; -// 个人感觉,8是经验值,或者链表为8的时候,性能开始下降,大家知道链表查找慢,当元素过多的时候,非常影响性能的。 -// 因此采取为8的时候转成红黑树。元素过少,其实没必要直接红黑树,元素过少,链表查找的时间也不是很能影响性能。 -// 在 hash 函数设计合理的情况下,发生 hash 碰撞 8 次的几率为百万分之 6,概率说话。 -// 6是因为如果 hash 碰撞次数在 8 附近徘徊,会一直发生链表和红黑树的转化,为了预防这种情况的发生。 -``` - -## hash -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` -hash 函数是先拿到通过 key 的 hashcode,是 32 位的 int 值,然后让 hashcode 的高 16 位和低 16 位进行异或操作。这个也叫扰动函数,这么设计有二点原因: -- 一定要尽可能降低 hash 碰撞,越分散越好; -- 算法一定要尽可能高效,因为这是高频操作, 因此采用位运算; - -## put -> 这里介绍1.8的,1.7的其实很简单,被1.8包括了 -```java -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, - boolean evict) { - Node[] tab; Node p; int n, i; - if ((tab = table) == null || (n = tab.length) == 0) // 1. 判断当前桶是否为空,空的就需要初始化(resize中会判断是否进行初始化) - n = (tab = resize()).length; - if ((p = tab[i = (n - 1) & hash]) == null) // 2. 根据当前key的hashcode定位到具体的桶中并判断是否为空,为空则表明没有Hash冲突,就直接在当前位置创建一个新桶 - tab[i] = newNode(hash, key, value, null); - else { - Node e; K k; - if (p.hash == hash && // 3. 如果当前桶有值(Hash冲突),那么就要比较当前桶中的key、key的hashcode与写入的key是否相等,相等就赋值给e,在第8步的时候会统一进行赋值及返回 - ((k = p.key) == key || (key != null && key.equals(k)))) - e = p; - else if (p instanceof TreeNode) // 4. 如果当前桶为红黑树,那就要按照红黑树的方式写入数据 - e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); - else { // 5. 如果是个链表,就需要将当前的key、value封装称一个新节点写入到当前桶的后面形成链表。 - for (int binCount = 0; ; ++binCount) { - if ((e = p.next) == null) { - p.next = newNode(hash, key, value, null); - if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 6. 接着判断当前链表的大小是否大于预设的阈值,大于就要转换成为红黑树 -- - treeifyBin(tab, hash); - break; - } - if (e.hash == hash && // 7. 如果在遍历过程中找到key相同时直接退出遍历。 - ((k = e.key) == key || (key != null && key.equals(k)))) - break; - p = e; - } - } - if (e != null) { // existing mapping for key 8. 如果`e != null`就相当于存在相同的key,那就需要将值覆盖。 - V oldValue = e.value; - if (!onlyIfAbsent || oldValue == null) - e.value = value; - afterNodeAccess(e); - return oldValue; - } - } - ++modCount; - if (++size > threshold) // 9. 最后判断是否需要进行扩容。 - resize(); - afterNodeInsertion(evict); - return null; -} -``` -看代码有点晕乎! - -![put过程](https://imgkr.cn-bj.ufileos.com/b2206341-08ce-4e67-87bd-3a467ccac31e.png) - -1. 判断数组是否为空,为空进行初始化; -2. 不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index; -3. 查看 table[index] 是否存在数据,没有数据就构造一个 Node 节点存放在 table[index] 中; - -4. 存在数据,说明发生了 hash 冲突(存在二个节点 key 的 hash 值一样), 继续判断 key 是否相等,相等,用新的 value 替换原数据(onlyIfAbsent 为 false); - -5. 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中; -6. 如果不是树型节点,创建普通 Node 加入链表中;判断链表长度是否大于 8, 大于的话链表转换为红黑树; - -7. 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。 - -## get -```java -public V get(Object key) { - Node e; - return (e = getNode(hash(key), key)) == null ? null : e.value; -} -final Node getNode(int hash, Object key) { - Node[] tab; Node first, e; int n; K k; - if ((tab = table) != null && (n = tab.length) > 0 && - (first = tab[(n - 1) & hash]) != null) { - if (first.hash == hash && // always check first node - ((k = first.key) == key || (key != null && key.equals(k)))) - return first; - if ((e = first.next) != null) { - if (first instanceof TreeNode) - return ((TreeNode)first).getTreeNode(hash, key); - do { - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } while ((e = e.next) != null); - } - } - return null; -} -``` -- 首先将key hash之后取得所定位的桶 -- 如果桶为空,则直接返回null -- 否则判断桶的第一个位置(有可能是链表、红黑树)的key是否为查询的key,是就直接返回value -- 如果第一个不匹配,则判断它的下一个是红黑树还是链表 -- 红黑树就按照树的查找方式返回值 -- 不然就按照链表的方式遍历匹配返回值 - -## 1.8的优化 -1. 数组+链表改成了数组+链表或红黑树; -2. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7 将新元素放到数组中,原始节点作为新节点的后继节点,1.8 遍历链表,将元素放置到链表的最后; -3. 扩容的时候 1.7 需要对原数组中的元素进行重新 hash 定位在新数组的位置,1.8 采用更简单的判断逻辑,位置不变或索引+旧容量大小; -4. 在插入时,1.7 先判断是否需要扩容,再插入,1.8 先进行插入,插入完成再判断是否需要扩容; - -## 并发问题 -- HashMap扩容的时候会调用resize()方法,就是这里的并发操作容易在一个桶上形成环形链表 -- 这样当获取一个不存在的key时,计算出的index正好是环形链表的下标就会出现死循环。 -- **但是1.7的头插法造成的问题,1.8改变了插入顺序,就解决了这个问题,但是为了内存可见性等安全性,还是需要ConCurrentHashMap** - -### Java 中有 HashTable、Collections.synchronizedMap、以及 ConcurrentHashMap 可以实现线程安全的 Map。 -- HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个数组,粒度比较大 -- Collections.synchronizedMap 是使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现 -- ConcurrentHashMap 使用分段锁,降低了锁粒度,让并发度大大提高。(jdk1.8 CAS+ synchronized) - -[死循环分析](https://zhuanlan.zhihu.com/p/67915754) - -## 哈希遍历 - -```java -Iterator> entryIterator = map.entrySet().iterator(); - while (entryIterator.hasNext()) { - Map.Entry next = entryIterator.next(); - System.out.println("key=" + next.getKey() + " value=" + next.getValue()); - } - -Iterator iterator = map.keySet().iterator(); - while (iterator.hasNext()){ - String key = iterator.next(); - System.out.println("key=" + key + " value=" + map.get(key)); - } -``` -- 建议使用第一种,同时可以把key value取出。 -- 第二种还需要通过key取一次key,效率较低。 - -# HashSet -> HashSet中不允许有重复元素,这是因为HashSet是基于HashMap实现的,HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个`private static final Object PRESENT = new Object();`。 HashSet跟HashMap一样,都是一个存放链表的数组。 - -# TreeMap -> TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现。所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用户 key 的比较。 - -# LinkedHashMap -> LinkedHashMap 内部维护了一个单链表,有头尾节点,同时 LinkedHashMap 节点 Entry 内部除了继承 HashMap 的 Node 属性,还有 before 和 after 用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。 \ No newline at end of file diff --git "a/Interview/codes/collection/LinkedList\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Interview/codes/collection/LinkedList\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" deleted file mode 100644 index 4a9ae73a..00000000 --- "a/Interview/codes/collection/LinkedList\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" +++ /dev/null @@ -1,109 +0,0 @@ -# LinkedList -> LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。 关于栈或队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能。 关于特点,想必大家都知道了。 - -## 底层数据 -> LinkedList底层通过双向链表实现。双向链表的每个节点用内部类Node表示。LinkedList通过**first和last引用分别指向链表的第一个和最后一个元素**。注意这里没有所谓的哑元,当链表为空的时候**first和last都指向null**。 -```java -transient int size = 0; -transient Node first; // 经常用 -transient Node last; // 经常用 -// Node是私有的内部类 -private static class Node { - E item; - Node next; // 这里就不多说了 - Node prev; // - - Node(Node prev, E element, Node next) { - this.item = element; - this.next = next; - this.prev = prev; - } -} -``` -构造函数就不用看了... - -## getFirst和getLast -> 这个挺简单的 -```java -public E getFirst() { - final Node f = first; // 第一个节点 - if (f == null) - throw new NoSuchElementException(); - return f.item; -} -public E getLast() { - final Node l = last; // 最后一个节点 - if (l == null) - throw new NoSuchElementException(); - return l.item; -} -``` - -**removeFirst和removeLast**和上面的源码差不多,挺简单。链表的删除,脑子里画图思考思考就明白了,添加也是一样。 - -## add调用的是`linkLast` -```java -void linkLast(E e) { - final Node l = last; // 要在尾部添加,所以要找last - final Node newNode = new Node<>(l, e, null); // last->e->null - last = newNode; - if (l == null) - first = newNode; // 如果l为空,说明没有链表没有元素,那么first和last都指向e, - else - l.next = newNode; // 否则就将l.next指向新节点 - size++; - modCount++; // 依然存在并发危险哦 -} -``` - -## 看一下remove -```java -public boolean remove(Object o) { - if (o == null) { // 将链表存在null的给移除掉 - for (Node x = first; x != null; x = x.next) { - if (x.item == null) { - unlink(x); - return true; - } - } - } else { - for (Node x = first; x != null; x = x.next) { - if (o.equals(x.item)) { // 否则直接遍历与符合当前对象,unlink即可。 - unlink(x); - return true; - } - } - } - return false; -} -``` -## unlink得看 -> 这个脑子思考一下那个图,其实就明白,很源码的话,也没问题的。 -```java -E unlink(Node x) { - // assert x != null; - final E element = x.item; // 当前值 - final Node next = x.next; // 后 - final Node prev = x.prev; // 前 - - if (prev == null) { // 如果x的前是null,说明,x是首节点,直接让first指向next - first = next; - } else { // x不是首节点,意味着pre是有值哦。 - prev.next = next; // prev.next的箭头指向x的next - x.prev = null; // 将x.prev指向null 双向链表? - } - - if (next == null) { // 同上,说明x是最后节点了 - last = prev; // 让last指向x的prev - } else { - next.prev = prev; // 否则x不是最后节点, 让next.prev 指向prev - x.next = null; // x.next 指向null , 双向链表 - } - - x.item = null; // GC回收 - size--; - modCount++; - return element; -} -``` -**其他几个方法的源码中核心都有这些...** \ No newline at end of file diff --git "a/Interview/codes/collection/\346\211\213\345\206\231LRU\347\256\227\346\263\225.md" "b/Interview/codes/collection/\346\211\213\345\206\231LRU\347\256\227\346\263\225.md" deleted file mode 100644 index 8995e5ea..00000000 --- "a/Interview/codes/collection/\346\211\213\345\206\231LRU\347\256\227\346\263\225.md" +++ /dev/null @@ -1,73 +0,0 @@ -# LRU算法 -> LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。 - -## 一般过程 -![过程图](https://pic4.zhimg.com/80/v2-71b21233c615b1ce899cd4bd3122cbab_1440w.jpg) - -- 新数据插入到链表头部; -- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部; -- 当链表满的时候,将链表尾部的数据丢弃。 - -再来来个具体的过程 -![具体过程](https://pic3.zhimg.com/80/v2-998b52e7534278b364e439bbeaf61d5e_1440w.jpg) - -1. 最开始时,内存空间是空的,因此依次进入A、B、C是没有问题的 -2. 当加入D时,就出现了问题,内存空间不够了,因此根据LRU算法,内存空间中A待的时间最为久远,选择A,将其淘汰 -3. 当再次引用B时,内存空间中的B又处于活跃状态,而C则变成了内存空间中,近段时间最久未使用的 -4. 当再次向内存空间加入E时,这时内存空间又不足了,选择在内存空间中待的最久的C将其淘汰出内存,这时的内存空间存放的对象就是E->B->D - -## 伪代码实现 - -### 思路 -> 双向链表+HashMap,Java中的LinkedHashMap就实现了该算法。 - -### 定义节点 -```java -class Node { - int key; - int value; - Node pre; - Node next; -} -``` -### 定义HashMap -`HashMap map = new HashMap<>();` - -### get -> 其实挺简单的。 -```java -public int get(int key) { - if (map.containsKey(key)) { - Node n = map.get(key); // 获取内存中存在的值,比如A - remove(n); //使用链表的方法,移除该节点 - setHead(n); //依然使用链表的方法,将该节点放入头部 - return n.value; - } - return -1; -} -``` -- 由于当一个节点通过key来访问到这个节点,那么这个节点就是刚被访问了, -- 就把这个节点删除掉,然后放到队列头,这样队列的头部都是最近访问的, -- 队列尾部是最近没有被访问的。 - -### set -```java -public void set(int key, int value) { - if (map.containsKey(key)) { - Node old = map.get(key); - old.value = value; - remove(old); // 移除旧节点 - setHead(old); // 放到队头 - } else { - Node created = new Node(key, value); - if (map.size() >= capacity) { - map.remove(end.key); // clear该key - remove(end); //链表也是依次 - setHead(created); // 将created放入队头 - } else { - setHead(created); // 如果没满,直接放入队头 - } - map.put(key,created); - } -} -``` \ No newline at end of file diff --git "a/Interview/codes/collection/\351\233\206\345\220\210\346\200\273\350\247\210.md" "b/Interview/codes/collection/\351\233\206\345\220\210\346\200\273\350\247\210.md" deleted file mode 100644 index b0b28954..00000000 --- "a/Interview/codes/collection/\351\233\206\345\220\210\346\200\273\350\247\210.md" +++ /dev/null @@ -1,15 +0,0 @@ -# 集合总述 -通过查看JDK8的Collection的文档源码: - -![有点凶](https://imgkr.cn-bj.ufileos.com/98753cd6-dba3-4291-83c3-7964b39c5da3.png) - -再看一Map的: - - -![也挺凶](https://imgkr.cn-bj.ufileos.com/0af46521-0eb6-4666-898e-3536cc3b7c4e.png) - -不说了,来个更清晰一点的: - -![清晰uml图](https://www.pdai.tech/_images/java_collections_overview.png) - -因此,容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。关于各自有什么特点,待后边带着源码详细介绍先。 \ No newline at end of file diff --git "a/Interview/linux/linux-\345\237\272\347\241\200.md" "b/Interview/linux/linux-\345\237\272\347\241\200.md" deleted file mode 100644 index fb815ec4..00000000 --- "a/Interview/linux/linux-\345\237\272\347\241\200.md" +++ /dev/null @@ -1,695 +0,0 @@ -## 引言 - -**linux-基础** - -## 常用操作以及概念 - -### 快捷键 - -- Tab: 命令和文件名补全; -- Ctrl+C: 中断正在运行的程序; -- Ctrl+D: 结束键盘输入(End Of File,EOF) - -### 求助 - - - -#### --help - -指令的基本用法与选项介绍。 - -#### man - -man 是 manual 的缩写,将指令的具体信息显示出来。 - -#### info - -info 与 man 类似,但是 info 将文档分成一个个页面,每个页面可以进行跳转。 - -#### doc - -/usr/share/doc 存放着软件的一整套说明文件。 - -### 关机 - -#### who - -在关机前需要先使用 who 命令查看有没有其它用户在线。 - -#### sync - -为了加快对磁盘文件的读写速度,位于内存中的文件数据不会立即同步到磁盘上,因此关机之前需要先进行 sync 同步操作。 - -#### shutdown - -```bash -## shutdown [-krhc] 时间 [信息] --k : 不会关机,只是发送警告信息,通知所有在线的用户 --r : 将系统的服务停掉后就重新启动 --h : 将系统的服务停掉后就立即关机 --c : 取消已经在进行的 shutdown 指令内容 -``` - -### PATH - -可以在环境变量 PATH 中声明可执行文件的路径,路径之间用 : 分隔。 - -`/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin` - -### sudo - -sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令. - -### 包管理工具 - -RPM 和 DPKG 为最常见的两类软件包管理工具: - -- RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。 -- 与 RPM 进行竞争的是基于 Debian 操作系统 (Ubuntu) 的 DEB 软件包管理工具 DPKG,全称为 Debian Package,功能方面与 RPM 相似。 - -YUM 基于 RPM,具有依赖管理功能,并具有软件升级的功能。 - -### 发行版 - -Linux 发行版是 Linux 内核及各种应用软件的集成版本。 - -| 基于的包管理工具 | 商业发行版 | 社区发行版 | -| :--------------: | :--------: | :-------------: | -| RPM | Red Hat | Fedora / CentOS | -| DPKG | Ubuntu | Debian | - -### VIM - -- 一般指令模式(Command mode): VIM 的默认模式,可以用于移动游标查看内容; -- 编辑模式(Insert mode): 按下 "i" 等按键之后进入,可以对文本进行编辑; -- 指令列模式(Bottom-line mode): 按下 ":" 按键之后进入,用于保存退出等操作。 - -在指令列模式下,有以下命令用于离开或者保存文件。 - -| 命令 | 作用 | -| :--: | :----------------------------------------------------------: | -| :w | 写入磁盘 | -| :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 | -| :q | 离开 | -| :q! | 强制离开不保存 | -| :wq | 写入磁盘后离开 | -| :wq! | 强制写入磁盘后离开 | - -## 磁盘 - -### 磁盘接口 - -#### IDE - -IDE(ATA)全称 Advanced Technology Attachment,接口速度最大为 133MB/s,因为并口线的抗干扰性太差,且排线占用空间较大,不利电脑内部散热,已逐渐被 SATA 所取代。 - -#### SATA - -SATA 全称 Serial ATA,也就是使用串口的 ATA 接口,抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能。SATA-II 的接口速度为 300MiB/s,而新的 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。 - -#### SCSI - -SCSI 全称是 Small Computer System Interface(小型机系统接口),经历多代的发展,从早期的 SCSI-II 到目前的 Ultra320 SCSI 以及 Fiber-Channel(光纤通道),接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。 - -#### SAS - -SAS(Serial Attached SCSI)是新一代的 SCSI 技术,和 SATA 硬盘相同,都是采取序列式技术以获得更高的传输速度,可达到 6Gb/s。此外也透过缩小连接线改善系统内部空间等。 - -## 分区 - -### 分区表 - -磁盘分区表主要有两种格式,一种是限制较多的 MBR 分区表,一种是较新且限制较少的 GPT 分区表。 - -#### MBR - -MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中主要开机记录占 446 bytes,分区表占 64 bytes。 - -分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它使用其它扇区用记录额外的分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。 - -Linux 也把分区当成文件,分区文件的命名方式为: 磁盘文件名 + 编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。 - -#### GPT - -不同的磁盘有不同的扇区大小,例如 512 bytes 和最新磁盘的 4 k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA),LBA 默认大小为 512 bytes。 - -GPT 第 1 个区块记录了主要开机记录(MBR),紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录,这个部份纪录了分区表本身的位置与大小和备份分区的位置,同时放置了分区表的校验码 (CRC32),操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误,可以使用备份分区进行恢复。 - -GPT 没有扩展分区概念,都是主分区,每个 LAB 可以分 4 个分区,因此总共可以分 4 * 32 = 128 个分区。 - -MBR 不支持 2.2 TB 以上的硬盘,GPT 则最多支持到 233 TB = 8 ZB。 - -### 开机检测程序 - -#### BIOS - -BIOS(Basic Input/Output System,基本输入输出系统),它是一个固件(嵌入在硬件中的软件),BIOS 程序存放在断电后内容不会丢失的只读内存中。 - -BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的主要开机记录(MBR),由主要开机记录(MBR)执行其中的开机管理程序,这个开机管理程序会加载操作系统的核心文件。 - -主要开机记录(MBR)中的开机管理程序提供以下功能: 选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。 - -下图中,第一扇区的主要开机记录(MBR)中的开机管理程序提供了两个选单: M1、M2,M1 指向了 Windows 操作系统,而 M2 指向其它分区的启动扇区,里面包含了另外一个开机管理程序,提供了一个指向 Linux 的选单。 - -安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉主要开机记录(MBR),而 Linux 可以选择将开机管理程序安装在主要开机记录(MBR)或者其它分区的启动扇区,并且可以设置开机管理程序的选单。 - -#### UEFI - -BIOS 不可以读取 GPT 分区表,而 UEFI 可以。 - -## 文件系统 - -### 分区与文件系统 - -对分区进行格式化是为了在分区上建立文件系统。一个分区通常只能格式化为一个文件系统,但是磁盘阵列等技术可以将一个分区格式化为多个文件系统。 - -### 组成 - -最主要的几个组成部分如下: - -- inode: 一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号; -- block: 记录文件的内容,文件太大时,会占用多个 block。 -- superblock: 记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等; -- block bitmap: 记录 block 是否被使用的位域。 - -### 文件读取 - -对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。 - -### 磁盘碎片 - -指一个文件内容所在的 block 过于分散。 - -### Block - -在 Ext2 文件系统中所支持的 block 大小有 1K,2K 及 4K 三种,不同的大小限制了单个文件和文件系统的最大大小。 - -一个 block 只能被一个文件所使用,未使用的部分直接浪费了。因此如果需要存储大量的小文件,那么最好选用比较小的 block。 - -### inode - -inode 具体包含以下信息: - -- 权限 (read/write/excute); -- 拥有者与群组 (owner/group); -- 容量; -- 建立或状态改变的时间 (ctime); -- 最近一次的读取时间 (atime); -- 最近修改的时间 (mtime); -- 定义文件特性的旗标 (flag),如 SetUID...; -- 该文件真正内容的指向 (pointer)。 - -inode 具有以下特点: - -- 每个 inode 大小均固定为 128 bytes (新的 ext4 与 xfs 可设定到 256 bytes); -- 每个文件都仅会占用一个 inode。 - -inode 中记录了文件内容所在的 block 编号,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用是指,让 inode 记录的引用 block 块记录引用信息。 - -### 目录 - -建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。 - -可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。 - -### 日志 - -如果突然断电,那么文件系统会发生错误,例如断电前只修改了 block bitmap,而还没有将数据真正写入 block 中。 - -ext3/ext4 文件系统引入了日志功能,可以利用日志来修复文件系统。 - -### 挂载 - -挂载利用目录作为文件系统的进入点,也就是说,进入目录之后就可以读取文件系统的数据。 - -### 目录配置 - -为了使不同 Linux 发行版本的目录结构保持一致性,Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下: - -- / (root, 根目录) -- /usr (unix software resource): 所有系统默认软件都会安装到这个目录; -- /var (variable): 存放系统或程序运行过程中的数据文件。 - -## 文件 - -### 文件属性 - -用户分为三种: 文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。 - -使用 ls 查看一个文件时,会显示一个文件的信息,例如 `drwxr-xr-x. 3 root root 17 May 6 00:14 .config`,对这个信息的解释如下: - -- drwxr-xr-x: 文件类型以及权限,第 1 位为文件类型字段,后 9 位为文件权限字段 -- 3: 链接数 -- root: 文件拥有者 -- root: 所属群组 -- 17: 文件大小 -- May 6 00:14: 文件最后被修改的时间 -- .config: 文件名 - -常见的文件类型及其含义有: - -- d: 目录 -- -: 文件 -- l: 链接文件 - -9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。 - -文件时间有以下三种: - -- modification time (mtime): 文件的内容更新就会更新; -- status time (ctime): 文件的状态(权限、属性)更新就会更新; -- access time (atime): 读取文件时就会更新。 - -### 文件与目录的基本操作 - -#### ls - -列出文件或者目录的信息,目录的信息就是其中包含的文件。 - -```bash -## ls [-aAdfFhilnrRSt] file|dir --a : 列出全部的文件 --d : 仅列出目录本身 --l : 以长数据串行列出,包含文件的属性与权限等等数据 - -``` - -#### cd - -更新当前目录 - -`cd [相对路径或者绝对路径]` - -#### mkdir - -创建目录 - -```bash -## mkdir [-mp] 目录名称 --m : 配置目录权限 --p : 递归创建目录 -``` - -#### rmdir - -删除目录,目录必须为空 - -```bash -rmdir [-p] 目录名称 --p : 递归删除目录 -``` - -#### touch - -更新文件时间或者建立新文件 - -```bash -## touch [-acdmt] filename --a : 更新 atime --c : 更新 ctime,若该文件不存在则不建立新文件 --m : 更新 mtime --d : 后面可以接更新日期而不使用当前日期,也可以使用 --date="日期或时间" --t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm] - -``` - -#### cp - -复制文件 - -如果源文件有两个以上,则目的文件一定要是目录才行。 - -```bash -cp [-adfilprsu] source destination --a : 相当于 -dr --preserve=all 的意思,至于 dr 请参考下列说明 --d : 若来源文件为链接文件,则复制链接文件属性而非文件本身 --i : 若目标文件已经存在时,在覆盖前会先询问 --p : 连同文件的属性一起复制过去 --r : 递归持续复制 --u : destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制 ---preserve=all : 除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了 - -``` - -#### rm - -删除文件 - -```bash -## rm [-fir] 文件或目录 --r : 递归删除 -``` - -#### mv - -移动文件 - -```bash -## mv [-fiu] source destination -## mv [options] source1 source2 source3 .... directory --f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖 -``` - -### 修改权限 - -可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1,即每个权限对应的数字权值为 r : 4、w : 2、x : 1。 - -`## chmod [-R] xyz dirname/filename` - -示例: 将 .bashrc 文件的权限修改为 -rwxr-xr--。 - -`## chmod 754 .bashrc` - -也可以使用符号来设定权限。 - -```bash -## chmod [ugoa] [+-=] [rwx] dirname/filename -- u: 拥有者 -- g: 所属群组 -- o: 其他人 -- a: 所有人 -- +: 添加权限 -- -: 移除权限 -- =: 设定权限 -``` - -示例: 为 .bashrc 文件的所有用户添加写权限。 - -`## chmod a+w .bashrc` - -### 文件默认权限 - -- 文件默认权限: 文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw- 。 -- 目录默认权限: 目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。 - -可以通过 umask 设置或者查看文件的默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r--。 - -### 目录的权限 - -文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。 - -目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。 - -### 链接 - -```bash -## ln [-sf] source_filename dist_filename --s : 默认是 hard link,加 -s 为 symbolic link --f : 如果目标文件存在时,先删除目标文件 -``` - -### 实体链接 - -在目录下创建一个条目,记录着文件名与 inode 编号,这个 inode 就是源文件的 inode。 - -删除任意一个条目,文件还是存在,只要引用数量不为 0。 - -有以下限制: 不能跨越文件系统、不能对目录进行链接。 - -```bash -## ln /etc/crontab . -## ll -i /etc/crontab crontab -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab -``` - -### 符号链接 - -符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 Windows 的快捷方式。 - -当源文件被删除了,链接文件就打不开了。 - -可以为目录建立链接。 - -```bash -## ll -i /etc/crontab /root/crontab2 -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab -53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab -``` - -### 获取文件内容 - -#### cat - -取得文件内容。 - -```bash -## cat [-AbEnTv] filename --n : 打印出行号,连同空白行也会有行号,-b 不会 -``` - -#### tac - -是 cat 的反向操作,从最后一行开始打印。 - -#### more - -和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。 - -#### less - -和 more 类似,但是多了一个向前翻页的功能。 - -#### head - -取得文件前几行。 - -```bash -## head [-n number] filename --n : 后面接数字,代表显示几行的意思 -``` - -#### tail - -是head的反向操作,之水取得是后几行 - -#### od - -以字符或者十六进制的形式显示二进制文件。 - -### 指令与文件搜索 - -#### which - -指令搜索 - -```bash -## which [-a] command --a : 将所有指令列出,而不是只列第一个 -``` - -#### whereis - -文件搜索。速度比较快,因为它只搜索几个特定的目录。 - -```bash -## whereis [-bmsu] dirname/filename -``` - -#### locate - -文件搜索。可以用关键字或者正则表达式进行搜索。 - -locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。 - -```bash -## locate [-ir] keyword --r: 正则表达式 -``` - -#### find - -文件搜索。可以使用文件的属性和权限进行搜索。 - -```bash -## find [basedir] [option] -example: find . -name "shadow*" -``` - -## 压缩与打包 - -### 压缩文件名 - -#### gzip - -gzip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。 - -经过 gzip 压缩过,源文件就不存在了。 - -有 9 个不同的压缩等级可以使用。 - -可以使用 zcat、zmore、zless 来读取压缩文件的内容。 - -```bash -$ gzip [-cdtv#] filename --c : 将压缩的数据输出到屏幕上 --d : 解压缩 --t : 检验压缩文件是否出错 --v : 显示压缩比等信息 --## : ## 为数字的意思,代表压缩等级,数字越大压缩比越高,默认为 6 -``` - -#### bzip2 - -提供比 gzip 更高的压缩比。 - -查看命令: bzcat、bzmore、bzless、bzgrep。 - -#### xz - -提供比 bzip2 更佳的压缩比。 - -可以看到,gzip、bzip2、xz 的压缩比不断优化。不过要注意的是,压缩比越高,压缩的时间也越长。 - -查看命令: xzcat、xzmore、xzless、xzgrep。 - -### 打包 - -压缩指令只能对一个文件进行压缩,而打包能够将多个文件打包成一个大文件。tar 不仅可以用于打包,也可以使用 gip、bzip2、xz 将打包文件进行压缩。 - -```bash -$ tar [-z|-j|-J] [cv] [-f 新建的 tar 文件] filename... ==打包压缩 -$ tar [-z|-j|-J] [tv] [-f 已有的 tar 文件] ==查看 -$ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩 --z : 使用 zip; --j : 使用 bzip2; --J : 使用 xz; --c : 新建打包文件; --t : 查看打包文件里面有哪些文件; --x : 解打包或解压缩的功能; --v : 在压缩/解压缩的过程中,显示正在处理的文件名; --f : filename: 要处理的文件; --C 目录 : 在特定目录解压缩。 -``` - -| 使用方式 | 命令 | -| :------: | ----------------------------------------------------- | -| 打包压缩 | tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称 | -| 查 看 | tar -jtv -f filename.tar.bz2 | -| 解压缩 | tar -jxv -f filename.tar.bz2 -C 要解压缩的目录 | - -## bash - -可以通过 Shell 请求内核提供服务,Bash 正是 Shell 的一种。 - -### 特性 - -- 命令历史:记录使用过的命令 -- 命令与文件补全:快捷键:tab -- shell scripts -- 通配符: 例如 ls -l /usr/bin/X* 列出 /usr/bin 下面所有以 X 开头的文件 - -### 变量操作 - -对一个变量赋值直接使用 =。 - -对变量取用需要在变量前加上 $ ,也可以用 ${} 的形式; - -输出变量使用 echo 命令。 - -```bash -$ x=abc -$ echo $x -$ echo ${x} -``` - -变量内容如果有空格,必须使用双引号或者单引号。 - -- 双引号内的特殊字符可以保留原本特性,例如 x="lang is $LANG",则 x 的值为 lang is zh_TW.UTF-8; -- 单引号内的特殊字符就是特殊字符本身,例如 x='lang is $LANG',则 x 的值为 lang is $LANG。 - -可以使用 `指令` 或者 $(指令) 的方式将指令的执行结果赋值给变量。例如 version=$(uname -r),则 version 的值为 4.15.0-22-generic。 - -可以使用 `指令` 或者 $(指令) 的方式将指令的执行结果赋值给变量。例如 version=$(uname -r),则 version 的值为 4.15.0-22-generic。 - -Bash 的变量可以声明为数组和整数数字。注意数字类型没有浮点数。如果不进行声明,默认是字符串类型。变量的声明使用 declare 命令: - -```bash -$ declare [-aixr] variable --a : 定义为数组类型 --i : 定义为整数类型 --x : 定义为环境变量 --r : 定义为 readonly 类型 -``` - -对数组操作 - -```bash -$ array[1]=a -$ array[2]=b -$ echo ${array[1]} -``` - -## 正则 - -g/re/p(globally search a regular expression and print),使用正则表示式进行全局查找并打印。 - -```bash -$ grep [-acinv] [--color=auto] 搜寻字符串 filename --c : 统计个数 --i : 忽略大小写 --n : 输出行号 --v : 反向选择,也就是显示出没有 搜寻字符串 内容的那一行 ---color=auto : 找到的关键字加颜色显示 -``` - -示例: 把含有 the 字符串的行提取出来(注意默认会有 --color=auto 选项,因此以下内容在 Linux 中有颜色显示 the 字符串) - -```bash -$ grep -n 'the' regular_express.txt -8:I can't finish the test. -12:the symbol '*' is represented as start. -15:You are the best is mean you are the no. 1. -16:The world Happy is the same with "glad". -18:google is the best tools for search keyword -``` - -## 进程管理 - -### 查看进程 - -#### ps - -查看某个时间点的进程信息 - -示例一: 查看自己的进程`ps -l` - -示例二: 查看系统所有进程`ps aux` - -示例三: 查看特定的进程`ps aux | grep python` - -#### top - -实时显示进程信息 - -示例: 两秒钟刷新一次`top -d 2` - -#### pstree - -查看进程树 - -示例: 查看所有进程树`pstree -A` - -#### netstat - -查看占用端口的进程 - -示例: 查看特定端口的进程`netstat -anp | grep port` - -### 孤儿进程 - -一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。 - -孤儿进程将被init进程(进程号为1)所收养,并有init进程对它们完成状态收集工作。 - -由于孤儿进程会被init进程收养,所以孤儿进程不会对系统造成危害。 - -### 僵尸进程 - -一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过wait()或者waitpid()获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用wait()或waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。 - -僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。 - -系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。 - -要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。 \ No newline at end of file diff --git a/Interview/mianjing/jingdong/jingdong01.md b/Interview/mianjing/jingdong/jingdong01.md deleted file mode 100644 index abe66525..00000000 --- a/Interview/mianjing/jingdong/jingdong01.md +++ /dev/null @@ -1,190 +0,0 @@ -## 京东Java实习一面电话面 -- [原链接](https://www.nowcoder.com/discuss/414511) - -### Java集合中线程安全的有哪些? -- Vector->SynchronizedList->CopyOnWriteArrayList -- SynchronizedSet->CopyOnWriteArraySet -- SynchronizedMap->ConcurrentHashMap - -### 线程的创建方式 -- 继承Thread类创建线程 -```java -public class ThreadTest extends Thread{ - //重写run()方法,run()方法的方法体是线程执行体 - public void run(){ - for(int i=0;i<5;i++){ - //使用线程的getName()方法可以直接获取当前线程的名称 - System.out.println(this.getName() + " " + i); - } - } - - public static void main(String[] args){ - //输出Java程序运行时默认运行的主线程名称 - System.out.println(Thread.currentThread().getName()); - //创建第一个线程并开始执行 - new ThreadTest().start(); - //创建第二个线程并开始执行 - new ThreadTest().start(); - } -} -``` -- 实现Runnable接口创建线程 -```java -public class ThreadTest implements Runnable{ - private int i; - //重写run()方法,run()方法的方法体是线程执行体 - public void run(){ - //实现Runnable接口时,只能使用如下方法获取线程名 - System.out.println(Thread.currentThread().getName() + " " + i); - i++; - } - - public static void main(String[] args){ - ThreadTest tt = new ThreadTest(); - //创建第一个线程并开始执行 - //输出 新线程1 0 - new Thread(tt,"新线程1").start(); - //创建第二个线程并开始执行 - //输出 新线程2 1 - new Thread(tt,"新线程2").start(); - //使用Lambda表达式创建Runnable对象 - new Thread(()->{ - System.out.print("hello"); - System.out.println("'world"); - }).start(); - } -} -``` -- 使用Callable和Future创建线程 -```java -public class ThreadTest { - public static void main(String[] args){ - //使用FutureTask来包装Callable对象 - //使用Lambda表达式来创建Callable对象 - FutureTask task = new FutureTask<>(()->{ - System.out.println(Thread.currentThread().getName() + " " + "开始执行任务!"); - return 0; - }); - //实质还是以Callable对象来创建并启动线程 - //输出 新线程 开始执行任务! - new Thread(task,"新线程").start(); - try{ - //获取线程的返回值 - //输出 0 - System.out.print(task.get()); - }catch (Exception ex){ - ex.printStackTrace(); - } - - } -} -``` -- 使用线程池例如用Executor框架 -```java -public class ThreadTest { - public static void main(String[] args){ - ExecutorService executorService = Executors.newFixedThreadPool(1); - executorService.submit(()->{ - System.out.println(Thread.currentThread().getName()); - }); - } -} -``` - -### 堆排序的实现过程(唯一的一道算法题) -```java -public class HeapSort { - public static void heapSort(int[] arr){ - if (arr == null || arr.length < 2) return; - for (int i = 0; i < arr.length; i++) { - heapInsert(arr, i); // 依次从0~i形成大根堆 - } - int heapSize = arr.length; - swap(arr, 0, --heapSize); - while (heapSize > 0) { - heapify(arr, 0, heapSize); - swap(arr, 0, --heapSize); - } - } - - public static void heapInsert(int[] arr, int index) { - // 建立大根堆 - while (arr[index] > arr[(index - 1) / 2]) { - swap(arr, index, (index - 1) / 2); - index = (index - 1) / 2; - } - } - - public static void heapify(int[] arr, int index, int heapSize) { - // 调整成大根堆 - int left = index * 2 + 1; - while (left < heapSize) { - int largest = left + 1 < heapSize && arr[left + 1] > arr[left] - ? left + 1 - : left; - largest = arr[largest] > arr[index] ? largest : index; - if (largest == index) { - break; // 自己已经是最大了,直接跳出 - } - swap(arr, largest, index); - index = largest; - left = index * 2 + 1; - } - } - - public static void swap(int[] arr, int x, int y) { - int temp = arr[x]; - arr[x] = arr[y]; - arr[y] = temp; - } -} -``` -![HeapSort](http://media.dreamcat.ink/uPic/HeapSort.png) - - -### 垃圾回收算法 各自优缺点 -- 标记-清除 -> 该算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题: -1. 效率问题 -2. 空间问题(标记清除后会产生大量的不连续碎片) - -- 标记-整理 -> 根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。 - -1. 慢 -2. 解决了碎片 - -- 复制 -> 为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。 - -1. 快 - -- 分代收集 -> 比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。 - -1. 根据特点选择不同场景下的收集算法,更加灵活。 - -### 数据库的三大范式 -#### 第一范式 -在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。 -所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。 -如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,第一范式就是无重复的列。 - -#### 第二范式 -第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。 -第二范式(2NF)要求数据库表中的每个实例或行必须可以被惟一地区分。 -为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。这个惟一属性列被称为主关键字或主键、主码。 -第二范式(2NF)要求实体的属性完全依赖于主关键字。 -所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。 -为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,第二范式就是非主属性非部分依赖于主关键字。 - -### 第三范式 -满足第三范式(3NF)必须先满足第二范式(2NF)。 -简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。 -例如,存在一个部门信息表,其中每个部门有部门编号(dept_id)、部门名称、部门简介等信息。 -那么在员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。 -如果不存在部门信息表,则根据第三范式(3NF)也应该构建它,否则就会有大量的数据冗余。简而言之,第三范式就是属性不依赖于其它非主属性。(我的理解是消除冗余) - -### 对象存储为json文件,不借助第三方jar包,说说思路。 -递归迭代 -- [参考](https://blog.csdn.net/qq_35893120/article/details/84288551?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-4&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-4) diff --git a/Interview/mianjing/jingdong/jingdong02.md b/Interview/mianjing/jingdong/jingdong02.md deleted file mode 100644 index 27e6b438..00000000 --- a/Interview/mianjing/jingdong/jingdong02.md +++ /dev/null @@ -1,299 +0,0 @@ -## 京东java实习面经 -- [原文链接](https://www.nowcoder.com/discuss/419373) - -## 一面 -### redis为什么可以这么快,持久化机制 -Redis性能如此高的原因,我总结了如下几点: -- 纯内存操作 -- 单线程 -- 高效的数据结构 -- 合理的数据编码 -- 其他方面的优化 - -在 Redis 中,常用的 5 种数据结构和应用场景如下: -- **String**:缓存、计数器、分布式锁等。 -- **List**:链表、队列、微博关注人时间轴列表等。 -- **Hash**:用户信息、Hash 表等。 -- **Set**:去重、赞、踩、共同好友等。 -- **Zset**:访问量排行榜、点击量排行榜等。 - -[https://zhuanlan.zhihu.com/p/57089960](https://zhuanlan.zhihu.com/p/57089960) - -### redis持久化机制 -Redis支持持久化,而且支持两种不同的持久化操作 -- **快照**(snapshotting,RDB) -- **只追加文件**(append-only file,AOF) - -#### RDB -> Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行 备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性 能),还可以将快照留在原地以便重启服务器的时候使用。 - -#### AOF -> 与快照持久化相比,AOF持久化的实时性更好,因此已成为主流的持久化方案。 -``` -appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度 -appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘 -appendfsync no #让操作系统决定何时进行同步 -``` - -#### Redis4.0 -> Redis 4.0 开始支持 RDB 和 AOF 的混合持久化 - -### spring中的ioc运行机制 -```java -public void refresh() throws BeansException, IllegalStateException { - // 来个锁,不然 refresh() 还没结束,你又来个启动或销毁容器的操作,那不就乱套了嘛 - synchronized (this.startupShutdownMonitor) { - - // 准备工作,记录下容器的启动时间、标记“已启动”状态、处理配置文件中的占位符 - prepareRefresh(); - - // 这步比较关键,这步完成后,配置文件就会解析成一个个 Bean 定义,注册到 BeanFactory 中, - // 当然,这里说的 Bean 还没有初始化,只是配置信息都提取出来了, - // 注册也只是将这些信息都保存到了注册中心(说到底核心是一个 beanName-> beanDefinition 的 map) - ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); - - // 设置 BeanFactory 的类加载器,添加几个 BeanPostProcessor,手动注册几个特殊的 bean - // 这块待会会展开说 - prepareBeanFactory(beanFactory); - - try { - // 【这里需要知道 BeanFactoryPostProcessor 这个知识点,Bean 如果实现了此接口, - // 那么在容器初始化以后,Spring 会负责调用里面的 postProcessBeanFactory 方法。】 - - // 这里是提供给子类的扩展点,到这里的时候,所有的 Bean 都加载、注册完成了,但是都还没有初始化 - // 具体的子类可以在这步的时候添加一些特殊的 BeanFactoryPostProcessor 的实现类或做点什么事 - postProcessBeanFactory(beanFactory); - // 调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法 - invokeBeanFactoryPostProcessors(beanFactory); - - // 注册 BeanPostProcessor 的实现类,注意看和 BeanFactoryPostProcessor 的区别 - // 此接口两个方法: postProcessBeforeInitialization 和 postProcessAfterInitialization - // 两个方法分别在 Bean 初始化之前和初始化之后得到执行。注意,到这里 Bean 还没初始化 - registerBeanPostProcessors(beanFactory); - - // 初始化当前 ApplicationContext 的 MessageSource,国际化这里就不展开说了,不然没完没了了 - initMessageSource(); - - // 初始化当前 ApplicationContext 的事件广播器,这里也不展开了 - initApplicationEventMulticaster(); - - // 从方法名就可以知道,典型的模板方法(钩子方法), - // 具体的子类可以在这里初始化一些特殊的 Bean(在初始化 singleton beans 之前) - onRefresh(); - - // 注册事件监听器,监听器需要实现 ApplicationListener 接口。这也不是我们的重点,过 - registerListeners(); - - // 重点,重点,重点 - // 初始化所有的 singleton beans - //(lazy-init 的除外) - finishBeanFactoryInitialization(beanFactory); - - // 最后,广播事件,ApplicationContext 初始化完成 - finishRefresh(); - } - - catch (BeansException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Exception encountered during context initialization - " + - "cancelling refresh attempt: " + ex); - } - - // Destroy already created singletons to avoid dangling resources. - // 销毁已经初始化的 singleton 的 Beans,以免有些 bean 会一直占用资源 - destroyBeans(); - - // Reset 'active' flag. - cancelRefresh(ex); - - // 把异常往外抛 - throw ex; - } - - finally { - // Reset common introspection caches in Spring's core, since we - // might not ever need metadata for singleton beans anymore... - resetCommonCaches(); - } - } -} -``` -![spring-ioc](http://media.dreamcat.ink/uPic/spring-ioc.png) - -#### 大概总结 -1. Spring容器在启动的时候,先会保存所有注册进来的Bean的定义信息; - 1. xml注册bean - 2. 注解注册bean;@Service、@Component、@Bean,xxx - 3. Spring容器会在合适时机创建这些Bean - 1. 用到这个bean的创建;利用getBean创建bean;创建好以后保存在容器中。 - 2. 统一创建剩下所有的bean的时候;finishBeanFactoryInitialization(); - 4. 后置处理器;BeanPostProcessor - 1. 每一个bean创建完成,都会使用各种后置处理器进行处理,来增强bena的功能,比如: - - AutowiredAnnotationBeanPostProcessor:处理自动注入 - - AnnotationAwareAspectJAutoProxyCreator:来做AOP功能; - - xxx - 5. 事件驱动模型: - - ApplicationListener:事件监听; - - ApplicationEventMulticaster:事件派发; - -- [http://dreamcat.ink/2020/01/31/spring-springaop-yuan-ma-fen-xi/](http://dreamcat.ink/2020/01/31/spring-springaop-yuan-ma-fen-xi/) -- [https://javadoop.com/post/spring-ioc](https://javadoop.com/post/spring-ioc) - -### spring中aop的原理 -> AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。 -> Spring AOP就是基于动态代理的 - -大致流程: -1. @EnableAspectJAutoProxy 开启AOP功能 -2. @EnableAspectJAutoProxy 会给容器中注册一个组件 AnnotationAwareAspectJAutoProxyCreator -3. AnnotationAwareAspectJAutoProxyCreator是一个后置处理器 -4. 容器的创建流程: - 1. registerBeanPostProcessors()注册后置处理器,创建AnnotationAwareAspectJAutoProxyCreator对象 - 2. finishBeanFactoryInitialization()初始化剩下的单实例bean - 1. 创建业务逻辑组件和切面组件 - 2. AnnotationAwareAspectJAutoProxyCreator拦截组件的创建过程 - 3. 组件创建完之后,判断组件是否需要增强,如果是:切面的通知方法,包装成增强器(advisor),给业务逻辑组件创建一个代理对象(cglib) -5. 执行目标方法: - 1. 代理对象执行目标方法 - 2. CglibAopProxy.intercept() - 1. 得到目标方法的拦截器链(增强器包装成拦截器MethodInterceptor) - 2. 利用拦截器的链式机制,依次进入每一个拦截器进行执行 - 3. 效果: - - 正常执行:前置通知->目标方法->后置通知->返回通知 - - 出现异常:前置通知->目标方法->后置通知->异常通知 - -### spring中的单例模式和设计模式中的单例模式有什么区别 -- 单例模式是指在一个JVM进程中仅有一个实例,而Spring单例是指一个Spring Bean容器(ApplicationContext)中仅有一个实例。 -- 意味着: - - 在一个JVM进程中(理论上,一个运行的JAVA程序就必定有自己一个独立的JVM)仅有一个实例,于是无论在程序中的何处获取实例,始终都返回同一个对象。 - - 与此相比,Spring的单例Bean是与其容器(ApplicationContext)密切相关的,所以在一个JVM进程中,如果有多个Spring容器,即使是单例bean,也一定会创建多个实例。 - -## 二面 - -### redis为什么这么快 -略 - -### 介绍redis运行机制 -> redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。 - -文件事件处理器的结构包含 4 个部分: -- 多个 socket -- IO多路复用程序 -- 文件事件分派器 -- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器) - -多个socket可能会并发产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个socket,会将socket产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。 - -### hashmap多线程下安全问题,如何解决 -比如死循环分析: -- [https://zhuanlan.zhihu.com/p/67915754](https://zhuanlan.zhihu.com/p/67915754) - -### wait()和sleep()区别 -- 两者最主要的区别在于:**sleep方法没有释放锁,而wait方法释放了锁**。 -- 两者都可以暂停线程的执行。 -- wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。 -- wait方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify或者notifyAll方法。sleep方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。 - -### synchronized和volatile区别 -synchronized的修饰范围: -- 修饰一个代码块 -- 修饰一个方法 -- 修饰一个类 -- 修饰一个静态的方法 - -> 个人的理解是:因为同步关键字Synchronized不能修饰变量(不能直接使用synchronized声明一个变量),不能使变量得到共享,故引入了轻量级的Volatie - -volatile: -- volatile可以修饰变量,共享变量。 -- 保障了共享变量对所有线程的可见性。即可保证在线程A将其修改时,线程B可以立刻得到。 -- 禁止指令重排序 - -### 线程池的参数定义,大小 -先放一波变量定义 -```java - /** - * 用给定的初始参数创建一个新的ThreadPoolExecutor。 - */ - public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 - int maximumPoolSize,//线程池的最大线程数 - long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间 - TimeUnit unit,//时间单位 - BlockingQueue workQueue,//任务队列,用来储存等待执行任务的队列 - ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可 - RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 - ) { - if (corePoolSize < 0 || - maximumPoolSize <= 0 || - maximumPoolSize < corePoolSize || - keepAliveTime < 0) - throw new IllegalArgumentException(); - if (workQueue == null || threadFactory == null || handler == null) - throw new NullPointerException(); - this.corePoolSize = corePoolSize; - this.maximumPoolSize = maximumPoolSize; - this.workQueue = workQueue; - this.keepAliveTime = unit.toNanos(keepAliveTime); - this.threadFactory = threadFactory; - this.handler = handler; - } -``` -![线程池参数](http://media.dreamcat.ink/uPic/线程池参数.png) - -ThreadPoolExecutor 3 个最重要的参数: -- `corePoolSize`:核心线程数定义了最小可以同时运行的线程数量。 -- `maximumPoolSize`:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- `workQueue`:当新任务来的时候会先判断当前运行的线程数量是否达到了核心线程数,如果达到的话,信任就会被从存放到队列中中。 - -ThreadPoolExecutor 其他常见参数: -- `keepAliveTime`:当线程池中的线程数量大于`corePoolSize`的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了`keepAliveTime`才会被回收销毁 -- `unit`:`keepAliveTime`参数的时间单位 -- `threadFactory`:executor创建新线程的时候会用到。 -- `handle`:饱和策略。关于饱和策略下面单独介绍 -如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor`定义一些策略: -- `ThreadPoolExecutor.AbortPolicy`:抛出`RejectedExecutionException`来拒绝新任务的处理。 -- `ThreadPoolExecutor.CallerRunsPolicy`:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -- `ThreadPoolExecutor.DiscardPolicy`: 不处理新任务,直接丢弃掉。 -- `ThreadPoolExecutor.DiscardOldestPolicy`: 此策略将丢弃最早的未处理的任务请求。 - -大小: -> 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。 -> 但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。 - -- **CPU 密集型任务(N+1)**: 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 -- **I/O 密集型任务(2N)**: 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。 - -### java为何设计成单继承 -可以举个例子: -> 在这里有个A类,我们又编写了两个类B类和C类,并且B类和C类分别继承了A类,并且对A类的同一个方法进行了覆盖。 -> 如果此时我们再次编写了一个D类,并且D类以多继承的方式同时集成了B类和C类,那么D类也会继承B类和C类从A类中重载的方法,那么问题来了,D类也开始犯迷糊了,我到底应该哪个继承哪个类中的方法呢, -> 因为类是结构性的,这样就会造成结构上的混乱。这就是多继承的菱形继承问题。 - -可以实现对个接口: -> Java接口是行为性的,也就是说它只是定义某个行为的名称,而具体的行为的实现是集成接口的类实现的, -> 因此就算两个接口中定义了两个名称完全相同的方法,当某个类去集成这两个接口时, -> 类中也只会有一个相应的方法,这个方法的具体实现是这个类来进行编写的,所以并不会出现结构混乱的情况。 - -### 继承和组合有啥区别 -继承: -> 继承是Is a 的关系,比如说Student继承Person,则说明Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。 - -- 父类的内部细节对子类是可见的。 -- 子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。 -- 子类与父类是一种高耦合,违背了面向对象思想。 -- 继承关系最大的弱点是打破了封装,子类能够访问父类的实现细节,子类与父类之间紧密耦合,子类缺乏独立性,从而影响了子类的可维护性。 -- 不支持动态继承。在运行时,子类无法选择不同的父类。 - -组合: -- 不破坏封装,整体类与局部类之间松耦合,彼此相对独立。 -- 具有较好的可扩展性。 -- 支持动态组合。在运行时,整体对象可以选择不同类型的局部对象。 - -组合是has a的关系, 继承是is a的关系 -> 引用一句老话应该更能分清继承和组合的区别:组合可以被说成“我请了个老头在我家里干活” ,继承则是“我父亲在家里帮我干活"。 - -总结: -- 除非考虑使用多态,否则优先使用组合。 -- 要实现类似”多重继承“的设计的时候,使用组合。 -- 要考虑多态又要考虑实现“多重继承”的时候,使用组合+接口。 - - diff --git a/Interview/mianjing/jingdong/jingdong03.md b/Interview/mianjing/jingdong/jingdong03.md deleted file mode 100644 index c5c2cf8e..00000000 --- a/Interview/mianjing/jingdong/jingdong03.md +++ /dev/null @@ -1,196 +0,0 @@ -## 京东一,二面面经,许愿offer!!! -- [原文](https://www.nowcoder.com/discuss/419687) - -### hashmap数据结构 -#### put -**1.7** -直接上图: -![hashmap-put-1.7](http://media.dreamcat.ink/uPic/hashmap-put-1.7.png) -**1.8** -直接上图: -![hashmap-put-1.8](http://media.dreamcat.ink/uPic/hashmap-put-1.8.png) - -#### 100个结点,hashmap初始容量多少合适? -> Map在使用过程中不断的往里面存放数据,当数量达到了16 * 0.75 = 12就需要将当前16的容量进行扩容,而扩容这个过程涉及到rehash(重新哈希)、复制数据等操作,所有非常消耗性能。 - -#### 为什么1.8要引入红黑树,红黑树是平衡树吗? -- 当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。 -- 是 - -#### 红黑树为什么要分红节点和黑节点? -> 红黑树的查询性能略微逊色于AVL树,因为他比avl树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的avl树最多多一次比较,但是,红黑树在插入和删除上完爆avl树,avl树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于avl树为了维持平衡的开销要小得多 - -### JVM的内存模型 -1.8之前: -![JVM内存模型-1.8之前](http://media.dreamcat.ink/uPic/JVM内存模型-1.8之前.png) - -1.8: -![JVM内存模型-1.8](http://media.dreamcat.ink/uPic/JVM内存模型-1.8.png) - -按照1.8总结: -**线程私有**: -- 程序计数器 -- 虚拟机栈 -- 本地方法栈 - -**线程共享**: -- 堆 -- 直接内存 - -**程序计数器**: -- 字节码解释器通过改变程序计数器来一次读取命令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程切换回来的时候能够知道线程上次运行到哪儿。 - -> 注意:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束儿死亡。 - -**Java虚拟机栈**: -> 与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。 - -Java内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分: -- 局部变量表 -- 操作数栈: - - 8大基本类型 - - 对象引用:可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置 -- 动态链接 -- 方法出口 - -Java虚拟机栈会出现两种异常:`StackOverFlowError`和`OutOfMemoryError`。 -- `StackOverFlowError`:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出`StackOverFlowError`异常。 -- `OutOfMemoryError`:若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛去`OutOfMemoryError`异常。 - -**本地方法栈**: -> 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 其他和Java虚拟机差不多的 - -**堆**: -> Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 -> Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。 - -![堆内部分区](https://user-gold-cdn.xitu.io/2020/3/31/17130c2c0c80892e?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) -上图所示的 eden 区、s0("From") 区、s1("To") 区都属于新生代,tentired 区属于老年代。 -大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s1("To"),并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。 -对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。经过这次GC后,Eden区和"From"区已经被清空。 -这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。 -不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到年老代中。 - -## 二面 -### Thread和Runnable开启线程有什么区别? -- 避免了Java单继承的局限性; -- 把线程代码和任务的代码分离,解耦合(解除线程代码和任务的代码模块之间的依赖关系)。代码的扩展性非常好; - -### 线程池 -```java - /** - * 用给定的初始参数创建一个新的ThreadPoolExecutor。 - */ - public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 - int maximumPoolSize,//线程池的最大线程数 - long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间 - TimeUnit unit,//时间单位 - BlockingQueue workQueue,//任务队列,用来储存等待执行任务的队列 - ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可 - RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 - ) { - if (corePoolSize < 0 || - maximumPoolSize <= 0 || - maximumPoolSize < corePoolSize || - keepAliveTime < 0) - throw new IllegalArgumentException(); - if (workQueue == null || threadFactory == null || handler == null) - throw new NullPointerException(); - this.corePoolSize = corePoolSize; - this.maximumPoolSize = maximumPoolSize; - this.workQueue = workQueue; - this.keepAliveTime = unit.toNanos(keepAliveTime); - this.threadFactory = threadFactory; - this.handler = handler; - } -``` -![线程池参数](http://media.dreamcat.ink/uPic/线程池参数.png) - -`ThreadPoolExecutor` 3 个最重要的参数: - -- `corePoolSize` : 核心线程数线程数定义了最小可以同时运行的线程数量。 -- `maximumPoolSize` : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 -- `workQueue`: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。 - -`ThreadPoolExecutor`其他常见参数: -- `keepAliveTime`:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁; -- `unit` : `keepAliveTime` 参数的时间单位。 -- `threadFactory` :`executor` 创建新线程的时候会用到。 -- `handler` :饱和策略。关于饱和策略下面单独介绍一下. - -`ThreadPoolTaskExecutor` 定义一些策略: -- `ThreadPoolExecutor.AbortPolicy`:抛出 `RejectedExecutionException来拒绝新任务的处理`。 -- `ThreadPoolExecutor.CallerRunsPolicy`:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。 -- `ThreadPoolExecutor.DiscardPolicy`: 不处理新任务,直接丢弃掉。 -- `ThreadPoolExecutor.DiscardOldestPolicy`: 此策略将丢弃最早的未处理的任务请求。 - -### new对象时的过程 -![Java创建对象的过程](http://media.dreamcat.ink/uPic/Java创建对象的过程.png) - -1. 类加载检查,虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 -2. 分配内存,在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 -3. 初始化零值,内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 -4. 设置对象头,初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 -5. 执行init方法,在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 - -**内存布局**: -在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域: -- 对象头 -- 实例数据 -- 对齐填充 -Hotspot 虚拟机的对象头包括两部分信息 -- 第一部分用于存储对象自身的自身运行时数据(哈希码、GC 分代年龄、锁状态标志等等) -- 另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 -实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。 - -> 对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 - -**对象的访问方式**: -建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式由虚拟机实现而定,目前主流的访问方式有 -- 使用句柄 -- 直接指针 - -### mysql的优化?索引??联合索引?? -#### 索引 -- 普通索引:仅加速查询 -- 唯一索引:加速查询+列值唯一(可以有null) -- 主键索引:加速查询+列值唯一(不可以有null)+表中只有一个 -- 组合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并 -- 全文索引:对文本的内容进行分词,进行搜索 -- 索引合并:使用多个单列索引组合搜索 -- 覆盖索引:select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖 - -#### 优化 -**索引优化**: -- 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性 -- 可以大大加快数据的检索速度,这也是创建索引的最主要的原因 -- 帮助服务器避免排序和临时表 -- 将随机IO变为顺序IO -- 可以加速表和表之间的连接,特别是咋实现数据的参考完整性方面的特别喲意义 - -**数据库表优化** -- 范式优化:比如消除冗余 -- 反范式优化:比如适当加冗余等(减少join) -- 限定数据的范围:务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内。 -- 读/写分离:经典的数据库拆分方案,主库负责写,从库负责读 -- 拆分表:分区将数据在物理上分隔开,不同分区的数据可以制定保存在处于不同磁盘上的数据文件里。这样,当对这个表进行查询时,只需要在表分区中进行扫描,而不必进行全表扫描,明显缩短了查询时间。 - -**联合索引** -> MySQL中的索引可以以一定顺序引用多个列,这种索引叫做联合索引一般的,一个联合索引是一个有序元组,其中各个元素均为数据表的一列。另外,单列索引可以看成联合索引元素数为1的特例。 - -索引匹配的最左原则具体是说,假如索引列分别为A,B,C,顺序也是A,B,C: -``` -- 那么查询的时候,如果查询【A】【A,B】 【A,B,C】,那么可以通过索引查询 -- 如果查询的时候,采用【A,C】,那么C这个虽然是索引,但是由于中间缺失了B,因此C这个索引是用不到的,只能用到A索引 -- 如果查询的时候,采用【B】 【B,C】 【C】,由于没有用到第一列索引,不是最左前缀,那么后面的索引也是用不到了 -- 如果查询的时候,采用范围查询,并且是最左前缀,也就是第一列索引,那么可以用到索引,但是范围后面的列无法用到索引 -``` - -### 5L,3L的杯子,如何装4L的水?? -5L,3L的杯子,如何装4L的水: -- 5L 桶装满水 -- 由 5L 桶向 3L 桶倒水 , 倒满为止 -- 把 3L 桶的水倒掉 , 由 5L 桶向 3L 桶倒水 -- 把 5L 桶装满 -- 由 5L 桶向 3L 桶倒水 , 倒满为止 diff --git a/Interview/mianjing/jingdong/jingdong04.md b/Interview/mianjing/jingdong/jingdong04.md deleted file mode 100644 index fbc667cc..00000000 --- a/Interview/mianjing/jingdong/jingdong04.md +++ /dev/null @@ -1,717 +0,0 @@ -## 京东Java春季实习 -- [原文链接](https://www.nowcoder.com/discuss/413662) - -## 一面 - -### hashcode和equals方法 -#### == -> 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。 - -#### equals -> 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: -- 情况1:类没有覆盖equals方法。则通过equals比较该类的两个对象时,等价于通过"=="比较这两个对象。 -- 情况2:类覆盖了equals方法。一般,我们都覆盖了equals方法来比较两个对象的内容呢是否相相等;若它们的内容相等,则返回true(即,认为这两个对象相等)。 -- String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。 -- 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前你引用。如果没有就在常量池中重新创建哪一个String对象。 - -#### hashcode -> 它的作用是获取哈希吗,也称为散列码,它实际上是返回一个int整数。这个哈希吗的作用是确定该对象在哈希表中的索引位置。hashcode定义在jdk的object.java中,这就意味着Java中的任何类都包涵hashcode函数。 - -#### 为什么要用hashcode -我们先以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode: -> 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较, -> 如果没有相符的hashcode,HashSet会假设对象没有重复出现。 -> 但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。 -> 如果两者相同,HashSet 就不会让其加入操作成功。 -> 如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。 - -### 接口和抽象类的区别 -1. 接口的方法默认是public,所有方法在接口中不能有实现(除了java8),而抽象类可以有非抽象的方法。 -2. 接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。 -3. 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过implement关键字扩展多个接口。 -4. 接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰) -5. 从设计层面来讲,抽象是对类的抽象,是一种能模板设计,而接口是对行为的抽象, 是一种行为的规范。 - -### 重写和重载的区别 - -#### 重载 -> 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 - -#### 重写 -> 重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。 -> 另外,如果父类方法访问修饰符为private则子类就不能重写该方法。 -> 也就是说方法提供的行为改变,而方法的外貌并没有改变。 - -### 集合类有哪些,有什么区别 -> 容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 - -#### Set -- TreeSet -> 基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如HashSet,HashSet查找的时间复杂为O(1),TreeSet则为O(logN)。 - -- HashSet -> 基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用Iterator遍历HashSet得到的结构是不确定的。 - -- LinkedHashSet -> 具有HashSet的查找效率,且内部使用双向链表维护元素的插入顺序。 - -#### List -- ArrayList -> 基于动态数组实现,支持随机访问。 - -- Vector -> 和ArrayList类似,但它是线程安全的。 - -- LinkedList -> 基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList还可以用作栈、队列和双向队列。 - -#### Queue -- LinkedList -> 可以用来实现双向队列。 - -- PriorityQueue -> 基于堆结构实现,可以用它实现优先队列。 - -#### Map -- TreeMap -> 基于红黑树实现 - -- HashMap -> 基于哈希表实现 - -- HashTable - - 和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。 - - 它是遗留类,不应该去使用它。 - - 现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁(1.8前)。 - -- LinkedHashMap -> 使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 - -### 如何实现多线程 -- 继承Thread类创建线程 -```java -public class ThreadTest extends Thread{ - //重写run()方法,run()方法的方法体是线程执行体 - public void run(){ - for(int i=0;i<5;i++){ - //使用线程的getName()方法可以直接获取当前线程的名称 - System.out.println(this.getName() + " " + i); - } - } - - public static void main(String[] args){ - //输出Java程序运行时默认运行的主线程名称 - System.out.println(Thread.currentThread().getName()); - //创建第一个线程并开始执行 - new ThreadTest().start(); - //创建第二个线程并开始执行 - new ThreadTest().start(); - } -} -``` -- 实现Runnable接口创建线程 -```java -public class ThreadTest implements Runnable{ - private int i; - //重写run()方法,run()方法的方法体是线程执行体 - public void run(){ - //实现Runnable接口时,只能使用如下方法获取线程名 - System.out.println(Thread.currentThread().getName() + " " + i); - i++; - } - - public static void main(String[] args){ - ThreadTest tt = new ThreadTest(); - //创建第一个线程并开始执行 - //输出 新线程1 0 - new Thread(tt,"新线程1").start(); - //创建第二个线程并开始执行 - //输出 新线程2 1 - new Thread(tt,"新线程2").start(); - //使用Lambda表达式创建Runnable对象 - new Thread(()->{ - System.out.print("hello"); - System.out.println("'world"); - }).start(); - } -} -``` -- 使用Callable和Future创建线程 -```java -public class ThreadTest { - public static void main(String[] args){ - //使用FutureTask来包装Callable对象 - //使用Lambda表达式来创建Callable对象 - FutureTask task = new FutureTask<>(()->{ - System.out.println(Thread.currentThread().getName() + " " + "开始执行任务!"); - return 0; - }); - //实质还是以Callable对象来创建并启动线程 - //输出 新线程 开始执行任务! - new Thread(task,"新线程").start(); - try{ - //获取线程的返回值 - //输出 0 - System.out.print(task.get()); - }catch (Exception ex){ - ex.printStackTrace(); - } - - } -} -``` - -- 使用线程池例如用Executor框架 -```java -public class ThreadTest { - public static void main(String[] args){ - ExecutorService executorService = Executors.newFixedThreadPool(1); - executorService.submit(()->{ - System.out.println(Thread.currentThread().getName()); - }); - } -} -``` - -### JVM内存分布 -#### 1.8之前 -![JVM内存模型-1.8之前](http://media.dreamcat.ink/uPic/JVM内存模型-1.8之前.png) - -#### 1.8及之后 -![JVM内存模型-1.8](http://media.dreamcat.ink/uPic/JVM内存模型-1.8.png) - -#### 大概总结 -线程私有: -- 程序计数器 -- 虚拟机栈 -- 本地方法栈 - -线程共享: -- 堆 -- 直接内存 - -##### 程序计数器 -- 字节码解释器通过改变程序计数器来一次读取命令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 -- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程切换回来的时候能够知道线程上次运行到哪 -> 注意:程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。 - -##### Java虚拟机栈 -> 与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的Java方法执行的内存模型,每次方法调用的数据都是通过栈传递的。 - -Java内存可以粗糙的区分为堆内存和栈内存,其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中的局部变量表部分: -- 局部变量表 -- 操作数栈: - - 8大基本类型 - - 对象引用:可能是一个指向对象其实地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置 -- 动态链接 -- 方法出口 - -Java虚拟机栈会出现两种异常:`StackOverFlowError`和`OutOfMemoryError`。 -- `StackOverFlowError`:若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的祖达深度的时候,就抛出`StackOverFlowError`异常。 -- `OutOfMemoryError`:若Java虚拟机栈的内存大小允许动态扩展,且线程请求栈时内存用完了,无法再动态扩展了,此时抛出`OutOfMemoryError`异常。 - -##### 本地方法栈 -> 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 其他和Java虚拟机差不多的 - -##### 堆 -> Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 - Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。 - -![堆内存分区](http://media.dreamcat.ink/uPic/堆内存分区.png) - -- 上图所示的 eden 区、s0("From") 区、s1("To") 区都属于新生代,tentired 区属于老年代。 -- 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s1("To"),并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。 -- 对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。经过这次GC后,Eden区和"From"区已经被清空。 -- 这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。 -- 不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到年老代中。 - -### 讲一讲Spring原理 -谈Spring原来,不得不谈Ioc了 -> IoC(Inverse of Control:控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理。IoC在其他语言中也有应用,并非Spring持有。 -> IoC容器是Spring用来实现IoC的载体,IC容器实际上就是个Map(key,value),Map中存放的是各种对象。 - -将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。 -这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 -IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 -在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。 -如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。 - -#### Spring中的ioc运行机制 -```java -public void refresh() throws BeansException, IllegalStateException { - // 来个锁,不然 refresh() 还没结束,你又来个启动或销毁容器的操作,那不就乱套了嘛 - synchronized (this.startupShutdownMonitor) { - - // 准备工作,记录下容器的启动时间、标记“已启动”状态、处理配置文件中的占位符 - prepareRefresh(); - - // 这步比较关键,这步完成后,配置文件就会解析成一个个 Bean 定义,注册到 BeanFactory 中, - // 当然,这里说的 Bean 还没有初始化,只是配置信息都提取出来了, - // 注册也只是将这些信息都保存到了注册中心(说到底核心是一个 beanName-> beanDefinition 的 map) - ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); - - // 设置 BeanFactory 的类加载器,添加几个 BeanPostProcessor,手动注册几个特殊的 bean - // 这块待会会展开说 - prepareBeanFactory(beanFactory); - - try { - // 【这里需要知道 BeanFactoryPostProcessor 这个知识点,Bean 如果实现了此接口, - // 那么在容器初始化以后,Spring 会负责调用里面的 postProcessBeanFactory 方法。】 - - // 这里是提供给子类的扩展点,到这里的时候,所有的 Bean 都加载、注册完成了,但是都还没有初始化 - // 具体的子类可以在这步的时候添加一些特殊的 BeanFactoryPostProcessor 的实现类或做点什么事 - postProcessBeanFactory(beanFactory); - // 调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法 - invokeBeanFactoryPostProcessors(beanFactory); - - // 注册 BeanPostProcessor 的实现类,注意看和 BeanFactoryPostProcessor 的区别 - // 此接口两个方法: postProcessBeforeInitialization 和 postProcessAfterInitialization - // 两个方法分别在 Bean 初始化之前和初始化之后得到执行。注意,到这里 Bean 还没初始化 - registerBeanPostProcessors(beanFactory); - - // 初始化当前 ApplicationContext 的 MessageSource,国际化这里就不展开说了,不然没完没了了 - initMessageSource(); - - // 初始化当前 ApplicationContext 的事件广播器,这里也不展开了 - initApplicationEventMulticaster(); - - // 从方法名就可以知道,典型的模板方法(钩子方法), - // 具体的子类可以在这里初始化一些特殊的 Bean(在初始化 singleton beans 之前) - onRefresh(); - - // 注册事件监听器,监听器需要实现 ApplicationListener 接口。这也不是我们的重点,过 - registerListeners(); - - // 重点,重点,重点 - // 初始化所有的 singleton beans - //(lazy-init 的除外) - finishBeanFactoryInitialization(beanFactory); - - // 最后,广播事件,ApplicationContext 初始化完成 - finishRefresh(); - } - - catch (BeansException ex) { - if (logger.isWarnEnabled()) { - logger.warn("Exception encountered during context initialization - " + - "cancelling refresh attempt: " + ex); - } - - // Destroy already created singletons to avoid dangling resources. - // 销毁已经初始化的 singleton 的 Beans,以免有些 bean 会一直占用资源 - destroyBeans(); - - // Reset 'active' flag. - cancelRefresh(ex); - - // 把异常往外抛 - throw ex; - } - - finally { - // Reset common introspection caches in Spring's core, since we - // might not ever need metadata for singleton beans anymore... - resetCommonCaches(); - } - } -} -``` -![spring-ioc](http://media.dreamcat.ink/uPic/spring-ioc.png) - -大概总结: -1. Spring容器在启动的时候,先会保存所有注册进来的Bean的定义信息; - 1. xml注册bean - 2. 注解注册bean;@Service、@Component、@Bean,xxx - 3. Spring容器会在合适时机创建这些Bean - 1. 用到这个bean的创建;利用getBean创建bean;创建好以后保存在容器中。 - 2. 统一创建剩下所有的bean的时候;finishBeanFactoryInitialization(); - 4. 后置处理器;BeanPostProcessor - 1. 每一个bean创建完成,都会使用各种后置处理器进行处理,来增强bena的功能,比如: - - AutowiredAnnotationBeanPostProcessor:处理自动注入 - - AnnotationAwareAspectJAutoProxyCreator:来做AOP功能; - 5. 事件驱动模型: - - ApplicationListener:事件监听; - - ApplicationEventMulticaster:事件派发; - - -### mysql的引擎,索引结构,索引有哪些类型? -#### 数据库引擎innodb与myisam的区别 -InnoDB: -- 是MySQL默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其他存储引擎。 -- 实现了四个标准的隔离级别,默认级别是可重复读。在可重复读隔离级别下,通过多版本并发控制(MVCC)+间隙锁(Next-Key Locking)防止幻影读。 -- 主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。 -- 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。 -- 支持真正的在线热备份。其他存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 - -MyISAM: -- 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。 -- 提供了大量的特性,包括压缩表、空间数据索引等。 -- 不支持事务。 -- 不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 - -#### 索引类型 -- FULLTEXT - 即为全文索引,目前只有MyISAM引擎支持。其可以在CREATE TABLE ,ALTER TABLE ,CREATE INDEX 使用,不过目前只有 CHAR、VARCHAR ,TEXT 列上可以创建全文索引。 -- HASH - 由于HASH的唯一(几乎100%的唯一)及类似键值对的形式,很适合作为索引。 - HASH索引可以一次定位,不需要像树形索引那样逐层查找,因此具有极高的效率。但是,这种高效是有条件的,即只在“=”和“in”条件下高效,对于范围查询、排序及组合索引仍然效率不高。 -- BTREE - BTREE索引就是一种将索引值按一定的算法,存入一个树形的数据结构中(二叉树),每次查询都是从树的入口root开始,依次遍历node,获取leaf。这是MySQL里默认和最常用的索引类型。 -- RTREE - RTREE在MySQL很少使用,仅支持geometry数据类型,支持该类型的存储引擎只有MyISAM、BDb、InnoDb、NDb、Archive几种。 - 相对于BTREE,RTREE的优势在于范围查找。 - -#### B+树 -B+ 树是一种树数据结构,通常用于关系型数据库(如Mysql)和操作系统的文件系统中。B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。B+ 树元素自底向上插入,这与二叉树恰好相反。 -在B树基础上,为叶子结点增加链表指针(B树+叶子有序链表),所有关键字都在叶子结点 中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中。 -b+树的非叶子节点不保存数据,只保存子树的临界值(最大或者最小),所以同样大小的节点,b+树相对于b树能够有更多的分支,使得这棵树更加矮胖,查询时做的IO操作次数也更少。 - -### mysql 的共享锁和排它锁 - -#### 共享锁 -> 共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。 - -#### 排它锁 -> 排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。 - -### mybatis如何防止sql注入 -**首先看一下下面两个sql语句的区别:** -```mysql - + SELECT + sc.uuid , + sc.begin_date , + sc.begin_time , + sc.bus_id , + sc.bus_status, + sc.seat_status + FROM + sb_count sc + ${ew.customSqlSegment} + +``` + +### getCountDetailById +> 查询场次详情信息 +```java +// 自定义查询,涉及到联表查询 +QueryWrapper wrapper = new QueryWrapper<>(); +wrapper.eq("sc.uuid", request.getCountId()); +``` +**xml** +```xml + +``` + +### repeatSeats +> 判断座位是否已重复 +```java +public boolean repeatSeats(String seats, Long coundId) { + // 查查数据库, 找到座位字段 + boolean b = false; // false:不重复,true:重复 + try { + Count count = countMapper.selectById(coundId); + // 比如,selectedSeats 是1,2 + // dbSeats:"", + // dbSeats:"1,2,3", + // dbSeats: "4,5" + // 前端传来的selectedSeats, 前端判断是否为空,要不然后端也判断一下得了 + if (seats.equals("")) { + return true; + } + if (count.getSelectedSeats().equals("")) { + return false; + } + String[] ss = seats.split(","); + String[] cs = count.getSelectedSeats().split(","); + // 这里考虑并发问题 + HashSet hashSet = new HashSet<>(Arrays.asList(cs)); + for (String s : ss) { + if (hashSet.contains(s)) return true; + } + } catch (Exception e) { + e.printStackTrace(); + log.error("selectedSeats", e); + return true; // 异常就算是重复 + } + return b; +} +``` +### addSeats +```java +if (!StrUtil.isEmpty(selectedSeats)) { + // 这里可以优化,字符串拼接,这样的方式爆内存 + // StringBuffer + newSelectedSeats = selectedSeats + "," + newSelectedSeats; +} +``` + +### filterRepeatSeats +> 回退座位 +```java +Count count = countMapper.selectById(coundId); +String[] ss = seats.split(","); +String[] cs = count.getSelectedSeats().split(","); +// 并发问题,注意 +HashSet hashSet = new HashSet<>(Arrays.asList(cs)); +for (String s : ss) { + if (hashSet.contains(s)) { + hashSet.remove(s); + } +} +if (hashSet.isEmpty()) { + count.setSelectedSeats(""); +} +// 考虑了并发 +StringBuffer sb = new StringBuffer(); +for (String s : hashSet) { + sb.append(s); + sb.append(","); +} +// 上面的方案可以用String的replace的方法替换,遍历要回退的座位,依次替换即可 +count.setSelectedSeats(sb.toString()); +countMapper.updateById(count); +``` +看一下String的replace的源码(1.8),感觉遍历还挺多,但的确不用自己去写了 +```java +public String replace(char oldChar, char newChar) { + if (oldChar != newChar) { + int len = value.length; + int i = -1; + char[] val = value; /* avoid getfield opcode */ + + while (++i < len) { + if (val[i] == oldChar) { + break; + } + } + if (i < len) { + char buf[] = new char[len]; + for (int j = 0; j < i; j++) { + buf[j] = val[j]; + } + while (i < len) { + char c = val[i]; + buf[i] = (c == oldChar) ? newChar : c; + i++; + } + return new String(buf, true); + } + } + return this; +} +``` + +## BusSchedule定时器 +> 这里可以使用redis的延时队列或者RocketMQ的消息队列 + +### schedulChangeBusStatus + +```java +/** + * 每天上午7点到晚上21点,每隔30分钟执行一次 + */ +@Scheduled(cron = "0 0/30 7-21 * * ?") +private void schedulChangeBusStatus() { + log.warn("schedulChangeBusStatus执行"); + busService.schedulChangeBusStatus(); +} +``` +看一下业务逻辑 +```java +public void schedulChangeBusStatus() { + // 获取时间 + String currTime = DateUtil.getHours(); + // 获取日期 + String day = DateUtil.getDay(); + log.warn("schedulChangeBusStatus->目前时间:" + currTime); + log.warn("schedulChangeBusStatus->目前时间:" + day); + System.out.println("目前时间:"+ currTime); + System.out.println("目前时间:"+ day); + QueryWrapper queryWrapper = new QueryWrapper<>(); + // 先取出beingtime和now相等的表或者end_time和now相等到表 + // 1. 取出当天 + // 2. 取出开始时间或者结束时间符合当前时间 + queryWrapper + .eq("begin_date", day) // 取出当天 + .and(o -> o.eq("begin_time", currTime) // 当前时间 + .or() + .eq("end_time", currTime)); + List counts = countMapper.selectList(queryWrapper); + log.warn("schedulChangeBusStatus->查询到的:" + counts.toString()); + // 开始作妖 + for (Count count : counts) { + String busStatus = count.getBusStatus(); + String beginTime = count.getBeginTime(); + String endTime = count.getEndTime(); + if (currTime.equals(beginTime)) { + if (busStatus.equals("0")) { // 沙河空闲 + count.setBusStatus("2"); // 沙河->清水河 + } + if (busStatus.equals("1")) { // 清水河空闲 + count.setBusStatus("3"); // 清水河->沙河 + } + count.setSelectedSeats(""); // 清空座位 + } + if (currTime.equals(endTime)) { + if (busStatus.equals("2")) { // 沙河->清水河 + count.setBusStatus("1"); // 清水河空闲 + } + if (busStatus.equals("3")) { // 清水河->沙河 + count.setBusStatus("0"); // 沙河空闲 + } + } + System.out.println("修改的:" + count); + log.warn("schedulChangeBusStatus->修改的:" + count); + // 写入数据库 + countMapper.updateById(count); + } + // 删缓存,这里非常重要...不删后果很严重 + String key1 = RedisConstants.COUNTS_EXPIRE + "0"; + String key2 = RedisConstants.COUNTS_EXPIRE + "1"; + if (redisUtils.hasKey(key1)) { + redisUtils.del(key1); + } + if (redisUtils.hasKey(key2)) { + redisUtils.del(key2); + } +} +``` +> 这里每隔30分钟扫库进行一次io,这里可以有优化... 假如我使用Redis的延迟队列 +1. 在用定时器添加场次的时候,可以将这些场次存入Redis的zset的队列中,元素为场次ID,元素对应的score是出发时间,这样就有17个(定时器每天凌晨添加17个) +- 还是用定时器轮询,采用每隔半小时轮询一次,我们取出队列中当前时间大于队列中权重的时间的场次ID,开始进行业务逻辑判断(更改场次状态) +- 更改过后,如果是发车时间,则删除队列中的场次id和score,重新添加队列中场次的id和结束时间score,或者直接修改score +- 如果是结束时间,则删除队列中的场次id和score + +### addCounts +> 这个项目,没有后台,因此场次需要定时器添加即可 +```java +/** + * 每天凌晨0点2分执行 + */ +@Scheduled(cron = "0 2 0 * * ? ") +private void addCounts(){ + log.warn("addCounts执行"); + busService.addCounts(); +} +``` +具体的业务逻辑: +```java +public void addCounts() { + // 获取日期 + String day = DateUtil.getDay(); + // 获取前17个场次 + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.last("limit 17"); + List counts = countMapper.selectList(wrapper); + // 开始修改 这里可以用java8 的特性, 还不是很熟悉,后期优化一下 + for (Count count : counts) { + // 更改日期 + count.setBeginDate(day); + // 更改uuid + count.setUuid(UUIDUtils.flakesUUID()); + // 清空座位 + count.setSelectedSeats(""); + // 将走位状态清零 + count.setSeatStatus("0"); + // 插入 + countMapper.insert(count); + } + + // 删缓存,不删后果依然很严重 + String key1 = RedisConstants.COUNTS_EXPIRE + "0"; + String key2 = RedisConstants.COUNTS_EXPIRE + "1"; + if (redisUtils.hasKey(key1)) { + redisUtils.del(key1); + } + if (redisUtils.hasKey(key2)) { + redisUtils.del(key2); + } +} + +``` diff --git "a/Java/bus/\347\224\250\346\210\267\346\234\215\345\212\241.md" "b/Java/bus/\347\224\250\346\210\267\346\234\215\345\212\241.md" new file mode 100644 index 00000000..3996daa8 --- /dev/null +++ "b/Java/bus/\347\224\250\346\210\267\346\234\215\345\212\241.md" @@ -0,0 +1,104 @@ +# 用户服务 + +## 用户模块的MySQl数据库建表语句如下: + +> 数据库名:school-bus (自定) +> +> DROP TABLE IF EXISTS `sb_user`; +> CREATE TABLE `sb_user` ( +> `uuid` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键编号', +> `user_name` varchar(50) NOT NULL DEFAULT '' COMMENT '用户账号', +> `user_pwd` varchar(50) NOT NULL DEFAULT '' COMMENT '用户密码', +> `nick_name` varchar(50) NOT NULL DEFAULT '' COMMENT '用户昵称', +> `user_sex` int(11) DEFAULT NULL COMMENT '用户性别 0-男,1-女', +> `email` varchar(50) NOT NULL DEFAULT '' COMMENT '用户邮箱', +> `user_phone` varchar(50) NOT NULL DEFAULT '' COMMENT '用户手机号', +> `begin_time` datetime NOT NULL COMMENT '创建时间', +> `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', +> `money` double NOT NULL COMMENT '用户余额', +> `pay_password` varchar(100) CHARACTER SET utf8mb4 NOT NULL COMMENT '支付密码', +> PRIMARY KEY (`uuid`), +> UNIQUE KEY `user_name` (`user_name`) +> ) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户表'; +> +> INSERT INTO `sb_user` (`uuid`, `user_name`, `user_pwd`, `nick_name`, `user_sex`, `email`, `user_phone`, `begin_time`, `update_time`, `money`, `pay_password`) VALUES ('2', 'admin', '0192023a7bbd73250516f069df18b500', '隔壁泰山', '0', 'admin@qq.com', '13888888888', '2020-02-25 20:35:51', '2020-02-25 20:35:51', '0', ''), +> ('3', 'jiangzh', '5e2de6bd1c9b50f6e27d4e55da43b917', '阿里郎', '0', 'jiangzh@qq.com', '13866666666', '2020-02-25 20:35:51', '2020-02-25 20:35:51', '0', ''), + +持久层框架使用`mybatis-plus`结合guns 4.2项目在`guns-rest`的test目录下提供的generator自动生成相关的SQL代码,很方便,具体使用请自行查看代码。 + +## UserController + +`gateway`模块中`modular.user`下,与同目录下`form`和`auth`文件夹搭配使用,`form`提供网络请求的表单类,`auth`为guns自带的jwt验证,具体的验证逻辑可自行修改。 + +### checkUsername + +> 根据提供的用户名去数据库中查找相关记录是否存在,找到就返回查找成功响应,没找到就返回错误响应。 + +### register + +> 这个注意请求体的参数即可,也没什么可说的 + +### getUserById + +> 注意缓存是否存在,存在直接返回即可,没有就调用业务`getUserById`获取结果 + +### updateUserInfo + +> 注意请求体的参数即可,并且若是更新数据,那么自然要删除UserInfo的缓存,保持数据一致,写不写缓存这一点都行,下次请求getUserById会写缓存。 + +### logout +> 注意redis删除缓存即可 + + +## UserServiceImpl + +### checkUsername +```java +QueryWrapper queryWrapper = new QueryWrapper<>(); +// 查询user_name列与getUsername相匹配的数据 +queryWrapper.eq("user_name", request.getUsername()); +User user = userMapper.selectOne(queryWrapper); +``` + +### regsiter +```java +// 密码采用了md5加密 +String md5Password = MD5Util.encrypt(user.getUserPwd()); +user.setUserPwd(md5Password); +``` + +### login + +```java +QueryWrapper queryWrapper = new QueryWrapper<>(); +// 根据user_name查询用户是否存在 +queryWrapper.eq("user_name", request.getUsername()); +User user = userMapper.selectOne(queryWrapper); +if (user != null && user.getUuid() > 0) { + // 用户不为空,用户id > 0 + String md5Password = MD5Util.encrypt(request.getPassword()); + if (user.getUserPwd().equals(md5Password)) { + // 数据库密码和所加密的md5密码一致 + res.setUserId(user.getUuid()); + res.setCode(SbCode.SUCCESS.getCode()); + res.setMsg(SbCode.SUCCESS.getMessage()); + } +} +``` + +### getUserById +```java +//根据id查询用户 +User user = userMapper.selectById(request.getId()); +UserDto userDto = userConverter.User2Res(user); +``` + +### updateUserInfo +```java +// 这里一采用了mapstruct映射,有兴趣的可以去网上找资料学习学习 +User user = userConverter.res2User(request); +``` + +## 总结 + +用户服务,没有什么比较复杂的业务,复杂的业务在订单服务、场次服务、支付服务、 diff --git "a/Java/bus/\350\256\242\345\215\225\346\234\215\345\212\241.md" "b/Java/bus/\350\256\242\345\215\225\346\234\215\345\212\241.md" new file mode 100644 index 00000000..5ac5d81e --- /dev/null +++ "b/Java/bus/\350\256\242\345\215\225\346\234\215\345\212\241.md" @@ -0,0 +1,283 @@ +# 订单服务 + +## OrderController + +### getNoTakeOrdersById +> 未乘坐订单 + +```java +if (redisUtils.hasKey(key)) { + // redis是否有该缓存 + Object obj = redisUtils.get(key); + NoTakeBusResponse response = (NoTakeBusResponse) obj; + for (NoTakeDto noTakeDto : response.getNoTakeDtos()) { + // 如果场次发车时间-当前时间是大0的,那么说明已经发车了 + String beginTime = noTakeDto.getBeginTime(); + if (beginTime.compareTo(DateUtil.getHours()) <= -1) { + // 删掉当前缓存 + redisUtils.del(key); + // 重新获取最新的数据 + response = orderService.getNoTakeOrdersById(request); + // 写缓存 + redisUtils.set(key, response, RedisConstants.NO_TAKE_OREDERS_EXPIRE.getTime()); + return new ResponseUtil().setData(response); + } + } + log.warn("getNoTakeOrdersById->redis\n"); + return new ResponseUtil().setData(obj); +} +``` + +### getNoPayOrdersById +> 获取未支付订单接口 +```java +// 从redis中是否有缓存 +if (redisUtils.hasKey(key)) { + // 有就获取 + Object obj = redisUtils.get(key); + log.warn("getNoPayOrdersById->redis\n"); + return new ResponseUtil().setData(obj); +} +``` +### getEvaluateOrdersById +> 根据评价状态获取用户订单接口 +略 + +### addOrder +> 添加订单接口 + +```java +String countKey = RedisConstants.COUNT_DETAIL_EXPIRE.getKey() + request.getCountId(); +// 座位缓存失效 +if (redisUtils.hasKey(countKey)) { + redisUtils.del(countKey); +} +String noPayKey = RedisConstants.NO_PAY_ORDERS_EXPIRE.getKey() + userId; +// 未支付列表缓存失效 +if (redisUtils.hasKey(noPayKey)) { + redisUtils.del(noPayKey); +} +``` +### selectOrderById +> 根据订单id获取详情订单 +略 + +### updateOrderStatus +> 更改订单状态 +```java +// 删掉订单详情缓存 +if (redisUtils.hasKey(selectOrderKey)) { + redisUtils.del(selectOrderKey); +} +// 删除未乘坐缓存 +if (redisUtils.hasKey(noTakeKey)) { + redisUtils.del(noTakeKey); +} +// 删除未支付缓存 +if (redisUtils.hasKey(noPayKey)) { + redisUtils.del(noPayKey); +} +// 删除评价缓存 +if (redisUtils.hasKey(evaluateKey)) { + redisUtils.del(evaluateKey); +} +``` + +## OrderServiceImpl + +### getNoTakeOrdersById +```java +IPage orderIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); +QueryWrapper queryWrapper = new QueryWrapper<>(); +// 获取系统年月日 +// 比如5.30 +String day = DateUtil.getDay(); +// 比如20:00 +String hours = DateUtil.getHours(); +System.out.println("当前日期:" + day); +System.out.println("当前时间:" + hours); +queryWrapper + .eq("user_id", request.getUserId()) // 用户id + .eq("order_status", "1")// 1:已经支付 + .ge("sc.begin_date", day) // 比如订单日期大于等于今天 + .ge("sc.begin_time", hours) // 订单时间大于等于当前时间 + .orderByAsc("sc.begin_time") // 排序 + .orderByDesc("so.order_time"); // 排序 +``` + +### getEvaluateOrdersById +```java +IPage orderIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); +QueryWrapper queryWrapper = new QueryWrapper<>(); +// 获取系统年月日 +String day = DateUtil.getDay(); +String hours = DateUtil.getHours(); +System.out.println("当前日期:" + day); +System.out.println("当前时间:" + hours); +queryWrapper + .eq("user_id", request.getUserId()) // 用户di + .eq("order_status", "1") // 状态为1 + // 两种情况: + // 1. 符合当天日期,并且订单场次发车时间小于当前日期 + // 2. 订单场次的发车日期小于当前日期 + .and(o -> o.eq("sc.begin_date", day) + .lt("sc.begin_time", hours) + .or().lt("sc.begin_date", day)) + .eq("evaluate_status", request.getEvaluateStatus()) // 评价状态 + .orderByDesc("sc.begin_time") // 排序 + .orderByDesc("so.order_time"); // 排序 +``` + +### getNoPayOrdersById +```java +IPage noPayDtoIPage = new Page<>(request.getCurrentPage(), request.getPageSize()); +QueryWrapper queryWrapper = new QueryWrapper<>(); +// 获取系统年月日 +String day = DateUtil.getDay(); +String hours = DateUtil.getHours(); +System.out.println("当前日期:" + day); +System.out.println("当前时间:" + hours); +queryWrapper + .eq("so.user_id", request.getUserId()) // 用户id + .eq("so.order_status", "0") // 未支付状态 + .ge("sc.begin_date", day) // 比如,订单场次日期大于当前日期 + .ge("sc.begin_time", hours)// 订单场次时间大于当前日期 + .orderByDesc("sc.begin_time") // 排序 + .orderByDesc("so.order_time"); // 未支付 +``` + +### addOrder +```java +public AddOrderResponse addOrder(AddOrderRequest request) { + // 判断座位,如果重复,直接退出,否则更新场次的座位信息 + AddOrderResponse response = new AddOrderResponse(); + // 全局orderId + Long orderId = UUIDUtils.flakesUUID(); + // 1。 判断座位,如果重复,直接退出,否则下一步 + // 2。 更新座位,如果没有异常,这是写操作 + // 3。 计算总金额,如果没有异常 + // 4。 添加订单,如果异常,这是写操作 + try { + // 1。 判断座位,如果重复,直接退出,否则下一步 + tag = MqTags.ORDER_SEATS_CANCEL.getTag(); + boolean repeatSeats = busService.repeatSeats(request.getSeatsIds(), request.getCountId()); + if (repeatSeats) { + // b:true 说明重复 + response.setCode(SbCode.SELECTED_SEATS.getCode()); + response.setMsg(SbCode.SELECTED_SEATS.getMessage()); + return response; + } +// CastException.cast(SbCode.SYSTEM_ERROR); + // 2。 更新座位,如果没有异常,这是写操作 + // 用tags来过滤消息 + tag = MqTags.ORDER_ADD_SEATS_CANCLE.getTag(); + boolean addSeats = busService.addSeats(request.getSeatsIds(), request.getCountId()); + if (!addSeats) { + response.setCode(SbCode.DB_EXCEPTION.getCode()); + response.setMsg(SbCode.DB_EXCEPTION.getMessage()); + return response; + } + // 模拟系统异常 +// CastException.cast(SbCode.SYSTEM_ERROR); + // 3。 计算总金额,如果没有异常 + tag = MqTags.ORDER_CALC_MONEY_CANCLE.getTag(); + String seatIds = request.getSeatsIds(); + Integer seatNumber = seatIds.split(",").length; + Double countPrice = request.getCountPrice(); + Double totalPrice = getTotalPrice(seatNumber, countPrice); + +// CastException.cast(SbCode.SYSTEM_ERROR); + // 4。 添加订单,如果异常,这是写操作 + Order order = orderConvertver.res2Order(request); + order.setOrderPrice(totalPrice); + order.setEvaluateStatus("0"); // 未评价 + order.setOrderStatus("0"); // 未支付 + order.setUuid(orderId); // 唯一id + tag = MqTags.ORDER_ADD_CANCLE.getTag(); + int insert = orderMapper.insert(order);// 插入 不判断了 +// CastException.cast(SbCode.SYSTEM_ERROR); + // 这里就不读了,耗时 +// QueryWrapper wrapper = new QueryWrapper<>(); +// wrapper.eq("so.uuid", order.getUuid()); +// OrderDto orderDto = orderMapper.selectOrderById(wrapper); + response.setCode(SbCode.SUCCESS.getCode()); + response.setMsg(SbCode.SUCCESS.getMessage()); + response.setOrderId(orderId); +// response.setOrderDto(orderDto); + // 这里放redis 未支付缓存,时间前端给定 + redisUtils.set(RedisConstants.ORDER_CANCLE_EXPIRE.getKey() + orderId, orderId, request.getExpireTime()); + return response; + } catch (Exception e) { + // 以上操作如果程序都不发生异常的话, 是不会执行这里的代码的 + // 也就是说不会发送回退消息的。 + // 目的是在高并发的情况下,程序内部发生异常,依然高可用 +// e.printStackTrace(); + log.error("订单业务发生异常"); + // 发消息,将座位退回,将订单退回 + MQDto mqDto = new MQDto(); + mqDto.setOrderId(orderId); + mqDto.setCountId(request.getCountId()); + mqDto.setSeatsIds(request.getSeatsIds()); + try { + String key = RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getKey() + Convert.toStr(orderId); + sendCancelOrder(topic,tag, key, JSON.toJSONString(mqDto)); + log.warn("订单回退消息发送成功..." + mqDto); + } catch (Exception ex) { + ex.printStackTrace(); + } + response.setCode(SbCode.SYSTEM_ERROR.getCode()); + response.setMsg(SbCode.SYSTEM_ERROR.getMessage()); + return response; + } +} +``` + +### selectOrderById +> 省略 + +### updateOrderStatus +```java +public OrderResponse updateOrderStatus(OrderRequest request) { + OrderResponse response = new OrderResponse(); + try { + // 获取orderDto + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("so.uuid", request.getUuid()); + OrderDto orderDto = orderMapper.selectOrderById(wrapper); + // 1, 检查状态是否为2 + if (request.getOrderStatus().equals("2")) { + // 说明关闭订单,回退座位 + busService.filterRepeatSeats(orderDto.getSeatsIds(), orderDto.getCountId()); + redisUtils.del(RedisConstants.COUNT_DETAIL_EXPIRE.getKey() + + orderDto.getCountId()); + // 清除场次详情的缓存 + } + if (request.getOrderStatus().equals("1")) { + // 说明已经支付,删掉5分钟的订单缓存 + redisUtils.del(RedisConstants.ORDER_CANCLE_EXPIRE.getKey() + request.getUuid()); + } + Order order = orderConvertver.res2Order(request); + // 更新状态 + orderMapper.updateById(order); + // 暂时就不获取了 + response.setCode(SbCode.SUCCESS.getCode()); + response.setMsg(SbCode.SUCCESS.getMessage()); + redisUtils.del(RedisConstants.NO_PAY_ORDERS_EXPIRE.getKey()+order.getUserId()); + redisUtils.del(RedisConstants.SELECT_ORDER_EXPIRE.getKey() + request.getUuid()); + } catch (Exception e) { + log.error("updateOrderStatus", e); + response.setCode(SbCode.DB_EXCEPTION.getCode()); + response.setMsg(SbCode.DB_EXCEPTION.getMessage()); + return response; + } + return response; +} +``` + +### deleteOrderById +> 省略 + +### sendCancelOrder +> 发送订单回退消息 + +后边会单独介绍消息队列 \ No newline at end of file diff --git a/Java/classify/README.md b/Java/classify/README.md new file mode 100644 index 00000000..15082b19 --- /dev/null +++ b/Java/classify/README.md @@ -0,0 +1,101 @@ +![个人体系v1](https://gitee.com/dreamcater/blog-img/raw/master/uPic/个人体系v1-Nkkq8b.png) + +## Java基础 +- [基本类型](basis/基本类型.md) +- [==、hashcode和equals](basis/==、hashcode和equals.md) +- [String](basis/String.md) +- [final](basis/final.md) +- [对象特性](basis/对象特性.md) +- [Object](basis/Object.md) +- [异常体系](basis/异常体系.md) +- [值传递](basis/值传递.md) +- [深浅拷贝](basis/深浅拷贝.md) +- [反射机制](basis/反射机制.md) +- [static](basis/static.md) +- [IO](basis/IO.md) +- [泛型](basis/泛型.md) +- [序列化](basis/序列化.md) + +## Java集合 +- [谈谈集合](col/谈谈集合.md) + +## Java多线程 +- [进程与线程](thread/进程和线程.md) +- [线程的创建方式](thread/线程的创建方式.md) +- [死锁](thread/死锁.md) +- [JMM内存模型](thread/JMM内存模型) +- [volatile](thread/volatile.md) +- [CAS](thread/CAS.md) +- [ThreadLocal](thread/ThreadLocal.md) +- [synchronized](thread/synchronized.md) +- [AQS](thread/AQS.md) +- [ReentrantLock](thread/ReentrantLock.md) +- [ReentrantReadWriteLock](thread/ReentrantReadWriteLock.md) +- [CountDownLatch](thread/CountDownLatch.md) +- [Java的锁](thread/Java锁的介绍.md) +- [生产者和消费者](thread/生产者和消费者.md) +- [线程池](thread/线程池.md) +- [BlockingQueue的一些问题](thread/BlockingQueue的一些问题.md) + +## Jvm +- [类文件结构](jvm/类文件结构.md) +- [类加载过程](jvm/类加载过程.md) +- [类加载器](jvm/类加载器.md) +- [JVM内存区域](jvm/JVM内存区域.md) +- [对象的创建过程](jvm/对象的创建过程.md) +- [垃圾回收](jvm/垃圾回收.md) +- [逃逸分析](jvm/逃逸分析.md) + +## Spring +- [Ioc和AOP](spring/Ioc和AOP.md) +- [Bean](spring/Bean.md) +- [Spring事务](spring/Spring事务.md) +- [SpringMVC](spring/SpringMVC.md) +- [SpringBoot](spring/SpringBoot.md) + +## 数据库 +- [MySQL是如何执行一条SQL的](mysql/MySQL是如何执行一条SQL的.md) +- [Innodb与MyISAM](mysql/Innodb与MyISAM.md) +- [ACID](mysql/ACID.md) +- [MySQL日志文件](mysql/MySQL日志文件.md) +- [索引](mysql/索引.md) +- [MySQL数据库结构优化](mysql/MySQL数据库结构优化.md) +- [MySQL的锁](mysql/MySQL的锁.md) +- [Redis模型](redis/Redis模型.md) +- [Redis数据结构](redis/Redis数据结构.md) +- [Redis持久化](redis/Redis持久化.md) +- [内存淘汰机制](redis/内存淘汰机制.md) +- [缓存穿透和缓存雪崩](redis/缓存穿透和缓存雪崩.md) +- [双写一致性](https://www.cnblogs.com/rjzheng/p/9041659.html) +- [并发竞争key](https://juejin.cn/post/6844903846750191630) +- [Redis分布式锁](redis/Redis分布式锁.md) + +## 分布式 +- [分布式概念](dis/分布式概念.md) +- [CAP和BASE](dis/CAP和BASE.md) +- [分布式一致性算法](dis/分布式一致性算法.md) +- [限流算法](dis/限流算法.md) +- [分布式事务](dis/分布式事务.md) +- [布隆过滤器](dis/布隆过滤器.md) +- [Dubbo](dis/Dubbo.md) +- [RocketMQ](dis/RocketMQ.md) +- [zookeeper](dis/zookeeper.md) +- [Sentinel](dis/Sentinel.md) + +## 计网 +- [网络模型](net/网络模型.md) +- [DNS](net/DNS.md) +- [HTTP和HTTPS](net/HTTP和HTTPS.md) +- [TCP和UDP](net/TCP和UDP.md) + +## 操作系统 +- [进程与线程](sys/进程与线程.md) +- [进程间通信](sys/进程间通信.md) +- [系统进程调度](sys/系统进程调度.md) +- [虚拟内存](sys/虚拟内存.md) +- [操作系统内存管理方式](sys/操作系统内存管理方式.md) +- [页面置换算法](sys/页面置换算法.md) +- [死锁](sys/死锁.md) + +## 其他 + diff --git "a/Java/classify/basis/==\343\200\201hashcode\345\222\214equals.md" "b/Java/classify/basis/==\343\200\201hashcode\345\222\214equals.md" new file mode 100644 index 00000000..9cca85c3 --- /dev/null +++ "b/Java/classify/basis/==\343\200\201hashcode\345\222\214equals.md" @@ -0,0 +1,133 @@ + +# ==、hashcode和equals +> 我感觉这问题被问的频率有点高, 那是因为考的知识点比较多 + +## == + +面试官:出个题: + +```java +Integer a = new Integer(5); +Integer b = new Integer(5); +System.out.println(a == b); // false +``` + +我:false,太简单了嘛。 == 比较的是两个对象的地址哦,你看: + +### 概念及作用 + +它的作用是**判断两个对象的地址是不是相等**。即,判断两个对象是不是同一个对象: + +- 基本数据类型==比较的是**值** + +- 引用数据类型==比较的是**内存地址** + +你要是:`System.out.println(a.equals(b)); \\ false` + +面试官:那这个呢: + +```java +int a = 5; +Integer b = new Integer(5); +System.out.println(a == b); // true +``` +### 装箱与拆箱 + +我:你想考我装箱拆箱嘛?其实,b看到了对面那个哥们是**基本类型**,我也是基本类型包装的呀,我把衣服一脱,那么就是**基本类型**了,那就是上面的结论了。b拆箱即可。 + +装箱:**自动将基本数据类型转换为包装器类型** + +拆箱:**自动将包装器类型转换为基本数据类型** + +基本类型有: + +- byte(1字节):Byte +- short(2字节):Short +- int(4字节):Integer +- long(8字节):Long +- float(4字节):Float +- double(8字节):Double +- char(2字节):Character +- boolean(未知):Boolean + +**在装箱的时候自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法** + +valueOf: + +```java +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +intValue: + +```java +public int intValue() { + return value; +} +``` + +## equals + +面试官:聊一下equals + +### 概念及原理 +我:equals是顶层父类Object的方法之一 + +```java +// 你看,object默认调用的== , 你对象如果不重写,可能会发上重大事故 +public boolean equals(Object obj) { + return (this == obj); // 比较对象的地址值 +} +``` + +顺带说一下Object的hashcode方法 + +```java +// Returns a hash code value for the object. +// 说白了,调用本地方法返回的就是该对象的地址值 +public native int hashCode(); +``` +### 作用 +Equals的作用也是判断两个对象是否相等。但它一般有两种使用情况: + +- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过`==`比较这两个对象。 + +- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。 + +```java +Integer a = new Integer(5); +Integer b = new Integer(5); +System.out.println(a.equals(b)); +``` + +可以看一下Integer的equals方法: + +```java +// 重写了Object的equals的方法 +public boolean equals(Object obj) { + if (obj instanceof Integer) { + // 比较value + return value == ((Integer)obj).intValue(); + } + return false; +} +``` + +## hashcode + +hashcode的例子: + +```java +public static void main(String[] args) { + Set set = new HashSet<>(); + set.add(1); + set.add(1); + System.out.println(set.toString()); +} +``` +**解释:** +在添加1的时候,先判断**hashcode是否相同**,如果相同,**继续判断equals比较值**,这样的好处是,**如果hashcode相同就直接返回false了,减少了一次equals的判断,因为通常hashcode的值判断是否相等比较快,而equals相对于hashcode来讲慢一些**。所以,如果不重写hashcode,我们看到object的hashcode是对象的内存值,那么set添加1判断的时候,hashcode永远不相等,那么就永远返回false,不管添加1,还是2,都是false。 \ No newline at end of file diff --git a/Java/classify/basis/IO.md b/Java/classify/basis/IO.md new file mode 100644 index 00000000..bcaf5d5d --- /dev/null +++ b/Java/classify/basis/IO.md @@ -0,0 +1,11 @@ +# IO +## BIO +**BIO (Blocking I/O)**:**同步阻塞I/O模式**,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。 + +## NIO +**NIO (New I/O)**:NIO是一种**同步非阻塞的I/O模型**,在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 `Channel` , `Selector`,`Buffer`等抽象。NIO中的N可以理解为`Non-blocking`,不单纯是New。它支持**面向缓冲**的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。 + +[NIO底层原理](https://blog.csdn.net/u013857458/article/details/82424104) + +## AIO +**AIO (Asynchronous I/O)**: AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是**异步非阻塞的IO模型**。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。 \ No newline at end of file diff --git a/Java/classify/basis/Object.md b/Java/classify/basis/Object.md new file mode 100644 index 00000000..38fd81c1 --- /dev/null +++ b/Java/classify/basis/Object.md @@ -0,0 +1,14 @@ +# Object + +```java +public final native Class getClass(); +public native int hashCode(); // 返回对象的哈希代码值。 +public boolean equals(Object obj) +protected native Object clone() // 创建并返回此对象的副本。 +public String toString() // 返回对象的字符串表示形式。 +public final native void notify(); // 唤醒正在该对象的监视器上等待的单个线程。 +public final native void notifyAll(); // 唤醒正在该对象的监视器上等待的全部线程。 +public final native void wait(); // 使当前线程等待,直到另一个线程调用此对象的方法或方法。 +protected void finalize(); // 当垃圾回收确定不再有对对象的引用时,由对象上的垃圾回收器调用。 +``` + diff --git a/Java/classify/basis/String.md b/Java/classify/basis/String.md new file mode 100644 index 00000000..f6a127db --- /dev/null +++ b/Java/classify/basis/String.md @@ -0,0 +1,119 @@ + +# String +> 难道面试官想考String的什么? + +## String、StringBuffer和StringBuilder的区别 + +面试官:说一下你知道的String、StringBuffer和StringBuilder的区别 + +我:其实总结说一下就行,三点不同 + +### 可变性 + +- 简单的来说:`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,`private final char value[]`,所以 `String` 对象是不可变的。(还不是为了线程安全和JVM缓存速度问题) + +- `StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串char[]value 但是没有用 `final` 关键字修饰,所以这两种对象都是可变的。 + +### 线程安全 + +- `String` 中的对象是不可变的,也就可以理解为常量,线程安全。 + +- `AbstractStringBuilder` 是 `StringBuilder` 与 `StringBuffer` 的公共父类,定义了一些字符串的基本操作,如 `expandCapacity`、`append`、`insert`、`indexOf` 等公共方法。`StringBuffer` 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。`StringBuilder` 并没有对方法进行加同步锁,所以是非线程安全的。 + +### 性能 + +- 每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。(人家目的是不可变,你一直让人家干可变的事情,那岂不是很难受?) + +- `StringBuffer` 每次都会对 `StringBuffer` 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 + +### 总结 + +- 操作少量的数据: 适用String + +- 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder + +- 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer + +> 注意:这里可能要问final关键字来解释一波String的char前面所加的final的好处 + +## final关键字 + +面试官:有木有想过为什么String的char前面加了final,有什么好处? + +我:答这个问题,你要先说final是干啥的 + +final关键字主要用在三个地方:变量、方法、类。 + + +- 对于一个final变量,如果是**基本数据类型的变量,则其数值一旦在初始化之后便不能更改**;如果是引用类型的变量,则在对其初始化之后便**不能再让其指向另一个对象**。 + +- 当用final修饰一个类时,表明**这个类不能被继承**。final类中的所有成员方法都会被隐式地指定为final方法。 + +- 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 + +**final修饰有啥好处**:(面试官想听这三点) + +- final的关键字**提高了性能**,JVM和java应用会**缓存final变量**; + +- final变量可以在多线程环境下保持**线程安全**; + +- 使用final的关键字提高了性能,JVM会对方法变量类进行优化; + +> 这里可能让解释String对象和JVM的常量池 + +## String对象和常量池 + +```java +public class StringTest { + public static void main(String[] args) { + String str1 = "todo"; // 常量池 + String str2 = "todo"; // 从常量池找了str1 + String str3 = "to"; // 常量池 + String str4 = "do"; // 常量池 + String str5 = str3 + str4; // 内部用StringBuilder拼接了一波。 因此, 并非常量池 + String str6 = new String(str1); // 创建对象了, 那还能是常量池的引用? + } +} +``` + +分析一波: +- 成的class文件中会在常量池中**保存“todo”、“to”和“do”三个String常量**。 +- 变量str1和str2均保存的是常量池中“todo”的引用,所以str1==str2成立; +- 在执行 str5 = str3 + str4这句时,**JVM会先创建一个StringBuilder对象,通过StringBuilder.append()方法将str3与str4的值拼接**,然后通过StringBuilder.toString()返回一个堆中的String对象的引用,赋值给str5,因此str1和str5指向的不是同一个String对象,str1 == str5不成立; +- String str6 = new String(str1)一句显式创建了一个新的String对象,因此str1 == str6不成立便是显而易见的事了。 + +## 了解String.intern()方法吗? + +面试官:说一下String.intern()干嘛用的? + +我:去字符串常量池拿吃的 + +- jdk6: + +执行intern()方法时,**若常量池中不存在等值的字符串,JVM就会在常量池中创建一个等值的字符串(这句话很重要),然后返回该字符串的引用**。 + +- jdk7: + +执行intern操作时,如果常量池已经存在该字符串,则直接返回字符串引用,否则**复制该堆中字符串对象的引用到常量池(堆对象引用复制到常量池)中并返回**。 + +简单说一下常量池版本的变化 + +### 1.6 + +- 静态常量池在Class文件中。 +- **运行时常量池(常量池:字面量和符号引用)在Perm Gen区(也就是方法区)中**。 +- **字符串常量池在运行时常量池**中。 + +### 1.7 + +- 静态常量池在Class文件中。 +- **运行时常量池依然在Perm Gen区(也就是方法区)中**。在JDK7版本中,永久代的转移工作就已经开始了,将譬如符号引用转移到了native heap;字面量转移到了java heap;类的静态变量转移到了java heap。但是运行时常量池依然还存在,只是很多内容被转移,其只存着这些被转移的引用。 +- **字符串常量池被分配到了Java堆的主要部分**。也就是**字符串常量池从运行时常量池分离出来了**。 + +### 1.8 + +- 静态常量池在Class文件中。 +- **JVM已经将运行时常量池从方法区中移了出来,在Java堆(Heap)中开辟了一块区域存放运行时常量池**。**同时永久代被移除,以元空间代替**。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。其主要用于存放一些元数据(类信息)。 +- **字符串常量池存在于Java堆中**。 + + diff --git a/Java/classify/basis/final.md b/Java/classify/basis/final.md new file mode 100644 index 00000000..b7dc3085 --- /dev/null +++ b/Java/classify/basis/final.md @@ -0,0 +1,22 @@ +# final + +面试官:有木有想过为什么String的char前面加了final,有什么好处? + +我:答这个问题,你要先说final是干啥的 + +final关键字主要用在三个地方:变量、方法、类。 + + +- 对于一个final变量,如果是**基本数据类型的变量,则其数值一旦在初始化之后便不能更改**;如果是引用类型的变量,则在对其初始化之后便**不能再让其指向另一个对象**。 + +- 当用final修饰一个类时,表明**这个类不能被继承**。final类中的所有成员方法都会被隐式地指定为final方法。 + +- 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。 + +**final修饰有啥好处**:(面试官想听这三点) + +- final的关键字**提高了性能**,JVM和java应用会**缓存final变量**; + +- final变量可以在多线程环境下保持**线程安全**; + +- 使用final的关键字提高了性能,JVM会对方法变量类进行优化; \ No newline at end of file diff --git a/Java/classify/basis/static.md b/Java/classify/basis/static.md new file mode 100644 index 00000000..f91e22b2 --- /dev/null +++ b/Java/classify/basis/static.md @@ -0,0 +1,5 @@ +# static +- **修饰成员变量和成员方法:** 被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被static 声明的成员变量属于静态成员变量,静态变量存放在 Java 内存区域的方法区。调用格式:`类名.静态变量名` `类名.静态方法名()` +- **静态代码块:** 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—>非静态代码块—>构造方法)。 该类不管创建多少对象,静态代码块只执行一次. +- **静态内部类(static修饰类的话只能修饰内部类):** 静态内部类与非静态内部类之间存在一个最大的区别: 非静态内部类在编译完成之后会隐含地保存着一个引用,该引用是指向创建它的外围类,但是静态内部类却没有。没有这个引用就意味着:1. 它的创建是不需要依赖外围类的创建。2. 它不能使用任何外围类的非static成员变量和方法。 +- **静态导包(用来导入类中的静态资源,1.5之后的新特性):** 格式为:`import static` 这两个关键字连用可以指定导入某个类中的指定静态资源,并且不需要使用类名调用类中静态成员,可以直接使用类中静态成员变量和成员方法。 \ No newline at end of file diff --git "a/Interview/codes/basics/Java\345\200\274\344\274\240\351\200\222\347\232\204\351\227\256\351\242\230.md" "b/Java/classify/basis/\345\200\274\344\274\240\351\200\222.md" similarity index 59% rename from "Interview/codes/basics/Java\345\200\274\344\274\240\351\200\222\347\232\204\351\227\256\351\242\230.md" rename to "Java/classify/basis/\345\200\274\344\274\240\351\200\222.md" index b5079b3a..1e9630fe 100644 --- "a/Interview/codes/basics/Java\345\200\274\344\274\240\351\200\222\347\232\204\351\227\256\351\242\230.md" +++ "b/Java/classify/basis/\345\200\274\344\274\240\351\200\222.md" @@ -1,8 +1,21 @@ -## Java值传递的问题 -> 按值调用(call by value)表示方法接收的是**调用者提供的值**,而按引用调用(call by reference)表示方法接收的是**调用者提供的变量地址**。一个方法可以修改**传递引用所对应的变量值**,而**不能修改传递值调用所对应的变量值**。 +# 值传递 -### 基本类型传递 +> Java值传递,举例子比较合适,总之,拷贝,拷来烤去的。 + +面试官:了解Java值传递嘛? + +我:了解,我总结了一波: + +- 一个方法**不能修改一个基本数据类型的参数**(即数值型或布尔型)。 + +- 一个方法可以改变**一个对象参数的状态**。 + +- 一个方法**不能让对象参数引用一个新的对象**。 + +## 基本类型 + +所谓第一个,举例子先: ```java public static void main(String[] args) { @@ -29,11 +42,15 @@ public static void swap(int a, int b) { // num2 = 20 ``` -整个图就更理解了嘛: -![](http://media.dreamcat.ink/uPic/%E5%9F%BA%E6%9C%AC%E7%B1%BB%E5%9E%8B%E4%BC%A0%E9%80%92%20.png) -> 在 swap 方法中,**a、b 的值进行交换,并不会影响到 num1、num2**。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,**a、b 相当于 num1、num2 的副本**,副本的内容无论怎么修改,都不会影响到原件本身。 -### 数组 +![基本类型传递](https://gitee.com/dreamcater/blog-img/raw/master/uPic/基本类型传递-QPPzXh.png) + +> 在 swap 方法中,**a、b 的值进行交换,并不会影响到 num1、num2**。因为,a、b 中的值,只是从 num1、num2 的复制过来的。也就是说,**a、b 相当于 num1、num2 的副本**,副本的内容无论怎么修改,都不会影响到原件本身。 说白了,就是深拷贝 + +## 数组类型传递 + +所谓第二个,举例子先: + ```java public static void main(String[] args) { int[] arr = { 1, 2, 3, 4, 5 }; @@ -50,13 +67,15 @@ public static void swap(int a, int b) { // 1 // 0 ``` -当然也可以用图助你一臂之力: -![](http://media.dreamcat.ink/uPic/%E6%95%B0%E7%BB%84%E7%B1%BB%E5%9E%8B%E4%BC%A0%E9%80%92.png) -> array 被初始化 arr 的拷贝也就是**一个对象的引用**,也就是说 array 和 arr 指向的是**同一个数组对象**。 因此,外部对引用对象的改变会反映到所对应的对象上。 -通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,**方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象**。 +![数组类型传递](https://gitee.com/dreamcater/blog-img/raw/master/uPic/数组类型传递-fxStps.png) + +> array 被初始化 arr 的拷贝也就是**一个对象的引用**,也就是说 array 和 arr 指向的是**同一个数组对象**。 因此,外部对引用对象的改变会反映到所对应的对象上。说白了,浅拷贝 + +## 对象引用 + +所谓第三个,举例子先: -### 再看 ```java public static void main(String[] args) { // 有些程序员认为 Java 程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。 @@ -80,14 +99,7 @@ public static void swap(int a, int b) { // y:Mai // s1:Mai // s2:Feng -``` -请看图: -![](http://media.dreamcat.ink/uPic/%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%E4%BC%A0%E9%80%92.png) -> 方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝 - -### 总结 -- 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。**第一个例子** -- 一个方法可以改变一个对象参数的状态。**第二个例子** -- 一个方法不能让对象参数引用一个新的对象。**第三个例子** +``` +![对象引用传递](https://gitee.com/dreamcater/blog-img/raw/master/uPic/对象引用传递-m3CrXE.png) diff --git "a/Java/classify/basis/\345\217\215\345\260\204\346\234\272\345\210\266.md" "b/Java/classify/basis/\345\217\215\345\260\204\346\234\272\345\210\266.md" new file mode 100644 index 00000000..09928184 --- /dev/null +++ "b/Java/classify/basis/\345\217\215\345\260\204\346\234\272\345\210\266.md" @@ -0,0 +1,84 @@ +# 反射机制 + +> 面试遇到这个问题,必须好好的想想如何回答这个问题,我就是这么回答的。害 + +面试官:反射是什么? + +我:在Java的反射机制中是指在**运行状态**中,对于任意一个类都能够知道这个类所有的**属性和方法**;并且对于任意一个对象,都能够调用它的**任意一个方法**;这种**动态获取信息以及动态调用对象方法**的功能成为 Java 语言的反射机制。 + +面试官:哦?有什么好处? + +我:怎么说呢,跟多态是的,比如在Java程序中许多对象在运行是都会出现两种类型:**编译时类型和运行时类型**。其中,编译时类型由**声明对象时使用的类型来决定**,运行时的类型由**实际赋值给对象的类型决定** 。比如 + +`People = = new Man();`程序在运行的时候,有时候需要注入外部资源,那么这个外部资源在编译时是People,如果想要它的运行时类型中的某个方法,为了解决这些问题,程序在运行时发现对象和类的真实信息,但是**编译时根本无法预知该对象和类属于哪些**类,程序只能靠运**行时信息来发现该对象和类的信息**,那就要用到反射了。 + +## 反射的API + +面试官:举几个反射的API + +我: + +1. Class 类:反射的核心类,可以获取类的属性,方法等信息。 +2. Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。 +3. Method 类: Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。 +4. Constructor 类: Java.lang.reflec 包中的类,表示类的构造方法 + +## 获取class的方式 + +面试官:获取class对象的三种方式? + +我: + +```java +Student student = new Student(); *// 这一new 产生一个Student对象,一个Class对象。* + +Class studentClass2 = Student.class; // 调用某个类的 class 属性来获取该类对应的 Class 对象 + +Class studentClass3 = Class.forName("com.reflect.Student") // 使用 Class 类中的 forName() 静态方法 ( 最安全 / 性能最好 ) + +``` + +面试官:三者区别? + +我: + +- Class.class 的形式会使 JVM 将使用类装载器将类装入内存(前提是类还没有装入内存),**不做类的初始化工作**,返回 Class 对象。 +- Class.forName() 的形式会装入类并做类的**静态初始化**,返回 Class 对象。 +- getClass() 的形式会对类进行**静态初始化**、**非静态初始化**,返回引用运行时真正所指的对象(因为子对象的引用可能会赋给父对象的引用变量中)所属的类的 Class 对象。 + +> 静态属性初始化是在加载类的时候初始化,而非静态属性初始化是 new 类实例对象的时候初始化。它们三种情况在生成 Class 对象的时候都会先判断内存中是否已经加载此类。 + +面试官:ClassLoader呢? + +我:ClassLoader就是遵循**双亲委派模型最终调用启动类加载器的类加载器**,实现的功能是“通过一个**类的全限定名来获取描述此类的二进制字节流**”,获取到二进制流后放到JVM中,加载的类**默认不会进行初始化**。 + +而Class.forName()会静态初始化,那么看源码: + +```java +@CallerSensitive +public static Class forName(String className) + throws ClassNotFoundException { + Class caller = Reflection.getCallerClass(); + // 这里的true,就是初始化 + return forName0(className, true, ClassLoader.getClassLoader(caller), caller); +} + +// initialize:是否初始化 +private static native Class forName0(String name, boolean initialize, + ClassLoader loader, + Class caller) + throws ClassNotFoundException; +``` + +## 如何用反射创建对象 + +面试官: + +面试官:除了通过反射创建对象,还有? + +我:new呗,clone一个呗 + +面试官:反射都有哪些应用场景 + +我:我可以说Spring,Dubbo,RocketMQ吗?这些优秀的框架背后都用到了反射,这说明,反射的优点之一灵活,提高了代码的灵活度,但同时性能受损。因为反射要进行一系列的解释操作。 + diff --git "a/Java/classify/basis/\345\237\272\346\234\254\347\261\273\345\236\213.md" "b/Java/classify/basis/\345\237\272\346\234\254\347\261\273\345\236\213.md" new file mode 100644 index 00000000..88107c01 --- /dev/null +++ "b/Java/classify/basis/\345\237\272\346\234\254\347\261\273\345\236\213.md" @@ -0,0 +1,57 @@ +# 基本类型 + +> 简单说一下 + + +## 8大基本类型 + +![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) + + +> 特别注意Boolean 未精确定义字节。Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个元素boolean元素占8位。 + + +## 装箱和拆箱 + +- 装箱:将基本类型用它们对应的**引用类型**包装起来; +- 拆箱:将包装类型转换为**基本数据类型**; + +## 注意初始化 + +```java +int a; +System.out.println(a); // 这里会报错,因为a没有初始化 + +Integer b = new Integer(); // 构造函数必须传递一个默认值,要不然提示报错,无法初始化 +// 看一下Integer的源码的value, +public Integer(int value) { + this.value = value; +} +// 而value,这么以来,创建对象必须初始化咯 +private final int value; +``` + +## 注意字面量 + +```java +float a = 3.14; // 字面量3.14 是double类型, 所以这里会报错 +float a = 3.14f; // 这样就不会报错了,需要加个f + +// 转型 +short a = 1; // short类型 +a = a + 1; // short = short + int 会报错, int无法像下转型,但是short可以向上,这样右边就是Int,无法向下 +a++; // 这里是a = (short) (a + 1); +``` + +## 注意Integer.valueOf + +```java +// This method will always cache values in the range -128 to 127, +public static Integer valueOf(int i) { +if (i >= IntegerCache.low && i <= IntegerCache.high) // 条件 +return IntegerCache.cache[i + (-IntegerCache.low)];// 取缓存, +// Integeer的源码中: +// static final int low = -128; IntegerCache.low = -128 +return new Integer(i); +} +``` \ No newline at end of file diff --git "a/Java/classify/basis/\345\257\271\350\261\241\347\211\271\346\200\247.md" "b/Java/classify/basis/\345\257\271\350\261\241\347\211\271\346\200\247.md" new file mode 100644 index 00000000..660471c4 --- /dev/null +++ "b/Java/classify/basis/\345\257\271\350\261\241\347\211\271\346\200\247.md" @@ -0,0 +1,255 @@ +# 对象特性 + +## 封装 + +封装把一个对象的**属性私有化**,同时提供一些可以**被外界访问的属性的方法**。 + +```java +class Person{ + String name; // 属性 + int age; + + // 方法 + public Person(String name, int age){ + this.name = name; + this.age = age; + } +} +``` + +## 继承 + +继承是使用**已存在的类**的定义作为基础建立新类的技术,新类的定义可以增加**新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类**。通过使用继承我们能够非常方便地复用以前的代码。 + +```java +class Student extends Person{ + double grade; // 在已经的name,age中, 在学生类中添加了成绩或者班级 + + // 方法 + public Student(String name, int age, double grade) { + this.name = name; + this.age = age; + this.grade = grade; + } +} +``` + +## 多态 + +### 定义 + +三要素:加黑的地方! + +首先我觉得即**一个引用变量到底会指向哪个类的实例对象**,该**引用变量发出的方法调用到底是哪个类中实现的方法**,必须在由程序**运行期间**才能决定。强调在编译的时候,不知道该引用指向的是哪个对象的实例,包括调用哪个实例的方法,只有运行的时候,动态知道。 + +举个例子: + +任何事物的多个姿态,多个形态。比如,你说一个猫在吃东西,同样的,你也能说一个动物在吃东西。 + +```java +public class Test { + public static void main(String[] args){ + Animal animal = new Cat(); + animal.eat() // 猫也会吃饭 + // 你看到了一只猫,同样它也是动物 + // 比如有很多其他种类继承了动物哈, + // 当编译期间的animal引用变量,到底指的哪个实例对象,(重要)(主语是引用变量) + // 或者该引用调用的eat方法,到底是哪个实例对象的eat,编译期间恐怕不知道哦(主语是引用变量) + // 只有运行期间,哦哦, 原来是猫的eat方法哇... + } +} +``` + +### 表现形式 + + +- **Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但可具有不同的参数列表、返回值类型。调用方法时通过传递的参数类型来决定具体使用哪个方法**,这就是多态性。比如,java lang的包很多工具类如String工具类,那么就有很多相同的名字,但是参数类型、数量和返回值等等不一样。 + + +- **Java的方法重写,是父类与子类之间的多态性,子类可继承父类中的方法,但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。重写的参数列表和返回类型均不可修改**。这也是多态性。比如JUC的AQS框架,凡事继承了AQS的那几个类,其中几个重要的方法,都被重写了,很多这样的情况。 + +### 底层 + +首先要说:首先当程序运行需要某个类时,类加载器会将相应的class文件载入到JVM中,并在方法区建立该类的类型信息(包括方法代码,类变量、成员变量以及**方法表**。(标黑的这个玩意) + +面试官:方法表有啥? + +我:方法表的结构如同字段表一样,依次包括了**访问标志、名称索引、描述符索引、属性表集合**几项。 + +接着回答:**方法表是实现动态调用的核心。为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向记录该类方法的方法表,方法表中的每一个项都是对应方法的指针**。 + +到这里:就要分情况讨论了,一个是方法调用,一个是接口 + +##### 方法调用 +```java +class Person { + // 重写object的toString + public String toString(){ + return "I'm a person."; + } + public void eat(){} + public void speak(){} + +} + +class Boy extends Person{ + // 重写object的toString + public String toString(){ + return "I'm a boy"; + } + // 继承Person的speak + public void speak(){} + // 自己实现的自定义方法 + public void fight(){} +} + +class Girl extends Person{ + // 重写object的toString + public String toString(){ + return "I'm a girl"; + } + // 继承Person的speak + public void speak(){} + // 自己实现的自定义方法 + public void sing(){} +} +``` + +![多态方法调用-I0jtEe](https://gitee.com/dreamcater/blog-img/raw/master/uPic/多态方法调用-I0jtEe.png) + +这张图的指向:你可以根据颜色对应上,注意方法表条目指向的具体的**方法地址**。其次注意蓝色部分其继承自于 Person 的方法 eat() 和 speak() 分别指向 **Person 的方法实现和本身的实现**。如果子类改写了父类的方法,那么子类和父类的那些**同名的方法共享一个方法表项**。因此,**所有继承父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个定值**。Person 或 Object中的任意一个方法,在它们的方法表和其子类 Girl 和 Boy 的方法表中的位置 (index) 是一样的。这样 JVM 在调用实例方法其实只需要指定调用方法表中的第几个方法即可。 + + +调用过程: + +1. 在常量池里找到方法调用的**符号引用**(肯定先看到Person定义引用类型) +2. 查看Person的方法表,得到speak方法在该**方法表的偏移量**(假设为15),这样就得到该方法的直接引用。 +3. 根据this(invoker this字节码)指针得到**具体的对象**(即 girl 所指向的位于堆中的对象)。 +4. 根据对象得到该对象对应的方法表,根据偏移量15查看**有无重写(override)该方法**,如果重写,则可以直接调用(Girl的方法表的speak项指向自身的方法而非父类);如果没有重写,则需要拿到按照继承关系从下往上的基类(这里是Person类)的方法表,同样按照这个偏移量15查看有无该方法。 + + +#### 接口调用 + +一个类可以实现多个接口,那么就像多继承一样,这样的话,在方法表中的索引就会不一样,所以Java 对于接口方法的调用是采用**搜索方法表**的方式。 + +## 初始化顺序 + +有这样的两个类:Person和Student + +Person: + +```java +class Person{ + // 静态代码块 + static { + System.out.println("Person 静态方法"); + } + // 代码块 + { + System.out.println("Person 代码块"); + } + // 构造方法 + Person(){ + System.out.println("Person 构造方法"); + } +} +``` + +Student: + +```java +class Student extends Person{ + // 静态代码块 + static { + System.out.println("Student 静态方法"); + } + // 代码块 + { + System.out.println("Student 代码块"); + } + // 构造方法 + Student(){ + System.out.println("Student 构造方法"); + } +} +``` + +测试 + +```java +public class Main { + public static void main(String[] args) { + Student student = new Student(); + } +} + +// result: + +Person 静态方法 +Student 静态方法 +Person 代码块 +Person 构造方法 +Student 代码块 +Student 构造方法 +``` + +解释:还不是因为类加载器的双亲委派哈, 先走类方法,也就是说static属于类,所以先调用**Person static -> Student static**,接着走对象的初始化,那么对象初始化了,还是先走父类的初始化了,所以**Perosn {} -> Person(){}**,最后走子类的初始化**Student {} -> Student(){}** + + + + +## 接口和抽象类的区别 + +1. 方法是否能实现:所有**方法在接口中不能有实现**(Java 8 开始接口方法可以有默认实现),而**抽象类可以有非抽象的方法**。 +2. 变量:接口中除了**static、final变量**,不能有其他变量,而抽象类中则不一定。 +3. 实现:一个类可以实现**多个接口**,但**只能实现一个抽象类**。接口自己本身可以通过implement关键字扩展多个接口。 +4. 修饰符:接口方法默认修饰符是public,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。 +5 + +## 匿名内部类传参数为什么需要final + +```java +public class Main { + public static void main(String[] args) { + String str = "m"; + new Thread(){ + @Override + public void run() { + System.out.println(str); + } + }.start(); + } +} +// 在1.8版本之前是编译不通过的,1.8能编译能过,但是还是需要final保证局部变量的数据一致性 +``` + +反编译 + +```java + +public class Hello$1 extends Thread { + + private String val$str; + + Hello$1(String paramString) { + this.val$str = paramString; + } + + public void run() { + System.out.println(this.val$str); + } + +} +``` + +解释:**局部变量是被当成了参数传递给匿名对象的构造器(那就相当于拷贝一份,那就是浅拷贝了),这样的话,如果不管是基本类型还是引用类型,一旦这个局部变量是消失(局部变量随着方法的出栈而消失),还是数据被改变,那么此时匿名构造类是不知道的,毕竟是你浅拷贝了一份,那么如果加上final,这个数据或者引用永远都不会改变,保证数据一致性。注意:在JDK8中如果我们在匿名内部类中需要访问局部变量,那么这个局部变量不需要用final修饰符修饰。** + +## 四种修饰符的限制范围 + +1. public:可以被所有其他类所访问。 + +2. private:只能被自己访问和修改。 + +3. protected:自身,子类及同一个包中类可以访问。 + +4. default(默认):同一包中的类可以访问 \ No newline at end of file diff --git "a/Java/classify/basis/\345\272\217\345\210\227\345\214\226.md" "b/Java/classify/basis/\345\272\217\345\210\227\345\214\226.md" new file mode 100644 index 00000000..49010d9a --- /dev/null +++ "b/Java/classify/basis/\345\272\217\345\210\227\345\214\226.md" @@ -0,0 +1,14 @@ +> 很多rpc框架都要考虑序列化的问题,但是我没有过于深入 + +# 序列化 +1. 所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。 +2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。 +3. 如果想让某个变量不被序列化,使用transient修饰。 +4. 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。 +5. 反序列化时必须有序列化对象的class文件。 +6. 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。 +7. 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。 +8. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。 +9. 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。 + +[参考链接](https://juejin.im/post/5ce3cdc8e51d45777b1a3cdf#heading-9) \ No newline at end of file diff --git "a/Java/classify/basis/\345\274\202\345\270\270\344\275\223\347\263\273.md" "b/Java/classify/basis/\345\274\202\345\270\270\344\275\223\347\263\273.md" new file mode 100644 index 00000000..408d4a6a --- /dev/null +++ "b/Java/classify/basis/\345\274\202\345\270\270\344\275\223\347\263\273.md" @@ -0,0 +1,127 @@ + +> 面着面着,就修仙了,还原俺的真实面试场景。 + +# 异常体系 + +面试官:谈谈异常机制? + +我:只要涉及到谈谈,我内心都是崩溃的,不知道该如何说起 + +一上来,我可能先回答:Error和Exception是Throwable类的派生实例。说到此处,面试官还不得问一下Error? + + + +## Error + +面试官:给我讲讲什么是Error? + +我:其实就是明显的给自己挖坑,哈能咋滴!**Error描述了Java运行时系统的内部错误和资源耗尽错误,你比如栈溢出和堆溢出啦**?不好,面试官微笑了。 + +面试官:讲一下什么是栈溢出和堆溢出? + +我:哎,中枪了,这咋害能扯到虚拟机上了 + +- StackOverFlowError:**如果线程请求的栈深度大于虚拟机所允许的深度**,将抛出此异常。比如,**无限递归方法**,其实面试官按捺不住的问 + +面试官:为什么无限递归方法就可以抛出该异常? + +我:因为我们知道,**每次调用方法所产生资源都存放在了虚拟机栈中**,如果无限递归下去,那岂不是? + +面试官:虚拟机栈存了什么资源? + +我:我真的是!虚拟机栈存了**局部变量表、操作数栈、动态链接和方法出口**。 + +面试官:局部变量表中存了什么? + +我:啊?还好我会,存放了编译期可知的各种**基本数据类型(8大基本类型)**,**对象引用类型**,它不等同于对象本身,可能是一个指向对象**起始地址的引用指针**,也可能是指向一个**代表对象的句柄或其他与此对象相关的位置**。 + +面试官:好,开始讲堆溢出 + +我:害能给我绕回来...如果**虚拟机可动态扩展,如果扩展时无法申请到足够的内存**,就会抛出OutOfMemoryError异常,当然,**如果在堆中没有内存完成实例分配,并且堆也无法再扩展时**,也会抛出该异常。比如,我又挖坑,举例子:无限创建线程。这次我主动说原因:操作系统分配给每个进程内存是有限的,比如32位的windows限制为2G。虚拟机提供了参数来控制堆和方法区的内存的最大值,而剩下的内存,忽略其他因素,就由虚拟机栈和本地方法栈“瓜分天下了”。**每个线程分配到栈容越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。** + +面试官:**嘿嘿,方法区会溢出吗?** + +我:嘿嘿,会。比如方法区中有一个**运行时常量池**,晓得吧?其中String.intern()方法是一个native方法,它(1.6)的作用是:如果字符串常量池中已经包含了此String对象的字符串,则返回代表池中这个字符串String对象;**否则,将此String对象所包含的字符串添加到常量池中,并且返回此String对象的引用**。在1.7版本就不一样了,**而是从堆中实例String对象的引用复制到常量池并返回**。当然,还有很多带有**反射**机制的框架,大量使用反射创建类来填满方法区。 + +面试官:嘿嘿,直接内存会溢出吗? + +我:简直了,太能问了。那肯定也是能的哦,比如DirectByteBuffer。 + +## Exception + +面试官:可以了,聊Exception + +我:无限退出递模式!Exception又分解为**RuntimeException**(运行时)和程序本身没有问题,由于像IO错误这类问题导致的异常(编译)。 + +面试官:RuntimeException中有哪些,举一些? + +我:好的,比如,`NullPointerException`,`ArithmeticException`,`ClassCastException`,`ArrayIndexOutOfBoundsException`等 + +面试官:什么是受检异常和非受检异常? + +我:派生于**Error类或RuntimeException类**的所有异常称为非受检异常,所有其他的异常称为受检异常。 + +## 捕获异常 + +面试官:如何捕获异常? + +我: + +- `try` 块: 用于捕获异常。其后可接零个或多个`catch`块,如果没有`catch`块,则必须跟一个`finally`块。 + +- `catch` 块: 用于处理`try`捕获到的异常。 + +- `finally` 块: 无论是否捕获或处理异常,`finally`块里的语句都会被执行。当在`try`块或`catch`块中遇到`return`语句时,`finally`语句块将在方法返回之前被执行。 + +throw 抛出异常,throws是方法可能抛出异常的声明。(用在声明方法时,表示该方法可能要抛出异常) + +注意:finally return会覆盖try和catch的返回值 + +```java +public class Main { + public static void main(String[] args) { + System.out.println(test()); + } + + public static int test() { + int a = 0; + try { + int b = 1 / 0; // 不管发生不发生异常 + a++; + return a; + } catch (Exception e) { + a++; + return a; + } finally { + return 5; + } + } +} +// 结果都是5 +``` + +## 结束 + +面试官:可以,我们换一波问题。 + +我:既然聊到了非受检异常,我还想扯一波Spring事务,Spring事务失效的原因,其中原因之一有非受检异常的原因。 + +简单这里提一下原因: + +1. 数据库引擎不支持事务 + +2. 没有被 Spring 管理 + +3. 方法不是 public 的 + +4. 自身调用问题 + +5. 数据源没有配置事务管理器 + +6. 不支持事务(传播机制) + +7. 异常被吃了(捕获异常) + +8. 异常类型错误(checked异常失效) + + diff --git "a/Java/classify/basis/\346\263\233\345\236\213.md" "b/Java/classify/basis/\346\263\233\345\236\213.md" new file mode 100644 index 00000000..3ffeb7d7 --- /dev/null +++ "b/Java/classify/basis/\346\263\233\345\236\213.md" @@ -0,0 +1,13 @@ + +> 我也没有过多的深入这一块 + +## 泛型 +泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。 + +**好处**: +- **类型安全**,提供编译期间的类型检测 +- **前后兼容** +- **泛化代码,代码可以更多的重复利用** +- **性能较高**,用GJ(泛型JAVA)编写的代码可以为java编译器和虚拟机带来更多的类型信息,这些信息对java程序做进一步优化提供条件 + +[泛型擦除原理](https://www.jianshu.com/p/328efeb01940) \ No newline at end of file diff --git "a/Java/classify/basis/\346\267\261\346\265\205\346\213\267\350\264\235.md" "b/Java/classify/basis/\346\267\261\346\265\205\346\213\267\350\264\235.md" new file mode 100644 index 00000000..d9f2541b --- /dev/null +++ "b/Java/classify/basis/\346\267\261\346\265\205\346\213\267\350\264\235.md" @@ -0,0 +1,9 @@ +# 深浅拷贝 + +- **浅拷贝**:对**基本数据类型进行值传递**,对**引用数据类型进行引用传递般的拷贝**,此为浅拷贝。 +- **深拷贝**:对**基本数据类型进行值传递**,对**引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝** +- 也就二者对引用数据类型有区别 + + +知道结论即可,要想测试,可以[我博客-先占坑]() + diff --git "a/Java/classify/col/\350\260\210\350\260\210\351\233\206\345\220\210.md" "b/Java/classify/col/\350\260\210\350\260\210\351\233\206\345\220\210.md" new file mode 100644 index 00000000..0912a09e --- /dev/null +++ "b/Java/classify/col/\350\260\210\350\260\210\351\233\206\345\220\210.md" @@ -0,0 +1,348 @@ +[toc] + + +> 集合这一块,问就是并发问题,但前提先有总体了解。 + + + +![清晰uml图](https://www.pdai.tech/_images/java_collections_overview.png) + +## 大概 + +面试官:谈谈集合吧 + +我:可以的,我们首先要介绍集合顶层接口**Collection、Map**,而**List、Queue、Set**实现了Collection接口,List又有**ArrayList、LinkedList**,Queue又有**LinkedList、PriorityQueue**,Set又有**HashSet、TreeSet、LinkedHashSet**等。Map又有**HashMap,TreeMap,LinkedHashMap**,当然**HashTable**是继承**Dictionary**接口,实现了Map。 + +此时就开始谈JUC下的集合,比如**HashMap**对应的**ConcurrentHashMap**,**ConcurrentSkipListMap**;比如**ArrayList**对应**CopyOnWriteArrayList**,Set对应的**CopyOnWriteArraySet**等。阻塞队列暂时先不谈哈。 + +## ArrayList和LinkedList的区别 + +- ArrayList是实现了基于**动态数组**的数据结构,LinkedList基于**链表**的数据结构。 +- ArrayList查找速度快,添加和删除慢 +- LinkedList查找慢,添加和删除快 + +## HashMap和HashTable的区别 + +- HashTable是**线程安全的**,每个方法前加了**synchronized**,而HashMap**是非线程安全的** +- HashTable底层是**数组+链表**,而HashMap1.8版本是**数组+链表or红黑树** + + + +## HashMap和ConcurrentHashMap + +- HashMap是非线程安全的,ConcurrentHashMap是线程安全的 +- HashMap和ConcurrentHashMap在1.7都是数组+链表,1.8都是数组+链表or红黑树 +- ConcurrentHashMap在1.7是分段锁,1.8是去掉分段锁改成cas+synchronized + + + +## ArrayList有哪些并发问题 + +> 其实就是**size++** 这一步的问题。 越界就是两个线程临界值去**扩容**都满足,于是一个线程size++导致的,另外一个线程就溢出了,**null就是element[size] = e**,第一个线程还没来得及size++,第二个线程就在原先的索引上把值给覆盖了,并且在下一个索引为null。 + +越界 + +- 列表大小为9,即size=9 +- 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。 +- 线程B此时也进入add方法,它和获取的size的值也为9,也开始调用ensureCapacityInternal方法。 +- 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。 +- 线程B也发现需要大小为10,也可以容纳,返回。 +- 好了,**问题来了哈** +- 线程A开始进行设置值操作,elementData[size++] = e操作。此时size变为10。 +- 线程B也开始进行设置值操作,它尝试设置elementData[10] = e, 而elementData没有进行过扩容,它的下标最大为9 +- 于是此时会报出一个数组越界的异常`ArrayIndexOutOfBoundsException`。 + +null + +- 列表大小为10,即size=0 +- 线程A开始添加元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。也就是说,线程挂在了`element[0] = e`上。 +- 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一条的操作。此时线程B获取的size的值依然为0,于是它将B也放在了elementData下标为0的位置上。 +- **问题来了**,其实上面也是问题,覆盖了。。。 +- 线程A将size的值增加为1 +- 线程B开始将size的值增加为2 +- 当你获取1索引的时候,那不就是null了? + + + +## HashMap的参数 + +- 为什么容量要是 2 的整数次幂? + +因为获取 key 在数组中对应的下标是通过 key 的哈希值与数组长度 -1 进行与运算,如:tab[i = (n - 1) & hash] + +1. n 为 2 的整数次幂,这样 n-1 后之前为 1 的位后面全是 1,这样就能保证 (n-1) & hash 后相应的位数既可能是 0 又可能是 1,这取决于 hash 的值,这样能保证散列的均匀,同时与运算效率高 + +2. 如果 n 不是 2 的整数次幂,会造成更多的 hash 冲突 + +> 举个例子:如 16:10000, 16-1=15:1111, 1111 再与 hash 做 & 运算的时候,各个位置的取值取决于 hash,如果不是2的整数次幂,必然会有的0的位,这样再进行 & 操作的时候就为 0了,会造成哈希冲突。注意:HashMap的tableSizeFor方法做了处理,能保证n永远都是2次幂 + +- 为什么负载因子是0.75? + +> 负载因子过低,频繁扩容,扩容会重新哈希,性能下降;负载因子过高,容易浪费容量.(经验+概率) + +- 为什么红黑树的阈值是8? + +> 在 hash 函数设计合理的情况下,发生 hash 碰撞 8 次的几率为百万分之 6,概率说话。(泊松分布) + +- 为什么退化链表的阈值6? + +> 6是因为如果 hash 碰撞次数在 8 附近徘徊,会一直发生链表和红黑树的转化,为了预防这种情况的发生。 + +- hash + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +hash 函数是先拿到通过 key 的 hashcode,**是 32 位的 int 值**,然后让 **hashcode 的高 16 位和低 16 位进行异或操作**。这个也叫扰动函数,这么设计有二点原因: + +- **一定要尽可能降低 hash 碰撞,越分散越好**; +- 算法一定要尽可能高效,因为这是高频操作, 因此采用位运算; + +## HashMap的put、get和扩容 + +### put + +![hashmap-put-1.8](https://gitee.com/dreamcater/blog-img/raw/master/uPic/hashmap-put-1.8-WLBoNy.png) + +- 判断数组是否为空,为空进行初始化;(初始化) +- 不为空,计算 k 的 hash 值,通过(n - 1) & hash计算应当存放在数组中的下标 index;(通过hash计算index) +- 查看 table[index] 是否存在数据,没有数据就构造一个 Node 节点存放在 table[index] 中;(查看数组中是否哈希冲突) +- 存在数据,说明发生了 hash 冲突(存在二个节点 key 的 hash 值一样), 继续判断 key 是否相等,相等,用新的 value 替换原数据(onlyIfAbsent 为 false);(冲突,判断key是否相等,相等则替换) +- 如果不相等,判断当前节点类型是不是树型节点,如果是树型节点,创造树型节点插入红黑树中;(判断是否红黑树) +- 如果不是树型节点,创建普通 Node 加入链表中;判断链表长度是否大于 8, 大于的话链表转换为红黑树;(判断是否转成红黑树) +- 插入完成之后判断当前节点数是否大于阈值,如果大于开始扩容为原数组的二倍。(扩容) + + + +### get + +1. 判断:是否为空,为空,返回null +2. 不为空,判断第一个位置是否为查询key,是,返回value +3. 不是,下一个节点继续判断是否为红黑树,是,按树查找 +4. 不是,按链表查找 + + + +### 扩容 + +> 先说1.7吧 + +```java +for (HashMapEntry e : table) { + // 如果这个数组位置上有元素且存在哈希冲突的链表结构则继续遍历链表 + while (null != e) { + //取当前数组索引位上单向链表的下一个元素 + HashMapEntry next = e.next; + //重新依据hash值计算元素在扩容后数组中的索引位置 + int i = indexFor(e.hash, newCapacity); + e.next = newTable[i]; // 这一步和下一步就是头插法了,并且这两步出现线程不安全死循环问题 + newTable[i] = e; + e = next; // 遍历链表 + } +} +``` + +> 1.8 +> HashMap的扩容使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。也就是说省略了重新计算hash值的时间,而且新增的1位是0还是1机会是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。如果在新表的数组索引位置相同,则链表元素不会倒置。 + + + +### 并发问题 + +- HashMap扩容的时候会调用resize()方法,就是这里的并发操作容易在一个桶上形成环形链表 +- 这样当获取一个不存在的key时,计算出的index正好是环形链表的下标就会出现死循环。 +- **但是1.7的头插法造成的问题,1.8改变了插入顺序,就解决了这个问题,但是为了内存可见性等安全性,还是需要ConCurrentHashMap** +- HashTable 是直接在操作方法上加 synchronized 关键字,锁住整个数组,粒度比较大 +- Collections.synchronizedMap 是使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现 +- ConcurrentHashMap 使用分段锁,降低了锁粒度,让并发度大大提高。(jdk1.8 CAS+ synchronized) + + + +## ConcurrentHashMap + +### 1.7 + +#### segment + +- 唯一的区别(和HashMap)就是其中的核心数据如 value ,以及链表都是 volatile 修饰的,保证了获取时的可见性。 +- ConcurrentHashMap 采用了分段锁技术(相对hashTable降低锁的粒度),其中 Segment 继承于 `ReentrantLock`(可能还会扯AQS)。 +- 不会像HashTable那样不管是put还是get操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。 +- **每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。** + +#### put + +- 虽然HashEntry中的value是用volatile关键字修饰的,但是并不能保证并发的原子性,所以put操作仍然需要加锁处理。 +- 首先第一步的时候会尝试获取锁,如果获取失败肯定就是其他线程存在竞争,则利用 `scanAndLockForPut()` **自旋获取锁**。 + - 尝试获取自旋锁 + - 如果**重试的次数**达到了`MAX_SCAN_RETRIES` 则改为阻塞锁获取,保证能获取成功。 + +总的来说: + +- 将当前的Segment中的table通过key的hashcode定位到HashEntry +- 遍历该HashEntry,如果不为空则判断传入的key和当前遍历的key是否相等,相等则覆盖旧的value +- 不为空则需要新建一个HashEntry并加入到Segment中,同时会先判断是否需要扩容 +- 最后会解除在1中所获取当前Segment的锁。 + +#### get + +- 只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。 +- 由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。 +- ConcurrentHashMap 的 get 方法是非常高效的,**因为整个过程都不需要加锁。** + +#### size + +在 JDK1.7 中,第一种方案他会**使用不加锁的模式**去尝试多次计算 ConcurrentHashMap 的 size,最多**三次**,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的。 第二种方案是如果第一种方案不符合,他就会给**每个 Segment 加上锁**,然后计算 ConcurrentHashMap 的 size 返回 + +### 1.8 + +1.7 查询遍历链表效率太低(种种原因)。其中抛弃了原有的 Segment 分段锁,而采用了 `CAS + synchronized` 来保证并发安全性(会扯1.6对synchronized的优化) + +#### put + +- 根据key计算出hashcode +- 判断是否需要进行初始化 +- 如果f为null,说明table中这个位置第一次插入元素,利用Unsafe.compareAndSwapObject方法插入Node节点。 + - 如果CAS成功,说明Node节点已经插入,随后addCount(1L, binCount)方法会检查当前容量是否需要进行扩容。 + - 如果CAS失败,说明有其它线程提前插入了节点,自旋重新尝试在这个位置插入节点。 +- 如果f的hash值为-1,说明当前f是ForwardingNode节点,意味有其它线程正在扩容,则一起进行扩容操作。 +- 如果都不满足,则利用`synchronized`锁写入数据 +- 如果数量大于TREEIFY_THRESHOLD 则要转换为红黑树。 + +#### get + +- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。 +- 如果是红黑树那就按照树的方式获取值。 +- 就不满足那就按照链表的方式遍历获取值。 + +#### size + +ConcurrentHashMap 提供了 baseCount、counterCells 两个辅助变量和一个 CounterCell 辅助内部类。sumCount() 就是迭代 counterCells 来统计 sum 的过程。 put 操作时,肯定会影响 size(),在 put() 方法最后会调用 **addCount()** 方法。 + +在addCount()方法中: + +- 如果 counterCells == null, 则对 baseCount 做 CAS 自增操作。 +- 如果并发导致 baseCount CAS 失败了使用 counterCells。 +- 如果counterCells CAS 失败了,在 fullAddCount 方法中,会继续死循环操作,直到成功。 +- CounterCell使用了 @sun.misc.Contended 标记的类 + +> 缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,**如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享**。 + +实际上: + +- JDK1.8 size 是通过对 baseCount 和 counterCell 进行 CAS 计算,最终通过 baseCount 和 遍历 CounterCell 数组得出 size。 +- JDK 8 推荐使用mappingCount 方法,因为这个方法的返回值是 long 类型,不会因为 size 方法是 int 类型限制最大值。 + +[https://zhuanlan.zhihu.com/p/40627259](https://zhuanlan.zhihu.com/p/40627259) + + + +## HashSet底层 + +HashSet中不允许有重复元素,这是因为HashSet是基于HashMap实现的,HashSet中的元素都存放在HashMap的key上面,而value中的值都是统一的一个`private static final Object PRESENT = new Object();`。 HashSet跟HashMap一样,都是一个存放链表的数组。这样遇到重复元素就可以返回object了(意味着不是null),如果value是null的话,发现重复,那么就返回上一个value值null,那么不符合源码: + +```java +private static final Object PRESENT = new Object(); +public boolean add(E e) { + return map.put(e, PRESENT)==null; +} +System.out.println(map.put(1, o)); // null +System.out.println(map.put(1, o));// java.lang.Object@610455d6 +// 所以重复就false +``` + + + +## TreeSet底层 + +TreeSet底层则采用NavigableMap这个接口来保存TreeSet集合,而实际上NavigableMap只是一个接口,实际上TreeSet还是用TreeMap来保存set元素。 + +```java +TreeSet(NavigableMap m) { + this.m = m; +} + + public TreeSet() { + this(new TreeMap()); + + } +``` + +TreeMap采用一种被称为“红黑树”的排序二叉树来保存Map中的的每个Entry——每个Entry都被当做红黑树的一个节点来对待;TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。 + +## LinkedHashMap + +[可以参考](https://crossoverjie.top/2018/02/06/LinkedHashMap/) + +一般说完这个,可能让你手撸LRU,你可以撸个伪代码即可。 + +```java +// 双向链表+HashMap,Java中的LinkedHashMap就实现了该算法。 +// get +public int get(int key) { + if (map.containsKey(key)) { + Node n = map.get(key); // 获取内存中存在的值,比如A + remove(n); //使用链表的方法,移除该节点 + setHead(n); //依然使用链表的方法,将该节点放入头部 + return n.value; + } + return -1; +} +// put +public void set(int key, int value) { + if (map.containsKey(key)) { + Node old = map.get(key); + old.value = value; + remove(old); // 移除旧节点 + setHead(old); // 放到队头 + } else { + Node created = new Node(key, value); + if (map.size() >= capacity) { + map.remove(end.key); // clear该key + remove(end); //链表也是依次 + setHead(created); // 将created放入队头 + } else { + setHead(created); // 如果没满,直接放入队头 + } + map.put(key,created); + } +} +``` + +```java +//lc: 146. LRU缓存机制 +class LRUCache { + private int cap; + private Map map = new LinkedHashMap<>(); + public LRUCache(int capacity) { + this.cap = capacity; + } + + public int get(int key) { + if (map.containsKey(key)) { + int value = map.get(key); + // 查一次,就将查到到仍在队尾 + map.remove(key); + map.put(key,value); + return value; + } + return -1; + } + + public void put(int key, int value) { + if (map.containsKey(key)) { + map.remove(key); + } else if (map.size() == cap) { + // 满了 + Iterator> iterator = map.entrySet().iterator(); + iterator.next(); + iterator.remove(); + } + map.put(key, value); + } +} +``` + diff --git "a/Java/classify/dis/CAP\345\222\214BASE.md" "b/Java/classify/dis/CAP\345\222\214BASE.md" new file mode 100644 index 00000000..c6f3b126 --- /dev/null +++ "b/Java/classify/dis/CAP\345\222\214BASE.md" @@ -0,0 +1,14 @@ + + +## CAP +- C(一致性):对某个指定的客户端来说,读操作能返回最新的写操作。对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。 +- A(可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回50,而不是返回40。 +- P(分区容错性):当出现网络分区后,系统能够继续工作。打个比方,集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。 + +- CP:对于CP来说,放弃可用性,追求一致性和分区容错性,我们的zookeeper其实就是追求的强一致。 +- AP:对于AP来说,放弃一致性(这里说的一致性是强一致性),追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的BASE也是根据AP来扩展。 + +## BASE +BASE是BasicallyAvailable(基本可用)、Softstate(软状态)和Eventuallyconsistent(最终一致性)三个短语的缩写。是对CAP中AP的一个扩展-基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。 +- 软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。 +- 最终一致:**最终一致是指经过一段时间后,所有节点数据都将会达到一致**。BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和ACID是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。 \ No newline at end of file diff --git a/Java/classify/dis/Dubbo.md b/Java/classify/dis/Dubbo.md new file mode 100644 index 00000000..64041f3d --- /dev/null +++ b/Java/classify/dis/Dubbo.md @@ -0,0 +1,133 @@ +[toc] + +## Dubbo是什么? + +Dubbo是一款**高性能**、**轻量级**的开源JavaRPC框架,它提供了三大核心能力:**面向接口的远程方法调用**,**智能容错和负载均衡**,以及**服务自动注册和发现**。简单来说Dubbo是一个**分布式服务框架**,致力于提供**高性能和透明化的RPC远程服务调用方案**,以及**SOA服务治理方案。** + +- **负载均衡**——同一个服务部署在不同的机器时该调用那一台机器上的服务。 +- **服务调用链路生成**——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo可以为我们解决服务之间互相是如何调用的。 +- **服务访问压力以及时长统计、资源调度和治理**——基于访问压力实时管理集群容量,提高集群利用率。 +- **服务降级**——某个服务挂掉之后调用备用服务。 + +## Dubbo的图解? +![dubbo架构-qvfIHl](https://gitee.com/dreamcater/blog-img/raw/master/uPic/dubbo架构-qvfIHl.png) + +- **Provider:**暴露服务的服务提供方 +- **Consumer:**调用远程服务的服务消费方 +- **Registry:**服务注册与发现的注册中心 +- **Monitor:**统计服务的调用次数和调用时间的监控中心 +- **Container:**服务运行容器 + +**调用关系说明**: + +- 服务容器负责启动,加载,运行服务提供者。 +- 服务提供者在启动时,向注册中心注册自己提供的服务。 +- 服务消费者在启动时,向注册中心订阅自己所需的服务。 +- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 +- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 +- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 + +**各个组件总结**: + +- **注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小** +- **监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示** +- **注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外** +- **注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者** +- **注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表** +- **注册中心和监控中心都是可选的,服务消费者可以直连服务提供者** +- **服务提供者无状态,任意一台宕掉后,不影响使用** +- **服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复** + +## Dubbo和SpringCloud的区别? +- **底层**:`Dubbo`底层是使用Netty的NIO框架,基于TCP协议传输,使用Hession序列化完成RPC通信;`SpringCloud`是基于HTTP协议+REST接口调用远程过程的通信,HTTP请求会有更大的报文,占的带宽也会更多。但是REST相比RPC更为灵活,不存在代码级别的强依赖。 +- **集成**:springcloud相关组件多,有自己的注册中心网关等,集成方便,Dubbo需要自己额外去集成。Dubbo是SOA时代的产物,它的关注点主要在于**服务的调用,流量分发、流量监控和熔断**。而SpringCloud诞生于微服务架构时代,考虑的是微服务治理的方方面面,另外由于依托了Spring、SpringBoot的优势之上,两个框架在开始目标就不一致,Dubbo定位服务治理、SpirngCloud是一个生态。 + + +## Dubbo的容错机制 +1. Failover Cluster失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数 +2. Failfast Cluster快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 +3. Failsafe Cluster失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 +4. Failback Cluster失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 +5. Forking Cluster并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。 +6. 广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息 + +## Dubbo的限流是怎么实现的? +Dubbo默认是**令牌桶**算法实现限流。在某段时间内,以某种速度向桶里面只能放n个令牌,然后来一个请求就减少一个令牌,如果桶内的令牌没有了,则不能继续执行请求。 + +限流主要是通过TPSLimitFilter实现。 + +## 常见的限流算法有哪些? +- 计数算法 +- 滑动窗口,解决计数算法同一时刻进入很多请求 +- 令牌桶算法 +- 漏桶算法 + + +## 什么是dubbo服务降级? +- dubbo在服务调用时,可能由于服务器宕机、网络超时、并发数太高等,导致调用失败。**服务降级**就是指在非业务异常导致的服务不可用时,可以返回默认值,避免影响主业务的处理。 +- dubbo可以通过mock配置实现服务降级。 + +## Dubbo的注册中心挂了,还可以继续通信吗? +可以。因为在开始初试化的时候,消费者会将提供者的地址等信息拉取到**本地缓存**中。 + +## 负载均衡 +个人理解: +> 比如我们的系统中的某个服务的**访问量特别大**,我们将这个服务部署在了**多台服务器**上,当客户端发起请求的时候,**多台服务器都可以处理这个请求**。那么,如何正确选择处理该请求的服务器就很关键。假如,你就要一台服务器来处理该服务的请求,那该服务部署在多台服务器的意义就不复存在了。负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题,我们从负载均衡的这四个字就能明显感受到它的意义。 + +1. RandomLoadBalance:随机负载均衡。随机的选择一个。是Dubbo的默认负载均衡策略。 +2. RoundRobinLoadBalance:轮询负载均衡。轮询选择一个。 +3. LeastActiveLoadBalance:最少活跃调用数,相同活跃数的随机。活跃数指调用前后计数差。使慢的 Provider 收到更少请求,因为越慢的 Provider 的调用前后计数差会越大。 +4. ConsistentHashLoadBalance:一致性哈希负载均衡。相同参数的请求总是落在同一台机器上。 + +[http://dubbo.apache.org/zh-cn/blog/dubbo-loadbalance.html](http://dubbo.apache.org/zh-cn/blog/dubbo-loadbalance.html) + +## 为什么使用zk当dubbo的注册中心 +Zookeeper的数据模型很简单,有一系列被称为ZNode的数据节点组成,与传统的磁盘文件系统不同的是,zk将全量数据存储在内存中,可谓是高性能,而且支持集群,可谓高可用,另外支持事件监听。这些特点决定了zk特别适合作为注册中心(数据发布/订阅)。不过要注意网络闪断引发的节点摘除问题。 + +## 几个服务注册与发现的对比 +[https://zhuanlan.zhihu.com/p/145296163](https://zhuanlan.zhihu.com/p/145296163) + +## SPI源码(过程) +先说一下Java的SPI机制 +Java的SPI机制利用ServiceLoader的load方法传递个接口,就会得到该接口的所有的实现类 +要在指定的META-INF的services下 +但是有一说一,只能通过iterator来遍历判断想要的实现类 +而Dubbo和Spring的SPI比Java的灵活一些,可以通过key来获取对应的实例 + +直接说Dubbo的SPI源码过程 +先说一下Dubbo的SPI机制,不仅支持有着Java的SPI,还有着AOP的功能,同时有着DI功能 +1. 通过getExtensionLoader得到该接口的load,不过获取之间会对一些type检查,同时有缓存机制。 +2. 然后通过load调用getExtension,也是一系列检查和缓存,最关键的就是createExtension +3. 其中getExtensionClasses,这个方法返回对应name的接口的实例对象,接着来到injectExtension注入属性 +4. 如果有wrapper包装,就是通过接口的实例类有木有构造器,如果有,最后injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));无限遍历AOP,也就是构造器注入,最后返回带包装的接口的实例对象。 +5. 以上是没有讲依赖注入的过程,官网上有。 + +## 服务导出与暴漏(源码流程) +源码省略,针对于面试就说源码流程即可 +总结一波吧: +1. 当我们看源码知道,导出和暴漏在IoC初始化的最后一步的finishRefresh中的ServiceBean中。 +2. 其中在onApplicationEvent执行export->doExport,在doExport中首先检查provider呀,register呀,monitor呀等,最后来到关键的一步doExportUrls(); +3. 在这一步当中,实际上,就是对注册的url和导出的url拼接,并且将导出的url远程注册到注册中心,最后暴漏一下自己的url,具体的话就第四步。 +4. doExportUrlsFor1Protocol 包括:1. exportLocal,默认本地导出,2. 远程导出:proxyFactory.getInvoker,然后得到wrapperInvoker,最后就是这个关键了protocol.export(wrapperInvoker),然后会有个子流程去构造buildInvokerChain,调用链。这个是服务调用链路 +5. 实际上找Protocol.class接口的实例代理类,默认是dubbo协议,因此调用的dubbo的实例代理类的export方法,继续使用dubbo协议的url,一步一步绑定nettyClient客户端,最后导出自己的调用链。 + + +## 服务引入与目录(源码过程) + +肯定是ReferenceBean + +1. 当我们看源码知道,首先进来的ReferenceBean的get方法->ReferenceConfig的init方法内部 +2. checkDefault检查消费端的全局配置,接着通过SPI获取消费服务的实现类,经过一些列检查又进入了HashMap的缓存当中 +3. init方法中的最后一步createProxy中,这个方法就是将要引入订阅注册中心的服务端的目录,首先是refprotocol.refer方法从注册中心引入和订阅,该方法是核心。 +4. 首先通过RegistryProtocol的refer中,如果是zk协议,那么就启动zk客户端去连接,接着进入doRefer方法中,先在注册中心,注册消费端服务,接着开始通过subscribe订阅注册中心的目录,category、providers、configurators和routers,然后进入notify,调用listener.notify(categoryList),通知categoryList +5. 这时候来到了协议Dubbo的refer中,开始构造路由链,首先buildInvokerChain调用链,Dubbo启动的是netty客户端哦,debug时候看出来的,获取的是netty的client,最后构建成功就返回。 +6. 最后将所有的目录添加到cluster中,并返回invoker,其实该invoker是MockClusterInvoker,ref是它的代理实现类最后初始化完毕。 + +总感觉处处invoker(执行)类似于发送请求一样。 + +## 服务调用、服务降级和集群容错 +先说一下invorker,在服务引入那里最终返回的是MockClusterInvoker的代理实现类,意思就是说,首先进入Java的动态代理,InvokerInvocationHandler,然后调用invork,进入MockClusterInvoker,然后调用invoke进入默认的FailoverClusterInvoker的invoker。每个invoker就是InvokerDelegate委托实现类 +1. 根据我上面说的,其实从服务目录获取所有的提供者Invokers,在经过MockClusterInvoker的时候,如果配置了服务降级,服务降级就是通过mock机制而已,那么如果调用失败,先走Mock的服务降级策略,如果没有配置,然后开始初始化负载均衡策略, +2. 就进入了容错策略的Invoker类,然后通过负载均衡选择一个invoker,开始调用过滤链,最后才会执行我们的Dubbo协议上的客户端,应该是netty吧,去执行invoker +3. 服务那边开始被触发事件之后,也会执行自己的过滤链,然后最后执行自己的InvokerDelegate服务实现委托类,将结果先返回给自己,然后在通过负责处理请求的控制器传给消费端。 +4. 以上是一次调用过程粗略的经过。 diff --git a/Java/classify/dis/RocketMQ.md b/Java/classify/dis/RocketMQ.md new file mode 100644 index 00000000..2181e150 --- /dev/null +++ b/Java/classify/dis/RocketMQ.md @@ -0,0 +1,169 @@ + + +> 注意:先去了解消息队列:https://www.zhihu.com/question/34243607 + + +## 为什么选择RocketMQ作消息队列 +- ActiveMQ 的社区算是比较成熟,但是较目前来说,**ActiveMQ 的性能比较差,而且版本迭代很慢**,不推荐使用。 +- RabbitMQ 在**吞吐量方面虽然稍逊于 Kafka 和 RocketMQ** ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。但是也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做erlang源码级别的研究和定制。如果**业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选**。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 +- RocketMQ 阿里出品,**Java 系开源项目**,源代码我们可以直接阅读,然后可以**定制自己公司的MQ**,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。其次具有分布式事务消息的功能,可以达到消息的最终一致性。 +- kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。**kafka 唯一的一点劣势是有可能消息重复消费**,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集。 + +## RocketMQ组件 +- Producer:消息发布的角色,主要负责把消息发送到Broker,支持分布式集群方式部署。 +- Consumer:消息消费者的角色,主要负责从Broker订阅消息消费,支持分布式集群方式部署。 +- Broker:消息存储的角色,主要负责消息的存储、投递和查询,以及服务高可用的保证,支持分布式集群方式部署。 +- NameServer:是一个非常简单的Topic路由注册中心,其角色类似于Dubbo中依赖的Zookeeper,支持Broker动态注册和发现。 + - 服务注册:NameServer接收Broker集群注册的信息,保存下来作为路由信息的基本数据,并提供心跳检测机制,检查Broker是否存活。 + - 路由信息管理:NameServer保存了Broker集群的路由信息,用于提供给客户端查询Broker的队列信息。Producer和Consumer通过NameServer可以知道Broker集群的路由信息,从而进行消息的投递和消费。 + +## MQ在高并发情况下,假设队列满了如何防止消息丢失? +- 生产者可以采用重试机制。因为消费者会不停的消费消息,可以重试将消息放入队列。 +- 死信队列,可以理解为备胎(推荐) + - 即在消息过期,队列满了,消息被拒绝的时候,都可以扔给死信队列。 + - 如果出现死信队列和普通队列都满的情况,此时考虑消费者消费能力不足,可以对消费者开多线程进行处理。 + +## RocketMQ事务消息原理 + +![mq事务消息-NaWGxH](https://gitee.com/dreamcater/blog-img/raw/master/uPic/mq事务消息-NaWGxH.png) + +想了想,mq也可以 +1. 先给Brock发送一条消息:我要下单了,注意哈 +2. Brock给本地事务回馈消息:ack,好的,我知道了(半投递状态,消费端看不到) +3. 本地事务开始执行业务逻辑,这里首先(校验场次id的座位是否重复,如果没有,直接执行下单:这两个业务非原子,上个锁,要不然可能会出现同样的座位)。下单成功,则返回commit给Brock。消费者此时就可以看到这条消息了。 +4. 如果下单不成功,则返回rollback,Brock一般三天自动删除该无效的消息,消费者也看不到。 +5. 消费者看到了这条消息,调用绑定座位服务,如果失败了,则重试。(消费端不能失败,要不然不能保持一致,如果还是一直失败,则人工处理。) 注意:幂等性 + +## 谈谈死信队列 +**死信队列用于处理无法被正常消费的消息,即死信消息**。 + +当一条消息初次消费失败,**消息队列 RocketMQ 会自动进行消息重试**;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该**消费者对应的特殊队列中**,该特殊队列称为**死信队列**。 + +**死信消息的特点**: + +- 不会再被消费者正常消费。 +- 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3 天内及时处理。 + +**死信队列的特点**: + +- 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。 +- 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。 +- 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。 + +消息队列 RocketMQ 控制台提供对死信消息的查询、导出和重发的功能。 + +## 使用异步消息时如何保证数据的一致性 + +- **借助数据库的事务**:这需要在数据库中创建一个**本地消息表**,这样可以通过**一个事务来控制本地业务逻辑更新**和**本地消息表的写入在同一个事务中**,一旦消息落库失败,则直接全部回滚。如果消息落库成功,后续就可以根据情况基于本地数据库中的消息数据对消息进行重投了。关于本地消息表和消息队列中状态如何保持一致,可以采用 2PC 的方式。在发消息之前落库,然后发消息,在得到同步结果或者消息回调的时候更新本地数据库表中消息状态。然后只需要通过**定时轮询**的方式对状态未已记录但是未发送的消息重新投递就行了。但是这种方案有个前提,就是要求消息的消费者**做好幂等控制**,这个其实异步消息的消费者一般都需要考虑的。 +- 除了使用数据库以外,还可以使用 **Redis** 等缓存。这样就是无法利用关系型数据库自带的事务回滚了。 + +## RockMQ不适用Zookeeper作为注册中心的原因,以及自制的NameServer优缺点? +- ZooKeeper 作为支持**顺序一致性**的中间件,在某些情况下,它为了满足一致性,会丢失一定时间内的**可用性**,RocketMQ 需要注册中心只是为了**发现组件地址**,在某些情况下,RocketMQ 的注册中心可以出现数据不一致性,这同时也是 **NameServer 的缺点,因为 NameServer 集群间互不通信,它们之间的注册信息可能会不一致**。 +- 另外,当有新的服务器加入时,**NameServer 并不会立马通知到 Produer**,而是由 **Produer 定时去请求 NameServer 获取最新的 Broker/Consumer 信息**(这种情况是通过 Producer 发送消息时,负载均衡解决) +- 包括组件通信间使用 Netty 的自定义协议 +- 消息重试负载均衡策略(具体参考 Dubbo 负载均衡策略) +- 消息过滤器(Producer 发送消息到 Broker,Broker 存储消息信息,Consumer 消费时请求 Broker 端从磁盘文件查询消息文件时,在 Broker 端就使用过滤服务器进行过滤) +- Broker 同步双写和异步双写中 Master 和 Slave 的交互 + +## 实现延迟队列 +rocketmq发送延时消息时先把消息按照延迟时间段发送到指定的队列中(rocketmq把每种延迟时间段的消息都存放到同一个队列中)然后通过一个定时器进行轮训这些队列,查看消息是否到期,如果到期就把这个消息发送到指定topic的队列中,这样的好处是同一队列中的消息延时时间是一致的,还有一个好处是这个队列中的消息时按照消息到期时间进行递增排序的,说的简单直白就是队列中消息越靠前的到期时间越早 + +缺点:定时器采用了timer,timer是单线程运行,如果延迟消息数量很大的情况下,可能单线程处理不过来,造成消息到期后也没有发送出去的情况 + +改进点:可以在每个延迟队列上各采用一个timer,或者使用timer进行扫描,加一个线程池对消息进行处理,这样可以提供效率 + +## 消息队列如何保证顺序消费 +生产者中把 orderId 进行取模,把相同模的数据放到 messagequeue 里面,消费者消费同一个 messagequeue,只要消费者这边有序消费,那么可以保证数据被顺序消费。 + +RocketMQ:顺序消息一般使用集群模式,是指对消息消费者内的线程池中的线程对消息消费队列只能串行消费。并并发消息消费最本质的区别是消息消费时必须成功锁定消息消费队列,在Broker端会存储消息消费队列的锁占用情况。 +[https://blog.csdn.net/AAA821/article/details/86650471](https://blog.csdn.net/AAA821/article/details/86650471) + +## RocketMQ消息失败策略 +[https://developer.aliyun.com/article/717340](https://developer.aliyun.com/article/717340) + +## RocketMQ顺序消息消费 +[https://juejin.im/post/6844903982805024782](https://juejin.im/post/6844903982805024782) + +[https://www.cnblogs.com/hzmark/p/orderly_message.html](https://www.cnblogs.com/hzmark/p/orderly_message.html) + +## 零拷贝 +都知道内核和用户态了,不必多说了 +1. 从磁盘复制数据到内核态内存; +2. 从内核态内存复制到用户态内存; +3. 然后从用户态内存复制到网络驱动的内核态内存; +4. 最后是从网络驱动的内核态内存复制到网卡中进行传输。 + +但,通过使用mmap的方式,可以省去向用户态的内存复制,提高速度。这种机制在Java中是通过MappedByteBuffer实现的(零拷贝) + +[https://zhuanlan.zhihu.com/p/83398714](https://zhuanlan.zhihu.com/p/83398714) + +## 消息存储 +### 存储过程 +1. 消息生产者发送消息 +2. MQ收到消息,将消息进行持久化,在存储中新增一条记录 +3. 返回ACK给生产者 +4. MQ push 消息给对应的消费者,然后等待消费者返回ACK +5. 如果消息消费者在指定时间内成功返回ack,那么MQ认为消息消费成功,在存储中删除消息,即执行第6步;如果MQ在指定时间内没有收到ACK,则认为消息消费失败,会尝试重新push消息,重复执行4、5、6步骤 +6. MQ删除消息 + +想说一点,activeMQ的存储介质DB,这就影响了存储效率,其他几位MQ采用的文件系统,并且依照顺序写,极大跟随了SSD的步伐。 + + +### 存储结构 +RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。 + +- CommitLog:存储消息的元数据 +- ConsumerQueue:存储消息在CommitLog的索引 +- IndexFile:为了消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过IndexFile来查找消息的方法不影响发送与消费消息的主流程 + +## 刷盘机制 +**同步机制**:在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘, 然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。 +1. 封装刷盘请求 +2. 提交刷盘请求 +3. 线程阻塞5秒,等待刷盘结束 + +服务那边: +1. 加锁 +2. 遍历requestsRead +3. 刷盘 +4. 唤醒发送消息客户端 +5. 更新刷盘监测点 + +**异步机制**:在返回写成功状态时,消息**可能**只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。 + +在消息追加到内存后,立即返回给消息发送端。如果开启transientStorePoolEnable,RocketMQ会单独申请一个与目标物理文件(commitLog)同样大小的堆外内存,该堆外内存将使用内存锁定,确保不会被置换到虚拟内存中去,消息首先追加到堆外内存,然后提交到物理文件的内存映射中,然后刷写到磁盘。如果未开启transientStorePoolEnable,消息直接追加到物理文件直接映射文件中,然后刷写到磁盘中。 + +开启transientStorePoolEnable后异步刷盘步骤: + +1. 将消息直接追加到ByteBuffer(堆外内存) +2. CommitRealTimeService线程每隔200ms将ByteBuffer新追加内容提交到MappedByteBuffer中 +3. MappedByteBuffer在内存中追加提交的内容,wrotePosition指针向后移动 +4. commit操作成功返回,将committedPosition位置恢复 +5. FlushRealTimeService线程默认每500ms将MappedByteBuffer中新追加的内存刷写到磁盘 + +## 路由管理 +### 心跳机制 +- RocketMQ路由注册是通过Broker与NameServer的心跳功能实现的。 +- Broker启动时向集群中所有的NameServer发送心跳信息,每隔30s向集群中所有NameServer发送心跳包,NameServer收到心跳包时会更新brokerLiveTable缓存中BrokerLiveInfo的lastUpdataTimeStamp信息,然后NameServer每隔10s扫描brokerLiveTable,如果连续120S没有收到心跳包,NameServer将移除Broker的路由信息同时关闭Socket连接。 + +### 删除路由 +- `Broker`每隔30s向`NameServer`发送一个心跳包,心跳包包含`BrokerId`,`Broker`地址,`Broker`名称,`Broker`所属集群名称、`Broker`关联的`FilterServer`列表。但是如果`Broker`宕机,`NameServer`无法收到心跳包,此时`NameServer`如何来剔除这些失效的`Broker`呢? +- `NameServer`会每隔10s扫描`brokerLiveTable`状态表,如果`BrokerLive`的**lastUpdateTimestamp**的时间戳距当前时间超过120s,则认为`Broker`失效,移除该`Broker`,关闭与`Broker`连接,同时更新`topicQueueTable`、`brokerAddrTable`、`brokerLiveTable`、`filterServerTable`。 + +### 路由发现 +RocketMQ路由发现是非实时的,当Topic路由出现变化后,NameServer不会主动推送给客户端,而是由客户端定时拉取主题最新的路由。 + +其实就是一张图 +![](https://imgkr.cn-bj.ufileos.com/6ac077ee-bc03-4785-9200-426457563858.png) + + + + +## 长轮训 + +拉取长轮询分析:源码上实际上还是个监听器 +- RocketMQ未真正实现消息推模式,而是消费者主动向消息服务器拉取消息,RocketMQ推模式是循环向消息服务端发起消息拉取请求,如果消息消费者向RocketMQ拉取消息时,消息未到达消费队列时,如果不启用长轮询机制,则会在服务端等待shortPollingTimeMills时间后(挂起)再去判断消息是否已经到达指定消息队列,如果消息仍未到达则提示拉取消息客户端PULL—NOT—FOUND(消息不存在); +- 如果开启长轮询模式,RocketMQ一方面会每隔5s轮询检查一次消息是否可达,同时一有消息达到后立马通知挂起线程再次验证消息是否是自己感兴趣的消息,如果是则从CommitLog文件中提取消息返回给消息拉取客户端,否则直到挂起超时,超时时间由消息拉取方在消息拉取是封装在请求参数中,PUSH模式为15s,PULL模式通过DefaultMQPullConsumer#setBrokerSuspendMaxTimeMillis设置。RocketMQ通过在Broker客户端配置longPollingEnable为true来开启长轮询模式。 + + +说白了, push模式只不过在pull模式下加了个监控......哈哈哈哈 \ No newline at end of file diff --git a/Java/classify/dis/Sentinel.md b/Java/classify/dis/Sentinel.md new file mode 100644 index 00000000..26b077e9 --- /dev/null +++ b/Java/classify/dis/Sentinel.md @@ -0,0 +1,35 @@ + +## 为什么选择Sentinel? +Sentinel是一个面向分布式架构的轻量级服务保护框架,主要以流量控制、熔断降级、系统负载保护等多个维度。 + +隔离策略:信号量隔离(并发线程数限流) + +熔断策略: +1. 基于响应时间 +2. 异常比率 +3. 异常数 + +限流:基于QPS限流 + +控制台:查看秒级监控、机器发现等。 + +## 服务限流 +当**系统资源不够,不足以应对大量请求**,对系统按照预设的规则进行流量限制或功能限制 + +## 服务熔断 +当**调用目标服务的请求和调用大量超时或失败,服务调用方为避免造成长时间的阻塞造成影响其他服务**,后续对该服务接口的调用不再经过进行请求,直接执行本地的默认方法 + +## 服务降级 +**为了保证核心业务在大量请求下能正常运行,根据实际业务情况及流量,对部分服务降低优先级**,有策略的不处理或用简单的方式处理 + +## 为什么熔断降级 +系统承载的访问量是有限的,如果不做流量控制,会导致系统资源占满,服务超时,从而所有用户无法使用,通过服务限流控制请求的量,服务降级省掉非核心业务对系统资源的占用,最大化利用系统资源,尽可能服务更多用户 + +## 和Hystrix对比 +![sentinel和hystrix-qb3wFi](https://gitee.com/dreamcater/blog-img/raw/master/uPic/sentinel和hystrix-qb3wFi.png) + +**值得补充的是**:相比 Hystrix 基于线程池隔离进行限流,这种方案**虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响**。 + +Sentinel 并发线程数限流不负责创建和管理线程池,而是**简单统计当前请求上下文的线程数目,如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离**。 + +[官网补充](http://dubbo.apache.org/zh-cn/blog/sentinel-introduction-for-dubbo.html) \ No newline at end of file diff --git a/Java/classify/dis/zookeeper.md b/Java/classify/dis/zookeeper.md new file mode 100644 index 00000000..81e87ca1 --- /dev/null +++ b/Java/classify/dis/zookeeper.md @@ -0,0 +1,40 @@ + +## 什么是Zookeeper? +ZooKeeper 是一个开源的分布式协调服务 + +## Zookeeper使用场景? +1. 数据发布/订阅、 +2. 负载均衡、 +3. 命名服务、 +4. 分布式协调/通知、 +5. 集群管理、 +6. Master 选举、 +7. 分布式锁和分布式队列等功能。 + +## Zookeeper的特点 +- 顺序一致性: 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。 +- 原子性: 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。 +- 单一系统映像 : 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。 +- 可靠性: 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。 + +## Zookeeper的原理? +ZAB 协议&Paxos算法 +ZAB协议包括两种基本的模式:**崩溃恢复**和**消息广播**。当整个 Zookeeper 集群刚刚启动或**者Leader服务器宕机**、**重启**或者网络故障导致**少于过半的服务器与 Leader 服务器保持正常通信**时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步。当集群中超过**半数机器与该 Leader 服务器完成数据同步**之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。 + +## 选择算法和流程 + + +先说: +> 每次投票会包含所推举的服务器的 myid 和 ZXID、epoch,使用(myid, ZXID,epoch)来表示 +> zxid,也就是事务 id,为了保证事务的顺序一致性,zookeeper 采用了递增的事务 id 号(zxid)来标识事务。 +> PK 1. 优先检查 ZXID。ZXID 比较大的服务器优先作为Leader 2. 如果 ZXID 相同,那么就比较 myid。myid 较大的服务器作为 Leader 服务器。 + +目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下: + +1. 服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。 +2. 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。 +3. 服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为leader,服务器1,2成为follower。 +4. 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为follower。 +5. 服务器5启动,后面的逻辑同服务器4成为follower。 + +[https://www.cnblogs.com/wuzhenzhao/p/9983231.html](https://www.cnblogs.com/wuzhenzhao/p/9983231.html) \ No newline at end of file diff --git "a/Java/classify/dis/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\256\227\346\263\225.md" "b/Java/classify/dis/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\256\227\346\263\225.md" new file mode 100644 index 00000000..e13c1969 --- /dev/null +++ "b/Java/classify/dis/\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247\347\256\227\346\263\225.md" @@ -0,0 +1,39 @@ + +## paxos算法 +> 要讲这个算法,还要先扯背景:在常见的分布式系统中,总会发生诸如机器宕机或网络异常(等情况。Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。 + +> 其实在整个提议和投票过程当中,主要的角色就是“提议者”和“接受者” + +该算法大致流程:其实分为两个阶段 + +1. 因为存在多个“提议者”Proposer,如果都提意见,那么“接受者”Acceptor不就炸掉了嘛?到底接受谁啊?所以,要先明确哪个“提议者”是领袖,最厉害的那个,先把这个给挑出来。尽早的让意见统一,并且早点形成多数派。 +2. 由上阶段选出的意见领袖提出提议,“接受者”反馈意见。如果多数“接受者”接受了一个提议,那么这个提议就通过了。 + +[例子](https://ocavue.com/paxos.html#%E8%8A%82%E7%82%B9%E6%95%85%E9%9A%9C%E7%9A%84%E4%BE%8B%E5%AD%90) + +## ZAB +- ZAB协议包括两种基本的模式:**崩溃恢复**和**消息广播**。 +- 当整个 Zookeeper 集群刚刚启动或**者Leader服务器宕机**、**重启**或者网络故障导致**少于过半的服务器与 Leader 服务器保持正常通信**时,所有服务器进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中 Follower 服务器开始与新的 Leader 服务器进行数据同步。 +- 当集群中超过**半数机器与该 Leader 服务器完成数据同步**之后,退出恢复模式进入消息广播模式,Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。 + +## zk的leader选举算法和流程 +目前有5台服务器,每台服务器均没有数据,它们的编号分别是1,2,3,4,5,按编号依次启动,它们的选择举过程如下: + +1. 服务器1启动,给自己投票,然后发投票信息,由于其它机器还没有启动所以它收不到反馈信息,服务器1的状态一直属于Looking。 +2. 服务器2启动,给自己投票,同时与之前启动的服务器1交换结果,由于服务器2的编号大所以服务器2胜出,但此时投票数没有大于半数,所以两个服务器的状态依然是LOOKING。 +3. 服务器3启动,给自己投票,同时与之前启动的服务器1,2交换信息,由于服务器3的编号最大所以服务器3胜出,此时投票数正好大于半数,所以服务器3成为leader,服务器1,2成为follower。 +4. 服务器4启动,给自己投票,同时与之前启动的服务器1,2,3交换信息,尽管服务器4的编号大,但之前服务器3已经胜出,所以服务器4只能成为follower。 +5. 服务器5启动,后面的逻辑同服务器4成为follower。 + +[https://www.cnblogs.com/wuzhenzhao/p/9983231.html](https://www.cnblogs.com/wuzhenzhao/p/9983231.html) + + +## raft + +[https://zhuanlan.zhihu.com/p/66441389](https://zhuanlan.zhihu.com/p/66441389) + + + +## 分布式一致性哈希 + +[https://zhuanlan.zhihu.com/p/24440059](https://zhuanlan.zhihu.com/p/24440059) \ No newline at end of file diff --git "a/Java/classify/dis/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" "b/Java/classify/dis/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" new file mode 100644 index 00000000..54053faa --- /dev/null +++ "b/Java/classify/dis/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" @@ -0,0 +1,74 @@ + + +> 无非就是在分布式或者集群的环境下,业务上存在很多上游调用下游链,所以存在是了事务或者数据不一致的情况。 + +## 2PC +![2pc1-DMjtyZ](https://gitee.com/dreamcater/blog-img/raw/master/uPic/2pc1-DMjtyZ.jpg) + +--- +第一阶段: +- 协调者 向所有的 参与者 发送事务预处理请求,称之为Prepare,并开始等待各 参与者 的响应。 +- 各个 参与者 节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向 协调者 报告说:“我这边可以处理了/我这边不能处理”。. +- 如果 参与者 成功执行了事务操作,那么就反馈给协调者 Yes 响应,表示事务可以执行,如果没有 参与者 成功执行事务,那么就反馈给协调者 No 响应,表示事务不可以执行。 + +图就不放了,很简单 +第二阶段:成功 +- 协调者 向 所有参与者 节点发出Commit请求. +- 参与者 收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源。 + +缺点: +- 性能问题:无论是在第一阶段的过程中,还是在第二阶段,所有的参与者资源和协调者资源都是被锁住的,只有当所有节点准备完毕,事务 协调者 才会通知进行全局提交,参与者 进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。 +- 单节点故障:由于协调者的重要性,一旦 协调者 发生故障。参与者 会一直阻塞下去。尤其在第二阶段,协调者 发生故障,那么所有的 参与者 还都处于锁定事务资源的状态中,而无法继续完成事务操作。(虽然协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题) + +2PC出现单点问题的三种情况: +- 协调者正常,参与者宕机:由于 协调者 无法收集到所有 参与者 的反馈,会陷入阻塞情况。解决方案:引入超时机制,如果协调者在超过指定的时间还没有收到参与者的反馈,事务就失败,向所有节点发送终止事务请求。 +- 协调者宕机,参与者正常:无论处于哪个阶段,由于协调者宕机,无法发送提交请求,所有处于执行了操作但是未提交状态的参与者都会陷入阻塞情况.解决方案:引入协调者备份,同时协调者需记录操作日志.当检测到协调者宕机一段时间后,协调者备份取代协调者,并读取操作日志,向所有参与者询问状态。 +- 协调者和参与者都宕机 + + +## 3PC +对2pc的优化 + +- 引入超时机制 +- 在第一阶段和第二阶段中插入一个准备阶段,尝试获取数据库锁。如果可以就yes + +## TCC +Try-Confirm-Cancel +- 先是服务调用链路依次执行 Try 逻辑。 +- 如果都正常的话,TCC 分布式事务框架推进执行 Confirm 逻辑,完成整个事务。 +- 如果某个服务的 Try 逻辑有问题,TCC 分布式事务框架感知到之后就会推进执行各个服务的 Cancel 逻辑,撤销之前执行的各种操作。 + +这就是所谓的 TCC 分布式事务。TCC 分布式事务的核心思想,说白了,就是当遇到下面这些情况时: +- 某个服务的数据库宕机了。 +- 某个服务自己挂了。 +- 那个服务的 Redis、Elasticsearch、MQ 等基础设施故障了。 +- 某些资源不足了,比如说库存不够这些。 + + +先来 Try 一下,不要把业务逻辑完成,先试试看,看各个服务能不能基本正常运转,能不能先冻结我需要的资源。 + +如果 Try 都 OK,也就是说,底层的数据库、Redis、Elasticsearch、MQ 都是可以写入数据的,并且你保留好了需要使用的一些资源(比如冻结了一部分库存)。 + +接着,再执行各个服务的 Confirm 逻辑,基本上 Confirm 就可以很大概率保证一个分布式事务的完成了。 + +那如果 Try 阶段某个服务就失败了,比如说底层的数据库挂了,或者 Redis 挂了,等等。 + +此时就自动执行各个服务的 Cancel 逻辑,把之前的 Try 逻辑都回滚,所有服务都不要执行任何设计的业务逻辑。保证大家要么一起成功,要么一起失败。 + +等一等,你有没有想到一个问题?如果有一些意外的情况发生了,比如说订单服务突然挂了,然后再次重启,TCC 分布式事务框架是如何保证之前没执行完的分布式事务继续执行的呢? + +所以,TCC 事务框架都是要记录一些分布式事务的活动日志的,可以在磁盘上的日志文件里记录,也可以在数据库里记录。保存下来分布式事务运行的各个阶段和状态。 + +问题还没完,万一某个服务的 Cancel 或者 Confirm 逻辑执行一直失败怎么办呢? + +那也很简单,TCC 事务框架会通过活动日志记录各个服务的状态。举个例子,比如发现某个服务的 Cancel 或者 Confirm 一直没成功,会不停的重试调用它的 Cancel 或者 Confirm 逻辑,务必要它成功! + +当然了,如果你的代码没有写什么 Bug,有充足的测试,而且 Try 阶段都基本尝试了一下,那么其实一般 Confirm、Cancel 都是可以成功的! + +## 可靠消息最终一致性 +在上面的通用方案设计里,完全依赖可靠消息服务的各种自检机制来确保: + +- 如果上游服务的数据库操作没成功,下游服务是不会收到任何通知。 +- 如果上游服务的数据库操作成功了,**可靠消息服务死活都会确保将一个调用消息投递给下游服务,而且一定会确保下游服务务必成功处理这条消息**。 + +通过这套机制,保证了基于 MQ 的异步调用/通知的服务间的分布式事务保障。其实阿里开源的 RocketMQ,就实现了可靠消息服务的所有功能,核心思想跟上面类似。 \ No newline at end of file diff --git "a/Java/classify/dis/\345\210\206\345\270\203\345\274\217\346\246\202\345\277\265.md" "b/Java/classify/dis/\345\210\206\345\270\203\345\274\217\346\246\202\345\277\265.md" new file mode 100644 index 00000000..492bc810 --- /dev/null +++ "b/Java/classify/dis/\345\210\206\345\270\203\345\274\217\346\246\202\345\277\265.md" @@ -0,0 +1,9 @@ +## 集群、分布式和微服务的概念 +微服务是一种架构风格,一个大型复杂软件应用由**一个或多个微服务组成**。系统中的**各个微服务可被独立部署,各个微服务之间是松耦合的**。**每个微服务仅关注于完成一件任务并很好地完成该任务**。在所有情况下,每个任务代表着一个小的业务能力。 + +- 分布式将一个大的系统划分为多个业务模块,**业务模块分别部署到不同的机器上**,各个业务模块之间通过接口进行数据交互。区别**分布式的方式是根据不同机器不同业务**。 +- 集群模式是**不同服务器部署同一套服务对外访问,实现服务的负载均衡**。区别集群的方式是根据部署多台服务器业务是否相同。 +- 微服务的设计是为了不因为某个模块的升级和BUG影响现有的系统业务。微服务与分布式的细微差别是,**微服务的应用不一定是分散在多个服务器上,他也可以是同一个服务器**。 + +## 什么是RPC? +RPC(RemoteProcedureCall)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务A、B部署在两台不同的机器上,那么服务A如果想要调用服务B中的某个方法该怎么办呢?使用HTTP请求当然可以,但是可能会比较慢而且一些优化做的并不好。RPC的出现就是为了解决这个问题。 \ No newline at end of file diff --git "a/Java/classify/dis/\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" "b/Java/classify/dis/\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" new file mode 100644 index 00000000..e228756e --- /dev/null +++ "b/Java/classify/dis/\345\270\203\351\232\206\350\277\207\346\273\244\345\231\250.md" @@ -0,0 +1,7 @@ +## 布隆过滤器原理 +- 布隆过滤器可以检查值是 “可能在集合中” 还是 “绝对不在集合中”。 +- 布隆过滤器(Bloom Filter)本质上是由长度为 m 的位向量或位列表(仅包含 0 或 1 位值的列表)组成,最初所有的值均设置为 0 +- 为了将数据项添加到布隆过滤器中,我们会提供 K 个不同的哈希函数,并将结果位置上对应位的值置为 “1” + +总结:当我们搜索一个值的时候,若该值经过 K 个哈希函数运算后的任何一个索引位为 ”0“,那么该值肯定不在集合中。但如果所有哈希索引值均为 ”1“,则只能说该搜索的值可能存在集合中。 +[5分钟搞懂布隆过滤器,亿级数据过滤算法你值得拥有!](https://juejin.im/post/5de1e37c5188256e8e43adfc) \ No newline at end of file diff --git "a/Java/classify/dis/\351\231\220\346\265\201\347\256\227\346\263\225.md" "b/Java/classify/dis/\351\231\220\346\265\201\347\256\227\346\263\225.md" new file mode 100644 index 00000000..0accc1cb --- /dev/null +++ "b/Java/classify/dis/\351\231\220\346\265\201\347\256\227\346\263\225.md" @@ -0,0 +1,34 @@ + +> 在大型电商等系统中,我们回努力提升API的吞吐量和QPS(Query Per Second 每秒查询量),但总归有上限.为了应对巨大流量的瞬间提交,我们会做对应的限流处理. + +常见的限流算法有计数器,漏桶,令牌桶. + +## 计数器 + +计数器限流方式比较粗暴,**一次访问设置一次计数**,在系统内设置每秒的访问量,**超过访问量的访问直接丢弃**,实现访问限流.这种算法的弊端就是,在开始的时间,访问量被使用完后,1S内会有长时间的真空期是处于接口不可用的状态的. + +实现方式和拓展方式很多.比如可以使用redis进行1S的100次访问计数,来一个流量100-1当数量到达0时,拒绝后续的访问.也可以不拒绝而是将请求放入缓存队列,根据实际业务情况选择不同的实现方式. + +## 漏斗 + +在计数器算法中我们看到,当使用了所有的访问量后,接口会完全处于不可用状态.有些系统不喜欢这样的处理方式,可以选择漏斗算法进行限流. 漏斗算法的原理就像名字,是一个漏斗,**访问量从漏斗的大口进入,从漏斗的小口进入系统**.这样不管是多大的访问量进入漏斗,最后进入系统的访问量都是固定的.漏斗的好处就是,大批量访问进入时,漏斗有容量,不超过容量(容量的设计=固定处理的访问量*可接受等待时长)的数据都可以排队等待处理,超过的才会丢弃. + +实现方式可以使用队列,队列设置容量,访问可以大批量塞入队列,满队列后丢弃后续访问量.队列的出口以固定速率拿去访问量处理. + +这种方案由于出口速率是固定的,那么当就无法应对短时间的突发流量. + +## 令牌桶 + +令牌桶算法算是漏斗算法的改进版,为了处理短时间的突发流量而做了优化,令牌桶算法主要由三部分组成令牌流、数据流、令牌桶. + +- 令牌流:流通令牌的管道,用于生成的令牌的流通,放入令牌桶中. +- 数据流:进入系统的数据流量 +- 令牌桶:保存令牌的区域,可以理解为一个缓存区.令牌保存在这里用于使用. + +令牌桶算法会**按照一定的速率生成令牌放入令牌桶,访问要进入系统时,需要从令牌桶获取令牌,有令牌的可以进入,没有的被抛弃**.**由于令牌桶的令牌是源源不断生成的,当访问量小时,可以留存令牌达到令牌桶的上限,这样当短时间的突发访问量来时,积累的令牌数可以处理这个问题.当访问量持续大量流入时,由于生成令牌的速率是固定的,最后也就变成了类似漏斗算法的固定流量处理.** + +实现方式和漏斗也比较类似,可以使用一个队列保存令牌,一个定时任务用等速率生成令牌放入队列,访问量进入系统时,从队列获取令牌再进入系统. + +Google开源的guava包中RateLimiter类实现了令牌桶算法,不过这是单机的.集群可以按照上面的实现方式实现,队列使用中间件MQ实现,配合负载均衡算法,考虑集群各个服务器的承压情况做对应服务器的队列是比较建议的做法. + +[参考](https://zhuanlan.zhihu.com/p/95066428) \ No newline at end of file diff --git "a/Java/classify/jvm/JVM\345\206\205\345\255\230\345\214\272\345\237\237.md" "b/Java/classify/jvm/JVM\345\206\205\345\255\230\345\214\272\345\237\237.md" new file mode 100644 index 00000000..00aa7025 --- /dev/null +++ "b/Java/classify/jvm/JVM\345\206\205\345\255\230\345\214\272\345\237\237.md" @@ -0,0 +1,71 @@ + +# JVM内存区域 +> JVM这一块,经常还是经常被问到的 + +面试官:讲一讲JVM内存区域 + +我:行,先放两张图 + +![JVM内存模型-1.8之前](https://gitee.com/dreamcater/blog-img/raw/master/uPic/JVM内存模型-1.8之前-EKvhGB.png) + +![JVM内存模型-1.8](https://gitee.com/dreamcater/blog-img/raw/master/uPic/JVM内存模型-1.8-y9XNlT.png) + +总体来说,粗略的分为**堆和栈**,那么**栈是线程私有的**,而**堆是线程共享的**。那么**栈**又问分为**程序计数器**,**虚拟机栈**,**本地方法栈**。堆稍后再说,当然还有**方法区**,稍后单独说。 + +## 程序计数器 + +- **字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制**,如:顺序执行、选择、循环、异常处理。 +- 在多线程的情况下,**程序计数器用于记录当前线程执行的位置**,**从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了**。 +- **程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域**,它的生命周期随着线程的创建而创建,随着线程的结束而死亡 + +## 虚拟机栈 + +- 说白了,通俗的讲,主要是**对象中的方法产生的各种"材料"**。 +- 因此,虚拟机栈存放的是**局部变量表**、**操作数栈**、**动态链接**、**方法出口**。 +- 局部变量表存8**大基本数据类型以及引用类型**。 +- 当然,栈也会非常error: + - **StackOverFlowError**: 若 J**ava 虚拟机栈的内存大小不允许动态扩展**,**那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候**,就抛出 StackOverFlowError 异常。 + - **OutOfMemoryError**:若 **Java 虚拟机栈的内存大小允许动态扩展**,**且当线程请求栈时内存用完了**,**无法再动态扩展了**,此时抛出 OutOfMemoryError 异常。 + +## 本地方法栈 + +虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而**本地方法栈则为虚拟机使用到的 Native 方法服务**,JDK源码中很多本地方法哦。 + +## 方法区 + +这里单独说一下**方法区**: + +**方法区与 Java 堆一样,是各个线程共享的内存区域**,它用于存储已被虚拟机加载的**类信息**、**常量**、**静态变量**、即时编译器编译后的代码等数据。不过随着版本的变化,会发生变化。 + +- 1.6:运行时常量池在Perm Gen区(也就是方法区)中;字符串常量池在**运行时常量池**中。 +- 1.7:运行时常量池依然在Perm Gen区(也就是方法区)中在JDK7版本中,永久代的转移工作就已经开始了,将譬如**符号引用转移到了native heap**;**字面量转移到了java heap**;**类的静态变量转移到了java heap**。但是运行时常量池依然还存在,只是很多内容被转移,其只存着这些被转移的引用;字符串常量池被分配到了**Java堆的主要部分**。也就是字符串常量池从运行时常量池分离出来了。 +- 1.8:JVM已经将**运行时常量池从方法区中移了出来**,在**Java 堆(Heap)中开辟了一块区域存放运行时常量池**。同时永久代被移除,**以元空间代替**。**元空间并不在虚拟机中,而是使用本地内存**;**字符串常量池存在于Java堆中**。 + +方法区,依然会发生error,因为在之前的版本中,**当一个类启动的时候,也会加载很多class文件**,那么也会充满整个方法区,当满的时候,也会error的,当然,在以前的版本中,字符串常量池在方法区中,而**使用String.intern()方法,依然会占满空间并error**。 + +## 直接内存 + +**直接内存**并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError 异常出现(如**DirectByteBuffer**)。本机直接内存的分配不会受到 Java 堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。 + +## 堆 + +**堆**:此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。 + +- 分为四区,分别为eden区、s0("From)区、s1("To")和tentired +- 在初始阶段,新创建的对象被分配到Eden区,survivor的两块空间都为空。 +- 当Eden区满了的时候,minor GC触发 +- 经过扫描与标记,存活的对象被复制到S0,不存活的对象被回收 +- 在下一次的Minor GC中,Eden区的情况和上面一致,没有引用的对象被回收,存活的对象被复制到survivor区。然而在survivor区,S0的所有的数据都被复制到S1,需要注意的是,在上次minor GC过程中移动到S0中的相同存活的对象在复制到S1后其年龄要加1。此时Eden区S0区被清空,所有存活的数据都复制到了S1区,并且S1区存在着年龄不一样的对象(重点) +- 再下一次MinorGC则重复这个过程,这一次survivor的两个区对换,存活的对象被复制到S0,存活的对象年龄加1,Eden区和另一个survivor区被清空。 + +## Minor GC和Full GC + +**Minor GC和Full GC触发条件**: + +- Minor GC触发条件:当Eden区满时,触发Minor GC。 +- Full GC触发条件: + 1. 调用System.gc时,系统建议执行Full GC,但是不必然执行 + 2. 老年代空间不足 + 3. **方法区**空间不足 + 4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存 + 5. 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小 \ No newline at end of file diff --git "a/Java/classify/jvm/\345\236\203\345\234\276\345\233\236\346\224\266.md" "b/Java/classify/jvm/\345\236\203\345\234\276\345\233\236\346\224\266.md" new file mode 100644 index 00000000..f70f83ca --- /dev/null +++ "b/Java/classify/jvm/\345\236\203\345\234\276\345\233\236\346\224\266.md" @@ -0,0 +1,136 @@ +# 垃圾回收 + +> 这里可以主要讲CMS和G1,简单谈一些其他的 + +## JVM垃圾回收 + +面试官:知道JVM垃圾回收机制吗? + +我:知道,首先说一下垃圾回收算法,因为其中的一个分代收集,所以让堆又分为年轻代和老年代。 + +## 垃圾回收算法 + +垃圾回收算法: + +1. **标记-清除** + +该算法分为“标记”和“清除”阶段:首先**标记出所有需要回收的对象**,在标记完成后**统一回收所有被标记的对象**。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题: + +- **效率问题** +- **空间问题(标记清除后会产生大量不连续的碎片)** + +2. **标记-整理** + +根据**老年代的特点**提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是**让所有存活的对象向一端移动**,然后直接清理掉端边界以外的内存。(老年代一般存入的是大对象,时间比较久的对象) + +3. **复制** + +为了**解决效率**问题,“复制”收集算法出现了。它可以将**内存分为大小相同的两块**,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。**这样就使每次的内存回收都是对内存区间的一半进行回收**。(堆的年轻代又分为Eden、s0和s1) + +4. **分代** + +**比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。** + +一般情况: + +- 大多数情况下,**对象在新生代中 eden 区分配**。当 eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC. +- **大对象直接进入老年代**,大对象就是需要**大量连续内存空间的对象**(比如:字符串、数组)。频繁复制降低性能。 +- 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1. 对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁),就会被**晋升到老年代**中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 + +## 如何判断死亡 + +面试官:如何判断对象死亡? + +我:有两种策略,其一为**引用计数法**,其二为**可达性分析**。 + +1. **引用计数法** + +给对象中添加一个引用计数器,每当有一个地方**引用它**,**计数器就加 1**;**当引用失效**,**计数器就减 1**;任何时候**计数器为 0 的对象就是不可能再被使用的**。 + +**这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。** + +2. **可达性分析** + +这个算法的基本思想就是通过一系列的称为 **“GC Roots”** 的对象作为起点,**从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的**。 + +哪些可以作为GC Roots的根: + +- 虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中应用的对象。 +- 本地方法栈中JNI(native方法)引用的对象 +- 方法区中的类静态属性引用的对象 +- 方法区中常量引用的对象 + +面试官:如何枚举根节点? + +我:以上引用作用GC Roots的根,如果**方法区和大,要逐个检查这里面的引用,那么必然会消耗很多时间**,而且枚举根节点需要停顿的。在HotSpot的实现中,是使用一组称为**OopMap的数据结构**来达到这个目的的,在类加载完成的时候,**HotSpot就把对象内什么偏移量是什么类型的数据计算出来**,在JIT编译过程中,**也会在特定的位置记录下栈和寄存器中哪些位置是引用**。这样,GC在扫描时就可以直接得这心信息了。 + +但一个很现实的问题:**可能导致引用关系变化,或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,那么会需要大量的额外空间,这样GC成本很高,安全点由此而来**。 + +实际上,在JIT编译过程中,在**特定的位置记录下栈和寄存器哪些位置是引用**,实际上这些位置就是**安全点**,意思就是说,**程序执行时并非在所有地方都能停顿下来开始GC,只有在达到安全点时才能暂停**。 + +Safepoint机制保证了程序执行时,在**不太长的时间内就会遇到可进入GC的Safepoint**,但**如果线程处于Sleep或者Blocked状态**,这时候线程**无法响应JVM的中断请求**,JVM也显然不太可能等待线程重新被分配CPU时间,这种情况就需要**安全域**来解决。**安全域是指在一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。这时候安全点就被扩展到了Safe Region**。 + +## 垃圾回收器 + +面试官:给我讲讲垃圾收集器吧 + +我:当然没问题,有一张有趣的图 + +![垃圾回收器-DSVRAO](https://gitee.com/dreamcater/blog-img/raw/master/uPic/垃圾回收器-DSVRAO.png) + +小插曲:咱们知道,堆分为新生代和老年代,那么从这张图可以看出,新生代有Serial、ParNew和Parallel Scavenge而老年代自然也有Serial Old和Parallel Old,新生代和老年代都有串并行收集器,能互相搭配,但看CMS就很特殊,它是老年代的收集器,能从图中可以看出来,它不稳定呀,居然用Serial Old当备胎,而且为了能搭配CMS的并行垃圾收集器,就给它造了一个ParNew,哈哈哈(开个玩笑)。G1暂且不说,横跨新生和老年。在它这一块不分家,一把抓。 + +我就简单说一下串并行垃圾收集器,太古老了,面试官也不想听。 + +你像Serial和ParNew呀,其实在STW的时候,一个是**单线程**,一个是**多线程**回收垃圾。而ParNew和Parallel Scavenge的区别仅仅是**吞吐量**,后者重在吞吐量上(高效率利用CPU)。所以,**Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。而ParNew是在Server 模式下的虚拟机的首要选择之一。以上垃圾收集器新生代采用复制,而老年代采用标记-整理。** + +**CMS垃圾收集器**: + +**CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。** + +**CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。** + +从名字中的**Mark Sweep**这两个词可以看出,CMS 收集器是一种 **“标记-清除”算法**实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤: + +- 初始标记:暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; +- 并发标记:同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 +- 重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 +- 并发清除:开启用户线程,同时 GC 线程开始对为标记的区域做清扫。 + + +从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:**并发收集、低停顿**。但是它有下面三个明显的缺点: + +- **对 CPU 资源敏感;** +- **无法处理浮动垃圾;** +- **它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。** + +因此,为了解决以上缺点,**G1**就出现了: + +- **将整个Java堆划分为多个大小相等的独立区域(Region)**,虽然还保留新生代和老年代的概念,但**新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合** + +- 并行与并发: G1 能充分利用 **CPU、多核**环境下的硬件优势,使用多个 CPU来缩短 Stop-The-World 停顿时间。 +- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。 +- 空间整合:G1 从整体来看是基于**“标记整理”**算法实现的收集器;从局部上来看是基于**“复制”算法**实现的。 +- 可预测停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型。G1跟踪**各个Region里面的垃圾堆积的价值大小**(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个**优先列表**,每次根据允许的收集时间,**优先回收价值最大的Region**。 + +**G1的跨代引用**: + +在G1收集器中,**Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用**,虚拟机都是使用**Remembered Set(RS)**来避免全堆扫描的。**G1中每个Region都有一个与之对应的RS**,虚拟机发现程序**对Reference类型的数据进行写操作**时,会产生**一个Write Barrier暂时中断操作**,**检查Reference引用的对象是否处于不同的Region之间**(在分代的例子中就是检查是否老年代中的对象引用了新生代中方的对象)如果是,便**通过CardTable(每个Region块又细分了2000多个卡表,记录一波我引用了哪个对象)把相关引用信息记录到被引用对象所属的Region的RS之中**。当进行内存回收时,**在GC根节点的枚举范围中加入RS即可保证不对全堆扫描,也不会又遗漏**。 + +当然G1有也大致的四个过程: + +- 初始标记:仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改**TAMS(Nest Top Mark Start)**的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要**停顿线程**,但耗时很短。 +- 并发标记:从GC Root 开始对堆中对象进行**可达性分析**,找到存活对象,此阶段耗时较长,但**可与用户程序并发执行**。 +- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在**线程的Remembered Set Logs**里面,最终标记阶段需要**把Remembered Set Logs的数据合并到Remembered Set中**,这阶段需要**停顿线程**,但是**可并行执行**。 +- 筛选回收:首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 + +在这里,简单做一个CMS和G1的比较: + +1. CMS收集器是**获取最短回收停顿时间**为目标的收集器,因为CMS工作时,GC工作线程与用户线程可以并发执行,以此来达到降低收集停顿时间的目的(只有初始标记和重新标记会STW)。但**是CMS收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致引用程序变慢,总吞吐量下降**。 +2. CMS仅作用于老年代,是基于**标记清除算法**,所以清理的过程中**会有大量的空间碎片**。 +3. CMS收集器**无法处理浮动垃圾**,**由于CMS并发清理阶段用户线程还在运行**,伴随程序的运行自然会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理它们,只好留在下一次GC时将其清理掉。 +4. G1是一款面向服务端应用的垃圾收集器,**适用于多核处理器、大内存容量的服务端系统**。G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU或核心来缩短STW的停顿时间,它满足短时间停顿的同时达到一个高的吞吐量。 +5. **从JDK 9开始,G1成为默认的垃圾回收器**。当应用有以下任何一种特性时非常适合用G1:Full GC持续时间太长或者太频繁;对象的创建速率和存活率变动很大;应用不希望停顿时间长(长于0.5s甚至1s)。 +6. G1将空间划分成很多块(Region),然后他们各自进行回收。堆比较大的时候可以采用,采用复制算法,碎片化问题不严重。整体上看属于标记整理算法,局部(region之间)属于复制算法。 +7. G1 需要记忆集 (具体来说是卡表)来记录新生代和老年代之间的引用关系,这种数据结构在 G1 中需要占用大量的内存,可能达到整个堆内存容量的 20% 甚至更多。而且 **G1 中维护记忆集的成本较高**,带来了更高的执行负载,影响效率。**所以 CMS 在小内存应用上的表现要优于 G1,而大内存应用上 G1 更有优势,大小内存的界限是6GB到8GB**。 + diff --git "a/Java/classify/jvm/\345\257\271\350\261\241\347\232\204\345\210\233\345\273\272\350\277\207\347\250\213.md" "b/Java/classify/jvm/\345\257\271\350\261\241\347\232\204\345\210\233\345\273\272\350\277\207\347\250\213.md" new file mode 100644 index 00000000..524d01cf --- /dev/null +++ "b/Java/classify/jvm/\345\257\271\350\261\241\347\232\204\345\210\233\345\273\272\350\277\207\347\250\213.md" @@ -0,0 +1,79 @@ +# 对象的创建过程 + +面试官:类加载过程,你之前给我讲过,那么创建对象的过程你知道吗? + +我:我似乎知道。 + +![Java创建对象的过程](https://gitee.com/dreamcater/blog-img/raw/master/uPic/Java创建对象的过程-fC0wRb.png) + +## 类加载检查 + +虚拟机遇到一条 **new** 指令时,首先将去检查这个指令的参数**是否能在常量池中定位到这个类的符号引用**,并且检查这个符号引用代表的**类是否已被加载过**、**解析和初始化过**。如果没有,那必须先执行相应的类加载过程。 + +## 分配内存 + +在**类加载检查**通过后,接下来虚拟机将为新生对象**分配内存**。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。**分配方式**有 **“指针碰撞”** 和 **“空闲列表”** 两种,**选择那种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定**。 + +- **指针碰撞** + - **堆规整(没有内存碎片)** + - 复制算法 + - GC:Serial、ParNew +- **空闲列表** + - **堆内存不规整的情况下** + - 虚拟机会维护一个**列表**,该列表会**记录哪些内存块是可用的**,在分配的时候,找一块儿足够大的内存块来划分给对象实例,最后更新列表激励 + - GC:CMS + +- **并发问题** + - **CAS+失败重试:** CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。**虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。** + - **TLAB:** 为**每一个线程预先在 Eden 区分配一块儿内存**,JVM 在给线程中的对象分配内存时,**首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配** + +## 初始化零值 + +内存分配完成后,虚拟机需要将分配到的内存空间都**初始化为零值(不包括对象头)**,这一步操作保证了对象的实例字段在 Java 代码中**可以不赋初始值就直接使用**,程序能访问到这些字段的数据类型所对应的零值。 + +## 设置对象头 + +初始化零值完成之后,**虚拟机要对对象进行必要的设置**,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 **这些信息存放在对象头中。** 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 + +## 指向init方法 + +在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,**方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行**方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 + +## 内存布局 + +面试官:刚才说了内存布局,给我讲一下 + +我:好的,内存布局分别为**对象头,实例数据,对其填充** + +1. **对象头** + +**Hotspot 虚拟机的对象头包括两部分信息**,**第一部分用于存储对象自身的自身运行时数据**(哈**希码、GC 分代年龄、锁状态标志**等等),**另一部分是类型指针**,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 + +2. **实例数据** + +**实例数据部分是对象真正存储的有效信息**,也是在程序中所定义的各种类型的字段内容。 + +3. **对齐填充** + +**对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。** 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 + +## 句柄的访问方式 + +面试官:给我讲讲对象的访问方式 + +我:明白,两种:使用句柄和使用直接指针 + +1. **句柄**: + +![使用句柄-f4Cw9x](https://gitee.com/dreamcater/blog-img/raw/master/uPic/使用句柄-f4Cw9x.png) + +如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了**对象实例数据**与**类型数据**各自的具体地址信息; + +2. **直接指针**: + +![直接指针-8m3HHz](https://gitee.com/dreamcater/blog-img/raw/master/uPic/直接指针-8m3HHz.png) + +如果使用直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象的地址。 + +话说:这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是**稳定**的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 **reference 本身不需要修改**。使用直接指针访问方式最大的好处就是**速度快**,它节省了一次指针定位的时间开销。 + diff --git "a/Java/classify/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" "b/Java/classify/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" new file mode 100644 index 00000000..3ebaa2b5 --- /dev/null +++ "b/Java/classify/jvm/\347\261\273\345\212\240\350\275\275\345\231\250.md" @@ -0,0 +1,81 @@ + +## 类加载器 + +面试官:谈谈类加载器吧 + +我:行,那还不得介绍三个类加载器? + +- BootstrapClassLoader(启动类加载器):最顶层的加载类,由C++实现,负责加载 `%JAVA_HOME%/lib`目录下的jar包和类或者或被 `-Xbootclasspath`参数指定的路径中的所有类。 +- ExtensionClassLoader(扩展类加载器):主要负责加载目录 `%JRE_HOME%/lib/ext` 目录下的jar包和类,或被 `java.ext.dirs` 系统变量所指定的路径下的jar包。 +- AppClassLoader(应用程序类加载器) + +## 双亲委派 + +我可能直接扯双亲委派了 + +每一个类都有一个对应它的类加载器。系统中的 ClassLoder 在协同工作的时候会默认使用 **双亲委派模型** 。即在类加载的时候,**系统会首先判断当前类是否被加载过**。已经被加载的类会直接返回,否则才会尝试加载。加载的时候,首先会把该请求**委派该父类加载器**的 `loadClass()` 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 `BootstrapClassLoader` 中。当父类加载器无法处理时,才由自己来处理。当父类加载器为null时,会使用启动类加载器 `BootstrapClassLoader` 作为父类加载器。 + +![类加载器](https://gitee.com/dreamcater/blog-img/raw/master/uPic/类加载器-Z1WdFt.png) + +可以按图说话 + +我可能主动的扯好处了 + +双亲委派模型保证了**Java程序的稳定运行,可以避免类的重复加载**,也保证了 **Java 的核心 API 不被篡改**。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 `java.lang.Object` 类的话,那么程序运行的时候,系统就会出现多个不同的 `Object` 类。 + +面试官:什么情况下需要开始类加载过程的第一个阶段加载 + +我: + +1. 遇到**new**、**getstatic**、**putstatic**或**invokestatic**这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是: +2. 使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。 +3. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。 +4. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 +5. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 + +面试官:如何打破双亲委派模型 + +我:需要重写ClassLoader类的loadClass()方法: + +```java +// 其实重写该方法就行,但是打破可能会报错,系统找不到路径 +// 父类的加载(Object)也会交由我们自自定义的类加载器加载。而很明显在我们自定义的加载目录下是不会有Object.class这个文件的。 +protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException + { + synchronized (getClassLoadingLock(name)) { + // First, check if the class has already been loaded + Class c = findLoadedClass(name); + if (c == null) { + long t0 = System.nanoTime(); + try { + if (parent != null) { + c = parent.loadClass(name, false); // 调用父类的加载器 递归 + } else { + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) { + // ClassNotFoundException thrown if class not found + // from the non-null parent class loader + } + + if (c == null) { + // If still not found, then invoke findClass in order + // to find the class. + long t1 = System.nanoTime(); + c = findClass(name); + + // this is the defining class loader; record the stats + sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); + sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); + sun.misc.PerfCounter.getFindClasses().increment(); + } + } + if (resolve) { + resolveClass(c); + } + return c; + } + } +``` + diff --git "a/Java/classify/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" "b/Java/classify/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" new file mode 100644 index 00000000..e5523bff --- /dev/null +++ "b/Java/classify/jvm/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.md" @@ -0,0 +1,38 @@ +# 类加载过程 + +面试官:谈一谈类加载过程 + +我:加载->验证->准备->解析->初始化 + +![类加载过程](https://gitee.com/dreamcater/blog-img/raw/master/uPic/类加载过程-5A2XIO.png) + +## 加载 + +类加载过程的第一步,主要完成下面3件事情: + +- 通过**全类名**获取定义此类的**二进制字节流** +- 将字节流所代表的**静态存储结构**转换为方法区的**运行时数据结构** +- 在内存中生成一个代表该类的 **Class 对象**,作为**方法区这些数据的访问入口** + +## 验证 + +- 文件格式验证:主要验证Class文件**是否规范**等。 +- 元数据验证:对字节码描述的信息**语义分析**等。 +- 字节码验证:确保语义是ok的。 +- 符号引用验证:确保解析动作能执行。 + +## 准备 + +**准备阶段是正式为类变量分配内存并设置类变量初始值的阶段**,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意: + +- 这时候进行内存分配的仅包括**类变量**(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。 +- 这里所设置的初始值"通常情况"下是数据类型默认的**零值**(如0、0L、null、false等),比如我们定义了`public static int value=111` ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会复制)。特殊情况:比如给 value 变量加上了 **fianl 关键字**`public static final int value=111` ,那么准备阶段 value 的值就被复制为 111。 + +## 解析 + +解析阶段是虚拟机将常量池内的**符号引用替换为直接引用**的过程,也就是得到**类或者字段、方法在内存中的指针或者偏移量。** + +## 初始化 + +初始化是类加载的最后一步,也是真正执行类中定义的 **Java 程序代码**(字节码),初始化阶段是执行**类构造器** ` ()`方法的过程。 + diff --git "a/Java/classify/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" "b/Java/classify/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" new file mode 100644 index 00000000..0dc445ff --- /dev/null +++ "b/Java/classify/jvm/\347\261\273\346\226\207\344\273\266\347\273\223\346\236\204.md" @@ -0,0 +1,66 @@ + + +> 这里简要放一些重要的 + +## 类文件结构 + +```java +ClassFile { + u4 magic; //Class 文件的标志 + u2 minor_version;//Class 的小版本号 + u2 major_version;//Class 的大版本号 + u2 constant_pool_count;//常量池的数量 + cp_info constant_pool[constant_pool_count-1];//常量池 + u2 access_flags;//Class 的访问标记 + u2 this_class;//当前类 + u2 super_class;//父类 + u2 interfaces_count;//接口 + u2 interfaces[interfaces_count];//一个类可以实现多个接口 + u2 fields_count;//Class 文件的字段属性 + field_info fields[fields_count];//一个类会可以有个字段 + u2 methods_count;//Class 文件的方法数量 + method_info methods[methods_count];//一个类可以有个多个方法 + u2 attributes_count;//此类的属性表中的属性数 + attribute_info attributes[attributes_count];//属性表集合 +} +``` + +## 静态常量池 + +- 字面量 +- 符号引用 + - 类和接口的全限定名 + - 字段的名称和描述符 + - 方法的名称和描述符 +- 好处:**常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享**。 + +## 运行时常量池 + +当Class文件被加载完成后,java虚拟机会将静态常量池里的内容转移到运行时常量池里,在静态常量池的**符号引用有一部分是会被转变为直接引用**的,比如说类的**静态方法或私有方法,实例构造方法,父类方法**,这是因为这些方法不能被重写其他版本,所以能在加载的时候就可以将符号引用转变为直接引用,而其他的**一些方法是在这个方法被第一次调用的时候才会将符号引用转变为直接引用的**。 + +## 字符串常量池 + +**字符串常量池的存在使JVM提高了性能和减少了内存开销**。 + +- 每当我们使用字面量(String s=“1”;)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就将此字符串对象的地址赋值给引用s(引用s在Java栈。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,并将此字符串对象的地址赋值给引用s(引用s在Java栈中)。 +- 每当我们使用关键字new(String s=new String(”1”);)创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么不再在字符串常量池创建该字符串对象,而直接堆中创建该对象的副本,然后将堆中对象的地址赋值给引用s,如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中,然后在堆中创建该对象的副本,然后将堆中对象的地址赋值给引用s。 + +## 版本变化 + +### 1.6 + +- 静态常量池在Class文件中。 +- 运行时常量池在Perm Gen区(也就是方法区)中。 +- 字符串常量池在运行时常量池中。 + +### 1.7 + +- 静态常量池在Class文件中。 +- 运行时常量池依然在Perm Gen区(也就是方法区)中。在JDK7版本中,永久代的转移工作就已经开始了,将譬如符号引用转移到了native heap;字面量转移到了java heap;类的静态变量转移到了java heap。但是运行时常量池依然还存在,只是很多内容被转移,其只存着这些被转移的引用。 +- 字符串常量池被分配到了Java堆的主要部分。也就是字符串常量池从运行时常量池分离出来了。 + +### 1.8 + +- 静态常量池在Class文件中。 +- JVM已经将运行时常量池从方法区中移了出来,在Java 堆(Heap)中开辟了一块区域存放运行时常量池。同时永久代被移除,以元空间代替。元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。其主要用于存放一些元数据。 +- 字符串常量池存在于Java堆中。 \ No newline at end of file diff --git "a/Java/classify/jvm/\351\200\203\351\200\270\345\210\206\346\236\220.md" "b/Java/classify/jvm/\351\200\203\351\200\270\345\210\206\346\236\220.md" new file mode 100644 index 00000000..3f313e0d --- /dev/null +++ "b/Java/classify/jvm/\351\200\203\351\200\270\345\210\206\346\236\220.md" @@ -0,0 +1,19 @@ +# 逃逸分析 + +> 这一块知识还是要知道的呀,它是Java虚拟机中比较前沿优化的技术。 + +面试官:你了解逃逸分析吗? + +我:算是了解。逃逸分析的基本行为就是分析**对象动态作用域**:当一个对**象在方法中被定义后,它可能被外部方法引用,例如作为调用参数传递到其他方法中,称为方法逃逸**。甚至还有可能**被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸**。如果能证明**一个对象不会逃逸到方法或线程之外**,也就是**别的方法或线程无法通过任何途径访问到这个对象**,则可能为这个变量进行一些高效的优化: + +1. 栈上分配 + +Java虚拟机中,**如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存**将会是一个很不错的主意,**对象所占用的内存空间就可以随栈帧出栈而销毁**。在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那**大量的对象就会随着方法的结束而自动销毁了**,垃圾收集系统的压力将会小很多。 + +2. 同步消除 + +**线程同步本身是一个相对耗时的过程**,**如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问**,那这个变量的**读写肯定就不会有竞争**,对这个变量实施的同步措施也就可以消除。 + +3. 标量替换 + +标量是指一**个数据已经无法再分解成更小的数据来表示了**,Java虚拟机的原始数据类型都不能再进一步分解,它们就可以称为标量。如果逃逸分析证明**一个对象不会被外部访问**,并且**这个对象可以被拆散的话**,那程序真正执行的时候将**可能不创建这个对象**,而改**为直接创建它的若干个被这个方法使用的成员变量来代替**。除了可以让对象的成员变量在栈上(栈上存储的数据,有很大的概率会被虚拟机分配到物理机器高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。 \ No newline at end of file diff --git a/Java/classify/mysql/ACID.md b/Java/classify/mysql/ACID.md new file mode 100644 index 00000000..b94138eb --- /dev/null +++ b/Java/classify/mysql/ACID.md @@ -0,0 +1,75 @@ + +## ACID + +> 这个问题经常问,但是问我的比较少,其实MySQL这一块,问的最多还是优化问题 + +面试官:聊聊ACID是什么 + +我:分别是:**原子性(Atomicity)**、**一致性(Consistency)**、**隔离性(Isolation)**和**持久性(Durability)**。 + +### 原子性 + +根据定义,原子性是指一个事务是一个不可分割的工作单位,**其中的操作要么都做,要么都不做**。即要么转账成功,要么转账失败,是不存在中间的状态!比如:如果不保证原子性,OK,就会出现数据不一致的情形,A账户减去50元,而B账户增加50元操作失败。系统将无故丢失50元~。可能会聊undolog + +### 一致性 + +根据定义,一致性是指事务执行前后,数据处于一种合法的状态,这种状态是语义上的而不是语法上的。 那什么是合法的数据状态呢? oK,这个状态是满足预定的约束就叫做合法的状态,再通俗一点,这状态是由你自己来定义的。**满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!**比如:A给B转100,B不能因为通过这个事务增加了150吧?或者A扣了150吧? + +### 隔离性 + +根据定义,隔离性是指**多个事务并发执行的时候,事务内部的操作与其他事务是隔离的**,并发执行的各个事务之间不能互相干扰。 + +### 持久性 + +根据定义,**持久性是指事务一旦提交,它对数据库的改变就应该是永久性的**。接下来的其他操作或故障不应该对其有任何影响。这里可能会让你聊redolog + +## 并发事务带来的问题 + +面试官:并发事务带来的问题都有哪些 + +我:**脏读、不可重复读和幻读(实际上还有一个丢弃修改)** + +### 脏读 + +第一个事务首先读取变量为50,接着准备更新为100的时,并未提交,第二个事务已经读取为100,此时第一个事务做了回滚。最终第二个事务读取的变量和数据库的不一样。 + +### 丢弃修改 + +T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,**T2 的修改覆盖了 T1 的修改**。例如:事务1读取某表中的数据A=50,事务2也读取A=50,事务1修改A=A+50,事务2也修改A=A+50,最终结果A=100,事务1的修改被丢失。 + +### 不可重复读 + +T2 读取一个数据,T1 对该数据做了修改并提交。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 + +### 幻读 + +T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和第一次读取的结果不同。(和不可重复读的区别:一个是变量变化,一个是范围变化) + +## 数据库的隔离级别 + +面试官:数据库的隔离级别? + +我: + +首先,MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ**(可重读) + +其次,**这里需要注意的是**:与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别 下使用的是**Next-Key Lock 锁**算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以 说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要 求,即达到了 SQL标准的SERIALIZABLE(可串行化)隔离级别。 + +因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)并不会有任何性能损失**。 + +### 未提交读 + +事务中发生了修改,即使没有提交,其他事务也是可见的,比如对于一个数A原来50修改为100,但是我还没有提交修改,另一个事务看到这个修改,而这个时候原事务发生了回滚,这时候A还是50,但是另一个事务看到的A是100.**可能会导致脏读、幻读或不可重复读** + +### 提交读 + +对于一个事务从开始直到提交之前,所做的任何修改是其他事务不可见的,举例就是对于一个数A原来是50,然后提交修改成100,这个时候另一个事务在A提交修改之前,读取的A是50,刚读取完,A就被修改成100,这个时候另一个事务再进行读取发现A就突然变成100了;**可以阻止脏读,但是幻读或不可重复读仍有可能发生** + +### 可重复读 + +就是对一个记录读取多次的记录是相同的,比如对于一个数A读取的话一直是A,前后两次读取的A是一致的;**可以阻止脏读和不可重复读,但幻读仍有可能发生** + +### 可串行读 + +在并发情况下,和串行化的读取的结果是一致的,没有什么不同,比如不会发生脏读和幻读;**该级别可以防止脏读、不可重复读以及幻读** + diff --git "a/Java/classify/mysql/Innodb\344\270\216MyISAM.md" "b/Java/classify/mysql/Innodb\344\270\216MyISAM.md" new file mode 100644 index 00000000..281419a4 --- /dev/null +++ "b/Java/classify/mysql/Innodb\344\270\216MyISAM.md" @@ -0,0 +1,72 @@ + + +> 这个问题简单回答一下即可 + +## MySQL的引擎 + +面试官:MySQL的引擎都有哪些? + +我:我知道,MySQL内部可以分为服务层和存储引擎层两部分:**服务层包括连接器、查询缓存、分析器、优化器、执行器等;存储引擎层负责数据的存储和提取**。我就说一下自己了解的InnoDB和MyISAM引擎 + +## InnoDB + +- 是 MySQL 默认的**事务型存储引擎**,只有在需要它不支持的特性时,才考虑使用其它存储引擎。 +- 实现了四个标准的隔离级别,默认级别是**可重复读(REPEATABLE READ)**。在可重复读隔离级别下,通过**多版本并发控制**(MVCC)+ (Next-Key Locking)**防止幻影读**。 +- 主索引是**聚簇索引**,在**索引中保存了数据**,从而避免直接读取磁盘,因此对查询性能有很大的提升。 +- 内部做了很多优化,包括从磁盘读取数据时采用的**可预测性读**、能够加快读操作并且自动创建的**自适应哈希索引**、能够加速插入操作的**插入缓冲区**等。 +- 支持真正的**在线热备份**。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 + +## MyISAM + +- 设计简单,数据以**紧密格式存储**。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。 +- 提供了大量的特性,包括**压缩表、空间数据索引**等。 +- **不支持事务**。 +- **不支持行级锁,只能对整张表加锁**,读取时会对需要读到的所有表加共享锁,写入时则对表加排它锁。但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。 + + +## 索引文件 +我一般还会回答一个**索引文件**上的区别 + +### MyISAM + +1. MyISAM**索引文件和数据文件是分离**的,**索引文件仅保存数据记录的地址**,同样使用B+Tree作为索引结构,叶节点的**data域存放的是数据记录的地址** +2. 在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复 +3. MyISAM中索引检索的算法为**首先按照B+Tree搜索算法搜索索引**,**如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录** + +### InnoDB + +1. **InnoDB的数据文件本身就是索引文件**,这棵树的叶节点**data域保存了完整的数据记录**(聚集索引) +2. InnoDB的**辅助索引data域存储相应记录主键的值而不是地址** +3. **聚集索引这种实现方式使得按主键的搜索十分高效**,**但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录**。 + +## 分页查询 + +其实个人还知道一点,分页查询的时候还有一点区别,这点区别也是根据索引文件的区别来的。 + +咱们知道,使用limit分页查询,offset越大,性能越差,比如: + +```sql +-- 以真实的生产环境的6万条数据的一张表为例,比较一下优化前后的查询耗时: +-- 传统limit,文件扫描 +select * from table order by id limit 50000,2; +受影响的行: 0 +时间: 0.171s + +-- 子查询方式,索引扫描 +select * from table +where id >= (select id from table order by id limit 50000 , 1) +limit 2; +受影响的行: 0 +时间: 0.035s + +-- JOIN分页方式 +select * from table as t1 +join (select id from table order by id limit 50000, 1) as t2 +where t1.id <= t2.id order by t1.id limit 2; +受影响的行: 0 +时间: 0.036s +``` + +原因:因为 MySQL 并非是跳过偏移量直接去取后面的数据,而是先把偏移量+要取的条数,然后再把前面偏移量这一段的数据抛弃掉再返回的。比如上面的(50000,2),每次取2条,还要经过回表,发现不是想要的,舍弃。那肯定非常耗时间,而通过子查询通过id索引,只查询id,使用到了innodb的索引覆盖, 在内存缓冲区中进行检索,没有回表查询. 然后再用id >= 条件,进一步的缩小查询范围.这样就大大提高了效率。 + +而MyISAM,是直接索引是分离的,通过索引文件查到的数据记录地址,不需要回表,直接对应数据记录,效率也很高。 \ No newline at end of file diff --git "a/Java/classify/mysql/MySQL\346\225\260\346\215\256\345\272\223\347\273\223\346\236\204\344\274\230\345\214\226.md" "b/Java/classify/mysql/MySQL\346\225\260\346\215\256\345\272\223\347\273\223\346\236\204\344\274\230\345\214\226.md" new file mode 100644 index 00000000..5354f2a7 --- /dev/null +++ "b/Java/classify/mysql/MySQL\346\225\260\346\215\256\345\272\223\347\273\223\346\236\204\344\274\230\345\214\226.md" @@ -0,0 +1,94 @@ +[toc] + +> 一个是索引优化,一个是结构优化。。。。 + +## 三范式 + +面试官:先来个**三范式** + +我:好的 + +1. 第一范式:第一范式就是原子性,字段不可再分割; + +- 数据库表中的所有字段都具有单一属性(即字段不能再分割了) +- 单一属性的列是由基本数据类型所构成的 +- 设计出来的表都是简单的二维表 + +2. 第二范式:第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言) +3. 第三范式:所有非主键字段和主键字段之间不能产生传递依赖 + +[例子](https://www.jianshu.com/p/3e97c2a1687b) + +**反范式设计**:反范式化就是为了性能和读取效率的考虑而适当的对数据库设计范式的要求进行违反,**而允许存在少量的数据冗余,也就是使用空间来换取(查询)时间**;比如举个例子:**比如订单表中应该保留当前购买商品的价格、商品的名称(商品的价格是会变动的,这很危险)** + +因此,简单说一下**范式化设计的优缺点**: + +优点: + +- 可以尽量的减少数据冗余 +- 范式化的更新操作比反范式化更快 +- 范式化的表通常比反范式化更小 + +缺点: + +- 对于查询需要关联多个表 +- 更难进行索引优化 + +**反范式化设计的优缺点**: + +优点: + +- 可以减少表的关联 +- 可以更好的进行索引优化 + +缺点: + +- 存在数据冗余和数据维护异常 +- 对数据修改需要更多的成本 + +**如何选择varchar和char类型**: + +- varchar用于存储变长字符串,只占用的必要的存储空间。 +- char类型是定长的,char类型的最大宽度为255 +- 场景:varchar适用于存储很少被更新的字符串列;char适合存储长度近似的值,适合存储短字符串,适合存储经常更新的字符串 + +## 分库分表 + +1. **垂直分表** + +也就是“大表拆小表”,基于列字段进行的。一般是表中的字段较多,将不常用的, 数据较大,长度较长(比如text类型字段)的拆分到“扩展表“。 一般是针对那种几百列的大表,也避免查询时,数据量太大造成的“跨页”问题。 + +2. **垂直分库** + +垂直分库针对的是一个系统中的不同业务进行拆分,比如用户User一个库,商品Producet一个库,订单Order一个库。 切分后,要放在多个服务器上,而不是一个服务器上。为什么? 我们想象一下,一个购物网站对外提供服务,会有用户,商品,订单等的CRUD。没拆分之前, 全部都是落到单一的库上的,这会让数据库的单库处理能力成为瓶颈。按垂直分库后,如果还是放在一个数据库服务器上, 随着用户量增大,这会让单个数据库的处理能力成为瓶颈,还有单个服务器的磁盘空间,内存,tps等非常吃紧。所以我们要拆分到多个服务器上,这样上面的问题都解决了,以后也不会面对单机资源问题。垂直分库一定程度上能够突破IO、连接数及单机硬件资源的瓶颈。 + +3. **水平分表** + +针对数据量巨大的单张表(比如订单表),按照某种规则(RANGE,HASH取模等),切分到多张表里面去。 但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。不建议采用。 + +切分规则: + +- RANGE:从0到10000一个表,10001到20000一个表; +- HASH取模:一个商场系统,一般都是将用户,订单作为主表,然后将和它们相关的作为附表,这样不会造成跨库事务之类的问题。 取用户id,然后hash取模,分配到不同的数据库上。 +- 地理区域:比如按照华东,华南,华北这样来区分业务,七牛云应该就是如此。 +- 时间:按照时间切分,就是将6个月前,甚至一年前的数据切出去放到另外的一张表,因为随着时间流逝,这些表的数据 被查询的概率变小,所以没必要和“热数据”放在一起,这个也是“冷热数据分离”。 + +4. **水平分库分表** + +将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。 水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。 + +**分库分表带来的问题:** + +1. **事务支持** + +分库分表后,就成了分布式事务了。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价; 如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。 + +2. **join** + +TODO 分库分表后表之间的关联操作将受到限制,我们无法join位于不同分库的表,也无法join分表粒度不同的表, 结果原本一次查询能够完成的业务,可能需要多次查询才能完成。 粗略的解决方法: 全局表:基础数据,所有库都拷贝一份。 字段冗余:这样有些字段就不用join去查询了。 系统层组装:分别查询出所有,然后组装起来,较复杂。 + +**分库分表中间件:** + +Mycat 和 ShardingSphere(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar 3 款产品)。 + +[对比](https://my.oschina.net/u/4318872/blog/4281049) \ No newline at end of file diff --git "a/Java/classify/mysql/MySQL\346\227\245\345\277\227\346\226\207\344\273\266.md" "b/Java/classify/mysql/MySQL\346\227\245\345\277\227\346\226\207\344\273\266.md" new file mode 100644 index 00000000..26ccd5ed --- /dev/null +++ "b/Java/classify/mysql/MySQL\346\227\245\345\277\227\346\226\207\344\273\266.md" @@ -0,0 +1,34 @@ +# MySQL的日志文件 + +面试官:分别讲一下MySQL的几大文件,你懂的 + +我:我不懂,ok,好的。 + +- undoLog 也就是我们常说的**回滚日志文件** 主要用于事务中执行失败,进行回滚,以及MVCC中对于数据历史版本的查看。由**引擎层的InnoDB引擎实现**,是**逻辑日志**,记录数据修改被修改前的值,比如"把id='B' 修改为id = 'B2' ,那么undo日志就会用来存放id ='B'的记录”。当一条数据需要更新前,会先把修改前的记录存储在undolog中,如果这个修改出现异常,则会使用undo日志来实现回滚操作,保证事务的一致性。当事务提交之后,**undo log并不能立马被删除,而是会被放到待清理链表中,待判断没有事务用到该版本的信息时才可以清理相应undolog**。它保存了事务发生之前的数据的一个版本,用于回滚,**同时可以提供多版本并发控制下的读(MVCC)**,也即非锁定读。 +- redoLog 是重做日志文件是**记录数据修改之后的值**,**用于持久化到磁盘中**。redo log包括两部分:**一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的**;二是**磁盘上的重做日志文件(redo log file)**,该部分日志是持久的。由引**擎层的InnoDB引擎实现**,是**物理日志**,记录的是物理数据页修改的信息,比如“某个数据页上内容发生了哪些改动”。当一条数据需要更新时,InnoDB会先将数据更新,然后记录redoLog 在内存中,然后找个时间将redoLog的操作执行到磁盘上的文件上。不管是否提交成功我都记录,你要是回滚了,那我连回滚的修改也记录。它确保了事务的持久性。[参考](https://juejin.im/post/6844903573910716430) +- binlog由**Mysql的Server层实现**,是**逻辑日志**,记录的是sql语句的原始逻辑,比如"把id='B' 修改为id = ‘B2’。binlog会写入指定大小的物理文件中,是追加写入的,当前文件写满则会创建新的文件写入。 产生:**事务提交的时候,一次性将事务中的sql语句,按照一定的格式记录到binlog中。用于复制和恢复在主从复制中,从库利用主库上的binlog进行重播(执行日志中记录的修改逻辑),实现主从同步。业务数据不一致或者错了,用binlog恢复**。 +- MVCC多版本并发控制是MySQL中基于**乐观锁理论实现隔离级别**的方式,用于**读已提交和可重复读**取隔离级别的实现。在MySQL中,会在表中每一条数据后面添加两个字段:**最近修改该行数据的事务ID**,**指向该行(undolog表中)回滚段的指针**。**Read View判断行的可见性,创建一个新事务时,copy一份当前系统中的活跃事务列表。意思是,当前不应该被本事务看到的其他事务id列表**。 + +**binlog和redolog的区别**: + +1. redolog是在**InnoDB存储引擎层产生**,而**binlog是MySQL数据库的上层服务层产生**的。 +2. 两种日志记录的内容形式不同。MySQL的**binlog是逻辑日志**,其记录是**对应的SQL语句**。而**innodb存储引擎层面的重做日志是物理日志**。 +3. 两种日志与记录写入磁盘的时间点不同,**binlog日志只在事务提交完成后进行一次写入**。而**innodb存储引擎的重做日志在事务进行中不断地被写入,并日志不是随事务提交的顺序进行写入的**。 +4. **binlog不是循环使用,在写满或者重启之后,会生成新的binlog文件,redolog是循环使用**。 +5. **binlog可以作为恢复数据使用,主从复制搭建**,**redolog作为异常宕机或者介质故障后的数据恢复使用**。 + +**MVCC的缺点:** + +MVCC在大多数情况下代替了行锁,实现了对读的非阻塞,读不加锁,读写不冲突。缺点是每行记录**都需要额外的存储空间,需要做更多的行维护和检查工作**。 要知道的,MVCC机制下,会在更新前建立undo log,根据各种策略读取时非阻塞就是MVCC,undo log中的行就是MVCC中的多版本。 而undo log这个关键的东西,**记载的内容是串行化的结果,记录了多个事务的过程,不属于多版本共存**。 这么一看,似乎mysql的mvcc也并没有所谓的多版本共存 + +**读写分离原理**: + +主库(master)将变更写**binlog**日志,然后从库(slave)连接到主库之后,从库有一个**IO线程**,将主库的binlog日志**拷贝到自己本地**,写入一个中继日志中。接着从库中有一个SQL线程会从中继日志读取binlog,然后执行binlog日志中的内容,也就是在自己本地再次执行一遍SQL,这样就可以保证自己跟主库的数据是一样的。 + +这里有一个非常重要的一点,就是从库同步主库数据的过程是**串行化**的,也就是说**主库上并行**的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行SQL的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。 + +而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。 + +所以mysql实际上在这一块有两个机制,一个是**半同步复制**,用来解决主库数据丢失问题;一个是**并行复制**,用来解决主从同步延时问题。 + +所谓并行复制,指的是从库**开启多个线程,并行读取relay log中不同库的日志**,然后并行重放不同库的日志,这是库级别的并行。 \ No newline at end of file diff --git "a/Java/classify/mysql/MySQL\346\230\257\345\246\202\344\275\225\346\211\247\350\241\214\344\270\200\346\235\241SQL\347\232\204.md" "b/Java/classify/mysql/MySQL\346\230\257\345\246\202\344\275\225\346\211\247\350\241\214\344\270\200\346\235\241SQL\347\232\204.md" new file mode 100644 index 00000000..3021ee41 --- /dev/null +++ "b/Java/classify/mysql/MySQL\346\230\257\345\246\202\344\275\225\346\211\247\350\241\214\344\270\200\346\235\241SQL\347\232\204.md" @@ -0,0 +1,20 @@ + +## SQL执行顺序 +SQL的执行顺序:from---where--group by---having---select---order by + +## MySQL是如何执行一条SQL的 +![SQL执行的全部过程-MlS1d5](https://gitee.com/dreamcater/blog-img/raw/master/uPic/SQL执行的全部过程-MlS1d5.png) + +**MySQL内部可以分为服务层和存储引擎层两部分:** + +1. **服务层包括连接器、查询缓存、分析器、优化器、执行器等**,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。 +2. **存储引擎层负责数据的存储和提取**,其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认的存储引擎。 + +**Server层按顺序执行sql的步骤为**: +客户端请求: +- **连接器**(验证用户身份,给予权限) +- **查询缓存**(存在缓存则直接返回,不存在则执行后续操作) +- **分析器**(对SQL进行词法分析和语法分析操作) +- **优化器**(主要对执行的sql优化选择最优的执行方案方法) +- **执行器**(执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口) +- **去引擎层获取数据返回**(如果开启查询缓存则会缓存查询结果) \ No newline at end of file diff --git "a/Java/classify/mysql/MySQL\347\232\204\351\224\201.md" "b/Java/classify/mysql/MySQL\347\232\204\351\224\201.md" new file mode 100644 index 00000000..406db529 --- /dev/null +++ "b/Java/classify/mysql/MySQL\347\232\204\351\224\201.md" @@ -0,0 +1,69 @@ +# MySQL的锁 + +> MySQL的锁,其实跟Java差不了,一个思想。 + +面试官:MySQL的锁,介绍一下 + +我: + +MyISAM:MyISAM只有表锁,其中又分为共享读锁和独占写锁。 + +- MyISAM表的读操作,不会阻塞其他用户对同一个表的读请求,但会阻塞对同一个表的写请求。 +- MyISAM表的写操作,会阻塞其他用户对同一个表的读和写操作。 +- MyISAM表的读、写操作之间、以及写操作之间是串行的。 + +Innodb行锁:共享锁,排他锁 + +- 对于UPDATE、DELETE、INSERT语句,Innodb会自动给涉及的数据集加排他锁(X);对于普通SELECT语句,Innodb不会加任何锁。 + +```sql +//显示共享锁(S) : + SELECT * FROM table_name WHERE .... LOCK IN SHARE MODE + //显示排他锁(X): + SELECT * FROM table_name WHERE .... FOR UPDATE. +``` + +- 记录锁(Record Locks):记录锁是封锁记录,记录锁也叫行锁,注意:行锁是针对索引的,如果表中没有索引,那么就会锁整张表 +- 间隙锁(GAP)对于键值在条件范围内但并不存在的记录,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。 +- 临键锁(Next-Key Lock):(Record Locks+GAP),锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。 + +面试官:给你张表怎么用cas实现高并发下的update操作 + +我: + +第一种: + +```xml +// cas, 期望值和数据表中的旧值一致,才更新。 +# newStock = oldStock-desStock; + + UPDATE t_order SET stock=#{newStock} WHERE id=#{orderId} AND stock=#{oldStock} + +``` + +```java +// orderId:订单id +// getStock:库存:旧值 +// desStock:可以是期望值,但这里预减值 +int result = orderManager.desStockByCas(orderId, orderDo.getStock(), desStock); +``` + +第二种: + +```xml +// 用版本号 +// 我期望的版本号, 和旧版本号一致才更新,并且版本号累加... + + UPDATE t_order SET stock=stock-#{desStock}, version=version+1 + WHERE id=#{orderId} AND version=#{oldVersion} + +``` + +如 + +```java +// orderId:订单id +// getVersion:获取数据库版本号,旧版本 +// desStock:可以是期望值,但这里预减值 +int result = orderManager.desStockByOptimistic(orderId, orderDo.getVersion(), desStock); +``` \ No newline at end of file diff --git "a/Java/classify/mysql/\347\264\242\345\274\225.md" "b/Java/classify/mysql/\347\264\242\345\274\225.md" new file mode 100644 index 00000000..7f730414 --- /dev/null +++ "b/Java/classify/mysql/\347\264\242\345\274\225.md" @@ -0,0 +1,167 @@ + + +> 这一块,最好能知道怎么优化 + +面试官:索引介绍一下 + +## 索引类型 + +我:ok,先说一下**索引类型**: + +- FULLTEXT:即为全文索引,目前只有MyISAM引擎支持。其可以在CREATE TABLE ,ALTER TABLE ,CREATE INDEX 使用,不过目前只有 CHAR、VARCHAR ,TEXT 列上可以创建全文索引。 +- HASH:由于HASH的唯一及类似键值对的形式,很适合作为索引。 HASH索引可以一次定位,不需要像树形索引那样逐层查找,因此具有极高的效率。但是,这种高效是有条件的,即只在“=”和“in”条件下高效,对于范围查询、排序及组合索引仍然效率不高。 +- BTREE:BTREE索引就是一种将索引值按一定的算法,存入一个树形的数据结构中(二叉树),每次查询都是从树的入口root开始,依次遍历node,获取leaf。这是MySQL里默认和最常用的索引类型。 +- RTREE:RTREE在MySQL很少使用,仅支持geometry数据类型,支持该类型的存储引擎只有MyISAM、BDb、InnoDb、NDb、Archive几种。 相对于BTREE,RTREE的优势在于范围查找。 + +## 索引种类 + +再说一下**索引种类**: + +- 普通索引:仅加速查询 +- 唯一索引:加速查询 + 列值唯一(可以有null) +- 主键索引:加速查询 + 列值唯一(不可以有null)+ 表中只有一个 +- 组合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并 +- 全文索引:对文本的内容进行分词,进行搜索 +- 索引合并:使用多个单列索引组合搜索 +- 覆盖索引:select的数据列只用从索引中就能够取得,不必读取数据行,换句话说查询列要被所建的索引覆盖 +- 聚簇索引:表数据是和主键一起存储的,主键索引的叶结点存储行数据(包含了主键值),二级索引的叶结点存储行的主键值。使用的是B+树作为索引的存储结构,非叶子节点都是索引关键字,但非叶子节点中的关键字中不存储对应记录的具体内容或内容地址。叶子节点上的数据是主键与具体记录(数据内容) + +## 索引结构 + +其次说**索引结构**: + +**MyISAM**: + +1. MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址,同样使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址 +2. 在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复 +3. MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录 + +**InnoDB**: + +1. InnoDB的数据文件本身就是索引文件,这棵树的叶节点data域保存了完整的数据记录(聚集索引) +2. InnoDB的辅助索引data域存储相应记录主键的值而不是地址 +3. 聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。 + +补充一下**为什么InnoDB索引是B+**: + +- Hash索引:Hash索引底层是哈希表,哈希表是一种以key-value存储数据的结构,所以多个数据在存储关系上是完全没有任何顺序关系的,所以,对于区间查询是无法直接通过索引查询的,就需要全表扫描。所以,哈希索引只适用于等值查询的场景。而B+ 树是一种多路平衡查询树,所以他的节点是天然有序的(左子节点小于父节点、父节点小于右子节点),所以对于范围查询的时候不需要做全表扫描 +- 二叉查找树:解决了排序的基本问题,但是由于无法保证平衡,可能退化为链表。 +- 平衡二叉树:通过旋转解决了平衡的问题,但是旋转操作效率太低。 +- 红黑树:通过舍弃严格的平衡和引入红黑节点,解决了 AVL旋转效率过低的问题,但是在磁盘等场景下,树仍然太高,IO次数太多。 +- B+树:在B树的基础上,**B+树相对于B树能够有更多的分支,使得这棵树更加矮胖,查询时做的IO操作次数也更少**;此外将叶节点使用指针连接成链表,范围查询更加高效。B+树的**非叶子节点不保存数据**,只保存**子树的临界值**(最大或者最小),所以同样大小的节点。 +- [B+树的高度](https://www.jianshu.com/p/544e97672deb) + +补充B树: + +B树(英语: B-tree)是一种自平衡的树,能够保持数据有序。这种数据结构能够让**查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成**。B树,概括来说是一个**一般化的二叉查找树**(binary search tree),可以拥有最多2个子节点。与自平衡二叉查找树不同,B树适用于读写相对大的数据块的存储系统,例如磁盘。 + +- 关键字集合分布在整颗树中; +- 任何一个关键字出现且只出现在一个结点中; +- 搜索有可能在非叶子结点结束; +- 其搜索性能等价于在关键字全集内做一次二分查找; + +## 索引失效 + +查询什么时候不走**索引**: + +1. **模糊查询 %like** +2. **索引列参与计算,使用了函数** +3. **非最左前缀顺序** +4. **where对null判断** +5. **where不等于** +6. or操作有至少一个字段没有索引 +7. 需要回表的查询结果集过大(超过配置的范围) +8. **将打算加索引的列设置为NOT NULL,否则将导致引擎放弃使用索引而进行全表扫描** + +## 索引最左 + +**索引最左原则:** + +**举例子**: +如果索引列分别为A,B,C,顺序也是A,B,C: + +- 那么查询的时候,如果查询【A】【A,B】 【A,B,C】,那么可以通过索引查询 +- 如果查询的时候,采用【A,C】,那么C这个虽然是索引,但是由于中间缺失了B,因此C这个索引是用不到的,只能用到A索引 +- 如果查询的时候,采用【B】 【B,C】 【C】,由于没有用到第一列索引,不是最左前缀,那么后面的索引也是用不到了 +- 如果查询的时候,采用范围查询,并且是最左前缀,也就是第一列索引,那么可以用到索引,但是范围后面的列无法用到索引(比如,a>= 3 and b = 4 and c = 5; A走索引,bc不走)(比如,a = 3 and b >= 4 and c = 5; a和b走,c不走) + +**组合索引的底层其实按照第一个索引排序,从排序里面查第二个索引,以此类推。如果第一个索引失效,或者没有经过第一个索引,后面没发在前面的基础上查询。** + +## 为什么使用索引 + +- 通过创建唯一性索引,可以保证数据库表中每一行数据的**唯一性**。 +- 可以大大加快数据的**检索速度**,这也是创建索引的最主要的原因。 +- 帮助服务器**避免排序和临时表**。 +- 将**随机IO变为顺序IO**。 +- 可以**加速表和表之间的连接**,特别是在实现数据的参考完整性方面特别有意义。 + +但是使用索引要看一条准则--- 那就是读写比例,我们知道索引的缺点: + +- 当对表中的数据进行增加、删除和修改的时候,**索引也要动态的维护**,这样就降低了数据的维护速度。 +- 索引需要**占物理空间**,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立簇索引,那么需要的空间就会更大。 +- **创建索引和维护索引要耗费时间**,这种时间随着数据量的增加而增加 + +**你想,如果某个场景,发送10条请求,9条写,1条读。 加索引岂不是在浪费效率和空间?** + +## explain + +面试官:聊聊**explain** + +我:好的,不过这一块内容好多,我只说几个关键的吧 + +1. id : 表示SQL执行的顺序的标识,SQL从大到小的执行 +2. select_type:表示查询中每个select子句的类型 +3. table:显示这一行的数据是关于哪张表的,有时不是真实的表名字 +4. type:表示MySQL在表中找到所需行的方式,又称“访问类型”。常用的类型有: ALL, index, range, ref, eq_ref, const, system, NULL(从左到右,性能从差到好) +5. possible_keys:指出MySQL能使用哪个索引在表中找到记录,查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询使用 +6. Key:key列显示MySQL实际决定使用的键(索引),如果没有选择索引,键是NULL。 +7. key_len:表示索引中使用的字节数,可通过该列计算查询中使用的索引的长度(key_len显示的值为索引字段的最大可能长度,并非实际使用长度,即key_len是根据表定义计算而得,不是通过表内检索出的) +8. ref:表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值 +9. rows: 表示MySQL根据表统计信息及索引选用情况,估算的找到所需的记录所需要读取的行数,理论上行数越少,查询性能越好 +10. Extra:该列包含MySQL解决查询的详细信息 + +## 慢查询优化 + +面试官:慢查询优化 + +我:我试试 + +打开慢查询日志 + +1. 先运行看看是否真的很慢,注意设置SQL_NO_CACHE +2. where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度最高 +3. explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询) +4. order by limit 形式的sql语句让排序的表优先查(这里要注意如果数据量大,要注意了) +5. 了解业务方使用场景 +6. 加索引时参照建索引的几大原则 +7. 观察结果,不符合预期继续从0分析 + + + +咱们知道,使用limit分页查询,offset越大,性能越差,比如: + +```sql +-- 以真实的生产环境的6万条数据的一张表为例,比较一下优化前后的查询耗时: +-- 传统limit,文件扫描 +select * from table order by id limit 50000,2; +受影响的行: 0 +时间: 0.171s + +-- 子查询方式,索引扫描 +select * from table +where id >= (select id from table order by id limit 50000 , 1) +limit 2; +受影响的行: 0 +时间: 0.035s + +-- JOIN分页方式 +select * from table as t1 +join (select id from table order by id limit 50000, 1) as t2 +where t1.id <= t2.id order by t1.id limit 2; +受影响的行: 0 +时间: 0.036s +``` + +原因:因为 MySQL 并非是跳过偏移量直接去取后面的数据,而是先把偏移量+要取的条数,然后再把前面偏移量这一段的数据抛弃掉再返回的。比如上面的(50000,2),每次取2条,还要经过回表,发现不是想要的,舍弃。那肯定非常耗时间,而通过子查询通过id索引,只查询id,使用到了innodb的索引覆盖, 在内存缓冲区中进行检索,没有回表查询. 然后再用id >= 条件,进一步的缩小查询范围.这样就大大提高了效率。 + +而MyISAM,是直接索引是分离的,通过索引文件查到的数据记录地址,不需要回表,直接对应数据记录,效率也很高。 \ No newline at end of file diff --git a/Java/classify/net/DNS.md b/Java/classify/net/DNS.md new file mode 100644 index 00000000..6ced16f3 --- /dev/null +++ b/Java/classify/net/DNS.md @@ -0,0 +1,42 @@ + +## DNS + +### DNS是什么 + +**官方解释**:DNS(Domain Name System,域名系统),因特网上作为**域名和IP地址相互映射**的一个**分布式数据库**,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。通过主机名,最终得到该主机名对应的IP地址的过程叫做域名解析(或主机名解析)。 + +**通俗的讲**,我们更习惯于记住一个网站的名字,比如www.baidu.com,而不是记住它的ip地址,比如:167.23.10.2。 + +### 谈谈DNS解析过程 +![DNS解析过程-eiVd6a](https://gitee.com/dreamcater/blog-img/raw/master/uPic/DNS解析过程-eiVd6a.png) + +- 请求一旦发起,若是chrome浏览器,先在浏览器找之前**有没有缓存过的域名所对应的ip地址**,有的话,直接跳过dns解析了,若是没有,就会**找硬盘的hosts文件**,看看有没有,有的话,直接找到hosts文件里面的ip + +[字节问了修改hosts,浏览器会变吗?](https://blog.csdn.net/woshizhangliang999/article/details/51457864) + +- 如果本地的hosts文件没有能的到对应的ip地址,浏览器会发出一个**dns请求到本地dns服务器**,**本地dns服务器一般都是你的网络接入服务器商提供**,比如中国电信,中国移动等。 +- 查询你输入的网址的DNS请求到达本地DNS服务器之后,**本地DNS服务器会首先查询它的缓存记录**,如果缓存中有此条记录,就可以直接返回结果,此过程是**递归的方式进行查询**。如果没有,本地DNS服务器还要向**DNS根服务器**进行查询。 +- 本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。 +- 最后,本地DNS服务器向**域名的解析服务器**发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。 + +### DNS查询方式 + +#### 递归解析 + +当局部DNS服务器自己不能回答客户机的DNS查询时,它就需要向其他DNS服务器进行查询。此时有两种方式。**局部DNS服务器自己负责向其他DNS服务器进行查询,一般是先向该域名的根域服务器查询,再由根域名服务器一级级向下查询**。最后得到的查询结果返回给局部DNS服务器,再由局部DNS服务器返回给客户端。 + +#### 迭代解析 + +当局部DNS服务器自己不能回答客户机的DNS查询时,也可以通过迭代查询的方式进行解析。局部DNS服务器不是自己向其他DNS服务器进行查询,**而是把能解析该域名的其他DNS服务器的IP地址返回给客户端DNS程序**,客户端DNS程序再继续向这些DNS服务器进行查询,直到得到查询结果为止。也就是说,迭代解析只是帮你找到相关的服务器而已,而不会帮你去查。比如说:baidu.com的服务器ip地址在192.168.4.5这里,你自己去查吧,本人比较忙,只能帮你到这里了。 + +### DNS负载均衡 + +当一个网站有足够多的用户的时候,假如每次请求的资源都位于同一台机器上面,那么这台机器随时可能会蹦掉。处理办法就是用DNS负载均衡技术,它的原理是在**DNS服务器中为同一个主机名配置多个IP地址,在应答DNS查询时,DNS服务器对每个查询将以DNS文件中主机记录的IP地址按顺序返回不同的解析结果,将客户端的访问引导到不同的机器上去,使得不同的客户端访问不同的服务器**,从而达到负载均衡的目的。例如可以根据每台机器的负载量,该机器离用户地理位置的距离等等。 + +### 为什么域名解析用UDP协议? + +因为UDP快啊!UDP的DNS协议只要一个请求、一个应答就好了。而使用基于TCP的DNS协议要三次握手、发送数据以及应答、四次挥手。但是UDP协议传输内容不能超过512字节。不过客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。 + +### 为什么区域传送用TCP协议? + +因为TCP协议可靠性好啊!你要从主DNS上复制内容啊,你用不可靠的UDP? 因为TCP协议传输的内容大啊,你用最大只能传512字节的UDP协议?万一同步的数据大于512字节,你怎么办? \ No newline at end of file diff --git "a/Java/classify/net/HTTP\345\222\214HTTPS.md" "b/Java/classify/net/HTTP\345\222\214HTTPS.md" new file mode 100644 index 00000000..9e0aa524 --- /dev/null +++ "b/Java/classify/net/HTTP\345\222\214HTTPS.md" @@ -0,0 +1,212 @@ + +## HTTP + +### GET和POST的区别? + +1. GET使用URL或Cookie传参,而POST将数据放在BODY中 +2. GET方式提交的数据有长度限制,则POST的数据则可以非常大 +3. POST比GET安全,因为数据在地址栏上不可见,没毛病 +4. **本质区别**:GET请求是幂等性的,POST请求不是。 + +> 这里的幂等性:幂等性是指一次和多次请求某一个资源应该具有同样的副作用。简单来说意味着对同一URL的多个请求应该返回同样的结果。 + +正因为它们有这样的区别,所以不应该且**不能用get请求做数据的增删改这些有副作用的操作**。因为get请求是幂等的,**在网络不好的隧道中会尝试重试**。如果用get请求增数据,会有**重复操作**的风险,而这种重复操作可能会导致副作用(浏览器和操作系统并不知道你会用get请求去做增操作)。 + +### 响应码 + +#### 1xx 信息 + +**100 Continue** :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。 + +#### 2xx 成功 + +- **200 OK** +- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 +- **206 Partial Content** :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。 + +#### 3xx 重定向 + +- **301 Moved Permanently** :永久性重定向 +- **302 Found** :临时性重定向 +- **303 See Other** :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。 +- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。 +- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。 + +#### 4xx 客户端错误 + +- **400 Bad Request** :请求报文中存在语法错误。 +- **401 Unauthorized** :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。 +- **403 Forbidden** :请求被拒绝。没有权限 +- **404 Not Found**:路由不存在,或者没找到 + +#### 5xx 服务器错误 + +- **500 Internal Server Error** :服务器正在执行请求时发生错误。 +- **503 Service Unavailable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 + + +### HTTP首部 + +> 这块有点多,可参考[http首部](https://github.com/DreamCats/JavaBooks/blob/master/Interview/network/计算机网络原理-http那些事儿.md#http首部) + +### Cookies + +HTTP 协议是**无状态**的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 + +Cookie 是**服务器发送到用户浏览器并保存在本地的一小块数据**,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。 + +用途 + +- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) +- 个性化设置(如用户自定义设置、主题等) +- 浏览器行为跟踪(如跟踪分析用户行为等) + + +### Session + +除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 + +Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。 + +### Cookie和Session的选择 + +- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存储任何类型的数据,因此在考虑数据复杂性时首选 Session; +- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; +- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 + +### JWT + +JWT(json web token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。 + +cookie+session这种模式通常是保存在**内存**中,而且服务从单服务到多服务会面临的session共享问题,随着用户量的增多,开销就会越大。而JWT不是这样的,**只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可**。 + +**JWT的构成**: + +第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。详情请见[官网](https://jwt.io/introduction/) + +**JWT总结**: + +1. 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。 +2. payload部分,JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。 +3. 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。它不需要在服务端保存会话信息, 所以它易于应用的扩展。 + +[jwt优缺点](https://snailclimb.gitee.io/javaguide/#/docs/system-design/authority-certification/JWT-advantages-and-disadvantages) + +### 浏览器在与服务器建立了一个 TCP 连接后是否会在一个 HTTP 请求完成后断开?什么情况下会断开? + +在 HTTP/1.0 中,一个服务器在发送完一个 HTTP 响应后,会断开 TCP 链接。但是这样每次请求都会重新建立和断开 TCP 连接,代价过大。所以虽然标准中没有设定,**某些服务器对 Connection: keep-alive 的 Header 进行了支持**。 + +**持久连接**:既然维持 TCP 连接好处这么多,HTTP/1.1 就把 Connection 头写进标准,并且默认开启持久连接,除非请求中写明 Connection: close,那么浏览器和服务器之间是会维持一段时间的 TCP 连接,不会一个请求结束就断掉。 + + +### 一个TCP连接可以对应几个HTTP请求? + +如果维持连接,一个 TCP 连接是可以发送多个 HTTP 请求的。 + +### 一个 TCP 连接中 HTTP 请求发送可以一起发送么(比如一起发三个请求,再三个响应一起接收)? + +HTTP/1.1 存在一个问题,单个 TCP 连接在同一时刻只能处理一个请求,意思是说:两个请求的生命周期不能重叠,任意两个 HTTP 请求从开始到结束的时间在同一个 TCP 连接里不能重叠。 + +在 HTTP/1.1 存在 Pipelining 技术可以完成这个多个请求同时发送,但是由于浏览器默认关闭,所以可以认为这是不可行的。在 HTTP2 中由于 Multiplexing 特点的存在,多个 HTTP 请求可以在同一个 TCP 连接中并行进行。 + +那么在 HTTP/1.1 时代,浏览器是如何提高页面加载效率的呢?主要有下面两点: + +- 维持和服务器已经建立的 TCP 连接,在同一连接上顺序处理多个请求。 +- 和服务器建立多个 TCP 连接。 + +### 为什么有的时候刷新页面不需要重新建立 SSL 连接? + +TCP 连接有的时候会被浏览器和服务端维持一段时间。TCP 不需要重新建立,SSL 自然也会用之前的。 + +### 浏览器对同一 Host 建立 TCP 连接到数量有没有限制? + +**有。Chrome 最多允许对同一个 Host 建立六个 TCP 连接。不同的浏览器有一些区别。** + + +### 在浏览器中输入url地址后显示主页的过程? + +> - 根据域名,进行DNS域名解析; +> - 拿到解析的IP地址,建立TCP连接; +> - 向IP地址,发送HTTP请求; +> - 服务器处理请求; +> - 返回响应结果; +> - 关闭TCP连接; +> - 浏览器解析HTML; +> - 浏览器布局渲染; + + +### HTTP1.x的缺点 +1. HTTP/1.0 一次只允许在一个TCP连接上发起一个请求,HTTP/1.1使用的流水线技术也只能部分处理请求分析,仍然会存在队列头阻塞问题,因此客户端在需要发起多次请求时,通常会采用建立多连接来减少延迟 +2. 单向请求,只能由客户端发起 +3. 请求报文与响应报文首部信息冗余量大。 +4. 数据未压缩,导致数据的传输量大。 + +### HTTP2.0有哪些改动 +1. 多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。 +2. 二进制分帧:应用层(HTTP/2)和传输层(TCP or UDP)之间增加一个二进制分帧层。 +3. 首部压缩(Header Compression) +4. 服务端推送(Server Push) + + +## HTTPS + +### HTTPS是什么 + +HTTPS 并不是新协议,而是让 **HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPS 使用了隧道进行通信**。通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 + +### HTTP的缺点 + +- 使用明文进行通信,内容可能会被窃听; +- 不验证通信方的身份,通信方的身份有可能遭遇伪装; +- 无法证明报文的完整性,报文有可能遭篡改。 + +### 对称密钥加密 + +对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。 + +- 优点:运算速度快 +- 缺点:无法安全地将密钥传输给通信方 + +### 非对称密钥加密 + +非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。 + +公开密钥所有人都可以获得,**通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密**,**接收方收到通信内容后使用私有密钥解密**。 + +非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。 + +- 优点:可以更安全地将公开密钥传输给通信发送方; +- 缺点:运算速度慢。 + +### HTTPS采用的加密方式 + +HTTPS 采用混合的加密机制,使用**非对称密钥加密用于传输对称密钥来保证传输过程的安全性**,之后使用**对称密钥加密进行通信来保证通信过程的效率**。DES+RSA + +![rsa原理-rWg4oK](https://gitee.com/dreamcater/blog-img/raw/master/uPic/rsa原理-rWg4oK.png) + +确保传输安全过程(其实就是rsa原理): + +1. Client给出**协议版本号**、一个客户端生成的**随机数**(Client random),以及客户端支持的**加密方法**。 +2. Server确认双方使用的**加密方法**,并给出**数字证书**、以及一个服务器生成的**随机数**(Server random)。 +3. Client确认**数字证书有效**,然后生成一个新的**随机数**(Premaster secret),并使用**数字证书中的公钥,加密这个随机数**,发给Server。 +4. Server使用自己的**私钥,获取Client发来的随机数**(Premaster secret)。 +5. Client和Server根据约定的加密方法,使用前面的**三个随机数,生成”对话密钥”**(session key),用来加密接下来的整个对话过程。 + +### 认证 + +通过使用 **证书** 来对通信方进行认证。 + +数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。 + +服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 + +进行 HTTPS 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。 + +[加密全套流程](https://www.cnblogs.com/handsomeBoys/p/6556336.html) + +[https://www.cnblogs.com/xdyixia/p/11610102.html](https://www.cnblogs.com/xdyixia/p/11610102.html) + +### HTTPS的缺点 + +- 因为需要进行加密解密等过程,因此速度会更慢; +- 需要支付证书授权的高额费用。 \ No newline at end of file diff --git "a/Java/classify/net/TCP\345\222\214UDP.md" "b/Java/classify/net/TCP\345\222\214UDP.md" new file mode 100644 index 00000000..56eb065d --- /dev/null +++ "b/Java/classify/net/TCP\345\222\214UDP.md" @@ -0,0 +1,152 @@ + +## TCP/UDP + +### TCP + +#### TCP是什么? + +`TCP(Transmission Control Protocol 传输控制协议)`是一种面向连接的、可靠的、基于字节流的传输层通信协议。 + +#### TCP头部报文 + +##### source port 和 destination port + +> 两者分别为「源端口号」和「目的端口号」。源端口号就是指本地端口,目的端口就是远程端口。 + +可以这么理解,我们有很多软件,每个软件都对应一个端口,假如,你想和我数据交互,咱们得互相知道你我的端口号。 + +再来一个很官方的: + +> 扩展:应用程序的端口号和应用程序所在主机的 IP 地址统称为 socket(套接字),IP:端口号, 在互联网上 socket 唯一标识每一个应用程序,源端口+源IP+目的端口+目的IP称为”套接字对“,一对套接字就是一个连接,一个客户端与服务器之间的连接。 + +##### Sequence Number + +> 称为「序列号」。用于 TCP 通信过程中某一传输方向上字节流的每个字节的编号,为了确保数据通信的有序性,避免网络中乱序的问题。接收端根据这个编号进行确认,保证分割的数据段在原始数据包的位置。初始序列号由自己定,而后绪的序列号由对端的 ACK 决定:SN_x = ACK_y (x 的序列号 = y 发给 x 的 ACK)。 + +说白了,类似于身份证一样,而且还得发送此时此刻的所在的位置,就相当于身份证上的地址一样。 + +##### Acknowledge Number + +> 称为「确认序列号」。确认序列号是接收确认端所期望收到的下一序列号。确认序号应当是上次已成功收到数据字节序号加1,只有当标志位中的 ACK 标志为 1 时该确认序列号的字段才有效。主要用来解决不丢包的问题。 + +##### TCP Flag + +`TCP` 首部中有 6 个标志比特,它们中的多个可同时被设置为 `1`,主要是用于操控 `TCP` 的状态机的,依次为`URG,ACK,PSH,RST,SYN,FIN`。 + +当然只介绍三个: + +1. **ACK**:这个标识可以理解为发送端发送数据到接收端,发送的时候 ACK 为 0,标识接收端还未应答,一旦接收端接收数据之后,就将 ACK 置为 1,发送端接收到之后,就知道了接收端已经接收了数据。 +2. **SYN**:表示「同步序列号」,是 TCP 握手的发送的第一个数据包。用来建立 TCP 的连接。SYN 标志位和 ACK 标志位搭配使用,当连接请求的时候,SYN=1,ACK=0连接被响应的时候,SYN=1,ACK=1;这个标志的数据包经常被用来进行端口扫描。扫描者发送一个只有 SYN 的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口。 +3. **FIN**:表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送FIN标志位的 TCP 数据包后,连接将被断开。这个标志的数据包也经常被用于进行端口扫描。发送端只剩最后的一段数据了,同时要告诉接收端后边没有数据可以接受了,所以用FIN标识一下,接收端看到这个FIN之后,哦!这是接受的最后的数据,接受完就关闭了;**TCP四次分手必然问**。 + +##### Window size + +> 称为滑动窗口大小。所说的滑动窗口,用来进行流量控制。 + +### TCP三次握手 + +![TCP三次握手-2ujCx2](https://gitee.com/dreamcater/blog-img/raw/master/uPic/TCP三次握手-2ujCx2.svg) + +- **初始状态**:客户端处于 `closed(关闭)`状态,服务器处于 `listen(监听)` 状态。 +- **第一次握手**:客户端发送请求报文将 `SYN = 1`同步序列号和初始化序列号`seq = x`发送给服务端,发送完之后客户端处于`SYN_Send`状态。 +- **第二次握手**:服务端收到 `SYN` 请求报文之后,如果同意连接,会以自己的同步序列号`SYN(服务端) = 1`、初始化序列号 `seq = y`和确认序列号(期望下次收到的数据包)`ack = x + 1` 以及确认号`ACK = 1`报文作为应答,服务器为`SYN_Receive`状态。(问题来了,两次握手之后,所以老哥,你需要给我三次握手来传个话告诉我一声。你要是不告诉我,万一我认为你跑了,然后我可能出于安全性的考虑继续给你发一次,看看你回不回我。) +- **第三次握手**: 客户端接收到服务端的 `SYN + ACK`之后,知道可以下次可以发送了下一序列的数据包了,然后发送同步序列号 `ack = y + 1`和数据包的序列号 `seq = x + 1`以及确认号`ACK = 1`确认包作为应答,客户端转为`established`状态。(分别站在双方的角度上思考,各自ok) + +1. 你吃饭了嘛?(seq=x),收到请回答(SYN=1) +2. 收到(ACK=1),吃饭了(ack=x+1),你吃饭了吗?(seq=y),收到请回答(SYN=1) +3. 收到(ACK=1),吃饭了(ack=y+1),那么我们聊一下接下里的事情(established) + +### TCP四次分手 + +![TCP四次分手-Nt8NUx](https://gitee.com/dreamcater/blog-img/raw/master/uPic/TCP四次分手-Nt8NUx.png) + +- **初始化状态**:客户端和服务端都在连接状态,接下来开始进行四次分手断开连接操作。 +- **第一次分手**:第一次分手无论是客户端还是服务端都可以发起,因为 TCP 是全双工的。 + +> 假如客户端发送的数据已经发送完毕,发送FIN = 1 **告诉服务端,客户端所有数据已经全发完了**,**服务端你可以关闭接收了**,但是如果你们服务端有数据要发给客户端,客户端照样可以接收的。此时客户端处于FIN = 1等待服务端确认释放连接状态。 + +- **第二次分手**:服务端接收到客户端的释放请求连接之后,**知道客户端没有数据要发给自己了**,**然后服务端发送ACK = 1告诉客户端收到你发给我的信息**,此时服务端处于 CLOSE_WAIT 等待关闭状态。(服务端先回应给客户端一声,我知道了,但服务端的发送数据能力即将等待关闭,于是接下来第三次就来了。) +- **第三次分手**:此时服务端向客户端把所有的数据发送完了,然后发送一个FIN = 1,**用于告诉客户端,服务端的所有数据发送完毕**,**客户端你也可以关闭接收数据连接了**。此时服务端状态处于LAST_ACK状态,来等待确认客户端是否收到了自己的请求。(服务端等客户端回复是否收到呢,不收到的话,服务端不知道客户端是不是挂掉了还是咋回事呢) +- **第四次分手**:此时如果客户端收到了服务端发送完的信息之后,就发送ACK = 1,告诉服务端,客户端已经收到了你的信息。**有一个 2 MSL 的延迟等待**。 + +#### 为什么要有2MSL等待延迟? + +对应这样一种情况,最后客户端发送的ACK = 1给服务端的**过程中丢失**了,服务端没收到,服务端怎么认为的?我已经发送完数据了,怎么客户端没回应我?是不是中途丢失了?然后服务端再次发起断开连接的请求,一个来回就是2MSL。 + +客户端给服务端发送的ACK = 1丢失,**服务端等待 1MSL没收到**,**然后重新发送消息需要1MSL**。如果再次接收到服务端的消息,则**重启2MSL计时器**,**发送确认请求**。客户端只需等待2MSL,如果没有再次收到服务端的消息,就说明服务端已经接收到自己确认消息;此时双方都关闭的连接,TCP 四次分手完毕 + +#### 为什么四次分手? + +任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。举个例子:A 和 B 打电话,通话即将结束后,A 说“我没啥要说的了”,B回答“我知道了”,但是 B 可能还会有要说的话,A 不能要求 B 跟着自己的节奏结束通话,于是 B 可能又巴拉巴拉说了一通,最后 B 说“我说完了”,A 回答“知道了”,这样通话才算结束。 + +### TCP粘包 + +**TCP粘包**是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。 + + +个人觉得:应用层的报文在以流的形式传输时,每一个报文的报头紧接着上一个报文的报文尾部,这就是所谓的“粘包”问题。 + +- 由TCP**连接复用**造成的粘包问题。 +- 因为TCP默认会使用**Nagle算法**,此算法会导致粘包问题。 + - 只有上一个分组得到确认,才会发送下一个分组; + - 收集多个小分组,在一个确认到来时一起发送。 +- **数据包过大**造成的粘包问题。 +- 流量控制,**拥塞控制**也可能导致粘包。 +- **接收方不及时接收缓冲区的包,造成多个包接收** + +**解决**: + +1. **Nagle算法**问题导致的,需要结合应用场景适当关闭该算法 +2. 尾部标记序列。通过特殊标识符表示数据包的边界,例如\n\r,\t,或者一些隐藏字符。 +3. 头部标记分步接收。在TCP报文的头部加上表示数据长度。 +4. 应用层发送数据时**定长**发送。 + +[https://blog.csdn.net/xp178171640/article/details/104746379/](https://blog.csdn.net/xp178171640/article/details/104746379/) + +[https://blog.csdn.net/songchuwang1868/article/details/87707127](https://blog.csdn.net/songchuwang1868/article/details/87707127) + +### TCP 协议如何保证可靠传输? + +- **确认和重传**:接收方收到报文就会确认,发送方发送一段时间后没有收到确认就会重传。 +- **数据校验**:TCP报文头有校验和,用于校验报文是否损坏。 +- **数据合理分片和排序**:tcp会按最大传输单元(MTU)合理分片,接收方会缓存未按序到达的数据,重新排序后交给应用层。而UDP:IP数据报大于1500字节,大于MTU。这个时候发送方的IP层就需要分片,把数据报分成若干片,是的每一片都小于MTU。而接收方IP层则需要进行数据报的重组。由于UDP的特性,某一片数据丢失时,接收方便无法重组数据报,导致丢弃整个UDP数据报。 +- **流量控制**:当接收方来不及处理发送方的数据,能通过滑动窗口,提示发送方降低发送的速率,防止包丢失。 +- **拥塞控制**:当网络拥塞时,通过拥塞窗口,减少数据的发送,防止包丢失。 + +### TCP 利用滑动窗口实现流量控制的机制? + +> 流量控制是为了控制发送方发送速率,保证接收方来得及接收。TCP 利用滑动窗口实现流量控制。 + +TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着**接收方还有多大的缓冲区可以用于接收数据**。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方一般不能再发送数据报,但有两种情况除外,一种情况是可以发送紧急数据。 + +> 例如,允许用户终止在远端机上的运行进程。另一种情况是发送方可以发送一个 1 字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。 + +### TCP拥塞控制的机制以及算法? + +> 在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。 + +TCP 发送方要维持一个 **拥塞窗口(cwnd) 的状态变量**。拥塞控制窗口的大小**取决于网络的拥塞程度**,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。TCP的拥塞控制采用了四种算法,即 **慢开始** 、 **拥塞避免** 、**快重传** 和 **快恢复**。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。 + +### TCP的长连接和短连接 +[https://www.cnblogs.com/chinaops/p/9303041.html](https://www.cnblogs.com/chinaops/p/9303041.html) + +### UDP + +提供**无连接**的,尽最大努力的数据传输服务(**不保证数据传输的可靠性**)。 + +#### UDP的特点 + +- UDP是**无连接的**; +- UDP使用**尽最大努力交付**,即不保证可靠交付,因此主机不需要维持复杂的链接状态(这里面有许多参数); +- UDP是**面向报文**的; +- UDP**没有拥塞控制**,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等); +- UDP**支持一对一、一对多、多对一和多对多**的交互通信; +- UDP的**首部开销小**,只有8个字节,比TCP的20个字节的首部要短。 + +那么,再说一次TCP的特点: + +- **TCP是面向连接的**。(就好像打电话一样,通话前需要先拨号建立连接,通话结束后要挂机释放连接); +- 每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的(**一对一**); +- TCP**提供可靠交付的服务**。通过TCP连接传送的数据,无差错、不丢失、不重复、并且按序到达; +- TCP**提供全双工通信**。TCP允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双方通信的数据; +- **面向字节流**。TCP中的“流”(stream)指的是流入进程或从进程流出的字节序列。“面向字节流”的含义是:虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。 \ No newline at end of file diff --git "a/Java/classify/net/\347\275\221\347\273\234\346\250\241\345\236\213.md" "b/Java/classify/net/\347\275\221\347\273\234\346\250\241\345\236\213.md" new file mode 100644 index 00000000..da907eb8 --- /dev/null +++ "b/Java/classify/net/\347\275\221\347\273\234\346\250\241\345\236\213.md" @@ -0,0 +1,49 @@ + +## 网络模型 + +![分层模型-EiHhGW](https://gitee.com/dreamcater/blog-img/raw/master/uPic/分层模型-EiHhGW.png) + +### 简要概括 + +- 物理层:底层数据传输,如网线;网卡标准。 + +- 数据链路层:定义数据的基本格式,如何传输,如何标识;如网卡MAC地址。 + +- 网络层:定义IP编址,定义路由功能;如不同设备的数据转发。 + +- 传输层:端到端传输数据的基本功能;如 TCP、UDP。 + +- 会话层:控制应用程序之间会话能力;如不同软件数据分发给不同软件。 + +- 标识层:数据格式标识,基本压缩加密功能。 + +- 应用层:各种应用软件,包括 Web 应用。 + +### 流程 + +比如,计算机 A 和 计算机 B 要进行信息交互,比如 A 上开发了一个网页,需要 B 去访问。B 发出一个请求给 A,那么请求数据从 B 的 **应用层开始向下传到表示层、再从表示层传到会话层直到物理层,通过物理层传递到 A,A 的物理层接到请求后将请求向上传递到自己的应用层,应用层再将要请求的数据向自己的物理层方向传递然后 B 接到数据传递数据到自己的应用层**。 + +说明: + +- 在四层,既传输层数据被称作**段**(Segments); +- 三层网络层数据被称做**包**(Packages); +- 二层数据链路层时数据被称为**帧**(Frames); +- 一层物理层时数据被称为**比特流**(Bits)。 + +### 常见的端口号和协议号 + +![常见端口号-PemUq1](https://gitee.com/dreamcater/blog-img/raw/master/uPic/常见端口号-PemUq1.png) + +### 总结 + +- 网络七层模型是一个标准,而非实现。 +- 网络四层模型是一个实现的应用模型。 +- 网络四层模型由七层模型简化合并而来。 + +### ping命令基于哪一层协议的原理是什么? + +ping命令基于网络层的命令,是基于ICMP协议工作的。 + +### ARP +ARP是一种解决地址问题的协议。以目标IP地址为线索,用来定位下一个应该接收数据分包的网络设备对应的MAC地址。 +起初要通过广播发送一个ARP请求包,这个包里存放了其MAC地址的主机IP地址,由于广播的包可以被同一个链路上所有的主机或路由器接收,因此ARP的请求包也就会被这同一个链路上所有的主机和路由器进行解析。如果ARP请求包中的目标IP地址与自己的IP地址的一致,那么这个节点就将自己的MAC地址塞入ARP响应包返回给主机A。 \ No newline at end of file diff --git "a/Java/classify/redis/Redis\345\210\206\345\270\203\345\274\217\351\224\201.md" "b/Java/classify/redis/Redis\345\210\206\345\270\203\345\274\217\351\224\201.md" new file mode 100644 index 00000000..5a4b7400 --- /dev/null +++ "b/Java/classify/redis/Redis\345\210\206\345\270\203\345\274\217\351\224\201.md" @@ -0,0 +1,32 @@ +# 分布式锁 + +> 毕竟判断和绑定座位(或者下单)非原子性,为了降低锁的粒度,可以将判断和绑定座位锁在一个事务里。集群:Redisson + +- Key为xx_座位号,过期时间为随机1-5s(用setex的命令,该命令是key和过期时间是原子性的) +- 每次先Redis中判断该key存在不存在,如果存在,要么阻塞,要么就返回给用户,座位已被选择。 +- 如果不存在,先上锁,然后再判断和绑定座位(或者下单)。其实这里有个隐藏的问题。如果绑定座位非常耗时,超过了过期时间1-5s,就凉凉了。其实这里设置过期时间,就是防止一直因为某种原因阻塞而不释放锁 +- 前三步,少了个签证value,如果不设置,那么当锁过期了,业务逻辑才走完,准备删除的时候,B客户端获取到了该锁,但是A把B的key锁删除了,然而B还不知道。 +- 因此,要解决这个问题,可以设置value签证,结束的时候判断一次,该value是不是自己的value,这样就不会误删。 + +## RedLock算法流程 + +首先有这样的问题: + +1. 客户端 A 从 Master 上获取锁。 +2. 在锁未被复制到某 Slave 节点的时候,Master 节点 Down 掉了。 +3. 某 Slave 节点成为新的 Master。 +4. 客户端 B 可从新 Master 上获取锁。 + +假设有5个实例 + +1. 比如过期时间为TTL:10min +2. 记录当前时间:比如T1 = 12:00 +3. 客户端分别向5个实例获取锁,比如申请锁的时间依次为:12:01...12:05,最后获取实例的锁为T2:12:05(获取锁的超时时间要远远小于过期时间,防止死等。) +4. 如果获取锁的实例大于3个(过半机制),那么就相当于获取到锁了,该锁的真正的有效时间为TTL-(T2-T1) = 5min +5. 如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁 + +[https://juejin.im/post/5cc165816fb9a03202221dd5](https://juejin.im/post/5cc165816fb9a03202221dd5) + +## zk实现分布式锁 + +[补充-Zookeeper锁的实现](https://juejin.im/post/5c01532ef265da61362232ed) \ No newline at end of file diff --git "a/Java/classify/redis/Redis\346\214\201\344\271\205\345\214\226.md" "b/Java/classify/redis/Redis\346\214\201\344\271\205\345\214\226.md" new file mode 100644 index 00000000..44ce106a --- /dev/null +++ "b/Java/classify/redis/Redis\346\214\201\344\271\205\345\214\226.md" @@ -0,0 +1,46 @@ + +面试官:Redis的持久化了解嘛? + +我:了解,Redis的持久化分为两种:**RDB和AOF** + +## RDB + +**RDB**是一种**快照存储持久化**方式,具体就是将`Redis`某一时刻的内存数据保存到硬盘的文件当中,默认保存的文件名为`dump.rdb`。在`Redis`服务器启动时,会重新加载`dump.rdb`文件的数据到内存当中恢复数据。 + +优点: + +1. RDB会生成多个数据文件,**每个数据文件都代表了某一个时刻中redis的数据**,这种多个数据文件的方式,非常适合**做冷备**。 +2. RDB对redis对外提供读写服务的时候,影像非常小,因为redis 主进程只需要fork一个子进程出来,让子进程对磁盘io来进行rdb持久化 +3. **RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快**。 + +缺点: + +1. **如果redis要故障时要尽可能少的丢失数据,RDB没有AOF好**,例如1:00进行的快照,在1:10又要进行快照的时候宕机了,这个时候就会丢失10分钟的数据。 +2. RDB每次fork出子进程来执行RDB快照生成文件时,如果文件特别大,可能会导致客户端提供服务暂停数毫秒或者几秒 + +![rdb-uc03rm](https://gitee.com/dreamcater/blog-img/raw/master/uPic/rdb-uc03rm.png) + +## AOF + +**AOF**:把所有的**对Redis的服务器进行修改的命令都存到一个文件里,命令的集合**。 使用AOF做持久化,每一个写命令都通过write函数追加到appendonly.aof中。aof的默认策略是每秒钟fsync一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。 缺点是对于相同的数据集来说,AOF的文件体积通常要大于RDB文件的体积。根据所使用的fsync策略,AOF的速度可能会慢于RDB。 Redis默认是快照RDB的持久化方式。对于主从同步来说,主从刚刚连接的时候,进行全量同步(RDB);全同步结束后,进行增量同步(AOF)。 + +优点: + +1. **AOF可以更好的保护数据不丢失,一般AOF会以每隔1秒**,通过后台的一个线程去执行一次fsync操作,如果redis进程挂掉,最多丢失1秒的数据。 +2. **AOF以appen-only的模式写入,所以没有任何磁盘寻址的开销,写入性能非常高**。 +3. AOF日志文件的命令通过非常可读的方式进行记录,这个非常适合做灾难性的误删除紧急恢复,如果某人不小心用flushall命令清空了所有数据,只要这个时候还没有执行rewrite,那么就可以将日志文件中的flushall删除,进行恢复。 + +缺点: + +1. 对于同一份文件**AOF文件比RDB数据快照要大**。 +2. AOF开启后支持写的QPS会比RDB支持的写的QPS低,因为AOF一般会配置成每秒fsync操作,每秒的fsync操作还是很高的 +3. **数据恢复比较慢,不适合做冷备**。 + +![aof-ElLyYr](https://gitee.com/dreamcater/blog-img/raw/master/uPic/aof-ElLyYr.png) + +**如何选择:** + +如何选择: + +综合AOF和RDB两种持久化方式,**用AOF来保证数据不丢失,作为恢复数据的第一选择**;**用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,可以使用RDB进行快速的数据恢复**。 + diff --git "a/Java/classify/redis/Redis\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/Java/classify/redis/Redis\346\225\260\346\215\256\347\273\223\346\236\204.md" new file mode 100644 index 00000000..79d4ea81 --- /dev/null +++ "b/Java/classify/redis/Redis\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -0,0 +1,51 @@ + +## String + +String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 常规key-value缓存应用; **常规计数:微博数,粉丝数**等。 + +## Hash + +Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以Hash数据结构来**存储用户信息,商品信息**等等。 + +简单说一下结构 + +- 字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。 +- Redis中的字典使用哈希表作为底层结构实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。 +- Redis使用MurmurHash2算法来计算键的哈希值。 +- 哈希表使用链地址法来解决键冲突。 + +注意:这里和Java的HashMap不同的rehash过程 + +1. Redis的rehash过程是扩展和收缩,而且还是渐进式的rehash +2. Redis的字典有两个哈希表ht[0]和ht[1] +3. 为字典的ht[1]哈希表分配空间,如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used *2的2^n;如果执行的是收缩操作,那么ht[1]的大小第一个大于等于ht[0].used的2^n。(举个例子,ht[0]的长度为10,那么扩展就是2^5的32,如果是压缩的话2^4=16) +4. 如果ht[0]的键值非常多的话,一次性转移过去,是一个非常耗时的操作哦,因此并非一次性,采取渐进式rehash转移。 + +## List + +list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如**微博的关注列表,粉丝列表, 消息列表**等功能都可以用Redis的 list 结构来实现。 + +Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。 + +另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。 + +## Set + +set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。 + +当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在 一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。 + +比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常 方便的实现如**共同关注、共同粉丝、共同喜好**等功能。这个过程也就是求交集的过程,具体命令如下:`sinterstore key1 key2 key3`将交集存在key1内 + + +## Zset + +和set相比,sorted set增加了一个**权重参数score**,使得集合中的元素能够按score进行**有序**排列。 + +举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种**礼物排行榜**,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 SortedSet 结构进行存储。 + +跳跃表,暂时先放一个链接[https://zhuanlan.zhihu.com/p/53975333](https://zhuanlan.zhihu.com/p/53975333) + +- 简单来说跳跃表是一种有序数据结构,它通过在**每个节点中维持多个指向其他节点的指针**,从而达到快速访问节点的目的。 +- 跳跃表平均O(longN),最坏O(N)复杂度的节点查找 +- 跳跃表有个层的概念:层带有两个属性:**前进指针和跨度**,前进指针用于**访问位于表尾方向的其他节点**,而跨度则记录了**前进指针所指向节点和当前节点的距离**。一般情况下,层越多,查找效率越高。 \ No newline at end of file diff --git "a/Java/classify/redis/Redis\346\250\241\345\236\213.md" "b/Java/classify/redis/Redis\346\250\241\345\236\213.md" new file mode 100644 index 00000000..ee0616c1 --- /dev/null +++ "b/Java/classify/redis/Redis\346\250\241\345\236\213.md" @@ -0,0 +1,75 @@ + +### Redis模型 + +面试官:Redis为什么快? + +我:内心:不知道为什么一直问这个问题。 + +1. 纯内存操作 +2. 单线程操作,避免了**频繁的上下文切换** +3. 合理高效的数据结构 +4. 采用了**非阻塞I/O多路复用**机制 + +实际上Redis服务器是一个事件驱动程序,分为**文件事件**和**时间事件**,就主要讲一下文件事件。 + +Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器 + +- 文件事件处理器使用**I/O多路复用程序来同时监听多个套接字**,并根据套接字目前执行的任务来为套接字**关联不同的事件处理器** +- 当被监听的套接字准备好执行连接**应答、读取、写入、关闭**等操作时,与操作相对于的文件事件就会产生,这时**文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件**。 +- 简单点:**就是一堆套接字请求,被一个叫做I/O多路复用程序监听,通过文件事件分派器一个一个和事件处理器绑定在一起去处理**。 + +I/O多路复用程序是有常见的select、epoll等系统调用所实现的。有个小故事,自行理解BIO、NIO、select、poll、epoll等 + +故事情节为:**老李去买火车票,三天后买到一张退票。参演人员(老李,黄牛,售票员,快递员),往返车站耗费1小时。** + +**往返车站可以看成系统调用,调用一次一小时** + +### 1. 阻塞I/O模型 + +老李去火车站买票,排队三天买到一张退票。 + +耗费:在车站吃喝拉撒睡 3天,其他事一件没干。 + +### 2. 非阻塞I/O模型 + +老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。 + +耗费:往返车站6次,路上6小时,其他时间做了好多事。 + +2比1多了个自己轮训调用 + +### 3. I/O复用模型 + +1. select/poll + +老李去火车站买票,委托黄牛,然后每隔6小时电话**黄牛**询问,黄牛三天内买到票,然后老李去火车站交钱领票。 + +耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次 + +实际上,就是自己不断调select(像个船一样,装了很多描述符)询问哪些描述符可读可写,比如又一个可读了,咱就调用可读系统调用就ok了 + +2. epoll + +老李去火车站买票,委托黄牛,**黄牛买到后即通知老李去领**,然后老李去火车站交钱领票。 +耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话 + +实际上,自己不用管了,当有可读的时候,直接中断你,然后你自己去读 + +### 4. 信号驱动I/O模型 + +老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。 + +耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话 + +不要黄牛了,省了这个单线程,系统通知你,你收到以后自己去读 + +### 5. 异步I/O模型 + +老李去火车站,告诉售票员要买票,售票员买到票之后,打电话通知老李把票放在某某储物箱,老李根据储物箱地址自己去取票。 + +耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话 + +只需要注册一次,得到消息之后,就去另外一个地址上取走票 + +黄牛是多路复用,他不仅可以帮你买票,还可以其他人买票,还可以买飞机票,高铁票等。 + diff --git "a/Java/classify/redis/\345\206\205\345\255\230\346\267\230\346\261\260\346\234\272\345\210\266.md" "b/Java/classify/redis/\345\206\205\345\255\230\346\267\230\346\261\260\346\234\272\345\210\266.md" new file mode 100644 index 00000000..0be06449 --- /dev/null +++ "b/Java/classify/redis/\345\206\205\345\255\230\346\267\230\346\261\260\346\234\272\345\210\266.md" @@ -0,0 +1,23 @@ +# 内存淘汰机制 + +面试官:先讲一下Redis的过期时间 + +我: + +定期删除+惰性删除 + +- 定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载! +- 惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的哈! + +面试官:如果定期删除漏掉了很多过期 key,然后你也没及时去查, 也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢? + +我:**redis 内存淘汰机制。** + +redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。redis 提供 6种数据淘汰策略: + +- volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 +- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰 +- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰 +- allkeys-lru:从数据集中挑选最近最少使用的数据淘汰 +- allkeys-random:从数据集中任意选择数据淘汰 +- no-enviction(驱逐):禁止驱逐数据 \ No newline at end of file diff --git "a/Java/classify/redis/\347\274\223\345\255\230\347\251\277\351\200\217\345\222\214\347\274\223\345\255\230\351\233\252\345\264\251.md" "b/Java/classify/redis/\347\274\223\345\255\230\347\251\277\351\200\217\345\222\214\347\274\223\345\255\230\351\233\252\345\264\251.md" new file mode 100644 index 00000000..e3e362d2 --- /dev/null +++ "b/Java/classify/redis/\347\274\223\345\255\230\347\251\277\351\200\217\345\222\214\347\274\223\345\255\230\351\233\252\345\264\251.md" @@ -0,0 +1,21 @@ +> 感觉这个被问烂了 + +面试官:聊聊什么是缓存穿透和雪崩 + +我:ok + +**缓存穿透**: + +一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。 + +1. 在接口做校验 +2. 存null值(缓存击穿加锁) +3. 布隆过滤器拦截: 将所有可能的查询key先映射到布隆过滤器中,查询时先判断key是否存在布隆过滤器中,存在才继续向下执行,如果不存在,则直接返回。布隆过滤器将值进行多次哈希bit存储,布隆过滤器说某个元素在,可能会被误判。布隆过滤器说某个元素不在,那么一定不在。 + +**缓存雪崩:** + +缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。 + +1. 使用 Redis 高可用架构:使用 Redis 集群来保证 Redis 服务不会挂掉 +2. 缓存时间不一致,给缓存的失效时间,加上一个随机值,避免集体失效 +3. 限流降级策略:有一定的备案,比如个性推荐服务不可用了,换成热点数据推荐服务 \ No newline at end of file diff --git a/Java/classify/spring/Bean.md b/Java/classify/spring/Bean.md new file mode 100644 index 00000000..08cb05c7 --- /dev/null +++ b/Java/classify/spring/Bean.md @@ -0,0 +1,49 @@ + +## bean的作用域 +- singleton 作用域:表示在 Spring 容器中只有一个 Bean 实例,以单例的形式存在,是默认的 Bean 作用域。 +- prototype 作用域:原型作用域,每次调用 Bean 时都会创建一个新实例,也就是说每次调用 getBean() 方法时,相当于执行了 new Bean()。 +- request 作用域:每次 Http 请求时都会创建一个新的 Bean,该作用域仅适应于 WebApplicationContext 环境。 +- session 作用域:同一个 Http Session 共享一个 Bean 对象,不同的 Session 拥有不同的 Bean 对象,仅适用于 WebApplicationContext 环境。 +- application 作用域:全局的 Web 作用域,类似于 Servlet 中的 Application。 + +## Spring的单例有线程安全问题吗 +大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 + +常见的有两种解决办法: + +- 在Bean对象中尽量避免定义可变的成员变量(不太现实)。 +- 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。 + +## 什么是三级缓存 +1. 第一级缓存:单例缓存池singletonObjects。 +2. 第二级缓存:早期提前暴露的对象缓存earlySingletonObjects。(属性还没有值对象也没有被初始化) +3. 第三级缓存:singletonFactories单例对象工厂缓存。 + + +## 创建Bean的整个过程 +1. 首先finishBeanFactoryInitialization->preInstantiateSingletons->getBean->doGetBean; +2. 在doGetBean中,transformedBeanName:主要负责判断一下有木有别名;getSingleton:从一级缓存singletonObjects拿bean,在getSingleton方法中,有一个判断条件就是isSingletonCurrentlyInCreation,判断为false,因为他是第一次进来,并且还没有正在创建该bean;dependsOn:依赖,暂时先不说他。 +3. 再来一次getSingleton:再一次的从singketonObjects缓存拿,依然没有的。接着有个重点beforeSingletonCreation:它把bean添加到临时的singletonsCurrentlyInCreation,这就意味着,下次再碰见它,那可就是true了。接着singletonFactory.getObject(),这里getObject调用的是传递的接口createBean方法。 +4. 在createBean方法中:有个doCreateBean->createBeanInstance方法:它就是直接实例化,实际上构造器有反应了(区分JVM创建对象和Spring创建对象),但是没有赋值(初始化);earlySingletonExposure:提前暴漏该bean。但要知道三个变量,为什么他是true:isSingleton(),是否单例,那肯定是哦;(这里解释了这里是单例才能提前曝漏,意味着才能存三级缓存)allowCircularReferences,默认变量为true,写死了;isSingletonCurrentlyInCreation,这里可就为true了,因为步骤3,已经将它设置为true了。那么会进来这个方法:addSingletonFactory +5. addSingletonFactory在这个方法中:将该bean放入到三级缓存singletonFactories中。(解决循环依赖) +6. 接下来,就是它了,populateBean:实际上就是属性赋值。(如果这里要有A依赖B,又发现三级缓存中没有B,那么它就会再次执行一次(递归开始)getBean->doGetBean->createBeanInstance(把B给实例化一下),同样的道理,这里会将B也会放入三级缓存中,B开始populateBean,那么它发现B依赖A,此时三级缓存中有A(精髓,牛逼),然后把A放到二级缓存中,同时从三级缓存中移除,接着得到A之后直接赋值,最后完成了初始化,然后来到addSingleton,将B仍到了一级缓存,同时将B从三级缓存仍出去)返回B,递归结束,得到B之后将B的赋值给A了。 +7. 最后将二级缓存的A删除,仍到一级缓存中。 + + + + +## bean的生命周期 +![bean的生命周期-GbJvfY](https://gitee.com/dreamcater/blog-img/raw/master/uPic/bean的生命周期-GbJvfY.png) + +- Bean 容器找到配置文件中 Spring Bean 的定义。 +- Bean 容器利用 Java Reflection API 创建一个Bean的实例。 +- 如果涉及到一些属性值 利用 `set()`方法设置一些属性值。 +- 如果 Bean 实现了 `BeanNameAware` 接口,调用 `setBeanName()`方法,传入Bean的名字。 +- 如果 Bean 实现了 `BeanClassLoaderAware` 接口,调用 `setBeanClassLoader()`方法,传入 `ClassLoader`对象的实例。 +- 与上面的类似,如果实现了其他 `*.Aware`接口,就调用相应的方法。 +- 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessBeforeInitialization()` 方法 +- 如果Bean实现了`InitializingBean`接口,执行`afterPropertiesSet()`方法。 +- 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。 +- 如果有和加载这个 Bean的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessAfterInitialization()` 方法 +- 当要销毁 Bean 的时候,如果 Bean 实现了 `DisposableBean` 接口,执行 `destroy()` 方法。 +- 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。 \ No newline at end of file diff --git "a/Java/classify/spring/Ioc\345\222\214AOP.md" "b/Java/classify/spring/Ioc\345\222\214AOP.md" new file mode 100644 index 00000000..23d016f9 --- /dev/null +++ "b/Java/classify/spring/Ioc\345\222\214AOP.md" @@ -0,0 +1,59 @@ + +## IoC +IoC(Inverse of Control:控制反转)是一种**设计思想**,就是 **将原本在程序中手动创建对象的控制权,交由Spring框架来管理。IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。** + +将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 **IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。** + +### 初始化流程 + +- Resource资源定位:这个Resouce指的是BeanDefinition的资源定位。这个过程就是容器找数据的过程,就像水桶装水需要先找到水一样。 +- BeanDefinition的载入和解析:这个载入过程是把用户定义好的Bean表示成Ioc容器内部的数据结构,而这个容器内部的数据结构就是BeanDefition。 +- BeanDefinition注册 +- prepareRefresh():预备一下, 标记启动时间,上下文环境,我要的材料(beanDefinition)准备好了嘛? +- obtainFreshBeanFactory(): + - 如果已经有了BeanFactory就销毁它里面的单例Bean并关闭这个BeanFactory。 + - 创建一个新的BeanFactory。 + - 对这个BeanFactory进行定制(customize),如allowBeanDefinitionOverriding等参数 + - 转载BeanDefinitions(读取配置文件,将xml转换成对应得BeanDefinition) + - 检查是否同时启动了两个BeanFactory。 +- prepareBeanFactory(beanFactory):设置beanFactory的类加载器,材料(BeanDefinition)解析器等 +- postProcessBeanFactory(beanFactory): + - 设置beanFactory的后置处理器 + - 具体的子类可以在这步的时候添加一些特殊的 BeanFactoryPostProcessor 的实现类或做点什么事 +- invokeBeanFactoryPostProcessors(beanFactory): + - 调用beanFactory的后置处理器(BeanDefinitionRegisterPostProcessor和BeanFactoryPostProcessor) + - 调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法 +- registerBeanPostProcessors(beanFactory): + - 注册 BeanPostProcessor 的实现类(bean的后置处理器) + - 此接口两个方法: postProcessBeforeInitialization 和 postProcessAfterInitialization 两个方法分别在 Bean 初始化之前和初始化之后得到执行。注意,到这里 Bean 还没初始化 +- initMessageSource():对上下文中的消息源进行初始化 +- initApplicationEventMulticaster():初始化上下文的事件广播器 +- onRefresh():- 模版方法,具体的子类可以在这里初始化一些特殊的 Bean(在初始化 singleton beans 之前) +- registerListeners():注册事件监听器 +- finishBeanFactoryInitialization(beanFactory):初始化所有的 singleton beans +- finishRefresh():最后,广播事件,ApplicationContext 初始化完成 + +## AOP + +AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,**却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来**,便于**减少系统的重复代码**,**降低模块间的耦合度**,并**有利于未来的可拓展性和可维护性**。 + +- **Spring AOP就是基于动态代理的** +- 如果要代理的对象,实现了某个接口,那么Spring AOP会使用**JDK Proxy**, +- 而对于没有实现接口的对象,这时候Spring AOP会使用 **Cglib** 生成一个被代理对象的子类来作为代理。 + + +![](https://user-gold-cdn.xitu.io/2018/9/14/165d631e56799a5c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +### 初始化流程 +registerAspectJAnnotationAutoProxyCreatorIfNecessary + +- 第一句,注册一个AnnotationAwareAspectJAutoProxyCreator(称它为自动代理器),这个Creator是AOP的操作核心,也是扫描Bean,代理Bean的操作所在。 +- 第二句,解析配置元素,决定代理的模式。其中有JDK动态代理,还有CGLIB代理,这部分后续会再细讲。 +- 第三句,作为系统组件,把Creator这个Bean,放到Spring容器中。让Spring实例化,启动这个Creator。 + +总结: +- Spring加载自动代理器AnnotationAwareAspectJAutoProxyCreator,当作一个系统组件。 +- 当一个bean加载到Spring中时,会触发自动代理器中的bean后置处理,然后会先扫描bean中所有的Advisor +- 然后用这些Adviosr和其他参数构建ProxyFactory +- ProxyFactory会根据配置和目标对象的类型寻找代理的方式(JDK动态代理或CGLIG代理) +- 然后代理出来的对象放回context中,完成Spring AOP代理 diff --git a/Java/classify/spring/SpringBoot.md b/Java/classify/spring/SpringBoot.md new file mode 100644 index 00000000..dd49e06e --- /dev/null +++ b/Java/classify/spring/SpringBoot.md @@ -0,0 +1,49 @@ + + +## Springboot自动装配原理 + +SpringBootApplication的注解 +[https://www.jianshu.com/p/943650ab7dfd](https://www.jianshu.com/p/943650ab7dfd) + +- @SpringBootConfiguration:允许在上下文中注册额外的bean或导入其他配置类 +- @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制 +- @ComponentScan: 扫描常用的注解 + +其中 @EnableAutoConfiguration 是实现自动配置的入口,该注解又通过 @Import 注解导入了AutoConfigurationImportSelector,在该类中加载 META-INF/spring.factories 的配置信息。然后筛选出以 EnableAutoConfiguration 为 key 的数据,加载到 IOC 容器中,实现自动配置功能! + + +## @Resource和@Autowired区别 + +### 共同点 +两者都可以写在字段和setter方法上。两者如果都写在字段上,那么就不需要再写setter方法。 + +```java +public class TestServiceImpl { + // 下面两种@Autowired只要使用一种即可 + @Autowired + private UserDao userDao; // 用于字段上 + + @Autowired + public void setUserDao(UserDao userDao) { // 用于属性的方法上 + this.userDao = userDao; + } +} +``` + +### 不同点 + +1. @Autowired + +@Autowired注解是**按照类型**(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。如果我们想使用**按照名称**(byName)来装配,可以结合**@Qualifier注解**一起使用。(通过类型匹配找到多个candidate,在没有@Qualifier、@Primary注解的情况下,会使用对象名作为最后的fallback匹配)如下: + +```java +public class TestServiceImpl { + @Autowired + @Qualifier("userDao") + private UserDao userDao; +} +``` + +2. @Resource + +@Resource默认按照ByName自动注入。@Resource有两个重要的属性:**name和type**,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。**所以,如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略** \ No newline at end of file diff --git a/Java/classify/spring/SpringMVC.md b/Java/classify/spring/SpringMVC.md new file mode 100644 index 00000000..8886d18f --- /dev/null +++ b/Java/classify/spring/SpringMVC.md @@ -0,0 +1,13 @@ + +# SpringMVC + +![springmvc工作原理](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-11/49790288.jpg) + +1. 客户端(浏览器)发送请求,直接请求到 `DispatcherServlet`。 +2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping`,解析请求对应的 `Handler`。 +3. 解析到对应的 `Handler`(也就是我们平常说的 `Controller` 控制器)后,开始由 `HandlerAdapter` 适配器处理。 +4. `HandlerAdapter` 会根据 `Handler`来调用真正的处理器开处理请求,并处理相应的业务逻辑。 +5. 处理器处理完业务后,会返回一个 `ModelAndView` 对象,`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 +6. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 +7. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 +8. 把 `View` 返回给请求者(浏览器) \ No newline at end of file diff --git "a/Java/classify/spring/Spring\344\272\213\345\212\241.md" "b/Java/classify/spring/Spring\344\272\213\345\212\241.md" new file mode 100644 index 00000000..72925428 --- /dev/null +++ "b/Java/classify/spring/Spring\344\272\213\345\212\241.md" @@ -0,0 +1,48 @@ + +## Spring事务 +### 隔离级别 +- **TransactionDefinition.ISOLATION_DEFAULT:** 使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别. +- **TransactionDefinition.ISOLATION_READ_UNCOMMITTED:** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读** +- **TransactionDefinition.ISOLATION_READ_COMMITTED:** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生** +- **TransactionDefinition.ISOLATION_REPEATABLE_READ:** 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生。** +- **TransactionDefinition.ISOLATION_SERIALIZABLE:** 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 + +### @Transactional(rollbackFor = Exception.class)注解了解吗 +1. @Transactional注解只能应用到public修饰符上,其它修饰符不起作用,但不报错。 +2. 默认情况下此注解会对unchecked异常进行回滚,对checked异常不回滚。 + +> checked异常:表示无效,不是程序中可以预测的。比如无效的用户输入,文件不存在,网络或者数据库链接错误。这些都是外在的原因,都不是程序内部可以控制的。unchecked异常:表示错误,程序的逻辑错误。是RuntimeException的子类,比如IllegalArgumentException, NullPointerException和IllegalStateException。 + +不回滚解决方案: +1. 检查方法是不是public +2. 检查异常是不是unchecked异常 +3. 如果是checked异常也想回滚的话,注解上写明异常类型即可@Transactional(rollbackFor=Exception.class) + +事务失效的8大原因: +1. 数据库引擎不支持事务 +2. 没有被 Spring 管理 +3. 方法不是 public 的 +4. 自身调用问题 +5. 数据源没有配置事务管理器 +6. 不支持事务(传播机制) +7. 异常被吃了(捕获异常) +8. 异常类型错误(checked异常失效) + +### Spring的的事务传播机制 +1. **required**(默认):支持使用当前事务,如果当前事务不存在,创建一个新事务。 +2. **requires_new**:创建一个新事务,如果当前事务存在,把当前事务挂起。 +3. **supports**:支持使用当前事务,如果当前事务不存在,则不使用事务。 +4. **not_supported**:无事务执行,如果当前事务存在,把当前事务挂起。 +5. **mandatory**:强制,支持使用当前事务,如果当前事务不存在,则抛出Exception。 +6. **never**:无事务执行,如果当前有事务则抛出Exception。 +7. **nested**:嵌套事务,如果当前事务存在,那么在嵌套的事务中执行。如果当前事务不存在,则表现跟REQUIRED一样。 + +### 事务源码 +- 开启@EnableTransactionManagement +- 利用TransactionManagementConfigurationSelector给容器中会导入组件 + - AutoProxyRegistrar + - 给容器中注册一个 InfrastructureAdvisorAutoProxyCreator 组件 + - 利用后置处理器机制在对象创建以后,包装对象,返回一个代理对象(增强器),代理对象执行方法利用拦截器链进行调用; + - ProxyTransactionManagementConfiguration(给容器中注册事务增强器) + - 事务增强器要用事务注解的信息,AnnotationTransactionAttributeSource解析事务注解 + - 事务拦截器 \ No newline at end of file diff --git "a/Java/classify/sys/\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\347\256\241\347\220\206\346\226\271\345\274\217.md" "b/Java/classify/sys/\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\347\256\241\347\220\206\346\226\271\345\274\217.md" new file mode 100644 index 00000000..26548dc4 --- /dev/null +++ "b/Java/classify/sys/\346\223\215\344\275\234\347\263\273\347\273\237\345\206\205\345\255\230\347\256\241\347\220\206\346\226\271\345\274\217.md" @@ -0,0 +1,22 @@ + +## 分页管理 + +分页存储管理是将一个进程的逻辑地址空间分成若干个大小相等的片,称为页面或页,并为各页加以编号,从0开始,如第0页、第1页等。相应地,也把内存空间分成与页面相同大小的若干个存储块,称为(物理)块或页框(frame),也同样为它们加以编号,如0#块、1#块等等。在为进程分配内存时,以块为单位将进程中的若干个页分别装入到多个可以不相邻接的物理块中。由于进程的最后一页经常装不满一块而形成了不可利用的碎片,称之为“页内碎片”。 + +**优缺点**:**没有外部碎片,内存利用率高。但各页中内容没有关联,不利于编程和共享**。 + +## 分段管理 + +程序通过分段(segmentation)划分为多个模块,如代码段、数据段、共享段。内存每段的大小都匹配程序段,不会产生内部碎片。 +**优缺点**: 可以针对不同类型的段采取不同的保护。 可以按段为单位来进行共享,包括通过动态链接进行代码共享。 **不会产生内部碎片,但会产生外部碎片,内存利用率比分页低**。 + +## 段页式管理 + +一个进程中所包含的具有独立逻辑功能的程序或数据仍被划分为段,并有各自的段号s。这反映相继承了段式管理的特征。其次,对于段s中的程序或数据,则按照一定的大小将其划分为不同的页。和页式系统一样,最后不足一页的部分仍占一页。这反映了段页式管理中的页式特征。从而,段页式管理时的进程的虚拟地址空间中的虚拟地址由三部分组成:即段号s,页号P和页内相对地址d。虚拟空间的最小单位是页而不是段,从而内存可用区也就被划分成为若干个大小相等的页面,且每段所拥有的程序和数据在内存中可以分开存放。分段的大小也不再受内存可用区的限制。 +**优缺点:**既有具有独立逻辑功能的段,又以大小相同的页为内存分配单位进而不会产生外部碎片。但仍会有内部碎片。 + +[操作系统如管理内存](https://blog.csdn.net/hguisu/article/details/5713164) + +## 内存分配过程 + +[https://blog.csdn.net/edonlii/article/details/22601043](https://blog.csdn.net/edonlii/article/details/22601043) \ No newline at end of file diff --git "a/Java/classify/sys/\346\255\273\351\224\201.md" "b/Java/classify/sys/\346\255\273\351\224\201.md" new file mode 100644 index 00000000..30fbc5e5 --- /dev/null +++ "b/Java/classify/sys/\346\255\273\351\224\201.md" @@ -0,0 +1,20 @@ +## 死锁的条件 + +所以:死锁的条件? + +- **互斥条件**:该资源任意一个时刻只由一个线程占用。(同一时刻,这个碗是我的,你不能碰) +- **请求与保持条件**:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(我拿着这个碗一直不放) +- **不剥夺条件**:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。(我碗中的饭没吃完,你不能抢,释放权是我自己的,我想什么时候放就什么时候放) +- **循环等待条件**:若干进程之间形成一种头尾相接的循环等待资源关系。(我拿了A碗,你拿了B碗,但是我还想要你的B碗,你还想我的A碗)比喻成棒棒糖也阔以。 + +面试官:所以解决死锁的办法是? + +我:好的,没毛病 + +- 预防死锁: + - **资源一次性分配**:破坏请求和保持条件。 + - **可剥夺资源**:当进程新申请的资源不满足时,释放已经分配的资源。破坏不可剥夺条件 + - **资源有序分配**:系统给进程编号,按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 +- 避免死锁:银行家算法:分配资源前先评估风险,会不会在分配后导致死锁。 即分配给一个进程资源的时候,该进程能否全部返还占用的资源。 +- 检测死锁:建立资源分配表和进程等待表。 +- 解除死锁:可以直接撤销死锁进程,或撤销代价最小的进程。 \ No newline at end of file diff --git "a/Java/classify/sys/\347\263\273\347\273\237\350\277\233\347\250\213\350\260\203\345\272\246.md" "b/Java/classify/sys/\347\263\273\347\273\237\350\277\233\347\250\213\350\260\203\345\272\246.md" new file mode 100644 index 00000000..88d08eee --- /dev/null +++ "b/Java/classify/sys/\347\263\273\347\273\237\350\277\233\347\250\213\350\260\203\345\272\246.md" @@ -0,0 +1,8 @@ + + +- FCFS(先来先服务,队列实现,非抢占的):先请求CPU的进程先分配到CPU +- SJF(最短作业优先调度算法):平均等待时间最短,但难以知道下一个CPU区间长度 +- 优先级调度算法(可以是抢占的,也可以是非抢占的):优先级越高越先分配到CPU,相同优先级先到先服务,存在的主要问题是:低优先级进程无穷等待CPU,会导致无穷阻塞或饥饿;解决方案:老化 +- 时间片轮转调度算法(可抢占的):队列中没有进程被分配超过一个时间片的CPU时间,除非它是唯一可运行的进程。如果进程的CPU区间超过了一个时间片,那么该进程就被抢占并放回就绪队列。 +- 多级队列调度算法:将就绪队列分成多个独立的队列,每个队列都有自己的调度算法,队列之间采用固定优先级抢占调度。其中,一个进程根据自身属性被永久地分配到一个队列中。 +- 多级反馈队列调度算法:与多级队列调度算法相比,其允许进程在队列之间移动:若进程使用过多CPU时间,那么它会被转移到更低的优先级队列;在较低优先级队列等待时间过长的进程会被转移到更高优先级队列,以防止饥饿发生。 \ No newline at end of file diff --git "a/Java/classify/sys/\350\231\232\346\213\237\345\206\205\345\255\230.md" "b/Java/classify/sys/\350\231\232\346\213\237\345\206\205\345\255\230.md" new file mode 100644 index 00000000..042f78da --- /dev/null +++ "b/Java/classify/sys/\350\231\232\346\213\237\345\206\205\345\255\230.md" @@ -0,0 +1,13 @@ + +> 为什么操作系统引进虚拟内存?说白了,直接让一个进程全部放在主内存当中,太占用内存啦,以前的计算机的内存可没有那么大,并且程序也很多。所以在进程和主内存之间存在虚拟内存。可以[参考](https://draveness.me/whys-the-design-os-virtual-memory/) + + +虚拟内存允许执行进程不必完全在内存中。虚拟内存的基本思想是: +- 每个进程拥有独立的地址空间,这个空间被分为大小相等的多个块,称为页(Page),每个页都是一段连续的地址。 +- 这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。 +- 当程序引用到一部分在物理内存中的地址空间时,由硬件立刻进行必要的映射;当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的命令。这样,对于进程而言,逻辑上似乎有很大的内存空间,实际上其中一部分对应物理内存上的一块(称为帧,通常页和帧大小相等),还有一些没加载在内存中的对应在硬盘上。 + +举个小例子: +吃饭。 + +比如你和同学去聚餐,根据菜谱点了一堆的菜,有汤,有肉,有素材等。 大可不必一下子全做完一起送过来(如果桌子不给力,比较小,并且时间成本,饭菜都凉了等),比如,同学想吃肉哈?不想先喝汤,那就把肉类端上来,对吧?等同学又想喝汤了,就把汤做一下端过来。 diff --git "a/Java/classify/sys/\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/Java/classify/sys/\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" new file mode 100644 index 00000000..cb636b53 --- /dev/null +++ "b/Java/classify/sys/\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -0,0 +1,54 @@ +# 线程与进程的区别 +面试官:说一下线程与进程的区别 + +我:好的,如下: + +- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位 + +- 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。 + +- 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的 + +- 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。 + +- 与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程 + +举例子:比如,main函数启动了JVM进程,同时main就是其中的线程,并且启动了JVM进程,那么还有垃圾回收等线程。 + +或者这样的例子:做个简单的比喻:进程=火车,线程=车厢 + +- 线程在进程下行进(单纯的车厢无法运行) +- 一个进程可以包含多个线程(一辆火车可以有多个车厢) +- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘) +- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易) +- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源) +- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢) +- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上) +- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁" +- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量” + + + +--- + +## 协程 + +协程: + +协程是一种用户态的轻量级线程, 我们的server的发展如下:IO密集型应用:多进程 -> 多线程 ->事件驱动 ->协程 + +协程拥有自己的寄存器上下文和栈. 协程调度切换时,将寄存器上下文和栈保存到其他地方,然后去做其他工作,当你的IO解除之后切回原来的状态,恢复先前保存的寄存器上下文和栈。 + + +优点: + +- 跨平台 +- 无需线程上下文切换的开销 +- 无需原子操作锁定及同步的开销 +- 方便切换控制流,简化编程模型 +- 高并发+高扩展行+低成本: 一个CPU支持上万的协程都不是问题,所以很适合用于高并发处理 + +缺点: + +- 无法利用多核资源:协程的本质是一个单线程,它不能同时将单个CPU的多个核作用上,协程需要和进程配合才能运行在多CPU上. +- 进行阻塞(Blocking)操作会阻塞到整个程序; 这一点和事件驱动一样,可以使用异步IO操作来解决. \ No newline at end of file diff --git "a/Java/classify/sys/\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/Java/classify/sys/\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" new file mode 100644 index 00000000..9ea43cef --- /dev/null +++ "b/Java/classify/sys/\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -0,0 +1,57 @@ + +# 进程间通信 + +> 首先要知道进程之间为什么要通信。 进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源(例如打开的文件描述符)。 但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程之间的通信。 + +## 进程通信的目的 + +- 数据传输:一个进程需要将它的数据发送给另一个进程。 +- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。 +- 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。 +- 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。 + + +## 通信方式 + +### 管道 + +- 普通管道:通常有两种限制,一是单工,只能单向传输;二是只能在父子或者兄弟进程间使用. +- 流管道:去除了第一种限制,为半双工,只能在父子或兄弟进程间使用,可以双向传输. +- 命名管道:去除了第二种限制,可以在许多并不相关的进程之间进行通讯. + +### 信号量 + +- 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。 +- 它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。 +- 因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 + +### 消息队列 + +消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 + +### 信号 + +- 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 + +### 共享内存 + +共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。 + +#### 共享内存的实现(mmap) +mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。 + +#### 系统调用mmap共享内存的两种方式 +- 使用普通文件提供的内存映射:适用于任何进程之间; +- 使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间; 由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。 + +### socket + +这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。 + +## 线程之间的通信 + +1. 锁机制:包括互斥锁、条件变量、读写锁 +2. 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量 +3. 信号机制(Signal):类似进程间的信号处理 + +> 线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。 \ No newline at end of file diff --git "a/Java/classify/sys/\351\241\265\351\235\242\347\275\256\346\215\242\347\256\227\346\263\225.md" "b/Java/classify/sys/\351\241\265\351\235\242\347\275\256\346\215\242\347\256\227\346\263\225.md" new file mode 100644 index 00000000..d5650418 --- /dev/null +++ "b/Java/classify/sys/\351\241\265\351\235\242\347\275\256\346\215\242\347\256\227\346\263\225.md" @@ -0,0 +1,7 @@ +> 地址映射过程中,若在页面中发现所要访问的页面不再内存中,则产生缺页中断。当发生缺页中断时操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。而用来选择淘汰哪一页的规则叫做页面置换算法。常见的置换算法有: + +- 最佳置换算法(OPT):所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。 +- 先进先出置换算法(FIFO):选择换出的页面是最先进入的页面。该算法会将那些经常被访问的页面换出,导致缺页率升高。 +- 第二次机会算法:当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索。 +- 最近最久未使用(LRU)算法:为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。 + diff --git a/Java/classify/thread/AQS.md b/Java/classify/thread/AQS.md new file mode 100644 index 00000000..497f219d --- /dev/null +++ b/Java/classify/thread/AQS.md @@ -0,0 +1,69 @@ +# AQS + +> 这个先留着,这个内容有点多,待我这周结束之后来写。 + +面试官:AQS是什么? + +## 原理 +我:AbstractQueuedSynchronizer抽象同步队列。说白了,就是个FIFO双向队列。其内部通过节点head和tail记录对首和队尾元素。 + +**state**:在AQS中维持了一个单一的状态信息state,可以通过**getState**、**setState**、**compareAndSetState**函数修改其值。 + +- **ReentrantLock**:state可以用来表示当前线程获取锁的可重入次数。 + +- **ReentrantReadWriteLock**:state的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数。 +- **semaphore**:state用来表示当前可用信号的个数。 +- **CountDownlatch**:state用来表示计数器当前的值。 + +对于AQS来讲,线程同步的关键是对状态值**state**进行操作。 + +## 方法 + +在独占方式下获取和释放资源使用的方法: + +```java +void acquire(int arg); +void acquireInterruptibly(int arg); +boolean release(int arg); +``` + +在共享方式获取和释放资源使用的方法: + +```java +void acquireShared(int arg); +void acquireSharedInterruptibly(int arg); +boolean releaseShared(int arg); +``` + +使用独占方式获取的资源是与**具体线程绑定**的,就是说如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时会**发现当前该资源不是自己持有的**,就会在**获取失败后被阻塞**。(比如ReentrantLock) + +对应的共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的资源,则当前线程只需要使用CAS方式进行获取即可。(比如semaphore) + +看一下**acquire**方法: + +```java +// tryAcquire 具体的子类去实现,并维护state的状态 +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 如果失败标记状态,入队 + selfInterrupt(); +} +``` + +看一下**release**方法: + +```java +// tryRelease 具体的子类是实现,并设置state的状态 +public final boolean release(int arg) { + if (tryRelease(arg)) { + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); // 调用unpark唤醒队列的线程,并调用tryAcquire尝试,看是否需要,如果不需要,继续挂起 + return true; + } + return false; +} +``` + +**acquireShared和releaseShared**和上面的方法的思想差不多 + diff --git "a/Interview/codes/collection/BlockingQueue\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" "b/Java/classify/thread/BlockingQueue\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" similarity index 100% rename from "Interview/codes/collection/BlockingQueue\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" rename to "Java/classify/thread/BlockingQueue\347\232\204\344\270\200\344\272\233\351\227\256\351\242\230.md" diff --git a/Java/classify/thread/CAS.md b/Java/classify/thread/CAS.md new file mode 100644 index 00000000..9b10034f --- /dev/null +++ b/Java/classify/thread/CAS.md @@ -0,0 +1,98 @@ +# CAS + +面试官:了解CAS嘛? + +我:了解,**我们在读Concurrent包下的类的源码时,发现无论是**ReenterLock内部的AQS,还是各种Atomic开头的原子类,内部都应用到了`CAS`,并且在调用getAndAddInt方法中,会有compareAndSwapInt方法。其实都是调用unsafe.compareAndSwap()方法。 + +## compareAndSwapInt + +细说compareAndSwapInt方法,那岂不是要看源码: + +```cpp +UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) + UnsafeWrapper("Unsafe_CompareAndSwapInt"); + oop p = JNIHandles::resolve(obj); // oop 安全点 引用 + jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); // 计算偏移地址 + return (jint)(Atomic::cmpxchg(x, addr, e)) == e; // 原子操作,比较和更新 +UNSAFE_END +``` + +Java中的`compareAndSwapInt(obj, offset, expect, update)`比较清楚,**意思就是如果`obj`内的`value`和`expect`相等,就证明没有其他线程改变过这个变量,那么就更新它为`update`,如果这一步的`CAS`没有成功,那就采用自旋的方式继续进行`CAS`操作**,取出乍一看这也是两个步骤了啊,其实在`JNI`里是借助于一个`CPU`指令完成的。所以还是原子操作。 + +原子操作:可理解为要么这些行为都执行,不被打扰,要不都不执行。实在不行,就理解为串行操作, + +再细说这个cpu指令,那就要看该源码了: + +```hpp +inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { + int mp = os::is_MP(); // 返回处理器(判断是多核还是单核,单核省略lock前缀) + __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" // 比较 + : "=a" (exchange_value) + : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp) + : "cc", "memory"); + return exchange_value; +} +``` + + + +调用了`Atomic::cmpxchg(x, addr, e)`, 其中参数**x是即将更新的值**,参数**e是原内存的值**。代码中能看到cmpxchg有基于各个平台的实现。 + +## ABA + +面试官:知道ABA问题嘛? + +我:知道,描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程**也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A** (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时**变量 x 的值还是 A**,所以 compareAndSet 操作是成功。 + +解决方法:**目前在JDK的atomic包里提供了一个类`AtomicStampedReference`来解决ABA问题。**说白了,就是版本号 + +举一个例子: + +```java +public class ABADemo { + static AtomicInteger atomicInteger = new AtomicInteger(100); + static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 1); + + public static void main(String[] args) { + System.out.println("=====ABA的问题产生====="); + new Thread(() -> { + atomicInteger.compareAndSet(100, 101); + atomicInteger.compareAndSet(101, 100); + }, "t1").start(); + + new Thread(() -> { + // 保证线程1完成一次ABA问题 + try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } + System.out.println(atomicInteger.compareAndSet(100, 2020) + " " + atomicInteger.get()); + }, "t2").start(); + try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } + + System.out.println("=====解决ABA的问题====="); + new Thread(() -> { + int stamp = atomicStampedReference.getStamp(); // 第一次获取版本号 + System.out.println(Thread.currentThread().getName() + " 第1次版本号" + stamp); + try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } + atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); + System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp()); + atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); + System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp()); + }, "t3").start(); + + new Thread(() -> { + int stamp = atomicStampedReference.getStamp(); + System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp); + try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } + boolean result = atomicStampedReference.compareAndSet(100, 2020, stamp, stamp + 1); + System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp()); + System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference()); + }, "t4").start(); + + } +} +``` + +## CAS设计思想 + +面试官:了解CAS的设计思想吗? + +我:思想?我理解的是乐观思想,你听我娓娓道来,何为乐观,乐观就是在我角度上认为"哈士奇"不会去偷吃我的菜,那么我就不用同步加个房间锁住门防止"哈士奇"吃。那设计的时候,cpu咋知道"哈士奇"吃过没,那就看这道菜是不是少了点,或者量少了一半等。那么主人回来吃的时候肯定不会开吃了,是吧?所以主人吃就跟CAS的更新值一样,这道菜没被"哈士奇"吃过是CAS的期望值。 那么cpu的这个cmpxchg就是干这个事的。 \ No newline at end of file diff --git a/Java/classify/thread/CountDownLatch.md b/Java/classify/thread/CountDownLatch.md new file mode 100644 index 00000000..2f49a51a --- /dev/null +++ b/Java/classify/thread/CountDownLatch.md @@ -0,0 +1,53 @@ +# CountDownLatch + +> 我们经常在主线程中开启多个线程去并行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。知道你们要说join,但是join不够灵活。 + +面试官:讲讲CountDownLatch原理 + +我:我试试 + +## 原理 + +首先状态变量state:**state用来表示计数器当前的值**,当线程调用CountDownLatch对象的await方法后, 当前线程会被阻塞,直到下面的情况之一发生才返回:当所有线程都调用了CountDownLatch对象的countDown方法后,也就是**计数器的值为0**时:其他线程调用了**当前线程的interrupt()方法中断了当前线程**,当前线程就会抛出InterruptedException异常。 + +## 方法 + +所以,我们看一下await方法: + +```java +public final void acquireSharedInterruptibly(int arg) + throws InterruptedException { + if (Thread.interrupted()) // 线程可中断 + throw new InterruptedException(); + if (tryAcquireShared(arg) < 0) // 如果等于-1, 说明还在挂起 + doAcquireSharedInterruptibly(arg); +} +protected int tryAcquireShared(int acquires) { + return (getState() == 0) ? 1 : -1; // 如果为0,则返回1,不为0,则返回-1 +} +``` + +看一下countDown方法: + +```java +public final boolean releaseShared(int arg) { + if (tryReleaseShared(arg)) { + doReleaseShared(); // 激活被阻塞的线程 + return true; + } + return false; +} +protected boolean tryReleaseShared(int releases) { // 尝试释放锁 + // Decrement count; signal when transition to zero + for (;;) { + int c = getState(); + if (c == 0) + return false; // 如果等于0, 返回false,不用释放 + int nextc = c-1; + if (compareAndSetState(c, nextc)) // 更新state + return nextc == 0; // nextc如果等于0了,说明资源释放成功,但是不管成功与否,都会退出循环 + // 并且去激活被await阻塞的线程 + } +} +``` + diff --git "a/Java/classify/thread/JMM\345\206\205\345\255\230\346\250\241\345\236\213.md" "b/Java/classify/thread/JMM\345\206\205\345\255\230\346\250\241\345\236\213.md" new file mode 100755 index 00000000..eeda039d --- /dev/null +++ "b/Java/classify/thread/JMM\345\206\205\345\255\230\346\250\241\345\236\213.md" @@ -0,0 +1,3 @@ +![volatile保证内存可见性和避免重排-WDKMZd](https://gitee.com/dreamcater/blog-img/raw/master/uPic/volatile保证内存可见性和避免重排-WDKMZd.png) + + diff --git "a/Multithread/Java\351\224\201\346\234\272\345\210\266.md" "b/Java/classify/thread/Java\351\224\201\347\232\204\344\273\213\347\273\215.md" similarity index 79% rename from "Multithread/Java\351\224\201\346\234\272\345\210\266.md" rename to "Java/classify/thread/Java\351\224\201\347\232\204\344\273\213\347\273\215.md" index e56a3a5f..e8c3f0a3 100644 --- "a/Multithread/Java\351\224\201\346\234\272\345\210\266.md" +++ "b/Java/classify/thread/Java\351\224\201\347\232\204\344\273\213\347\273\215.md" @@ -1,26 +1,9 @@ -## 引言 - -> Java常见的几种锁,比如: -> -> - 公平锁/非公平锁 -> - 可重入锁 -> - 独享锁/共享锁 -> - 互斥锁/读写锁 -> - 乐观锁/悲观锁 -> - 分段锁 -> - 偏向锁/轻量级锁/重量级锁 -> - 自旋锁 - - - +# 各种锁 ## 公平锁/非公平锁 - **公平锁指多个线程按照申请锁的顺序来获取锁。非公平锁指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象(很长时间都没获取到锁-非洲人...),ReentrantLock,了解一下。** ## 可重入锁 - **可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,典型的synchronized,了解一下** - ```java synchronized void setA() throws Exception { Thread.sleep(1000); @@ -32,12 +15,10 @@ synchronized void setB() throws Exception { ``` ## 独享锁/共享锁 - - 独享锁:是指该锁一次只能被一个线程所持有。 - 共享锁:是该锁可被多个线程所持有。 ## 互斥锁/读写锁 - **上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是其具体的实现** ## 乐观锁/悲观锁 @@ -51,11 +32,8 @@ synchronized void setB() throws Exception { ## 分段锁 1. **分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来哦实现高效的并发操作。** - -2. ** 以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是ReentrantLock(Segment继承了ReentrantLock)** - +2. **以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是ReentrantLock(Segment继承了ReentrantLock)** 3. **当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。** - 4. **分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。** ## 偏向锁/轻量级锁/重量级锁 @@ -72,13 +50,49 @@ synchronized void setB() throws Exception { 3. **自旋锁尽可能的减少线程的阻塞,适用于锁的竞争不激烈,且占用锁时间非常短的代码来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗。** 4. **但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适用使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cpu的线程又不能获取到cpu,造成cpu的浪费。** +### 手写自旋锁的例子 +```java +public class SpinLock { + + // 原子引用线程 + AtomicReference atomicReference = new AtomicReference<>(); + + public void mylock() { + Thread thread = Thread.currentThread(); + System.out.println(Thread.currentThread().getName() + " como in..."); + while (!atomicReference.compareAndSet(null, thread)) { +// System.out.println("不爽,重新获取一次值瞧瞧..."); + } + } + + public void myUnlock() { + Thread thread = Thread.currentThread(); + atomicReference.compareAndSet(thread, null); + System.out.println(Thread.currentThread().getName() + " invoke myUnLock..."); + } + + public static void main(String[] args) { + SpinLock spinLock = new SpinLock(); + new Thread(() -> { + spinLock.mylock(); + try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } + spinLock.myUnlock(); + }, "t1").start(); + try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } + + new Thread(() -> { + spinLock.mylock(); + spinLock.myUnlock(); + }, "t2").start(); + } + +} +``` + ## Java锁总结 **Java锁机制可归为Sychornized锁和Lock锁两类。Synchronized是基于JVM来保证数据同步的,而Lock则是硬件层面,依赖特殊的CPU指令来实现数据同步的。** - Synchronized是一个非公平、悲观、独享、互斥、可重入的重量级锁。 - ReentrantLock是一个默认非公平但可实现公平的、悲观、独享、互斥、可重入、重量级锁。 -- ReentrantReadWriteLock是一个默认非公平但可实现公平的、悲观、写独享、读共享、读写、可重入、重量级锁。 - - - +- ReentrantReadWriteLock是一个默认非公平但可实现公平的、悲观、写独享、读共享、读写、可重入、重量级锁。 \ No newline at end of file diff --git a/Java/classify/thread/ReentrantLock.md b/Java/classify/thread/ReentrantLock.md new file mode 100644 index 00000000..31191b6e --- /dev/null +++ b/Java/classify/thread/ReentrantLock.md @@ -0,0 +1,113 @@ +# ReentrantLock + +> 经常拿synchronized和ReentrantLock做比较,那就来看看这个锁的一些重点吧 + +面试官:了解ReentrantLock嘛? + +## 原理 + +我:当然,它实现了Lock接口,同时调用内部类sync继承的AQS,先说一下**state**:它代表获取该锁的**可重入次数**,在默认下,state的值为0表示**当前锁没有被任何线程持有**。当一个线程第一次获取该锁时会尝试使用CAS设置state的值为1,并且记录该锁的持有者为当前线程。若该线程第二次获取该锁后,状态值被设置2。 + + +## 方法 + +我们来看一下lock方法: + +```java +public void lock() { + sync.lock(); // 委托给sync了 +} +``` + +而且它具有非公平锁还是公平锁的特性。比如,我们可以看构造方法 + +```java +// 这不,由fair来决定是公平的还是非公平的 +public ReentrantLock(boolean fair) { + sync = fair ? new FairSync() : new NonfairSync(); +} +``` + +- 非公平锁: + +```java +final void lock() { + if (compareAndSetState(0, 1)) // cas设置state的状态,0->1 + setExclusiveOwnerThread(Thread.currentThread()); // 设置独占 + else + acquire(1); // 否则尝试,如果还是当前线程,就state累加,若不是,则挂起 +} +// 看看 acquire +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +// 看看重写的tryAcquire +protected final boolean tryAcquire(int acquires) { + return nonfairTryAcquire(acquires); +} +// 重点来了 +final boolean nonfairTryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState(); + if (c == 0) { // 0->acquires + if (compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } + } + else if (current == getExclusiveOwnerThread()) { // 否则判断该资源是否被该线程持有 + int nextc = c + acquires; // 持有,则+acquires + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; + } + return false; +} +``` + +- 公平锁和上面的差不多,就多了一个这样的判断: + +```java +if (c == 0) { + if (!hasQueuedPredecessors() && // 队列中是否轮到该线程了 + compareAndSetState(0, acquires)) { + setExclusiveOwnerThread(current); + return true; + } +} +``` + +我们来看一下unlock()方法: + +```java +public void unlock() { + sync.release(1); +} +// release看看 +public final boolean release(int arg) { + if (tryRelease(arg)) { // 依然调 + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} +// 看看sync重写的 +protected final boolean tryRelease(int releases) { + int c = getState() - releases;// 并不会设置为0,而是减releases + if (Thread.currentThread() != getExclusiveOwnerThread()) + throw new IllegalMonitorStateException(); + boolean free = false; + if (c == 0) { // 如果状态为0了,则free为true + free = true; + setExclusiveOwnerThread(null); // 并且将持有该资源的线程设置为null + } + setState(c); // cas操作 + return free; +} +``` + diff --git a/Java/classify/thread/ReentrantReadWriteLock.md b/Java/classify/thread/ReentrantReadWriteLock.md new file mode 100644 index 00000000..4ee834a9 --- /dev/null +++ b/Java/classify/thread/ReentrantReadWriteLock.md @@ -0,0 +1,190 @@ +# ReentrantReadWriteLock + +> 聊一聊读写锁 + +面试官:了解AQS的读写锁嘛,知道其原理嘛? + +我:刚好了解,我们知道,在一些读多写少的场景中,若是用ReentrantLock效率显然不高,于是ReentrantReadWriteLock问世。 + +## 原理 + +老规矩: + +```java +// 维护了readlock和writelock +private final ReentrantReadWriteLock.ReadLock readerLock; +/** Inner class providing writelock */ +private final ReentrantReadWriteLock.WriteLock writerLock; +/** Performs all synchronization mechanics */ +final Sync sync; // 同样的是sync 继承aqs + +// 可惜的是state依然是一个, 但是不慌 +// 高16位表示读状态,低16位表示获取到写锁的线程的可重入锁 +static final int SHARED_SHIFT = 16; +// 共享锁的读锁的状态单位值65536 +static final int SHARED_UNIT = (1 << SHARED_SHIFT); +// 共享锁线程最大个数65536 +static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; +// 排它锁写锁掩码,二进制 15个1 +static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; +// 返回读锁线程数 +static int sharedCount(int c) { return c >>> SHARED_SHIFT; } +// 返回写锁可重入个数 +static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } +``` + +## 方法 + +直奔主题:写锁的获取与释放 + +**lock:** + +```java +// lock +public void lock() { + sync.acquire(1); +} +// acquire +public final void acquire(int arg) { + if (!tryAcquire(arg) && // 老规矩了 + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +// tryAcquire +protected final boolean tryAcquire(int acquires) { + Thread current = Thread.currentThread(); + int c = getState(); + int w = exclusiveCount(c); // 得到低16位的值 + // 如果c!=0, 说明资源已经被其他读锁或者写锁的线程所获取 + if (c != 0) { + // w==0 代表低16位位0,那么高16位不为0,那么获取了读锁 + if (w == 0 || current != getExclusiveOwnerThread()) + return false; + // 到这一步,已经是写锁了,那么判断可重入次数 + if (w + exclusiveCount(acquires) > MAX_COUNT) + throw new Error("Maximum lock count exceeded"); + // Reentrant acquire + // 更新state + setState(c + acquires); + return true; + } + // 是否第一个写锁获取线程 + if (writerShouldBlock() || + !compareAndSetState(c, c + acquires)) + return false; + setExclusiveOwnerThread(current); + return true; +} +``` + +**unlock:** + +```java +// unlock +public void unlock() { + sync.release(1); +} +// 不提了 +public final boolean release(int arg) { + if (tryRelease(arg)) { + Node h = head; + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} +// 重点 +protected final boolean tryRelease(int releases) { + if (!isHeldExclusively()) + throw new IllegalMonitorStateException(); + int nextc = getState() - releases; // 依然是减 + boolean free = exclusiveCount(nextc) == 0; // 判断是否为0 + if (free) // 移除该写锁持有的线程 + setExclusiveOwnerThread(null); + setState(nextc); // 更新 + return free; +} +``` + +读锁的获取和释放: + +**lock:** + +```java +protected final int tryAcquireShared(int unused) { + Thread current = Thread.currentThread(); + int c = getState(); + // 判断是否被写锁占用 + if (exclusiveCount(c) != 0 && + getExclusiveOwnerThread() != current) + return -1; + // 获取读锁的数量 + int r = sharedCount(c); + // 尝试获取锁,多个读线程只有一个会成功,不成功的进入fullTryAcqureShared进行重试 + if (!readerShouldBlock() && + r < MAX_COUNT && + compareAndSetState(c, c + SHARED_UNIT)) { + // 第一个线程获取读锁 + if (r == 0) { + firstReader = current; + firstReaderHoldCount = 1; + // 如果当前线程是第一个获取读锁的线程 + } else if (firstReader == current) { + firstReaderHoldCount++; + } else { + // 记录最后一个获取读锁的线程的线程或记录其他线程读锁的可重入数 + HoldCounter rh = cachedHoldCounter; + if (rh == null || rh.tid != getThreadId(current)) + cachedHoldCounter = rh = readHolds.get(); + else if (rh.count == 0) + readHolds.set(rh); + rh.count++; + } + return 1; + } + // 自旋获取 + return fullTryAcquireShared(current); +} +``` + +**unlock:** + +```java +public final boolean releaseShared(int arg) { + if (tryReleaseShared(arg)) { + doReleaseShared(); // 释放所有 + return true; + } + return false; +} +protected final boolean tryReleaseShared(int unused) { + Thread current = Thread.currentThread(); + if (firstReader == current) { + // assert firstReaderHoldCount > 0; + if (firstReaderHoldCount == 1) + firstReader = null; + else + firstReaderHoldCount--; + } else { + HoldCounter rh = cachedHoldCounter; + if (rh == null || rh.tid != getThreadId(current)) + rh = readHolds.get(); + int count = rh.count; + if (count <= 1) { + readHolds.remove(); + if (count <= 0) + throw unmatchedUnlockException(); + } + --rh.count; + } + // 循环直到自己的读计数-1.CAS更新成功 + for (;;) { + int c = getState(); + int nextc = c - SHARED_UNIT; + if (compareAndSetState(c, nextc)) + return nextc == 0; + } +} +``` + diff --git a/Java/classify/thread/ThreadLocal.md b/Java/classify/thread/ThreadLocal.md new file mode 100644 index 00000000..a088c863 --- /dev/null +++ b/Java/classify/thread/ThreadLocal.md @@ -0,0 +1,75 @@ +# ThreadLocal + +面试官:了解ThreadLocal嘛?用过ThreadLocal嘛? + +我:了解过:它是这样的,假如想实现每一个线程都有自己的专属本地变量该如何解决呢? JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。 + +## 底层原理 + +面试官:你知道底层原理嘛? + +我:知道 + +首先看:结构 + +```java +public class Thread implements Runnable { + ...... +//与此线程有关的ThreadLocal值。由ThreadLocal类维护 +ThreadLocal.ThreadLocalMap threadLocals = null; + ...... +} +// 由源码可见,ThreadLocal存储的变量存在Thread的ThreadLocalMap中 +// 可理解为ThreadLocalMap是专门定制的一种Map +``` + +其次看set和get方法 + +```java +// set +public void set(T value) { + // 获取当前线程 + Thread t = Thread.currentThread(); + // 拿到定制的ThreadLocalMap的map + ThreadLocalMap map = getMap(t); + if (map != null) + //不为空,则绑定 + map.set(this, value); + else + // 否则创建 + createMap(t, value); +} +// getMap 返回的就是threadLocals +ThreadLocalMap getMap(Thread t) { + return t.threadLocals; +} + +// get +public T get() { + // 依然获取当前线程 + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) { + // 获取当前Entry + ThreadLocalMap.Entry e = map.getEntry(this); + if (e != null) { + @SuppressWarnings("unchecked") + // 如果不为空,则返回 + T result = (T)e.value; + return result; + } + } + return setInitialValue(); +} +``` + +**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。** + +**每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。`ThreadLocal` 是 map结构是为了让每个线程可以关联多个 `ThreadLocal`变量。这也就解释了ThreadLocal声明的变量为什么在每一个线程都有自己的专属本地变量。 + +## 内存泄漏 + +面试官:知道内存泄露嘛? + +我:知道,`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候会 key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法。 + diff --git a/Java/classify/thread/synchronized.md b/Java/classify/thread/synchronized.md new file mode 100644 index 00000000..91875ae6 --- /dev/null +++ b/Java/classify/thread/synchronized.md @@ -0,0 +1,81 @@ +# synchronized和lock的区别 + +> 很多面试官爱问synchronized和lock的区别 + +面试官:区别?哈哈哈 + +我:面有表情,好的。 + +- **两者都是可重入锁**:两者都是可重入锁。“可重入锁”概念是:**自己可以再次获取自己的内部锁**。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 +- **synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API**:synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 +- **ReenTrantLock 比 synchronized 增加了一些高级功能** + 1. **等待可中断**:过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 + 2. **可实现公平锁** + 3. **可实现选择性通知(锁可以绑定多个条件)**:线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” + 4. **性能已不是选择标准**:在jdk1.6之前synchronized 关键字吞吐量随线程数的增加,下降得非常严重。1.6之后,**synchronized 和 ReenTrantLock 的性能基本是持平了。** + +## synchronized修饰范围 + +面试官:先扯synchronized吧,修饰范围?可否了解? + +我:当然了解。 + +- 实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 +- 静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 +- 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁 + +这里可能让谈对象头里面都有什么哦,简单的说:**Hotspot 虚拟机的对象头包括两部分信息**,**第一部分用于存储对象自身的自身运行时数据**(哈希码、GC 分代年龄、锁状态标志等等),**另一部分是类型指针**,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 + +## 底层原理 + +面试官:聊聊底层原理 + +我:好的,代码块和方法有些区别。 + +- 代码块 + +**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** + +- 方法 + +**synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。** + +面试官:1.6对synchronized的一些优化都有哪些? + +我:JDK1.6对锁的实现引入了大量的优化,如**自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等**技术来减少锁操作的开销。 + +面试官:简单的说一下偏向锁、轻量级锁等。 + +- 偏向锁 + +引入偏向锁的目的和引入轻量级锁的目的很像,**他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗**。但是不同是:轻量级锁在无竞争的情况下**使用 CAS 操作去代替使用互斥量**。而偏向锁在**无竞争的情况下会把整个同步都消除掉**。 + +- 轻量级锁 + +轻量级锁不是为了代替重量级锁,**它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量**。另外,轻量级锁的加锁和解锁都用到了**CAS**操作。 + +- 锁消除 + +锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,**如果检测到那些共享数据不可能存在竞争,那么就执行锁消除**。锁消除可以节省毫无意义的请求锁的时间。 + +- 自旋锁和自适应锁 + +**一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。 + +**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定**。 + +面试官:简要说一下偏向锁->轻量级锁->重量级锁的升级过程 + +我:好的 + +- 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁 +- 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1 +- 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。 +- 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁 +- 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 +- 如果自旋成功则依然处于轻量级状态。 +- 如果自旋失败,则升级为重量级锁。 + + + +> 个人觉得够了...能一口气扯完这些,足以了。AQS就够受了。 \ No newline at end of file diff --git a/Java/classify/thread/volatile.md b/Java/classify/thread/volatile.md new file mode 100644 index 00000000..709ecf5b --- /dev/null +++ b/Java/classify/thread/volatile.md @@ -0,0 +1,80 @@ +# volatile的特性 + +面试官:了解volatile嘛?有啥子特性 + +我:了解,两个特性:**内存可见性、禁止重排序** + +禁止重排序:**不管是编译器还是JVM还是CPU,都会对一些指令进行重排序,目的是为了提高程序运行的速度,提高程序的性能,毫无疑问,在单线程下没毛病,多线程就似乎生病了。** + +给你稍微举举例子:禁止重排->单例模式 + +```java +// 来一波双重校验 +public class Test { + private volatile static Test instance = null; + private Test(){} + + private static Test getInstance() { + if (instance == null) { + synchronized (Test.class) { + if (instance == null) { + instance = new Test(); + } + } + } + return instance; + } +} +// instance类变量前面修饰的volatile?是吧? +``` + +问题在于:上面的代码是一个很常见的单例模式实现方式,但是上述代码在多线程环境下是有问题的。为什么呢,问题出在instance对象的初始化上,因为`instance = new Singleton();`这个初始化操作并不是原子的,在JVM上会对应下面的几条指令: + +```c +memory =allocate(); //1. 分配对象的内存空间 +ctorInstance(memory); //2. 初始化对象 +instance = memory; //3. 设置instance指向刚分配的内存地址 +``` + +上面三个指令中,步骤2依赖步骤1,但是步骤3不依赖步骤2,所以JVM可能针对他们进行指令重拍序优化,重排后的指令如下: + +```c +memory =allocate(); //1. 分配对象的内存空间 +instance = memory; //3. 设置instance指向刚分配的内存地址 +ctorInstance(memory); //2. 初始化对象 +``` + +这样优化之后,内存的初始化被放到了instance分配内存地址的后面,这样的话当线程1执行步骤3这段赋值指令后,刚好有另外一个线程2进入getInstance方法判断instance不为null,这个时候线程2拿到的instance对应的内存其实还未初始化,这个时候拿去使用就会导致出错。 + +这里多说多说一点:为什么在synchronized上面多加了一次判断 + +**还不是因为synchronized比较笨重,锁了代码块嘛,多线程不能每次都要进来块中,岂不是都要发生阻塞等这class的锁呀,直接给他上面判断一下不为空就直接跳出去了。提高了性能哇。** + +其实这里也能体现出volatile的内存可见性,让其他线程对这个实例可见。 + +我们继续说volatile的内存可见性 + +扯一波JMM内存模型 + +![volatile保证内存可见性和避免重排-ouQfjW](https://gitee.com/dreamcater/blog-img/raw/master/uPic/volatile保证内存可见性和避免重排-ouQfjW.png) + +根据这个图如何回答总结JMM内存模型,看各位的造化了,理解讲出来即可。结合例子讲也可以 + +1. 先说结构 +2. 再说为什么是这样的结构,原因是什么? +3. 然后扯流程 +4. 撒花结束 + +## 底层结构 + +面试官:知道底层结构嘛? + +我:禁止重排是利用内存屏障来解决的,其实最根本的还是cpu的一个**lock**指令:**它的作用是使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。所以通过这样一个空操作,可让前面volatile变量的修改对其他CPU立即可见。** + +- 锁住内存 +- 任何读必须在写完成之后再执行 +- 使其它线程这个值的栈缓存失效 + +## 不能保证原子性 + +![volatile不能保证数据一致性](https://gitee.com/dreamcater/blog-img/raw/master/uPic/volatile不能保证数据一致性-iGuPEi.png) diff --git "a/Java/classify/thread/\346\255\273\351\224\201.md" "b/Java/classify/thread/\346\255\273\351\224\201.md" new file mode 100644 index 00000000..6452f806 --- /dev/null +++ "b/Java/classify/thread/\346\255\273\351\224\201.md" @@ -0,0 +1,69 @@ +# 死锁 + +面试官:知道死锁嘛? + +我:知道,那我就谈一下Java的死锁吧,其实原理都一样。 + +先看个例子: + +```java +public class Test { + private static Object res1 = new Object(); + private static Object res2 = new Object(); + + public static void main(String[] args) { + new Thread(() -> { + synchronized (res1) { + System.out.println(Thread.currentThread().getName() + " res1"); + // 延迟一下, 确保B拿到了res2 + try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } + synchronized (res2) { + System.out.println(Thread.currentThread().getName() + " res2"); + } + } + }, "ThreadA").start(); + + new Thread(() -> { + synchronized (res2) { + System.out.println(Thread.currentThread().getName() + " res2"); + // 延迟一下,确保A拿到了res1 + synchronized (res1) { + System.out.println(Thread.currentThread().getName() + " res1"); + } + } + }, "ThreadB").start(); + } +} +``` + +## 死锁的条件 + +所以:死锁的条件? + +- **互斥条件**:该资源任意一个时刻只由一个线程占用。(同一时刻,这个碗是我的,你不能碰) +- **请求与保持条件**:一个进程因请求资源而阻塞时,对已获得的资源保持不放。(我拿着这个碗一直不放) +- **不剥夺条件**:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。(我碗中的饭没吃完,你不能抢,释放权是我自己的,我想什么时候放就什么时候放) +- **循环等待条件**:若干进程之间形成一种头尾相接的循环等待资源关系。(我拿了A碗,你拿了B碗,但是我还想要你的B碗,你还想我的A碗)比喻成棒棒糖也阔以。 + +面试官:所以解决死锁的办法是? + +我:好的,没毛病 + +- 预防死锁: + - **资源一次性分配**:破坏请求和保持条件。 + - **可剥夺资源**:当进程新申请的资源不满足时,释放已经分配的资源。破坏不可剥夺条件 + - **资源有序分配**:系统给进程编号,按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 +- 避免死锁:银行家算法:分配资源前先评估风险,会不会在分配后导致死锁。 即分配给一个进程资源的时候,该进程能否全部返还占用的资源。 +- 检测死锁:建立资源分配表和进程等待表。 +- 解除死锁:可以直接撤销死锁进程,或撤销代价最小的进程。 + +## 找死锁的步骤 + +所以:找死锁的步骤: + +1. 我们通过jps确定当前执行任务的进程号 + +2. 然后执行jstack命令查看当前进程堆栈信息 + +3. 然后将会看到`Found a total of 1 deadlock` + diff --git "a/Java/classify/thread/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205.md" "b/Java/classify/thread/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205.md" new file mode 100644 index 00000000..5afc3457 --- /dev/null +++ "b/Java/classify/thread/\347\224\237\344\272\247\350\200\205\345\222\214\346\266\210\350\264\271\350\200\205.md" @@ -0,0 +1,161 @@ + +### synchronized + +```java +public class Test { + + private final LinkedList lists = new LinkedList<>(); + + public synchronized void put(String s) { + while (lists.size() != 0) { // 用while怕有存在虚拟唤醒线程 + // 满了, 不生产了 + try { + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + lists.add(s); + System.out.println(Thread.currentThread().getName() + " " + lists.peekFirst()); + this.notifyAll(); // 这里可是通知所有被挂起的线程,包括其他的生产者线程 + } + + public synchronized void get() { + while (lists.size() == 0) { + try { + this.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + System.out.println(Thread.currentThread().getName() + " " + lists.removeFirst()); + this.notifyAll(); // 通知所有被wait挂起的线程 用notify可能就死锁了。 + } + + public static void main(String[] args) { + Test test = new Test(); + + // 启动消费者线程 + for (int i = 0; i < 5; i++) { + new Thread(test::get, "ConsA" + i).start(); + } + + // 启动生产者线程 + for (int i = 0; i < 5; i++) { + int tempI = i; + new Thread(() -> { + test.put("" + tempI); + }, "ProdA" + i).start(); + } + } +} +``` + +这里讲一下为什么用while,不用if? + +假如,此时队列元素为空,那么消费者肯定都挂起来了哈。在挂起前通知了生产者线程去生产,那么,生产者产了一个之后唤醒消费者,所有消费者醒了以后,就一个消费者抢到锁,开始消费,当消费过后释放锁,其他消费者线程的某一个抢到锁之后,从唤醒处走代码,如果是if,往下走取元素发现队列空的,直接抛异常。如果是while的话,还会继续判断队列是否为空,空就挂起。不会抛异常。 + +### ReentrantLock + +```java +public class Test { + + private LinkedList lists = new LinkedList<>(); + private Lock lock = new ReentrantLock(); + private Condition prod = lock.newCondition(); + private Condition cons = lock.newCondition(); + + public void put(String s) { + lock.lock(); + try { + // 1. 判断 + while (lists.size() != 0) { + // 只要队列有元素,就不生产了,就停会儿 + prod.await(); + } + // 2.干活 + lists.add(s); + System.out.println(Thread.currentThread().getName() + " " + lists.peekFirst()); + // 3. 通知 + cons.signalAll(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public void get() { + lock.lock(); + try { + // 1. 判断 + while (lists.size() == 0) { + // 队列为空,消费者肯定等待呀 + cons.await(); + } + // 2.干活 + System.out.println(Thread.currentThread().getName() + " " + lists.removeFirst()); + // 3. 通知 + prod.signalAll(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + lock.unlock(); + } + } + + public static void main(String[] args) { + Test test = new Test(); + for (int i = 0; i < 5; i++) { + int tempI = i; + new Thread(() -> { + test.put(tempI + ""); + }, "ProdA" + i).start(); + } + for (int i = 0; i < 5; i++) { + new Thread(test::get, "ConsA" + i).start(); + } + } +} +``` + +### BlockingQueue + +```java +public class Test { + public static void main(String[] args) { + ArrayBlockingQueue queue = new ArrayBlockingQueue<>(10); + // 生产者 + Runnable product = () -> { + while (true) { + try { + String s = "生产者:" + Thread.currentThread().getName() + " "+ new Object(); + System.out.println(s); + queue.put(s); + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + new Thread(product, "p1").start(); + new Thread(product, "p2").start(); + // 消费者 + Runnable consume = () -> { + while (true) { + try { + Object o = queue.take(); + System.out.println("消费者:" + Thread.currentThread().getName() + " " + o); + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + new Thread(consume, "c1").start(); + new Thread(consume, "c2").start(); + } +} +``` + +利用 BlockingQueue 实现生产者消费者模式的代码。虽然代码非常简单,但实际上 ArrayBlockingQueue 已经在背后完成了很多工作,比如队列满了就去阻塞生产者线程,队列有空就去唤醒生产者线程等。 \ No newline at end of file diff --git "a/Java/classify/thread/\347\272\277\347\250\213\346\261\240.md" "b/Java/classify/thread/\347\272\277\347\250\213\346\261\240.md" new file mode 100644 index 00000000..508f0e80 --- /dev/null +++ "b/Java/classify/thread/\347\272\277\347\250\213\346\261\240.md" @@ -0,0 +1,213 @@ +# 线程池 + +> 面试必问高频 + +面试官:了解线程池嘛? + +我:了解 + +面试官:为啥子采用线程池?有啥子优点? + +我:我大概分为3点 + +- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 +- **提高响应速度**。当任务到达时,任务可以不需要要等到线程创建就能立即执行。 +- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +面试官:都了解哪些线程池? + +我:我暂时知道的一些的如:**newFixedThreadPool**(固定线程池)、**newSingleThreadExecutor**(单个线程的线程池)、**newCachedThreadPool**(缓存线程的线程池)、**newScheduledThreadPool**(带定时器的线程池),还有几个就不说了。 + +我就举点源码吧 + +**newFixedThreadPool**: + +```java +// core和max是一样的 +// blockQueue是无界阻塞队列 +// 嗯, 不好不好!!! +public static ExecutorService newFixedThreadPool(int nThreads) { + return new ThreadPoolExecutor(nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); +} +``` + +**newSingleThreadExecutor** + +```java +// core和max无非都是1而已 +// blockQueue是无界阻塞队列 +// 嗯, 不好不好!!! +public static ExecutorService newSingleThreadExecutor() { + return new FinalizableDelegatedExecutorService + (new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue())); +} +``` + +**newCachedThreadPool** + +```java +// core 0 +// max有点狠,不怕暴栈? +// 队列还是SynchronousQueue,还真怕 +public static ExecutorService newCachedThreadPool() { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue()); +} +``` + +## 线程池参数 + +面试官:讲一下线程池的参数? + +我:没问题,ThreadPoolExecutor源码走起: + +```java +public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) +``` + +- corePoolSize:核心线程数线程数定义了最小可以同时运行的线程数量 +- maximumPoolSize:当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数 +- keepAliveTime:当线程数大于核心线程数时,多余的空闲线程存活的最长时间 +- TimeUnit:时间单位 +- BlockingQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中 +- ThreadFactory:线程工厂,用来创建线程,一般默认即可 +- RejectedExecutionHandler:拒绝策略 + +![线程池参数关系](https://gitee.com/dreamcater/blog-img/raw/master/uPic/线程池参数关系-JgjlWU.png) + +面试官:讲一下都有哪些拒绝策略 + +我:还好我提前准备 + +- AbortPolicy:抛出 `RejectedExecutionException`来拒绝新任务的处理。 +- CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。(说白了,谁管理任务的,谁就负责帮忙) +- DiscardPolicy:不处理新任务,直接丢弃掉。 +- DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。 + +面试官:线程池的线程数量怎么确定 + +我:分情况,一般来说... + +1. 一般来说,如果是CPU密集型应用,则线程池大小设置为N+1。 +2. 一般来说,如果是IO密集型应用,则线程池大小设置为2N+1。 +3. 在IO优化中,线程等待时间所占比例越高,需要越多线程,线程CPU时间所占比例越高,需要越少线程。这样的估算公式可能更适合:最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目 + +## shutdown和shutdownNow + +面试官:shutdown和shutdownNow的区别 + +我:上源码: + +```java +// 等待所有线程执行任务完毕之后退出 +public void shutdown() { + final ReentrantLock mainLock = this.mainLock; + // 获取锁 + mainLock.lock(); + try { + // 检查 + checkShutdownAccess(); + // 设置状态 + advanceRunState(SHUTDOWN); + interruptIdleWorkers(); + onShutdown(); // hook for ScheduledThreadPoolExecutor + } finally { + mainLock.unlock(); + } + // 主要在于这里,根据状态来是否立马停止还是等线程执行完毕过后停止 + tryTerminate(); // 这里就不贴了 +} +// 和上面的差不多,立马中断所有线程,关闭线程池 +public List shutdownNow() { + List tasks; + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + checkShutdownAccess(); + // 设置状态 + advanceRunState(STOP); + interruptWorkers(); + tasks = drainQueue(); + } finally { + mainLock.unlock(); + } + tryTerminate(); + return tasks; +} +``` + +## execute和submit的区别 + + +面试官:execute和submit的区别 + +我:心累 + +```java +// execute +public void execute(Runnable command) { + if (command == null) + throw new NullPointerException(); + /* + * Proceed in 3 steps: + * + * 1. If fewer than corePoolSize threads are running, try to + * start a new thread with the given command as its first + * task. The call to addWorker atomically checks runState and + * workerCount, and so prevents false alarms that would add + * threads when it shouldn't, by returning false. + * + * 2. If a task can be successfully queued, then we still need + * to double-check whether we should have added a thread + * (because existing ones died since last checking) or that + * the pool shut down since entry into this method. So we + * recheck state and if necessary roll back the enqueuing if + * stopped, or start a new thread if there are none. + * + * 3. If we cannot queue task, then we try to add a new + * thread. If it fails, we know we are shut down or saturated + * and so reject the task. + */ + // 按还是翻译了一下: + // 1.如果正在运行的线程少于corePoolSize线程,请尝试使用给定命令作为其第一个任务来启动新线程。 + // 对addWorker的调用从原子上检查runState和workerCount,从而通过返回false来防止在不应该添加线程的情况下发出虚假警报。 + // 2.如果一个任务可以成功排队,那么我们仍然需要仔细检查是否应该添加一个线程(因为现有线程自上次检查后就死掉了)或该池自进入该方法后就关闭了。 + // 因此,我们重新检查状态,并在必要时回滚排队(如果已停止),或者在没有线程时启动一个新线程。 + // 3.如果我们无法将任务排队,则尝试添加一个新线程。 + // 如果失败,我们知道我们已关闭或已饱和,因此拒绝该任务。 + // 总结:说白了,就是上面的流程图 + int c = ctl.get(); + if (workerCountOf(c) < corePoolSize) { + if (addWorker(command, true)) + return; + c = ctl.get(); + } + if (isRunning(c) && workQueue.offer(command)) { + int recheck = ctl.get(); + if (! isRunning(recheck) && remove(command)) + reject(command); + else if (workerCountOf(recheck) == 0) + addWorker(null, false); + } + else if (!addWorker(command, false)) + reject(command); +} + +//不过传递的参数,Runnable,那么就意味着没有返回值 +// 简单看一下submit吧。 +// 不必多说了 + Future submit(Callable task); +Future submit(Runnable task); +``` + diff --git "a/Java/classify/thread/\347\272\277\347\250\213\347\232\204\345\210\233\345\273\272\346\226\271\345\274\217.md" "b/Java/classify/thread/\347\272\277\347\250\213\347\232\204\345\210\233\345\273\272\346\226\271\345\274\217.md" new file mode 100644 index 00000000..d57d40fc --- /dev/null +++ "b/Java/classify/thread/\347\272\277\347\250\213\347\232\204\345\210\233\345\273\272\346\226\271\345\274\217.md" @@ -0,0 +1,173 @@ + +# 线程的创建方式 +面试官:线程的创建方式有哪些? + +我:我目前知道4种:分别如下: + +- Thread + + +```java +public class Test extents Thread { + public void run() { + // 重写Thread的run方法 + System.out.println("dream"); + } + + public static void main(String[] args) { + new Test().start(); + } +} +``` + +- Runnable + +```java +public class Test { + public static void main(String[] args) { + new Thread(() -> { + System.out.println("dream"); + }).start(); + } +} +``` + +- Callable + +```java +public class Test { + public static void main(String[] args) { + // FutureTask 构造方法包装了Callable和Runnable。 + FutureTask task = new FutureTask<>(() -> { + System.out.println("dream"); + return 0; + }); + new Thread(task).start(); + } +} + +``` + +- 线程池 + +```java + +public class Test { + public static void main(String[] args) { + ExecutorService threadPool = Executors.newFixedThreadPool(1); + threadPool.submit(() -> { + System.out.println("dream"); + }); + threadPool.shutdown(); + } +} +``` + +面试官:Runnable和Callable有啥区别? + +我:那得先看源码咯 + +请看 + +```java +@FunctionalInterface +public interface Runnable { + /** + * 被线程执行,没有返回值也无法抛出异常 + */ + public abstract void run(); +} + +@FunctionalInterface +public interface Callable { + /** + * 计算结果,或在无法这样做时抛出异常。 + * @return 计算得出的结果 + * @throws 如果无法计算结果,则抛出异常 + */ + V call() throws Exception; +} +``` + +1. Runnable没有返回值并且无法抛出异常 +2. 不巧,我Callable可以做到你不能做到的 + +线程池的源码后边讲,讲东西,要结合源码和例子讲!!! + + + +> 补充一下主线程等待子线程的两种方式 + +# 主线程等待子线程的两种方式 + +## sleep +> 这个不常用,但是简单一些 +```java +public class Test { + void m() { + System.out.println(Thread.currentThread().getName()); + } + + public static void main(String[] args) { + Test t1 = new Test(); + for (int i = 0; i < 5; i++) { + new Thread(t1::m, "Thread " + i).start(); + } + try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } + System.out.println("main thread"); + } +} +``` + +## join + +```java +public class Test { + void m() { + System.out.println(Thread.currentThread().getName()); + } + + public static void main(String[] args) { + Test t1 = new Test(); + ArrayList threads = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + threads.add(new Thread(t1::m, "Thread " + i)); + } + threads.forEach(o -> o.start()); + threads.forEach(o -> { + try { + o.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + System.out.println("main thread"); + } +} +``` + +## CountDownLatch +```java +public class Test { + private CountDownLatch latch; + + public Test(CountDownLatch latch) { + this.latch = latch; + } + + void m() { + System.out.println(Thread.currentThread().getName()); + latch.countDown(); + } + + public static void main(String[] args) throws InterruptedException { + CountDownLatch countDownLatch = new CountDownLatch(5); + Test t1 = new Test(countDownLatch); + for (int i = 0; i < 5; i++) { + new Thread(t1::m, "Thread " + i).start(); + } + countDownLatch.await(); + System.out.println("main thread"); + } +} +``` diff --git "a/Java/classify/thread/\350\277\233\347\250\213\345\222\214\347\272\277\347\250\213.md" "b/Java/classify/thread/\350\277\233\347\250\213\345\222\214\347\272\277\347\250\213.md" new file mode 100644 index 00000000..132a3272 --- /dev/null +++ "b/Java/classify/thread/\350\277\233\347\250\213\345\222\214\347\272\277\347\250\213.md" @@ -0,0 +1,122 @@ +# 进程和线程相关问题 + +## 什么是线程安全? +> 我的理解是:多个线程交替执行,本身是没有问题的,但是如果访问共享资源,结果可能会出现问题,于是就出现了线程不安全的问题。 + +1. 访问共享变量或资源 +2. 依赖时序的操作 +3. 不同数据之间存在绑定关系 +4. 对方没有声明自己是线程安全的 + +## 上下文切换 + +多线程编程中一般**线程的个数都大于 CPU 核心的个数**,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配**时间片并轮转**的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 + +实际上就是**任务从保存到再加载的过程就是一次上下文切换**。 + +## 并行与并发 + +- 并行:**单位时间内**,多个任务同时执行。 +- 并发:**同一时间段**,多个任务都在执行 (单位时间内不一定同时执行); + +## 进程与线程 + +> 面试官挺喜欢问这个问题的,他能引出来他想要问你的知识点。 + +面试官:说一下线程与进程的区别 + +我:好的,如下: + +- **进程是程序的一次执行过程,是系统运行程序的基本单位** + +- **线程是一个比进程更小的执行单位** + +- 一个进程在其执行的过程中可以产生**多个线程** + +- 与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程 + +举例子:比如,main函数启动了JVM进程,同时main就是其中的线程,并且启动了JVM进程,那么还有垃圾回收等线程。 + +或者这样的例子:做个简单的比喻:进程=火车,线程=车厢 + +- 线程在进程下行进(单纯的车厢无法运行) +- 一个进程可以包含多个线程(一辆火车可以有多个车厢) +- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘) +- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易) +- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源) +- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢) +- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上) +- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-"互斥锁" +- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量” + +面试官:直接调用Thread的run方法不行吗? + +我:肯定不行的,通过run方法启动线程其实就是调用一个类中的方法,**当作普通的方法的方式调用**。并没有创建一个线程,**程序中依旧只有一个主线程**,必须等到run()方法里面的代码执行完毕,才会继续执行下面的代码,这样就没有达到写线程的目的。如果是start方法,效果就不一样了。 + +## start源码 + +首先看一下start源码: + +```java +public synchronized void start() { + // 等于0意味着可以是线程的新建状态 + if (threadStatus != 0) + throw new IllegalThreadStateException(); + // 将该线程加入线程组 + group.add(this); + boolean started = false; + try { + start0(); // 核心, 本地方法,新建线程被。 + started = true; + } finally { + try { + if (!started) { + group.threadStartFailed(this); + } + } catch (Throwable ignore) { + } + } +} +``` + +当得到CPU的时间片后就会执行其中的**run()方法**。这个run()方法包含了要执行的这个线程的内容,run()方法运行结束,此线程也就终止了。 + +```java +@Override +public void run() { + if (target != null) { + target.run(); // target是:private Runnable target; Runnable接口 + } +} +// Runnable: +public abstract void run();//抽象方法 +``` + +面试官:线程的生命周期,讲一下。 + +我:ok,看图说话 + +![](https://www.pdai.tech/_images/pics/ace830df-9919-48ca-91b5-60b193f593d2.png) + +- 线程创建之后它将处于`New`(新建)状态,调用 `start()` 方法后开始运行,线程这时候处于 `READY`(可运行,也叫做就绪) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 `RUNNING`(运行) 状态。 +- 当线程执行 `wait()`方法之后,线程进入 `WAITING`(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 `TIME_WAITING`(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 `TIMED WAITING` 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。 +- 当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 `BLOCKED`(阻塞)状态。 +- 线程在执行 Runnable 的` run() `方法结束之后将会进入到 `TERMINATED`(终止) 状态。 + +## wait/notify和sleep + +面试官:wait/notify 和 sleep 方法的异同? + +我:ok + +相同点: + +1. 它们都可以让**线程阻塞**。 +2. 它们都可以响应 **interrupt** 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。 + +不同点: + +1. wait 方法必须在 **synchronized** 保护的代码中使用,而 sleep 方法并没有这个要求。 +2. 在同步代码中**执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁**。 +3. sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。 +4. **wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法**。 diff --git "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Dubbo.md" b/Java/crazy/Dubbo.md similarity index 100% rename from "Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Dubbo.md" rename to Java/crazy/Dubbo.md diff --git "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223JVM.md" b/Java/crazy/JVM.md similarity index 96% rename from "Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223JVM.md" rename to Java/crazy/JVM.md index 98deee6d..759b730c 100644 --- "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223JVM.md" +++ b/Java/crazy/JVM.md @@ -195,11 +195,11 @@ public class ClassLoaderDemo { #### 1.8之前 -![参考-JavaGuide-内存区域](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/JVM%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA%E5%9F%9F.png) +![内存区域](http://media.dreamcat.ink/uPic/JVM内存模型-1.8之前.png) #### 1.8之后 -![参考-JavaGuide-内存区域](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3Java%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA%E5%9F%9FJDK1.8.png) +![参考-JavaGuide-内存区域](http://media.dreamcat.ink/uPic/JVM内存模型-1.8.png) 按照1.8进行总结 @@ -255,7 +255,7 @@ Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共 Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(Garbage Collected Heap)**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。** -![](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3%E5%A0%86%E7%BB%93%E6%9E%84.png) +![](http://media.dreamcat.ink/uPic/堆内存分区.png) 上图所示的 eden 区、s0("From") 区、s1("To") 区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s1("To"),并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。经过这次GC后,Eden区和"From"区已经被清空。这个时候,"From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To"。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到年老代中。 @@ -275,7 +275,7 @@ JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1 #### 对象创建 -![参考-JavaGuide-对象创建的过程](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Java%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1%E7%9A%84%E8%BF%87%E7%A8%8B.png) +![对象创建的过程](http://media.dreamcat.ink/uPic/Java创建对象的过程.png) 1. 类加载检查,虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 @@ -585,15 +585,6 @@ G1 收集器的运作大致分为以下几个步骤: -Xms128m -Xmx4096m -Xss1024K -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags -XX:+PrintGCDetails -XX:+UseSerialGC ``` -- -Xms:初始大小内存,默认为物理内存1/64,等价于-XX:InitialHeapSize -- -Xmx:最大分配内存,默认物理内存1/4,等价于-XX:MaxHeapSize -- -Xss:设置单个线程栈的大小,默认542K~1024K ,等价于-XX:ThreadStackSize -- -Xmn:设置年轻代的大小 -- -XX:MetaspaceSize:设置元空间大小 -- -XX:+PrintGCDetails:输出详细GC收集日志信息,如[名称:GC前内存占用->GC后内存占用(该区内存总大小)] -- -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例,默认-XX:SurvivorRatio=8,Eden:S0:S1=8:1:1 -- -XX:NewRatio:设置年轻代与老年代在堆结构的占比,如:默认-XX:NewRatio=2 新生代在1,老年代2,年轻代占整个堆的1/3,NewRatio值几句诗设置老年代的占比,剩下的1给新生代 -- -XX:MaxTenuringThreshold:设置垃圾的最大年龄,默认-XX:MaxTenuringThreshold=15 -- -XX:+UseSerialGC:串行垃圾回收器 -- -XX:+UseParallelGC:并行垃圾回收器 + +![解释](http://media.dreamcat.ink/uPic/iShot2020-05-26上午09.58.16.png) \ No newline at end of file diff --git "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Java\345\237\272\347\241\200.md" "b/Java/crazy/Java\345\237\272\347\241\200.md" similarity index 100% rename from "Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Java\345\237\272\347\241\200.md" rename to "Java/crazy/Java\345\237\272\347\241\200.md" diff --git "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Java\345\244\232\347\272\277\347\250\213.md" "b/Java/crazy/Java\345\244\232\347\272\277\347\250\213.md" similarity index 99% rename from "Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Java\345\244\232\347\272\277\347\250\213.md" rename to "Java/crazy/Java\345\244\232\347\272\277\347\250\213.md" index 60b68d47..828efb83 100644 --- "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Java\345\244\232\347\272\277\347\250\213.md" +++ "b/Java/crazy/Java\345\244\232\347\272\277\347\250\213.md" @@ -855,7 +855,7 @@ public class CyclicBarrierDemo { ``` -总结:`CyclicBarrier` 内部通过一个 count 变量作为计数器,cout 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。 +总结:`CyclicBarrier` 内部通过一个 count 变量作为计数器,count 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。 #### Semaphore @@ -1300,7 +1300,7 @@ synchronized void setB() throws Exception { #### 分段锁 1. **分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来哦实现高效的并发操作。** -2. ** 以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是ReentrantLock(Segment继承了ReentrantLock)** +2. **以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是ReentrantLock(Segment继承了ReentrantLock)** 3. **当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。** 4. **分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。** @@ -1324,4 +1324,5 @@ synchronized void setB() throws Exception { - Synchronized是一个非公平、悲观、独享、互斥、可重入的重量级锁。 - ReentrantLock是一个默认非公平但可实现公平的、悲观、独享、互斥、可重入、重量级锁。 -- ReentrantReadWriteLock是一个默认非公平但可实现公平的、悲观、写独享、读共享、读写、可重入、重量级锁。 \ No newline at end of file +- ReentrantReadWriteLock是一个默认非公平但可实现公平的、悲观、写独享、读共享、读写、可重入、重量级锁。 + diff --git "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Java\351\233\206\345\220\210.md" "b/Java/crazy/Java\351\233\206\345\220\210.md" similarity index 100% rename from "Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Java\351\233\206\345\220\210.md" rename to "Java/crazy/Java\351\233\206\345\220\210.md" diff --git "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223MySQL.md" b/Java/crazy/MySQL.md similarity index 100% rename from "Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223MySQL.md" rename to Java/crazy/MySQL.md diff --git "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Mybatis.md" b/Java/crazy/Mybatis.md similarity index 100% rename from "Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Mybatis.md" rename to Java/crazy/Mybatis.md diff --git a/Java/crazy/README.md b/Java/crazy/README.md new file mode 100644 index 00000000..00d079a5 --- /dev/null +++ b/Java/crazy/README.md @@ -0,0 +1,12 @@ +- **个人吐血系列-总结Java基础**: [本地阅读](Java基础.md)->[掘金阅读](https://juejin.im/post/5e7e0615f265da795568754b) +- **个人吐血系列-总结Java集合**: [本地阅读](Java集合.md)->[掘金阅读](https://juejin.im/post/5e801e29e51d45470b4fce1c) +- **个人吐血系列-总结Java多线程**: [本地阅读](Java多线程)->[掘金阅读-1](https://juejin.im/post/5e7e0e4ce51d4546cd2fcc7c) [掘金阅读-2](https://juejin.im/post/5e7e0e4ce51d4546cd2fcc7c) +- **个人吐血系列-总结JVM**: [本地阅读](JVM.md)->[掘金阅读](https://juejin.im/post/5e8344486fb9a03c786ef885) +- **个人吐血系列-总结Spring**: [本地阅读](Spring.md)->[掘金阅读](https://juejin.im/post/5e846a4a6fb9a03c42378bc1) +- **个人吐血系列-总结Mybatis**: [本地阅读](Mybatis.md)->[掘金阅读](https://juejin.im/post/5e889b196fb9a03c875c8f50) +- **个人吐血系列-总结MySQL**: [本地阅读](MySQL.md)->[掘金阅读](https://juejin.im/post/5e94116551882573b86f970f) +- **个人吐血系列-总结Redis**: [本地阅读](Redis.md)->[掘金阅读](https://juejin.im/post/5e9d6a9ff265da47e34c0e8a) +- **个人吐血系列-总结计算机网络**: [本地阅读](计算机网络.md)->[掘金阅读](https://juejin.im/post/5ea383c251882573716ab496) +- **个人吐血系列-Dubbo**: [本地阅读](Dubbo.md)->[掘金阅读](https://juejin.im/post/5eb11127f265da7bb46bce26) +- **个人吐血系列-RocketMQ**: [本地阅读](RocketMQ)-> [掘金阅读](https://juejin.im/post/5ecf1f716fb9a047f338b972) + diff --git "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Redis.md" b/Java/crazy/Redis.md similarity index 100% rename from "Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Redis.md" rename to Java/crazy/Redis.md diff --git "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223RocketMQ.md" b/Java/crazy/RocketMQ.md similarity index 100% rename from "Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223RocketMQ.md" rename to Java/crazy/RocketMQ.md diff --git "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Spring.md" b/Java/crazy/Spring.md similarity index 96% rename from "Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Spring.md" rename to Java/crazy/Spring.md index 5e674e5a..c027f5ee 100644 --- "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223Spring.md" +++ b/Java/crazy/Spring.md @@ -77,10 +77,13 @@ public void refresh() throws BeansException, IllegalStateException { // 准备工作,记录下容器的启动时间、标记“已启动”状态、处理配置文件中的占位符 prepareRefresh(); - - // 这步比较关键,这步完成后,配置文件就会解析成一个个 Bean 定义,注册到 BeanFactory 中, - // 当然,这里说的 Bean 还没有初始化,只是配置信息都提取出来了, - // 注册也只是将这些信息都保存到了注册中心(说到底核心是一个 beanName-> beanDefinition 的 map) + // Tell the subclass to refresh the internal bean factory. + //获得容器ApplicationContext的子类BeanFactory。步骤如下: + //1.如果已经有了BeanFactory就销毁它里面的单例Bean并关闭这个BeanFactory。 + //2.创建一个新的BeanFactory。 + //3.对这个BeanFactory进行定制(customize),如allowBeanDefinitionOverriding等参数 + //4.转载BeanDefinitions(读取配置文件,将xml转换成对应得BeanDefinition) + //5.检查是否同时启动了两个BeanFactory。 ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 设置 BeanFactory 的类加载器,添加几个 BeanPostProcessor,手动注册几个特殊的 bean @@ -88,16 +91,14 @@ public void refresh() throws BeansException, IllegalStateException { prepareBeanFactory(beanFactory); try { - // 【这里需要知道 BeanFactoryPostProcessor 这个知识点,Bean 如果实现了此接口, - // 那么在容器初始化以后,Spring 会负责调用里面的 postProcessBeanFactory 方法。】 - - // 这里是提供给子类的扩展点,到这里的时候,所有的 Bean 都加载、注册完成了,但是都还没有初始化 + // 设置beanFactory的后置处理器 // 具体的子类可以在这步的时候添加一些特殊的 BeanFactoryPostProcessor 的实现类或做点什么事 postProcessBeanFactory(beanFactory); + // 调用beanFactory的后置处理器 // 调用 BeanFactoryPostProcessor 各个实现类的 postProcessBeanFactory(factory) 方法 invokeBeanFactoryPostProcessors(beanFactory); - // 注册 BeanPostProcessor 的实现类,注意看和 BeanFactoryPostProcessor 的区别 + // 注册 BeanPostProcessor 的实现类(bean的后置处理器) // 此接口两个方法: postProcessBeforeInitialization 和 postProcessAfterInitialization // 两个方法分别在 Bean 初始化之前和初始化之后得到执行。注意,到这里 Bean 还没初始化 registerBeanPostProcessors(beanFactory); @@ -108,7 +109,7 @@ public void refresh() throws BeansException, IllegalStateException { // 初始化当前 ApplicationContext 的事件广播器,这里也不展开了 initApplicationEventMulticaster(); - // 从方法名就可以知道,典型的模板方法(钩子方法), + // 模板方法(钩子方法), // 具体的子类可以在这里初始化一些特殊的 Bean(在初始化 singleton beans 之前) onRefresh(); diff --git "a/Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" "b/Java/crazy/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" similarity index 100% rename from "Interview/crazy/\344\270\252\344\272\272\345\220\220\350\241\200\347\263\273\345\210\227-\346\200\273\347\273\223\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" rename to "Java/crazy/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" diff --git "a/Collections/Java\351\235\242\347\273\217-ArrayList\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/ArrayList\346\272\220\347\240\201.md" similarity index 99% rename from "Collections/Java\351\235\242\347\273\217-ArrayList\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/ArrayList\346\272\220\347\240\201.md" index 70f75650..d3015437 100644 --- "a/Collections/Java\351\235\242\347\273\217-ArrayList\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/Java/jdk/ArrayList\346\272\220\347\240\201.md" @@ -2,7 +2,6 @@ **Java的集合框架,ArrayList源码解析等...** - ## 概述 diff --git "a/Java/jdk/Class\346\272\220\347\240\201.md" "b/Java/jdk/Class\346\272\220\347\240\201.md" new file mode 100644 index 00000000..3f2495ec --- /dev/null +++ "b/Java/jdk/Class\346\272\220\347\240\201.md" @@ -0,0 +1,151 @@ +## 类 + +```java +// final 修饰 +public final class Class implements java.io.Serializable, + GenericDeclaration, + Type, + AnnotatedElement +``` + +## 方法 + +### registerNatives() + +```java +// 本地方法,之间在object看到 +private static native void registerNatives(); +static { + registerNatives(); +} +``` + +### 构造方法 + +```java +private Class(ClassLoader loader) { + // Initialize final field for classLoader. The initialization value of non-null + // prevents future JIT optimizations from assuming this final field is null. + // 注入类加载器 + classLoader = loader; +} +``` + +### forName() + +```java +// 说白了,返回类对象 +className: 类的权限定名(包名+类名) +initialize: 是否初始化 +ClassLoader:类加载器 +caller:类装入器,没有就null + +@CallerSensitive +public static Class forName(String className) + throws ClassNotFoundException { + Class caller = Reflection.getCallerClass(); + return forName0(className, true, ClassLoader.getClassLoader(caller), caller); +} + +private static native Class forName0(String name, boolean initialize, + ClassLoader loader, + Class caller) + throws ClassNotFoundException; +``` + +### newInstance() + +```java +// 我见的比较少,一般步骤都是先拿class的构造器,接着new对象,如果私有,就暴力 +// 其实这里面还是调用构造器 +@CallerSensitive +public T newInstance() + throws InstantiationException, IllegalAccessException +{ + if (System.getSecurityManager() != null) { + checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), false); + } + // 有意思啊,居然不是严格正确 + // NOTE: the following code may not be strictly correct under + // the current Java memory model.// 当前的JMM + + // Constructor lookup // 构造器检查 + if (cachedConstructor == null) { + if (this == Class.class) { + throw new IllegalAccessException( + "Can not call newInstance() on the Class for java.lang.Class" + ); + } + try { + Class[] empty = {}; + final Constructor c = getConstructor0(empty, Member.DECLARED); + // Disable accessibility checks on the constructor + // since we have to do the security check here anyway + // (the stack depth is wrong for the Constructor's + // security check to work) + java.security.AccessController.doPrivileged( + new java.security.PrivilegedAction() { + public Void run() { + c.setAccessible(true); // 暴力解除 + return null; + } + }); + cachedConstructor = c; + } catch (NoSuchMethodException e) { + throw (InstantiationException) + new InstantiationException(getName()).initCause(e); + } + } + Constructor tmpConstructor = cachedConstructor; // 拿到构造器 + // Security check (same as in java.lang.reflect.Constructor) + int modifiers = tmpConstructor.getModifiers(); + if (!Reflection.quickCheckMemberAccess(this, modifiers)) { + Class caller = Reflection.getCallerClass(); + if (newInstanceCallerCache != caller) { + Reflection.ensureMemberAccess(caller, this, null, modifiers); + newInstanceCallerCache = caller; + } + } + // Run constructor + try { + return tmpConstructor.newInstance((Object[])null); // new实例 + } catch (InvocationTargetException e) { + Unsafe.getUnsafe().throwException(e.getTargetException()); + // Not reached + return null; + } +} +``` + +### getFields() + +```java +@CallerSensitive +public Field[] getFields() throws SecurityException { + // 检查, 然后获取 + checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true); + return copyFields(privateGetPublicFields(null)); +} +``` + +### getMethods() + +```java +@CallerSensitive +public Method[] getMethods() throws SecurityException { + // 和上面一样 + checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true); + return copyMethods(privateGetPublicMethods()); +} +``` + +### getConstructors() + +```java +@CallerSensitive +public Constructor[] getConstructors() throws SecurityException { + checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true); + return copyConstructors(privateGetDeclaredConstructors(true)); +} +``` + diff --git "a/Collections/Java\351\235\242\347\273\217-HashSet-HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/HashSet-HashMap\346\272\220\347\240\201.md" similarity index 100% rename from "Collections/Java\351\235\242\347\273\217-HashSet-HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/HashSet-HashMap\346\272\220\347\240\201.md" diff --git "a/Java/jdk/Integer\346\272\220\347\240\201.md" "b/Java/jdk/Integer\346\272\220\347\240\201.md" new file mode 100644 index 00000000..0d3f17d2 --- /dev/null +++ "b/Java/jdk/Integer\346\272\220\347\240\201.md" @@ -0,0 +1,176 @@ +## 类 + +```java +// 看了不少,至少很多工具类,并不希望你们去继承,因此加了final方法 +public final class Integer extends Number implements Comparable +``` + +## 变量 + +```java +have, -231. // 官方注释哈,自然懂 +@Native public static final int MIN_VALUE = 0x80000000; +have, 231-1. +@Native public static final int MAX_VALUE = 0x7fffffff; +private final int value; +@Native public static final int SIZE = 32; + +// 下面方法会用到该静态变量 +final static char[] digits = { + '0' , '1' , '2' , '3' , '4' , '5' , + '6' , '7' , '8' , '9' , 'a' , 'b' , + 'c' , 'd' , 'e' , 'f' , 'g' , 'h' , + 'i' , 'j' , 'k' , 'l' , 'm' , 'n' , + 'o' , 'p' , 'q' , 'r' , 's' , 't' , + 'u' , 'v' , 'w' , 'x' , 'y' , 'z' +}; +``` + +## 方法 + +### parseInt(String s, int radix) + +```java +// 你说说,lc的字符串转数字的题目,是不是? +public static int parseInt(String s, int radix) + throws NumberFormatException +{ + /* + * WARNING: This method may be invoked early during VM initialization + * before IntegerCache is initialized. Care must be taken to not use + * the valueOf method. + */ + + if (s == null) { + throw new NumberFormatException("null"); + } + + if (radix < Character.MIN_RADIX) { + throw new NumberFormatException("radix " + radix + + " less than Character.MIN_RADIX"); + } + + if (radix > Character.MAX_RADIX) { + throw new NumberFormatException("radix " + radix + + " greater than Character.MAX_RADIX"); + } + + int result = 0; // 返回结果值 + boolean negative = false; // 很熟悉的味道 + int i = 0, len = s.length(); // 干嘛?双指针? + int limit = -Integer.MAX_VALUE; // 限制一下哈 + int multmin; + int digit; + + if (len > 0) { + char firstChar = s.charAt(0); // 判断第一个字符是否"+" or "-" + if (firstChar < '0') { // Possible leading "+" or "-" + if (firstChar == '-') { + negative = true; // 负数标志 + limit = Integer.MIN_VALUE; + } else if (firstChar != '+') + throw NumberFormatException.forInputString(s); + + if (len == 1) // Cannot have lone "+" or "-" + throw NumberFormatException.forInputString(s); + i++; + } + multmin = limit / radix; // 这是算啥子?范围? + while (i < len) { + // Accumulating negatively avoids surprises near MAX_VALUE + digit = Character.digit(s.charAt(i++),radix); // digit我还是贴一下吧 + if (digit < 0) { + throw NumberFormatException.forInputString(s); + } + if (result < multmin) { + throw NumberFormatException.forInputString(s); + } + result *= radix; // 关键在这 + if (result < limit + digit) { + throw NumberFormatException.forInputString(s); + } + result -= digit; // 还有这 // 实际上这是负的,因为后面结果有个判断 + // 还有一种是是result = result * 10 + digit 也是可以, 根据正负判断标志 + } + } else { + throw NumberFormatException.forInputString(s); + } + return negative ? result : -result; +} +``` + +### valueOf(String s) + +```java +public static Integer valueOf(String s) throws NumberFormatException { + // 还是调用了一下parseInt + // 不过该方法进行了缓存,缓存范围是-128-127 + return Integer.valueOf(parseInt(s, 10)); +} + +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +``` + +### hashCode() + +```java +public int hashCode() { + return Integer.hashCode(value); +} +// 直接就是value哇 +public static int hashCode(int value) { + return value; +} +``` + +### equals() + +```java +public boolean equals(Object obj) { + if (obj instanceof Integer) { + // 比较的是基本类型的value + return value == ((Integer)obj).intValue(); + } + return false; +} +``` + +### bitCount(int i) + +```java +// 返回二进制1的个数,我可写不出来这么神奇的代码 +public static int bitCount(int i) { + // HD, Figure 5-2 + i = i - ((i >>> 1) & 0x55555555); + i = (i & 0x33333333) + ((i >>> 2) & 0x33333333); + i = (i + (i >>> 4)) & 0x0f0f0f0f; + i = i + (i >>> 8); + i = i + (i >>> 16); + return i & 0x3f; +} +``` + +### reverse(int i) + +```java +// 就连翻转都写这么难懂的嘛? +public static int reverse(int i) { + // HD, Figure 7-1 + i = (i & 0x55555555) << 1 | (i >>> 1) & 0x55555555; + i = (i & 0x33333333) << 2 | (i >>> 2) & 0x33333333; + i = (i & 0x0f0f0f0f) << 4 | (i >>> 4) & 0x0f0f0f0f; + i = (i << 24) | ((i & 0xff00) << 8) | + ((i >>> 8) & 0xff00) | (i >>> 24); + return i; +} +``` + + + +注意: + +其他几个包装类就不介绍了 \ No newline at end of file diff --git "a/Java/jdk/InvocationHandler\346\272\220\347\240\201.md" "b/Java/jdk/InvocationHandler\346\272\220\347\240\201.md" new file mode 100644 index 00000000..e4d6e48e --- /dev/null +++ "b/Java/jdk/InvocationHandler\346\272\220\347\240\201.md" @@ -0,0 +1,21 @@ +> 代理经常用到,回调方法,很多框架也的到。 +> +> 不过这仅仅是动态代理 + +## 类 + +```java +// 接口 +public interface InvocationHandler +``` + +## invoke + +```java +// proxy:调用方法的代理实例 +// method:代理实例的接口方法 +// args:参数 +public Object invoke(Object proxy, Method method, Object[] args) + throws Throwable; +``` + diff --git "a/Collections/Java\351\235\242\347\273\217-LinkedHashSet-Map\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/LinkedHashSet\346\272\220\347\240\201.md" similarity index 100% rename from "Collections/Java\351\235\242\347\273\217-LinkedHashSet-Map\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/LinkedHashSet\346\272\220\347\240\201.md" diff --git "a/Collections/Java\351\235\242\347\273\217-LinkedList\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/LinkedList\346\272\220\347\240\201.md" similarity index 100% rename from "Collections/Java\351\235\242\347\273\217-LinkedList\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/LinkedList\346\272\220\347\240\201.md" diff --git "a/Java/jdk/Object\346\272\220\347\240\201.md" "b/Java/jdk/Object\346\272\220\347\240\201.md" new file mode 100644 index 00000000..c6e4146e --- /dev/null +++ "b/Java/jdk/Object\346\272\220\347\240\201.md" @@ -0,0 +1,108 @@ +## 类 + +```java +public class Object // 顶层所有父类,不需要实现和继承。。 造娃第一人 +``` + +## 方法 + +### registerNatives + +```java +static { + registerNatives(); // 打开试试,看名字知道是注册本地方法的 +} + +// 打开是 +private static native void registerNatives(); // 额 +``` + +### getClass() + +```java +public final native Class getClass(); // 本地方法,返回runtime的类 +``` + +### hashcode() + +```java +public native int hashCode(); // 返回对象的哈希码值。 +``` + +注意:对象的哈希码,意味着,只要对象内存地址不一样,这玩意就不一样 + +### equals() + +```java +public boolean equals(Object obj) { + // 还是用了==, ==在对象的时候,比较地址值 + return (this == obj); +} +``` + +### clone() + +```java +// 创建并返回此对象的副本。 +protected native Object clone() throws CloneNotSupportedException; +``` + +### toString() + +```java +public String toString() { + // 哈希码的16进制 + return getClass().getName() + "@" + Integer.toHexString(hashCode()); +} +``` + +### notify() + +```java +// 本地方法,和wait结合使用,很香 +public final native void notify() +``` + +### notifyAll() + +```java +// 很上面一样,不过区别在于,这个能唤醒所有wait的线程 +public final native void notifyAll(); +``` + +### wait() + +```java +public final native void wait(long timeout) throws InterruptedException; + +public final void wait(long timeout, int nanos) throws InterruptedException { + if (timeout < 0) { + throw new IllegalArgumentException("timeout value is negative"); + } + + if (nanos < 0 || nanos > 999999) { + throw new IllegalArgumentException( + "nanosecond timeout value out of range"); + } + + if (nanos > 0) { + timeout++; + } + + wait(timeout); +} + +public final void wait() throws InterruptedException { + wait(0); +} +// 其实还是调用wait(long timeout) + +``` + +### Finalize() + +```java +// jvm gc 你们懂得 回收该对象 +protected void finalize() throws Throwable { } +``` + diff --git "a/Collections/Java\351\235\242\347\273\217-PriorityQueue\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/PriorityQueue\346\272\220\347\240\201.md" similarity index 100% rename from "Collections/Java\351\235\242\347\273\217-PriorityQueue\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/PriorityQueue\346\272\220\347\240\201.md" diff --git "a/Java/jdk/Proxy\346\272\220\347\240\201.md" "b/Java/jdk/Proxy\346\272\220\347\240\201.md" new file mode 100644 index 00000000..1369afc9 --- /dev/null +++ "b/Java/jdk/Proxy\346\272\220\347\240\201.md" @@ -0,0 +1,70 @@ +> 动态代理经常用到该类的一个方法 + +## newProxyInstance + +```java +1. ClassLoader:类加载器 +2. interfaces:被代理类所实现的接口 +3. InvocationHandler:Invoker回调接口 +@CallerSensitive +public static Object newProxyInstance(ClassLoader loader, + Class[] interfaces, + InvocationHandler h) + throws IllegalArgumentException +{ + // 回调不能是空 + Objects.requireNonNull(h); + // 拷贝 + final Class[] intfs = interfaces.clone(); + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + // 检查loader有木有问题吧 + checkProxyAccess(Reflection.getCallerClass(), loader, intfs); + } + + /* + * Look up or generate the designated proxy class. + */ + // 一看0,就知道JNT,注释上写的是为该接口生成代理类 + Class cl = getProxyClass0(loader, intfs); + + /* + * Invoke its constructor with the designated invocation handler. + */ + // 用指定的调用处理程序调用它的构造函数。 + try { + if (sm != null) { + checkNewProxyPermission(Reflection.getCallerClass(), cl); + } + // 获取代理类的构造器 + final Constructor cons = cl.getConstructor(constructorParams); + // 拿到参数的invoker + final InvocationHandler ih = h; + // 如果不是public, 直接暴力 + if (!Modifier.isPublic(cl.getModifiers())) { + AccessController.doPrivileged(new PrivilegedAction() { + // 暴力解除 + public Void run() { + cons.setAccessible(true); + return null; + } + }); + } + // 直接根据代理的构造器通过反射new一个实例,接着调用回调invoker + // 当用户通过代理类调用方法的时候,实际上会调用invorker,也就是参数的h + return cons.newInstance(new Object[]{h}); + } catch (IllegalAccessException|InstantiationException e) { + throw new InternalError(e.toString(), e); + } catch (InvocationTargetException e) { + Throwable t = e.getCause(); + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else { + throw new InternalError(t.toString(), t); + } + } catch (NoSuchMethodException e) { + throw new InternalError(e.toString(), e); + } +} +``` + diff --git a/Java/jdk/README.md b/Java/jdk/README.md new file mode 100644 index 00000000..c6d78c0c --- /dev/null +++ b/Java/jdk/README.md @@ -0,0 +1,21 @@ +> 说明:JDK1.8版本,我只看了一些重要且部分的源码,目前害没有看完,JUC部分的源码一定要看。看源码能带来很多思维上的好处,闲的时候多看看,能发现其中的原理,不分前后,都挺重要的。 + +- [Object](Object源码.md) +- [Integer](Integer源码.md) +- [String](String源码.md) +- [StringBuilder](StringBuilder源码.md) +- [Thread](Thread源码.md) +- [ThreadLocal](ThreadLocal源码.md) +- [WeakReference](WeakReference源码.md) +- [Proxy](Proxy源码.md) +- [Class](Class源码.md) +- [InvocationHandler](InvocationHandler源码.md) +- [ArrayList](ArrayList源码.md) +- [LinkedList](LinkedList源码.md) +- [HashSet-HashMap](HashSet-HashMap源码.md) +- [LinkedHashSet](LinkedHashSet源码.md) +- [Stack-Queue](Stack-Queue源码.md) +- [PriorityQueue](PriorityQueue源码.md) +- [TreeSet-TreeMap](TreeSet-TreeMap源码.md) + + diff --git "a/Java/jdk/SoftReference\346\272\220\347\240\201.md" "b/Java/jdk/SoftReference\346\272\220\347\240\201.md" new file mode 100644 index 00000000..9cd7a229 --- /dev/null +++ "b/Java/jdk/SoftReference\346\272\220\347\240\201.md" @@ -0,0 +1,31 @@ +## 类 + +```java +// 继承了Reference +public class SoftReference extends Reference +``` + +## 参数 + +```java +// 时间戳时钟,由垃圾收集器更新 +static private long clock; +// 通过每次调用get方法更新时间戳。当选择要清除的软引用时,VM可以使用这个字段,但这不是必需的。 +private long timestamp; +``` + +## 方法 + +### get() + +```java +// 返回该引用对象的引用。 +// 如果该引用对象已经被程序或垃圾收集器清除,则该方法返回null。 +public T get() { + T o = super.get(); + if (o != null && this.timestamp != clock) + this.timestamp = clock; + return o; +} +``` + diff --git "a/Collections/Java\351\235\242\347\273\217-Stack-Queue\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/Stack-Queue\346\272\220\347\240\201.md" similarity index 100% rename from "Collections/Java\351\235\242\347\273\217-Stack-Queue\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/Stack-Queue\346\272\220\347\240\201.md" diff --git "a/Java/jdk/StringBuilder\346\272\220\347\240\201.md" "b/Java/jdk/StringBuilder\346\272\220\347\240\201.md" new file mode 100644 index 00000000..1019d01f --- /dev/null +++ "b/Java/jdk/StringBuilder\346\272\220\347\240\201.md" @@ -0,0 +1,101 @@ +## 类 + +```java +// 虽然该类上加了final +// 但是一会看看变量char[] +// 继承了ASB +// StringBuilder没有char,但是他的抽象父类有 +public final class StringBuilder + extends AbstractStringBuilder + implements java.io.Serializable, CharSequence +``` + +## 变量 + +```java +// 没有被final修饰,因此可变 +char[] value; +// 计数 +int count; +``` + +## 方法 + +### append(char[] str) + +大家最常用的 + +```java +// 毕竟是char数组类型,需要判断扩容 +// 其次是count++,跟ArrayList差不多的,size++ +@Override +public AbstractStringBuilder append(char c) { + ensureCapacityInternal(count + 1); + value[count++] = c; + return this; +} +``` + +### charAt(int index) + +也是大家常用的 + +```java +// 很多都差不多的,看多了,也就那样了 +@Override +public char charAt(int index) { + // 判断是否索引越界 + if ((index < 0) || (index >= count)) + throw new StringIndexOutOfBoundsException(index); + // 按照索引取对应数组上的值 + return value[index]; +} +``` + +### length() + +```java +// 直接返回维护的count变量 +@Override +public int length() { + return count; +} +``` + +### reverse() + +```java +// 看名字就知道翻转 +public AbstractStringBuilder reverse() { + // 维护一个 变量? 是否代理? + boolean hasSurrogates = false; + int n = count - 1; + // 双指针交换位置?而且还是从中间 + // 相当于 abcde j在b, k在d + // j-- k++ + for (int j = (n-1) >> 1; j >= 0; j--) { + int k = n - j; + // 前 + char cj = value[j]; + // 后 + char ck = value[k]; + value[j] = ck; + value[k] = cj; + if (Character.isSurrogate(cj) || + Character.isSurrogate(ck)) { + hasSurrogates = true; + } + } + if (hasSurrogates) { + reverseAllValidSurrogatePairs(); + } + return this; +} +``` + + + +注意: + +StringBuffer跟StringBuilder差不多是一样的,只不过前者在很多方法上加了synchronized同步锁,因此它是线程安全的,而后者不是。 + diff --git "a/Java/jdk/String\346\272\220\347\240\201.md" "b/Java/jdk/String\346\272\220\347\240\201.md" new file mode 100644 index 00000000..b11262a2 --- /dev/null +++ "b/Java/jdk/String\346\272\220\347\240\201.md" @@ -0,0 +1,179 @@ +> 主要针对于面试需要看的参数和方法 + +## 类 + +```java +public final class String implements java.io.Serializable, Comparable, CharSequence +``` + +可以看出该String被final修饰了,因此该类的所有方法都被隐式的加了final + +## 参数 + +```java +// 该值用于字符存储。 +private final char value[]; +// 缓存字符串的哈希代码 +private int hash; +``` + +## 方法 + +### equals + +```java +public boolean equals(Object anObject) { + if (this == anObject) { + return true; + } + // 判断是否属于String类 + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; // 获取长度 + if (n == anotherString.value.length) { + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + // 一个一个判断 + while (n-- != 0) { + // 如果不相等,就直接false + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; + } +``` + +### hashcode + +```java +public int hashCode() { + int h = hash; + if (h == 0 && value.length > 0) { + char val[] = value; + + for (int i = 0; i < value.length; i++) { + h = 31 * h + val[i]; // 还是取决于val + } + hash = h; // 缓存hash + } + return h; +} +``` + +### length() + +```java +public int length() { + // 取value数组的长度 + return value.length; +} +``` + +### isEmpty() + +```java +public boolean isEmpty() { + // 长度是否为0 + return value.length == 0; +} +``` + +### toCharArray() + +```java +public char[] toCharArray() { + // Cannot use Arrays.copyOf because of class initialization order issues + // 没有调用Arrays的方法,而是直接调用本地方法,防止类初始化顺序问题 + char result[] = new char[value.length]; + System.arraycopy(value, 0, result, 0, value.length); + return result; +} +``` + +### trim() + +```java +public String trim() { + int len = value.length; + int st = 0; + char[] val = value; /* avoid getfield opcode */ + // 去除前面的空格 + while ((st < len) && (val[st] <= ' ')) { + st++; + } + // 去除后面的空格 + while ((st < len) && (val[len - 1] <= ' ')) { + len--; + } + return ((st > 0) || (len < value.length)) ? substring(st, len) : this; +} +``` + +### replace() + +```java +public String replace(char oldChar, char newChar) { + // 先判断新字符和旧字符是否一样 + if (oldChar != newChar) { + // 取该String的长度 + int len = value.length; + // 初始化个指针 + int i = -1; + // val和value指向同一个数组 + char[] val = value; /* avoid getfield opcode */ + // 先找到旧字符的索引 + while (++i < len) { + if (val[i] == oldChar) { + // 找到旧退出 + break; + } + } + if (i < len) { + // 先new一个缓存,长度len + char buf[] = new char[len]; + // 遍历0,i 把不需要替换的,放进去 + for (int j = 0; j < i; j++) { + buf[j] = val[j]; + } + // 替换新字符的过程 + while (i < len) { + // 先取出旧字符 + char c = val[i]; + // 判断是不是你传来的, 是就替换,不是,就不替换 + buf[i] = (c == oldChar) ? newChar : c; + // 可能重复替换 + i++; + } + return new String(buf, true); + } + } + return this; +} +``` + +### substring() + +```java +public String substring(int beginIndex, int endIndex) { + // 以下都是防止越界 + if (beginIndex < 0) { + throw new StringIndexOutOfBoundsException(beginIndex); + } + if (endIndex > value.length) { + throw new StringIndexOutOfBoundsException(endIndex); + } + int subLen = endIndex - beginIndex; + if (subLen < 0) { + throw new StringIndexOutOfBoundsException(subLen); + } + // new一个String, 这个重载方法,就不看了,底层还是调用了system.copyarray的本地方法 + return ((beginIndex == 0) && (endIndex == value.length)) ? this + : new String(value, beginIndex, subLen); +} +``` + diff --git "a/Java/jdk/ThreadLocal\346\272\220\347\240\201.md" "b/Java/jdk/ThreadLocal\346\272\220\347\240\201.md" new file mode 100644 index 00000000..1348f8d7 --- /dev/null +++ "b/Java/jdk/ThreadLocal\346\272\220\347\240\201.md" @@ -0,0 +1,75 @@ +## 类 + +```java +// 什么也没有,就有个泛型 +public class ThreadLocal +``` + +但是Entry + +```java +// +static class Entry extends WeakReference> { + /** The value associated with this ThreadLocal. */ + Object value; + + Entry(ThreadLocal k, Object v) { + super(k); // key super了, 弱引用 + value = v; + } +} +``` + + + +## 变量 + +```java +// hashcode 是指定一个固定值,开始自增 +private static AtomicInteger nextHashCode = + new AtomicInteger(); +// 连续生成的哈希码之间的区别——将隐式顺序线程本地id转换为接近最优扩散的倍增哈希值,用于两级幂的表。 +private static final int HASH_INCREMENT = 0x61c88647; +``` + +## 方法 + +### get() + +```java +// 依托Thread +public T get() { + // 获取线程 + Thread t = Thread.currentThread(); + // 得到定制的map + ThreadLocalMap map = getMap(t); + if (map != null) { + ThreadLocalMap.Entry e = map.getEntry(this); + if (e != null) { + @SuppressWarnings("unchecked") + T result = (T)e.value; + return result; + } + } + return setInitialValue(); +} +``` + +### set() + +```java +// 依然是map +public void set(T value) { + // 获取线程 + Thread t = Thread.currentThread(); + // 得到map + ThreadLocalMap map = getMap(t); + if (map != null) + // set + map.set(this, value); + else + // 没有就创建 + createMap(t, value); +} +``` + diff --git "a/Java/jdk/Thread\346\272\220\347\240\201.md" "b/Java/jdk/Thread\346\272\220\347\240\201.md" new file mode 100644 index 00000000..f61500dc --- /dev/null +++ "b/Java/jdk/Thread\346\272\220\347\240\201.md" @@ -0,0 +1,105 @@ +## 类 + +```java +// 实现了Runnable +public class Thread implements Runnable +``` + +## 变量 + +```java +// 默认不是守护线程 +private boolean daemon = false; +// run的时候要用到 +private Runnable target; +// 线程组 +private ThreadGroup group; + +// 这里很重要,TL的变量在Thread里面自定义的一种Map +ThreadLocal.ThreadLocalMap threadLocals = null; +``` + +## 方法 + +### start() + +```java +public synchronized void start() { + /** + * This method is not invoked for the main method thread or "system" + * group threads created/set up by the VM. Any new functionality added + * to this method in the future may have to also be added to the VM. + * + * A zero status value corresponds to state "NEW". + */ + if (threadStatus != 0) // 不为0,就抛出异常了 + throw new IllegalThreadStateException(); + + /* Notify the group that this thread is about to be started + * so that it can be added to the group's list of threads + * and the group's unstarted count can be decremented. */ + group.add(this); // 添加到线程组 + + boolean started = false; + try { + start0(); // 本地方法 + started = true; + } finally { + try { + if (!started) { + group.threadStartFailed(this); + } + } catch (Throwable ignore) { + /* do nothing. If start0 threw a Throwable then + it will be passed up the call stack */ + } + } +} + +private native void start0(); +``` + +### run() + +```java +// 实际上走的Runnable接口的run +@Override +public void run() { + if (target != null) { + target.run(); + } +} +``` + +### join(long millis) + +```java +public final synchronized void join(long millis) + throws InterruptedException { + long base = System.currentTimeMillis(); + long now = 0; + + if (millis < 0) { + throw new IllegalArgumentException("timeout value is negative"); + } + + if (millis == 0) { // 调用join,没有传参数,默认是0,那么会来到这里 + while (isAlive()) { // 当run方法走完,才能为false + wait(0); + } + } else { + while (isAlive()) { + long delay = millis - now; + if (delay <= 0) { + break; + } + wait(delay); + now = System.currentTimeMillis() - base; + } + } +} + +// 测试此线程是否处于活动状态。如果一个线程已经启动并且还没有死,那么它就是活的。 +public final native boolean isAlive(); +``` + diff --git "a/Collections/Java\351\235\242\347\273\217-TreeSet-TreeMap\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/Java/jdk/TreeSet-TreeMap\346\272\220\347\240\201.md" similarity index 100% rename from "Collections/Java\351\235\242\347\273\217-TreeSet-TreeMap\346\272\220\347\240\201\350\247\243\346\236\220.md" rename to "Java/jdk/TreeSet-TreeMap\346\272\220\347\240\201.md" diff --git "a/Java/jdk/WeakReference\346\272\220\347\240\201.md" "b/Java/jdk/WeakReference\346\272\220\347\240\201.md" new file mode 100644 index 00000000..c37b8f31 --- /dev/null +++ "b/Java/jdk/WeakReference\346\272\220\347\240\201.md" @@ -0,0 +1,9 @@ +## 类 + +```java + +// 弱引用对象,它不阻止将其引用变为可结束、结束,然后再回收。 +// 弱引用最常用于实现规范化映射。 +public class WeakReference extends Reference +``` + diff --git a/Java/mianjing/README.md b/Java/mianjing/README.md new file mode 100644 index 00000000..b9c3c03c --- /dev/null +++ b/Java/mianjing/README.md @@ -0,0 +1,36 @@ +个人建议: + +> 首先自己先有个知识点体系,不管是思维导图也好,还是大纲也好。 +> 那么其次看大量的面经,可以将每个面经当作一次面试,查看自己是否能讲出来,查漏补缺! +> 最后,不断沉淀即可。祝好运!!! + +文档说明: + +> 秋招后期,有些面试我没有总结,大同小异,仅供参考,可能自己想偷懒了,还有一些有2面3面的,我也懒的总结了,工作量不小,不过大概差不多,面经就是需要复盘,查漏补缺,面试官问的大部分跟你的简历有关系,从简历问的又深又广,不仅仅局限于简历哦,提示:这里的面经是关于Java后端的。 + +## 牛客网面经 + +- [拼多多所有问题汇总](拼多多所有问题汇总.md) +- [京东所有问题汇总](京东所有问题汇总.md) +- [美团所有问题汇总](美团所有问题汇总.md) +- [阿里所有问题汇总](阿里所有问题汇总.md) +- [百度所有问题汇总](百度所有问题汇总.md) +- [字节所有问题汇总](字节所有问题汇总.md) +- [招银所有问题汇总](招银所有问题汇总.md) +- [网易所有问题汇总](网易所有问题汇总.md) +- [远景所有问题汇总](远景所有问题汇总.md) +- [猿辅导所有问题汇总](猿辅导所有问题汇总.md) + +## 自己 + +- [shein](myshein.md) +- [京东](my京东.md) +- [作业帮](my作业帮.md) +- [字节](my字节.md) +- [招银](my招银.md) +- [猿辅导](my猿辅导.md) +- [用友](my用友.md) +- [百度](my百度.md) +- [网易](my网易.md) +- [腾讯](my腾讯.md) +- [贝壳](my贝壳.md) \ No newline at end of file diff --git a/Java/mianjing/myshein.md b/Java/mianjing/myshein.md new file mode 100644 index 00000000..9123cfa5 --- /dev/null +++ b/Java/mianjing/myshein.md @@ -0,0 +1,26 @@ +一面 +时间:8.14 14:30 时长:40 + +1. 自我介绍 +2. 介绍项目(20min) +3. 为什么用Dubbo +4. 为什么用JWT +5. 为什么用Redis +6. 为什么用RocketMQ +7. RocketMQ事务模型 +7. 为什么用分布式锁 +8. MyISAM和InnoDB的区别 +9. B和B+树的区别 +10. 什么会锁整张表 +11. Explain都有啥玩意 +12. 如果是你,如何慢查询优化 + +二面 +时间:8.24 19:50 时长:25min + +1. 自我介绍 +2. 简单说一下微服务 +3. 为什么用索引 +4. 什么情况下用索引 +5. 家庭情况 +6. 反问:如何看待消息队列产生 \ No newline at end of file diff --git "a/Java/mianjing/my\344\272\254\344\270\234.md" "b/Java/mianjing/my\344\272\254\344\270\234.md" new file mode 100644 index 00000000..fe667979 --- /dev/null +++ "b/Java/mianjing/my\344\272\254\344\270\234.md" @@ -0,0 +1,56 @@ + +第一个offer,虽然我记录的比较晚。 +因为我在等字节的意向书 + +时间:7月14日 10:30 时长:40min + +小插曲:和面试官聊了北京和成都的自我感受 + +1. String、StringBuilder、StringBuffer的区别 +2. String不可变的优点 +3. 装箱和拆箱的区别 +4. Integer和int有哪些不同 +5. 重载和重写的区别 +6. 接口和抽象类的区别 +7. ArrayList和LinkedList的区别 +8. HashTable和HashMap的区别 +9. 有哪些同步方法,讲一下 +10. 线程的创建方式 +11. 线程池的原理 +12. 简单说一下Spring +13. 简单说一下Dubbo +14. MQ的特点 +15. 为什么用RocketMQ +16. 说一下Redis的分布式锁 +17. 讲一下RedLock算法 +18. 解决消息幂等的方案 +19. SSO是什么? +20. 说一下Token和Redis交互的过程 +21. Dubbo指定特定的客户端方案 +22. Dubbo的负载均衡都有哪些 +23. 实现一下其中一个负载均衡 + + +时间:7月16日 10:00 时长:24min + +1. 自我介绍 +2. HashTable和HashMap的区别 +3. 项目主要做什么 +4. 班车到点更改状态的业务逻辑 +5. 未来发展 +6. 反问 + +时间:7月21日 14:20 时长:20min + +hr + +1. 自我介绍 +2. 你觉得自己有什么吸引人的地方 +3. 手里还有其他的offer嘛? +4. 对成都和北京有想法? +5. 对自己将来有什么打算? +6. 反问 + +时间:7月31日 17:30 + +已获offer \ No newline at end of file diff --git "a/Java/mianjing/my\344\275\234\344\270\232\345\270\256.md" "b/Java/mianjing/my\344\275\234\344\270\232\345\270\256.md" new file mode 100644 index 00000000..89b4cee5 --- /dev/null +++ "b/Java/mianjing/my\344\275\234\344\270\232\345\270\256.md" @@ -0,0 +1,24 @@ +一面 +时间:8.27 19:00 时长:45min + +1. 自我介绍 +2. 谈一谈Java的面向对象的特性 +3. 谈一谈多态 +4. 讲一讲并发的同步方法 +5. 讲一下synchroniezd和lock的区别 +6. 什么是乐观锁和悲观锁 +7. juc都有哪些? +8. AQS的底层原理 +9. MySQL的索引是什么? +10. 如果优化慢查询 +11. 如何优化数据库 +12. jdbc的线程池 +13. MySQL,Mongodb和ES各自的场景 +14. 讲一下缓存穿透,缓存雪崩以及方案 +15. 谈一谈SpringIOC和AOP +16. 控制反转前后的优缺点 +17. 谈一谈微服务 +18. 谈一谈中间件 +19. MQ如何的特性 +20. 什么是幂等性,如何解决 +21. 反问 \ No newline at end of file diff --git "a/Java/mianjing/my\345\255\227\350\212\202.md" "b/Java/mianjing/my\345\255\227\350\212\202.md" new file mode 100644 index 00000000..2cca2cae --- /dev/null +++ "b/Java/mianjing/my\345\255\227\350\212\202.md" @@ -0,0 +1,174 @@ + +已获意向书 + +时间:7.15 14:00 时长:70min + +一面: + +还是挺紧张的,怕的是他的算法题,不怕其他的。 + +上来还是问了我的买姓的由来。 + +1. 聊项目(问的极其深) +2. 简单介绍一下微服务班车预约系统 +3. 为什么用Dubbo? +4. 为什么分为四个服务? +5. 什么是SSO? +6. 什么是JWT? +7. SSO、JWT和Redis登陆的过程 +8. Redis的5大结构 +9. Zset的底层结构 +10. 订单自动取消业务逻辑(key过期和延迟队列) +11. Dubbo的原理 +12. Dubbo的负载均衡 +13. Dubbo的SPI源码 +14. Redis的分布式锁 +15. RedLock算法 +16. RocketMQ的事务最终一致性 +17. RocketMQ事务模型 +18. 为什么用Sentinel +19. 限流都有哪些算法 +20. 给我讲一下令牌桶 +21. 讲一下Dubbo的服务降级源码 +22. 做题:31.下一个排列 +23. 做题:全排列和全排列II +24. 写SQL:1。反正写分组查询结合条件 +25. 写SQL:2。在上题基础上加个子查询 +26. MySQL索引类型 +27. B和B+的区别 +28. 反问,好奇为什么不问我基础 + + +时间:7.15 16:00 时长:65 + +二面 + +依然追着项目扣细节 + +1. Redis的Token有没有存在安全问题?如何解决? +2. 也不知道怎么扯到https了,讲了https +3. TCP三次握手,各个细节 +4. TCP四次分手,各个细节 +5. TCP三次握手,哪个握手容易遭受到攻击 +6. TCP分手,为什么有2msl +7. 给我讲一下滑动窗口机制 +9. 给我讲一下拥塞控制机制 +10. 给我讲一下Nagle算法 +11. 哈希冲突哪有哪些解决? +12. Redis呢? +13. 写题:1,股票,你们懂得 +14. 写题:2,股票,重复购买 +15. 写题:3,股票,有交易费用 +16. 写题:4,SQL,忘题目了,分组+having+子查询一类的 +17. 平时遇到困难,如何解决? (我扯了10分钟) +18. 平时如何学习?(我扯了10分钟,你们信吗?) +19. 反问 + + + + +等面完,再放新一轮的字节个人面经 + +时间:7月23日:14:00 60min +1. 自我介绍 +2. 介绍项目(40min) +3. 我讲的很细,不知道为啥,二面我没讲那么细,可能累了 +4. TCP所有(10min) +5. HTTPS所有(5min) +6. 写题:链表倒数第k个结点 +7. 反问 + +二面,自我感觉凉了,有几个不会 + +时间:7月27日:14:00 45 min +1. 自我介绍 +2. 介绍项目(5min) +3. 谈谈多态 +4. 说一下多态的底层 +5. 说一下方法表都存什么? +6. 说一下每个类都有一张方法表吗?(这里卡壳了) +7. ConcurrentHashMap和HashMap的区别 +8. 介绍一下Java的锁 +9. 谈谈synchonized优化 +10. 讲一下锁升级 +11. 谈一下异常 +12. 讲一下erorr和exception的区别 +13. 讲一下受检异常和非受检异常 +14. 说一下RuntimeException都有哪些 +15. 说一下如何处理异常 +16. 说一下栈溢出 +17. 说一下堆溢出 +18. 为什么无限递归方法会爆栈? +19. 说一下栈都有什么? +20. 说一下局部变量表都有什么? +21. 谈一下JVM结构 +22. 方法区存什么? +23. 方法区会溢出不? +24. 类元数据哪些会溢出? +25. 进程和线程的区别 +26. 进程通信方式 +27. 内存共享的实现原理 +28. 写题:迭代二叉树的深度 +29. 反问 + + +最后一轮字节 + +一面 + +时间有点长了, 记不清楚了 +1. 自我介绍 +2. 简要介绍一下项目 +3. RocketMQ事务模型 +4. NameServer如何知道broker挂掉了 +5. 怎么防止消息丢失? +6. 谈一谈原子性 +7. 谈一谈Java的锁 +8. HTTP和HTTPS的区别 +9. HTTPS的连接过程 +10. 中间人是什么? +11. 什么是对称加密和非对称加密? +12. CA认证过程? +13. DNS解析过程? +14. 先判断hosts?还是先判断chrome的缓存?更改hosts,chrome缓存失效不? +15. 写题:0~n-1中缺失的数字(两种方法) +16. 反问 + +时间:7月29日:16:30 60 min + +二面 + +1. 自我介绍 +2. 简要介绍一下项目 +3. 多态的原理? +4. JVM的内存模型? +5. 讲一讲volatile? +6. CAS的底层原理 +7. TCP的三次握手 +8. TCP的四次分手 +9. DNS解析过程 +10. URL请求到渲染的过程 +11. 进程调度都有哪些 +12. 进程和线程的区别 +13. Android、React Native和Flutter +14. 写题:二叉搜索树的后序遍历序列 +15. 反问 + +时间:7月31日:14:00 65 min + +三面 + +1. 自我介绍 +2. 介绍项目(40min)害,没忍住 +3. 说一下HashMap(说了很久,7 8分钟的样子) +4. JVM内存模型 +5. 多态的原理 +6. Vue、Android、React Native和flutter +7. 写题:25. K 个一组翻转链表 +8. 反问 + +有些不记得了 + +时间:8月6日:15:30 + +已获意向书 \ No newline at end of file diff --git "a/Java/mianjing/my\346\213\233\351\223\266.md" "b/Java/mianjing/my\346\213\233\351\223\266.md" new file mode 100644 index 00000000..f43d19c3 --- /dev/null +++ "b/Java/mianjing/my\346\213\233\351\223\266.md" @@ -0,0 +1,63 @@ +时间:6月29日 16:32 时长:20min + +1. Mybatis的依赖pom包 +2. maven如何解决循环pom依赖 +3. Java的8大基础类型和所占字节 +4. 类加载器 +5. Redis的缓存穿透和缓存雪崩 +6. Redis的key键过期事件 +7. Redis的延迟队列 +8. RocketMQ的消息最终一致性 +9. 分布式事务(cap,2pc,3pc,tcc) +10. 平时如何学习技术 +11. 未来发展 + + + +时间:7月8日 15.55 时长:45min左右 + +1. 介绍集合 +2. list的并发问题 +3. set是否有序?去重原理?如何有序? +4. 异常机制 +5. 线程的5大状态 +6. 线程的创建方式 +7. 线程池原理 +8. 谈谈双亲委派机制 +9. 垃圾回收算法介绍一下 +10. 谈谈Spring +11. 前端发来一条请求,Spring是怎么处理的(我讲了计算机网络的各个步骤和SpringMVC,讲很细) +12. Redis的OOM溢出原因和解决方案 +13. Redis的缓存穿透 +14. 为什么用RocketMQ,为什么不用其他的MQ? +15. RocketMQ的分布式事务模型 +16. Dubbo的原理 +17. 反问 + +时间问题,没让我手撕代码(手撕代码紧张的一批 + + + +时间:7月14日 17.40 时长:40min + +女面试官,我还有这种运气?碰见女面试官!!!! ,简直不要太爽 + +1. 讲一波买姓的由来(居然对我的姓氏这么感兴趣。。。 阔以),,卡卡,讲了10分钟,从元朝baba的讲到现代 +2. 项目的由来? +3. 为什么要做微服务? +4. 为什么用Dubbo? +5. 为什么分为四个服务? +6. 谈谈Spring cloud +7. 为什么项目里面自己没有用多线程? +8. 哪些用了多线程? +9. 在你遇到什么困难的情况下,为什么用了Redis? +10. 为什么用RocketMQ? +11. 为什么不用2pc、3pc、tcc? +12. 你1个多月做这个项目,代码量不少吧?而且头一次做,怎么做完的?(我熬夜做完的。。,。。。。。。。。。) +13. 项目上线了吗?用什么方案部署上的?了解热部署吗? +14. 为什么没有实习? +15. 项目不问了, 来, 给我讲一下GC的所有垃圾回收器 +16. G1什么版本的时候出来的。。。。。。。。。。 +17. 给我讲讲Redis的五大结构的底层结构,尤其是跳跃表 +18. 有什么想问的?(我今天表现如何?) + diff --git "a/Java/mianjing/my\347\214\277\350\276\205\345\257\274.md" "b/Java/mianjing/my\347\214\277\350\276\205\345\257\274.md" new file mode 100644 index 00000000..b25fb388 --- /dev/null +++ "b/Java/mianjing/my\347\214\277\350\276\205\345\257\274.md" @@ -0,0 +1,17 @@ +一面: +时间 8.28:15:00 时长50min + +1. 自我介绍 +2. 介绍一下项目 +3. MySQL的索引 +4. 慢查询优化 +5. Redis的缓存穿透和雪崩 +6. Dubbo的负载均衡策略 +7. 分布式一致性哈希的原理 +8. zk和redis实现分布式锁的原理 +9. MQ的特性 +10. RocketMQ事务消息的模型 +11. 如何维持幂等性 +12. final的关键字的作用 +13. final关键字修饰引用类型,那么在GC有什么特点 +14. 写题:非递归的对称二叉树 diff --git "a/Java/mianjing/my\347\224\250\345\217\213.md" "b/Java/mianjing/my\347\224\250\345\217\213.md" new file mode 100644 index 00000000..cfad3280 --- /dev/null +++ "b/Java/mianjing/my\347\224\250\345\217\213.md" @@ -0,0 +1,15 @@ + +时间:7月21日 11:00 时长:35min + +1. 自我介绍 +2. 介绍一下项目 +3. 讲一下为什么写这个项目,初衷? +4. 为什么用dubbo +5. 为什么用MQ +6. 讲一下分布式事务 +7. 如果下游服务宕机了怎么办? +8. 讲一下分布式锁 +9. 讲一下RedLock算法 +10. 讲一下zk的分布式锁的实现 +11. 反问 + diff --git "a/Java/mianjing/my\347\231\276\345\272\246.md" "b/Java/mianjing/my\347\231\276\345\272\246.md" new file mode 100644 index 00000000..bc27e2ae --- /dev/null +++ "b/Java/mianjing/my\347\231\276\345\272\246.md" @@ -0,0 +1,144 @@ + +一面 +时间:8.12 12:00 时长:110分钟 + + +1. 自我介绍 +2. 项目介绍(20min) +3. 为什么采用微服务架构? +4. 为什么用Dubbo? +5. 为什么用Redis? +6. 谈一谈Redis的持久化 +7. Redis与MySQL双写一致性方案 +8. 谈一谈zk? +9. zk如何实现分布式锁? +10. zk的leader选举过程 +11. zk的zab算法讲一下 +12. ArrayList和LinkedList的区别 +13. ArrayList的add源码 +14. 讲一下生产者消费者模型(讲了三种方案) +15. 生产者为什么用while? +16. 说一下synchronized的修饰范围以及锁的部门? +17. 了解volatile吗?都有哪些特性?分别举例子 +18. JMM内存模型 +19. JVM运行时区域 +20. 堆、方法区和虚拟机栈分别是什么?存了什么? +21. 方法区的版本变化以及内部常量池的变化? +22. 堆为什么要分成新生代和老年代 +23. 新生代为什么又分为Eden、s0和s1 +24. 标记算法? +25. 可达性分析工作原理?哪些可以作为GC Roots根 +26. 虚拟机栈局部变量表存什么? +27. class加载过程讲一下? +28. new对象,发生了什么过程? 讲一下 +29. 写一道SQL题,不记得了,大致还要用到join和分组求和。 +30. left join right join inner join的区别 +31. TCP三次握手 +32. 写一道题:求一个数组的所有递增子序列。 +33. 反问 + +总结:我倾其所有,想睡个午觉。 + +```java +import java.util.ArrayList; +import java.util.List; + +public class Main { + + static List> sub = new ArrayList<>(); + static List> ret = new ArrayList<>(); + + public static void main(String[] args) { + int[] nums = {4, 6, 9, 8, 8}; + subsets(nums); + isInc(); + for (List list : ret) { + System.out.println(list.toString()); + } + } + + public static void isInc() { + for (List list : sub) { + for (int i = 1; i < list.size(); i++) { + if (list.get(i) < list.get(i - 1)) + break; + if (i == list.size() - 1) + ret.add(new ArrayList<>(list)); + } + } + } + + public static List> subsets(int[] nums) { +// Arrays.sort(nums); + for (int i = 2; i <= nums.length; i++) { + dfs(0, nums, new ArrayList(), i, new boolean[nums.length]); + } + return sub; + } + + private static void dfs(int start, int[] nums, ArrayList list, int size, boolean[] marked) { + if (list.size() == size) { + sub.add(new ArrayList<>(list)); + return; + } + for (int i = start; i < nums.length; i++) { + if (i != 0 && nums[i] == nums[i - 1] && !marked[i - 1]) + continue; + marked[i] = true; + list.add(nums[i]); + dfs(i + 1, nums, list, size, marked); + list.remove(list.size() - 1); + marked[i] = false; + } + } +} +``` + +```java +import java.util.*; + +public class Main { + + static Set> sub = new LinkedHashSet<>(); + static List> ret = new ArrayList<>(); + + public static void main(String[] args) { + int[] nums = {4, 6, 9, 8, 8}; + subsets(nums); + isInc(); + for (List list : ret) { + System.out.println(list.toString()); + } + } + + public static void isInc() { + for (List list : sub) { + for (int i = 1; i < list.size(); i++) { + if (list.get(i) < list.get(i - 1)) + break; + if (i == list.size() - 1) + ret.add(new ArrayList<>(list)); + } + } + } + + public static Set> subsets(int[] nums) { + for (int i = 2; i <= nums.length; i++) { + dfs(0, nums, new ArrayList(), i); + } + return sub; + } + + private static void dfs(int start, int[] nums, ArrayList list, int size) { + if (list.size() == size) { + sub.add(new ArrayList<>(list)); + return; + } + for (int i = start; i < nums.length; i++) { + list.add(nums[i]); + dfs(i + 1, nums, list, size); + list.remove(list.size() - 1); + } + } +} +``` \ No newline at end of file diff --git "a/Java/mianjing/my\347\275\221\346\230\223.md" "b/Java/mianjing/my\347\275\221\346\230\223.md" new file mode 100644 index 00000000..a1550598 --- /dev/null +++ "b/Java/mianjing/my\347\275\221\346\230\223.md" @@ -0,0 +1,45 @@ +一面 +时间:8.13 14:50 时长:35min + + +1. 自我介绍 +2. Redis的集群如何管理,如何选举 +3. G1垃圾收集器如何查找块的 +4. Using Index和Using Where的区别 +5. 如何分库分表? +6. int[10]和int[11]的区别 +7. int[10]和int[10][10]的区别 +8. newarray和anewarray的区别 +9. RocketMQ的消息事务模型原理 +10. RocketMQ的存储为什么那么块? +11. 熟悉分布式事务嘛?(熟悉这个词语吓到我了,直接说了解) +12. 谈一谈分布式如何优化 +13. 限流算法都有哪些? +14. 平时如何学习的? + + +二面 +时间:8.14 19:20 时长:35min + +1. 自我介绍 +2. 谈一谈深度学习 +3. 简单聊以下项目 +4. 为什么用分布式事务 +5. 为何选择Java +6. 如何学习Java +7. Java都看过哪些源码 +8. 谈一谈HashMap +9. 谈一谈ConcurrentHashMap +10. 为何选择网易 +11. JavaSPI机制 +12. Spring SPI机制 +13. 如何是你,你会怎么设计SPI +14. 怎么学习Dubbo和RocketMQ的 +15. 从源码上讲以下Spring bean的初始化前后所有过程(我没讲循环依赖,可惜了) +16. AOP源码 +17. Spring事务原理,以及失效 +18. 如何也让受检异常,事务不失效 +19. for和iterator的区别,并且for会出什么错误 +20. 有没有看过iterator的源码 +21. 反问 + diff --git "a/Java/mianjing/my\350\205\276\350\256\257.md" "b/Java/mianjing/my\350\205\276\350\256\257.md" new file mode 100644 index 00000000..cf5b6a9e --- /dev/null +++ "b/Java/mianjing/my\350\205\276\350\256\257.md" @@ -0,0 +1,44 @@ +一面 +时间:8.20 16:00 时长:130min + +当时问我熟悉不熟悉计算机网络和操作系统之类的,我说的是了解。。。 +那边技术栈不用java的,基本问的java不是特别深入 + + +1. hashmap的底层结构 +2. hashmap的一些参数介绍 +3. hashmap在1.8版改进的地方 +4. hashmap什么情况下查询复杂度高 +5. 红黑树的特点 +6. 红黑树的复杂度 +7. 并发安全的map类 +8. 都有哪些排序算法 +9. 介绍快排 +10. 介绍堆排 +11. 手写单例 +12. 谈一谈volatile +13. 谈一谈synchroinzed +14. mutex如何实现的?过程是什么? +15. 如果是你,你如何实现mutex? +16. 谈一谈gc +17. JVM如何调优 +18. cap是什么 +19. zab算法 +20. 实现paxos算法的工程还有哪个?raft... +21. 哪些中间件用了raft? +22. 为什么zk不用raft? +23. paxos的有哪些缺点 +24. mongodb的底层结构是什么 +25. mysql宕机了,数据怎么办? +26. 两个客户端去修改同一个id的字段,mysql会发生什么? +27. 写一道题:股票 +28. select和epoll的区别 +29. Redis实现分布式锁的过程 +30. 写一道题:翻转序列,求前n项和(我用了个简单方法,但复杂度高,后来面试管提示我用数学方法,他挺好的,就是我太笨了。) +31. 线程和协程的区别是什么? +32. 如果是你,你如何设计协程 +33. 反问 + +总结:算法题写的不好,估计是凉了,我都不好意思耽误面试官那么长时间。 + +大概率凉凉 \ No newline at end of file diff --git "a/Java/mianjing/my\350\264\235\345\243\263.md" "b/Java/mianjing/my\350\264\235\345\243\263.md" new file mode 100644 index 00000000..0793ea02 --- /dev/null +++ "b/Java/mianjing/my\350\264\235\345\243\263.md" @@ -0,0 +1,33 @@ +一面 +时间:8.15 12:00 时长:40min + +1. 写题:数组中的第K个最大元素 +2. Object所有的方法以及作用 +3. CMS收集器的原理和过程及优缺点 +4. HashMap和ConcurrentHashMap的区别(扯细一点) +5. 项目(20min,我太熟悉了) +6. 反问 + + +二面 +时间:8.15 14:00 时长:40min + +1. 自我介绍 +2. 项目(30min,这里我讲的很熟悉,把所有为什么都讲出来了,可以看我git) +3. 数据库结构如何优化 +4. Spring IOC流程 +5. 如何解决循环依赖(按照源码讲) +6. 反问 + + +hr +时间:8.15 15:00 时长:25min +1. 自我介绍 +2. 怎么学习的 +3. 最近看的哪一本书,感受如何 +4. 别人对你如何评价的 +5. 老家在哪 +6. 如何选城市的? +7. 面了几家公司,都有哪些offer(这里,永远都不知道如何回答) +8. 你是怎么看待薪资的 + diff --git "a/Java/mianjing/\344\272\254\344\270\234\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\344\272\254\344\270\234\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..242be943 --- /dev/null +++ "b/Java/mianjing/\344\272\254\344\270\234\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,133 @@ +# 京东所有问题汇总 + +## Java + +- Java虚拟机:2 +- Java虚拟机发生fullGC会造成什么?:2 +- Hashmap了解吗,说一下:1 +- SpringMVC处理请求的流程:5 +- 序列化及使用场景:3 +- 常用的集合类:1 +- ArrayList和LinkedList的区别:3 +- 动态代理如何实现,cglibd动态?如何实现AOP?:7 +- SpringIoC:4 +- Spring的事务管理怎么实现的:3 +- SpringMVC的controller怎么传对象参数,你对这些注解的底层了解有多少:1 +- 抽象类和接口的区别:2 +- 多线程的实现方式:4 +- 异常处理中return的执行顺序:1 +- 代码块的初始化执行顺序:1 +- spring的特性:1 +- hashmap,hashtable和concurrenthashmap区别:8 +- 线程的五个状态:2 +- 线程池:7 +- Jvm运行时数据区:5 +- 内存分配:2 +- 垃圾回收:4 +- 写一个单例模式:2 +- String StringBuilder stringbuffer:2 +- 线程与进程:1 +- oom怎么出现呢,内存泄漏:2 +- spring bean的生命周期:2 +- Spring bean 单例:1 +- HashSet:1 +- 四大引用:1 +- CMS:5 +- G1:4 +- volatile:3 +- sleep 和 wait 的区别:1 +- CAS:2 +- 哪些对象是可能会被 GC 回收的?:1 +- 类加载机制:2 +- springboot优点:1 +- jmm模型:1 +- 八种数据类型:1 +- integer缓冲机制:1 +- autowired和resource的区别:1 +- 说一下nio:1 +- synchronized:1 +- ReentrantLock:1 +- AQS:1 +- 工厂模式:1 +- ThreadLocal:1 +- happens-before:1 + + + + + +## 分布式 +- 分布式锁了解吗?不了解,说了分布式系统设计原则:1 +- 分布式事务:1 + +## MySQL +- 数据库了解吗,聊一下?:1 +- 知道MVCC吗,聊一下:4 +- 数据库引擎相关:5 +- 索引类型:3 +- 乐观锁和悲观锁:1 +- 数据库优化:2 +- 索引的最左匹配原则:3 +- 事务的隔离级别,mysql默认是重复读:6 +- 数据库连接池种类:1 +- MySQL的事务:2 +- mysql最大连接数有了解过吗:1 +- mysql cpu过高怎么排查呀:1(error log) +- binlog存的什么:1 +- 谈谈 explain 结果中⽐较关键的指标,以及指标的含义:1 +- 聚簇索引和非聚簇索引:1 +- B和B+树:3 + + + + +## Redis +- 为什么用Redis:1 +- Redis为什么快?:1 +- Redis的常用数据结构?:3 +- Redis的事务:1 +- redis主从复制怎么完成的:1 +- 跳跃表:1 +- redis的淘汰机制:1 +- Redis原子性原理:1 + + + +## 算法 +- 怎么判断一个数是不是2的n次幂?n&(n-1) +- 怎么求根号2?(二分):1 +- 快排的实现原理:1 +- 如何链表翻转,如何判断链表有环:2 +- 3sum:1 +- 讲讲红黑树:1 +- 删除链表多个值:1 +- 包含min的栈:1 +- 爬楼梯:1 + + +## ES +- 为什么用ES,ES为什么搜索快?:1 +- 为什么数据不全部存储在ES里面?:1 +- 合并两个有序数组:1 + +## MyBatis +- mybatise中#和$的区别:7 +- mybatis分页:1 +- mybatis缓存机制:1 + +## 计算机网络 +- TCP为什么要三次握手:4 +- Tcp,udp区别:1 +- Get,post区别:1 +- 四次分手:2 +- http和https的区别:3 +- TCP/IP 如何保证可靠性:1 +- http常见请求头:1 + +## 其他 +- gbk和utf-8:2 + +## Dubbo和Zk +- 大致说了一下zookeeper的leader过程;:1 +- 关于dubbo和zookeeper的服务架构,这个画出dubbo官网的服务注册订购调用的结构图;:1 +- 服务在多台主机怎么处理:负载均衡,例如轮训和hash等;:1 \ No newline at end of file diff --git "a/Java/mianjing/\345\255\227\350\212\202\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\345\255\227\350\212\202\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..dfcb2070 --- /dev/null +++ "b/Java/mianjing/\345\255\227\350\212\202\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,140 @@ +# Java的哈 + +## 算法(好多树) +- 每K个节点翻转链表(链表k个一旋转):2 +- 二叉搜索树转链表:1 +- 负载均衡算法:1 +- 排序算法哪些是稳定的:1 +- 二叉树求和:1 +- 序列化和反序列化二叉树:1 +- 如何判断一颗树是否是完全二叉树:1 +- 求数组的极值点:1 +- 最大连续子序列:1 +- 回文链表:1 +- 链表反转:1 +- 对称二叉树:1 +- 二叉树右视图:1 +- 移动零:1 +- 给定链表,确定中间数:1 +- 链表奇数位升序、偶数位降序:1 +- 二叉树的左视图:1 +- 一致性hash:1 +- 旋转数组的最小值:1 +- 判断两个链表是否交叉?:1 +- 三数之和:1 +- 两个链表相加:1 +- 给一个序列判断是不是二叉搜索树的后序遍历:1 +- 单词翻转 +- 二叉树层序遍历:1 +- 最长连续子串:1 +- 岛屿:1 +- 无重复字符的最长字串:1 +- 设计一个数据结构 list: rpush rpop lpush lpop index 五种方法的时间复杂度均为 O(1),数据量上限是100w(我回答使用双端队列+hashMap, 面试官说可以用两个数组实现) +- 求集合的子集:1 +- 字符串全排列:1 + +## Java(所有谈谈的,老哥我最喜欢了,狂吹) +- 常见的GC回收器,越详细越好:1 +- SpringMVC的请求过程:1 +- 常见的GC回收器,越详细越好:1 +- 线程池(所有你知道的),原理尽量详细些:2 +- HashMap底层实现:1 +- concurrenthashmap:1 +- ConcurrentHashMap的扩容机制:1 +- LinkedHashMap 底层数据结构?使用场景? :1 +- Spring AOP怎么实现,围绕bean生命周期去讲:1 +- 三大特性:1 +- 谈谈多态:2 +- 接口和抽象类的区别:1 +- 谈谈集合:1 +- Arraylist和LinkedList的区别:1 +- Hashmap底层:1 +- ==跟equals的区别:1 +- 有界无界队列:1 +- 线程的创建方法:1 +- 深拷贝、浅拷贝:1 +- sychronized:1 +- GC算法:1 +- JVM内存结构:1 +- 谈谈cas:1 +- 谈谈JVM:1 +- JDK动态代理:1 +- 类加载的过程:1 +- 说说Object类,作用,有什么方法:1 +- Treeset Treemap的底层实现:1 +- volatile:1 +- 谈谈反射:1 + +> 字节问的Java没啥难度,简单的一批 + +## 计算机网络 +- https通信过程:1 +- https加密过程,怎么判断证书的有效性:1 +- tcp、udp区别 +- tcp(所有):1 +- TCP拥塞控制:1 +- TCP滑动窗口:1 +- http 头部有哪些key:1 +- http状态码有哪些:1 +- DNS服务器怎么处理域名请求的,了解原理吗:1 +- GET、POST:1 +- HTTP2.0有哪些改动:1 +- 路由器怎么工作的:1 +- 七层协议讲一下:1 +- http是否无状态?如何有状态?session和Cookies的区别:1 + + +## MySQL +- 聚簇索引和非聚簇索引底层实现:1 +- 隔离级别:2 +- mysql在业务中怎么实现乐观锁:1(MVCC各种吹) +- MVCC原理,和for update有什么区别:1 +- Innodb\myisam区别:1 +- 谈谈B+树前世今生以及所有:1 +- ACID:1 +- 联合索引底层结构:1 +- SQL里where having分别是做什么的,有什么区别,什么时候用:1 +- MySQL索引类别:1 +- 左连接和内连接的区别:1 +- 谈谈binlog和redolog :1 + + +题: +- 获取所有课程得分均大于80分的学生的平均得分:2 + +## Redis +- 分布式锁怎么实现,Redis加锁过程:1 +- Redis的setnx有哪些注意点,比如宕机时会发生什么:1 +- zset底层原理:(吹它的跳跃表和压缩列表):3 +- Redis中的哨兵:1 +- 谈谈Redis集群 Redis Cluster,以及主从复制原理:1 +- redis的hashmap和java的hashmap有何异同:1 +- 持久化策略:1 +- 利用redis的可重入锁如何设计:1 +- redis分布式锁是否公平锁? + +## 操作系统 + +- 进程间通信有哪些,各个优缺点:2 +- select/poll/epoll:2 +- 用户态、内核态:1 +- 信号量 同步 互斥的区别:1 +- 页面的置换算法:1 +- 进程间的同步方式用过哪些:1 + +## linux +- linux如何查找CPU故障 + +## RocketMQ +- RocketMQ有哪些组件:1 + + +## Mybatis +- mybatis的缓存:1 + +## 项目 +- Jmeter压测的时候为什么会丢包 + + + +> 感觉字节基本没有Spring,可惜了,问的Java也比较基础!! 如果有中间件的话,多熟悉熟悉。 其次就是计算机网络和操作系统的知识多熟悉熟悉,最后就是大家都知道的算法!!! \ No newline at end of file diff --git "a/Java/mianjing/\346\213\233\351\223\266\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\346\213\233\351\223\266\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..2031ec63 --- /dev/null +++ "b/Java/mianjing/\346\213\233\351\223\266\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,134 @@ +# 招银所有问题汇总 + +## Java +- countdownlatch:1 +- submit/execute:1 +- Java多态实现原理:3 +- 线程池:3 +- CAS:2 +- synchronized:3 +- lock:4 +- 多个线程轮流生产a-z :1 +- 生产者-消费者模型:1 +- SpringIoc:2 +- SpringAOP:2 +- Spring的bean生命周期:1 +- Spring的事务如何控制:3 +- Spring的事务传播机制:3 +- 深浅拷贝:1 +- HashMap:6 +- concurrenthashmap:2 +- HashSet:1 +- java的命名规则:1 +- java异常:2 +- 举例9个运行时异常:2 +- Spring中的设计模式:1 +- 重载和重写有什么区别:2 +- SpringMVC:1 +- 抽象类和接口的区别:3 +- Java8新特性:1 +- ==、equlas和hashcode:2 +- Java反射:2 +- 线程的同步方法:3 +- 几种方法创建线程:3 +- 线程的几种状态:1 +- 线程如何从运行到挂起:1 +- 动态代理:2 +- Java类加载如何进行:1 +- java类加载的几种状态:1 +- java jvm中GC如何进行线程是否需要回收的判断:1 +- 可达性分析如何做:1 +- 什么代码能够使jvm进行gc:1 +- Java泛型:2 +- 封装、继承和多态:3 +- JVM模型:2 +- static:1 +- String:1 +- Java集合有哪些:1 +- ArrayList与LinkedList使用场景的区别:1 +- ArrayList动态扩容:1 +- Spring 和Spring Boot的区别:1 +- ThreadLocal:1 +- 生产者-消费者:1 +- io与nio的区别:1 +- nio的实现原理:1 +- nio中selector的状态有哪些:1 +- 手写匿名类:3 +- java内存溢出有哪些情况,如何排查:1 +- 工厂模式 + + +## MySQL +- 数据库存储引擎MyISAM和Innodb的区别:3 +- Innodb什么时候是行锁,什么时候是表锁:1 +- ACID:2 +- 隔离级别:3 +- 数据库删除数据需要的方法,哪种最快,快在哪里:1 +- mysql怎么实现分页:1 +- 数据库有哪些数据类型?:1 +- varchar和char的区别?:1 +- 数据库的索引有哪些?:1 +- b+树索引的叶子结点放的什么?:1 +- 联合索引的最左前缀原则怎么实现的?:1 +- 如何判断一个SQL语句走不走索引?explain:1 +- 遇到慢查询如何处理:1 +- 数据库的三大范式:1 +- b树与b+树的区别:1 +- 持久化:2 +- 数据库锁:3 +- 如何防止sql注入:1 +- MVCC:1 +- MySQL主从复制:1 + + + +## Redis +- 持久化:2 +- 模型: 1 +- 分布式锁的实现:2 +- 缓存和数据库一致如何保证:1 +- redis集群至少需要几个节点:1 +- 为什么需要Redis:1 +- 数据结构:2 +- 缓存击穿、缓存穿透和缓存雪崩:1 + + +## 计算机网络 +- osi七层和tcp/ip四层:1 +- tcp在哪一层:1 +- tcp三次握手,为什么需要第三次握手:1 +- http和https的区别?:2 +- https协议怎么实现?:1 +- 为什么要先用非对称算法加密,后面又用对称算法?:1 +- http请求头有哪些:1 +- get和post的区别:1 + + +## 操作系统 +- 操作系统有哪几个部分组成:1 +- 内存管理:1 +- malloc方法的过程:1 +- 虚拟内存:1 +- 进程通信:1 + +## 分布式事务 +- emmmm 错过了啥 + +## zookeeper +- 分布式锁的实现:1 + +## mybatis +- mybatis比单纯jdbc好在哪里:1 +- mybatis一级二级缓存 +- `#`和`$`的区别 + +## 数据结构与算法 +- 两个队列实现栈 +- 单例 +- 链表环 +- 怎么实现手机号中间四位用星号表示? + +## 微服务 +- 微服务架构的服务粒度怎么确定,服务怎么通信? + + diff --git "a/Java/mianjing/\346\213\274\345\244\232\345\244\232\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\346\213\274\345\244\232\345\244\232\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..ce647f18 --- /dev/null +++ "b/Java/mianjing/\346\213\274\345\244\232\345\244\232\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,118 @@ +# 拼多多所有问题汇总 + +## java +- Java特性:1 +- HashMap:5 +- ConcurrentHashMap:4 +- 乐观锁和悲观锁:1 +- CAS原理:4 +- 动态代理:2 +- 静态代理:1 +- 线程池的原理和运用:3 +- AQS:1 +- spring优化的自动配置:1 +- Spring的AOP:3 +- SpringBoot和SpringMVC区别在哪:1 +- SpringMVC原理:1 +- 单例模式:2 +- 工厂模式:1 +- StringBuilder和Stringbuffer:2 +- ArrayList和LinkedList:1 +- wait()和sleep()的区别:1 +- NIO和BIO的区别:1 +- java的异常:1 +- jvm垃圾回收:2 +- 可重入锁:1 +- Jvm内存区域划分:2 +- 程序计数器的作用:1 +- 本地方法栈和虚拟机栈的区别:1 +- Gc全流程:1 +- Gc算法:1 +- 一致性hash了解吗?:1 +- 同步和异步的区别:1 +- 非阻塞io和阻塞式io的区别:1 +- Volatile:1 +- 死锁:1 +- `A a =new A()`的过程:1 +- 类加载机制:1 +- OSGI加载:1 + +## MySQL +- 组合索引的字段顺序:2 +- MySQL的MVCC:1 +- B+树和B树:2 +- 最左原则:1 +- 数据库事务:1 +- 数据库分库分表:1 +- 数据库索引:1 +- MySQL一条语句的执行过程,解析过程?查询缓存?怎么判断是否命中?:1 +- 数据库两种存储引擎的区别:1 +- 事务隔离:1 +- 索引:1 +- 数据库连接池用的哪个? 数据库连接是线程安全的吗?:1 + + +## Redis +- 缓存穿透、缓存击穿、缓存雪崩都指什么?三者有什么区别,和缓存失效有什么区别:1 +- Redis分布式锁的实现:3 +- redis的数据类型, 使用场景:1 +- 怎么保证Redis的高可用:1 +- redis的ttl指令底层实现,redis过期策略:1 +- redis数据类型,跳表实现:1 +- Setnx加锁的原理:1 +- 怎么解除分布式锁?:1 +- 布隆过滤器了解吗?:1 +- 怎么实现lru?: +- Redis 线程安全,Redis为什么更快:1 + +## 操作系统 +- 线程和进程的区别:4 +- 分段和分页内存管理:1 +- 用户态和内核态的区别:1 + +## 分布式事务 +- 自己实现一个二阶段提交, 如何设计:1 + + + +## 计算机网络 +- 三次握手:2 +- 四次分手:2 +- TCP拥塞控制算法:1 +- TCP和UDP的区别:1 +- HTTPS的实现:2 +- cookie和session:1 +- 网络7层模型:1 +- Ip寻址流程:1 +- Tcp和udp的区别:1 +- http如何保持连接:1 +- 如果不用http,如何保持连接:1 +- cookie和token的区别:1 +- rpc、http、消息队列三者区别:1 +- 访问一个网站的过程:1 + + +## 算法 +- 加密算法:1 +- 找出一个字符串的最大合法十进制数:1 +- 环形打印数组:1 +- 实现一个可以解决冲突的HashMap,用的是比较简陋的hash bucket + 双链表:1 +- 判断单链表有没有环,找到环入口点:1 +- 字符串反转:1 +- 如何选择第k大的数:1 +- 任务调度:1 +- 用给定的数组实现队列,包含push和put方法,要求考虑多线程情况:1 +- 堆排序:1 +- 归并排序: +- 二叉树非递归前序遍历:2 +- 三个数的最大乘积:1 +- 一亿个数找出top 100:1 + + + +## MQ +- 消息队列有什么好处:1 + + +## linux +- linux内存回收策略:1 \ No newline at end of file diff --git "a/Java/mianjing/\347\214\277\350\276\205\345\257\274\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\347\214\277\350\276\205\345\257\274\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..ed4904b9 --- /dev/null +++ "b/Java/mianjing/\347\214\277\350\276\205\345\257\274\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,147 @@ + + +## 算法 + +- 括号匹配(lc:22):1 +- 对角线遍历(lc:498) +- 累加数(lc:306) +- 二叉树中序遍历的后继节点 +- 判断—棵二叉树是否是对称的 +- 有若干[30,50,100]优惠券,求给定一个数值求最优的方案。例如∶价格是40=>[30]80 =>[30,50].110=>[30,30,50] +- 搜索旋转数组 +- 带精度的sqrt +- 矩阵中的最长递增路(lc:329) +- 删除单向链表中重复的数字 +- 给一个以字符串表示的非负整数num,移除这个数中的k为数字,使得剩下的数字最小(单调栈) +- 给定一个二叉搜索树,并给出他的前序序列,输出中序序列,时间复杂度O(n),并给出证明 +- 非递归中序遍历 +- 非递归后序遍历 +- 单链表代表一个大整数,给它低位加上一个数,返回加上后的链表 +- 链表压缩,a->a->a->d->d->b->z->z->null,转换为3a->2d->1b->2z->null (写了一个生成新的链表,又要求重写,在原链表上修改) +- 给一棵树,当一个节点的左节点下的两个节点都是1,或者是右节点下的两个节点都是1时,这个节点满足条件。返回所有满足查找条件的节点 +- 求树的直径 +- 判断一棵树是否为二叉平衡树 +- 大数相乘 +- 两个降序链表的第k大元素 +- 二叉树的深度 +- 两个字符串的最长公共子串 +- 给一个队列,借助两个栈,将队列中的元素排序 +- 字符串排序 首先按照长度排序,然后按照字符串排序。I am a an student.------->I a am an student +- 两个字符串是否由相同字符构成 +- N皇后 +- 升序链表转成二叉平衡搜索树 +- 给定一个字符矩阵,里边只有O和X,把所有被X包裹的O变为X +- 一个单向链表,根据value的奇偶性拆成2个链表,并且返回的两个链表是升序的 +- 数组求每一个数的下一个比它大的数字(先写了O(n2),写完想起来了O(n)的栈) +- 给一个数,求出树中祖先节点和其后代节点差值的最大值(思路对了,手撕没过) +- 整数数组拼接成最小数 +- 顺时针递增打印数组 +- 最长回文串 +- 最大正方形面积 +- 合并k个有序链表 +- 区间列表的交集(lc:986) +- 判断两个字符串是否是相似字符串,相似的含义是只允许交换一次字符串的两个位置,使得和第二个字符串相等,那么他们就是相似的。 +```java +ab, ba => true +abax, abax => true +abc, abc => false +aac, abc => false +1 字符串长度是否相等 +2 排序后字符串是否相等 +3 双指针判断相同位置不同字符的对数是否等于2 +4 不同字符为0时时,是否有重复字符 +``` +- 删除倒数第K个节点 +- Z字打印二叉树 +- 非严格递增数组1,2,3,3,3,4,5,找到目标数字所在的第一个位置,无则输出-1,如3,输出2,要求O(log n);(变种二分) +- 从l到r反转链表 +- 最小路径和 +- 输入两个数组,从两个数组中各取一个值,输出两个值的和为0的所有组合并需要去重。 +```java +输入: nums1={1 ,-1,0,0},nums2={-1,-1,0,1} ,输出:{{1,-1},{0,0}} +``` +- topK +- 求一个长度为n的无序数组中,第m大的数 topk? +- 找二叉查找树的两个节点相差的最小值(非递归中序+对比相邻节点的差) +- 二叉树剪枝 +- 二叉树指定层的节点个数 +- 双向链表奇偶位置节点拆分 +- 反转链表,迭代+递归 +- 字符串相加 +- 整数拆分 +- 给一个字符串数组,把由相同字母组成的字符串分入同组 +- 同样给了一个整型数组 [100,2,1,4,3,3,101,200] 从这个数组找到连续元素的长度,比如1,2,3,4 最长输出4。 +- 分割数组(lc:915) +- LeetCode求两个链表和,从头到尾相加即可 +- 一个链表,给定一个目标值,比目标值大的节点去到链表后面,要求不改变相对顺序 比如1 3 2 1 3 2 1 给定目标值2,返回链表1 2 1 2 1 3 33. +- 最长公共子序列 +- 判断二叉树是不是BST +- 求一个数的平方根,精确到0.01。 +- 重排链表(lc:143) +- 利用循环链表写一个队列 + + + +## Java +- 介绍一下垃圾收集器 +- g1和cms有什么区别 +- 为什么g1可以在保证用户吞吐量的情况下进行垃圾收集 +- Java中实现加锁的方式 +- sychronized及锁升级过程 +- 底层AQS实现原理 +- ConcurrentHashMap +- 线程状态 +- 线程池 +- 深拷贝,浅拷贝 +- 说说线程安全 +- Lock和Synchronized的区别 +- 逃逸分析 +- JVM内存区域,这些区域会内存溢出吗,栈什么情况下会溢出,堆呢 +- 堆区的垃圾回收机制,为什要用分代策略? +- 为什么老年代不采用复制算法? +- SpringBoot自动装配的原理 +- Spring中Aop是如何实现的? +- Cglib和JDK的动态代理有什么区别? +- Spring是怎么实现Ioc的? +- Java的反射机制了解吗? +- 反射机制中获得实例的方式有几种?分别是什么? +- getClass和forName得到的Class实例是同一个实例还是不同的实例? +- 讲一下ClassLoader? +- String,StringBuilder,StringBuffer +- HashMap + +## mysql +- mysql建立索引的原则 +- 聚簇索引和非聚簇索引区别 +- B+树结构、为什么不用B树或二叉树 +- 事务四大特性 +- 事务隔离级别 +- 用mysql实现一个分布式锁 +- MVCC具体原理 + +## 计网 +- http讲一下 +- get和post的区别 +- get是否可以有请求体 +- 手写http request报文和 http response报文 +- 四次挥手过程 +- close_wait作用 +- TCP有什么特点? +- TCP具体的可靠性指什么?怎么确保数据的正确性? +- 流量控制和阻塞控制的区别是什么? +- Socket跟其他的方式有什么不一样? +- OSI七层网络模型 +- 网络层有哪些协议? +- ICMP解释一下,有什么作用?除了判断是否能正常通信还有什么作用? + +## mq +- 如果有几百万消息堆积,如何处理 +- 如何保证消息消费的可靠性,顺序性 +- 消息队列方式有什么优缺点? +- 消息队列为什么比共享内存慢?(需要额外的复制) +- 共享内存有什么缺点?它怎么保证同步操作? + + + +## 其他 +- 令牌桶 diff --git "a/Java/mianjing/\347\231\276\345\272\246\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\347\231\276\345\272\246\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..60655d15 --- /dev/null +++ "b/Java/mianjing/\347\231\276\345\272\246\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,205 @@ + +1. 自我介绍 +2. 聊项目(亮点,难点,解决方法以及优缺点) + +## Java + +- String StringBuffer StringBuilder: 3 +- ArrayList和LinkedList的区别:2 +- HashMap、HashTable和ConcurrentHashMap:13 +- HashSet和TreeSet底层有什么区别?:1 +- LinkedHashMap:2 +- == 、 hashcode、equals:5 +- int和Integer的区别:1 +- voatile:6 +- synchronized:10 +- ReentranLock:10 +- JVM内存区域:9 +- GC垃圾回收:2 +- 举几个垃圾回收器 (CMS,G1):4 +- spring aop:4 +- spring事务:5 +- jdk代理和cglib的区别:1 +- Java反射:2 +- Java的异常:3 +- Java泛型:1 +- CAS:4 +- 线程池:5 +- JVM 如何排查OOM:1 +- java对象结构:1 +- 对象头中存储哪些内容:1 +- ThreadLocal的原理:5 +- 创建线程的方式:2 +- 公平锁和非公平锁的区别:1 +- SpringBoot:3 +- SpringMVC:1 +- SpringBoot的starter,导入pom的时候是把所有的pom引入吗:1 +- 类加载的过程:4 +- JDK、JRE、JVM:2 +- Java有哪些数据类型,分别占多少个字节?:2 +- 抽象类与接口之间的区别:3 +- 聊一下集合类:1 +- class类加载和forname加载:1 +- native:1 +- 加载类的方式有哪些;:1 +- class.forName加载类与classloader加载类有什么区别:1 +- 类加载器有哪些:1 +- 死锁:4 +- sleep与wait方法什么区别:2 +- jvm的一些参数:1 +- finalize方法的作用:1 +- 对象锁和类锁的区别?:1 +- 线程的几种状态?:1 +- 深浅拷贝区别:1 +- 静态内部类和内部类的区别:1 +- 类的加载机制:1 +- 并行并发的区别:1 +- Spring Bean构造器原理:1 +- Spring注解实现原理:2 +- Spring的IOC怎么实现:3 +- 循环依赖是怎么解决的:1 +- 同一个类中的无事务的方法A调用有事务的方法B可以生效吗,为什么。:1 +- TLB是什么。:1 +- 多线程之间的通信。:1 +- Thread里面的run和start:1 +- 序列化和反序列化:1 +- Spring中Bean的作用域:1 +- 栈溢出和内存溢出:1 +- spring的三级缓存:1 + + +## 算法 +- 红黑树的性质:1 +- 二分查找:2 +- 给定字符串判断括号是否匹配 (用栈秒了):1 +- B+树的时间复杂度:1 +- B+数:1 +- 单链表排序:1 +- lc287寻找重复数:1 +- 快排:5 +- 堆排:3 +- 一致性hash:1 +- 手撸死锁:2 +- 链表的倒数第n个:1 +- 用多线程实现互斥同步打印123,10次:1 +- 判断回文数,不用字符数组和数字求余求除法:1 +- 给若干个正整数,求出拼接出的最大数(字典序排序):1 +- 手写LRU:3 +- 两个有序链表排序:1 +- 两个字符串的最大公共子串的个数:1 +- 手撸生产者和消费者模型:1 +- 循环链表入口结点;:1 +- 两个交叉链表交叉节点;:2 +- 旋转数组求最小值:1 +- 红黑树,B-树,B+树:1 +- 反转链表:2 +- 两个文件,包含上亿个URL,只有4G内存,如何找出两个文件相同的URL:1 +- 从一个数组中选出数量超过数组长度个数一半的数:1 +- 二叉树右视图:1 +- 找最长无重复字母的子串:1 +- 三数之和:1 +- 数组中的第K个最大元素:1 +- 跳台阶问题:1 +- 判断平衡二叉树:1 +- 两个string表示的大数相乘:1 +- 两个乱序数组求交集:1 +- 两个有序数组,找合并后的中位数:1 +- 搜索旋转排序数组:1 +- 几百万数据,,怎样拿到最大的50个数:1 +- 字符串中最长回文子串的长度:1 +- 最长公共子串:1 + +## 设计模式 +- 动态代理:1 +- 单例:7 +- 观察者:1 +- 装饰模式:1 + +## 计算机网络 +- cookie和session:1 +- HTTP请求报文 +- header(举几个常见的):1 +- REST和RPC概念区别:1 +- HTTP和HTTPS的区别:2 +- HTTPS是怎么建立连接的:4 +- HTTP的状态码:1 +- TCP三次握手:7 +- TCP四次分手:5 +- tcp的拥塞控制,什么时候慢开始。:1 +- dns解析过程,劫持,污染:1 +- 七层模型:2 +- HTTP3为什么用UDP:1 +- TCP和UDP的区别:1 +- User-Agent:1 +- xss攻击:1 +- www.baidu.com哪个是根域名,顶级域名,权威域名:1 +- 说一下arp协议的过程:1 +- 知道arp欺骗吗?:1 +- get post区别:1 +- restful接口和普通http接口有什么区别?:1 + + +## redis +- redis基本数据类型:5 +- redis集群如何保证节点数据安全 (答了哨兵):1 +- redis的分布式锁:1 +- Redis的缓存雪崩,缓存穿透:2 +- redis的持久化:1 +- Redis集群如何进行数据同步;:1 +- redis单线程为什么效率还很高:1 +- redis的模型:2 +- 如何发现热点 key,删除大key的影响分析及解决:1 +- Redis 定期清理数据的策略:1 + + +## MySQL +- 数据库隔离级别:4 +- 数据库引擎:2 +- MySQL的所有锁:2 +- 分页查询语句:1 +- 500页数据查第400页很慢,原因,解决方案:1 +- 数据库的悲观锁、乐观锁:1 +- B树和B+树的区别:1 +- MyIsam与Innodb的区别;:1 +- 为何不建议使用limit;:1 +- 写一个sql语句实现查询所有成绩都大于80的学生的姓名:1 + `select sname from student a join (select course, max(score) from student group by course) b on a.course = b.course and a.score = b.score;` +- 数据库索引,聚簇索引,非聚簇索引:1 +- 三范式:1 +- ACID:1 +- 关于大表分页,如果需要查很后面的数据,怎么优化:1 +- 数据库的主从复制的优缺点;:1 + + +## Linux +- linux修改文件权限命令:1 +- 文件权限3个数字分别代表什么:1 +- linux查看进程,停止进程:1 +- 查看端口号被谁占用:1 + + +## nginx +- nginx处理http请求的过程 :1 +- nginx配置负载均衡 :1 + + +## 操作系统 +- 进程和线程区别:4 +- 子进程和父进程,继承了哪些东西:1 +- BIO、NIO、Select、POll、Epoll:2 +- 多进程之间的通信(IPC)。:1 +- CPU调度算法:1 + +## zk +- zookeeper用来干什么:1 + + +## dubbo +- dubbo的客户端服务端的数据处理过程:1 +- dubbo 的注册中心 producer 和consumer怎样去注册和发现:1 + +## MyBatis +- `#`和`$`的区别:2 + +## 智力题 +- 3/5/8升的桶,8升装满水,怎么倒出四升的水?:1 \ No newline at end of file diff --git "a/Java/mianjing/\347\275\221\346\230\223\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\347\275\221\346\230\223\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..b3d781f3 --- /dev/null +++ "b/Java/mianjing/\347\275\221\346\230\223\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,126 @@ +## Java + +- HashMap所有:4 +- HashTable:3 +- ConcurrentHashMap:4 +- 四种修饰符的限制范围:1 +- Object类中的方法:1 +- 接口和抽象类的区别:1 +- 动态代理的两种方式:2 +- Java序列化的方式:1 +- 传值和传引用的区别:1 +- 一个ArrayList在循环过程中删除,会出现什么问题:1 +- 反射(执行某个方法):1 +- 多态:1 +- HashSet底层实现原理:2 + + +## Spring +- @transactional注解在什么情况下会失效:2 +- spring中对查询功能是否需要开启事务,是否有效:1 +- Spring的beanFactory和factoryBean的区别:2 +- Spring AOP的原理:2 +- Spring整个启动的初始化流程介绍一下:1 +- Spring除了单例模式还有什么其他模式吗?:1 +- 讲一讲Spring的事务:2 +- bean作用域:1 +- IOC怎么实现:1 +- Bean的生命周期是什么:1 + + + +## JVM +- JVM的内存结构:2 +- JVM方法栈的工作过程,方法栈和本地方法栈有什么区别:1 +- JVM的栈中引用如何和堆中的对象产生关联:1 +- GC的常见算法,CMS以及G1的垃圾回收过程,CMS的各个阶段哪两个是Stop the world的,CMS会不会产生碎片,G1的优势:3 +- 标记清除和标记整理算法的理解以及优缺点:1 +- eden survivor区的比例,为什么是这个比例,eden survivor的工作过程:1 +- JVM如何判断一个对象是否该被GC,可以视为root的都有哪几种类型:1 +- 强软弱虚引用的区别以及GC对他们执行怎样的操作:1 +- Java是否可以GC直接内存:1 +- Java类加载的过程:3 +- 双亲委派模型的过程以及优势:2 +- 破坏双亲委派的场景。加载同一个类怎么做:1 +- 常用的JVM调优参数:1 +- dump文件的分析:1 + +## 多线程 +- 多进程和多线程的区别?:1 +- Java实现多线程有哪几种方式:1 +- Callable和Future的了解:1 +- 线程池的参数有哪些,在线程池创建一个线程的过程:5 +- volitile关键字的作用,原理:4 +- synchronized关键字的用法,优缺点:3 +- Lock接口有哪些实现类,使用场景是什么:1 +- 可重入锁的用处及实现原理,写时复制的过程,读写锁,分段锁(ConcurrentHashMap中的segment):1 +- 悲观锁,乐观锁,优缺点,CAS有什么缺陷,该如何解决:1 +- ABC三个线程如何保证顺序执行:1 +- 线程的状态都有哪些:1 +- sleep和wait的区别:1 +- notify和notifyAll的区别:1 +- ThreadLocal的了解,实现原理:1 +- CAS和ABA问题:3 +- Java内存模型:2 +- 遇到了死锁,死循环该怎么办:1 + +## MySQL +- MySQL的索引:1 +- 常见的数据库优化手段:1 +- 数据库连接池:1 +- mvvc:1 +- 事务:2 +- select 加锁问题:1 +- MySQL数据库索引有哪几种:1 +- MySQL默认的隔离级别?:1 +- B+树:1 +- 聚簇索引和非聚簇索引有什么区别?:1 +- MySQL乐观锁与悲观锁的实现:1 +- 最左前缀:1 + + + +## 计算机网络 +- TCP,UDP区别:2 +- 三次握手,四次挥手,为什么要四次挥手:1 +- 长连接和短连接:1 +- 连接池适合长连接还是短连接:1 +- 从网站输入一个http的网址会发生什么:2 +- http缓存问题:1 +- 你应该如何优化一个网站 :1 +- 如果有大量的有很多time_wait怎么办?:1 + +## 设计模式 +- 观察者模式 +- 代理模式 +- 单例模式,有五种写法,可以参考文章单例模式的五种实现方式 +- 可以考Spring中使用了哪些设计模式 + +## Redis +- redis和memcached的区别:1 +- redis支持哪些数据结构:1 +- redis是单线程的么,所有的工作都是单线程么:1 +- redis如何存储一个String的:1 +- redis的部署方式,主从,集群:1 +- redis的哨兵模式,一个key值如何在redis集群中找到存储在哪里:1 +- redis持久化策略:1 +- 缓存穿透和缓存雪崩:1 +- 分布式锁用过吗?用Redis怎么实现:1 + + +## MyBatis +- mybatis的加载原理:1 + + +## 数据结构和算法 +- B+树:1 +- 快速排序:3 +- 堆排序:1 +- 插入排序:1 +- 一致性Hash算法,一致性Hash算法的应用:1 + + +## 分布式 +- Paxos算法稍微讲一下:1 +- ZooKeeper最少要几个节点,为什么?:1 +- CAP知道吗?大概讲一讲:1 \ No newline at end of file diff --git "a/Java/mianjing/\347\276\216\345\233\242\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\347\276\216\345\233\242\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..a3fbc848 --- /dev/null +++ "b/Java/mianjing/\347\276\216\345\233\242\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,159 @@ +# 美团所有问题汇总 + +## Java +- 解释一下包装类、装箱与拆箱:2 +- 垃圾回收:6 +- 线程池了解么:12 +- 拒绝策略:3 +- 集合有哪些:1 +- arraylist和linkedlist区别:3 +- hashmap怎么扩容(多线程扩容为什么会死循环),各种过程:9 +- hashset底层:1 +- 如何解决hash冲突:1 +- concurrentHashMap 1.7和1.8:9 +- 接口和抽象类区别:2 +- 新生代:1 +- JVM调优:1 +- 如何判断对象是否应该被回收:5 +- root根包括哪些:5 +- CMS回收过程,优缺点:3 +- G1回收过程:3 +- GC有哪些收集器:3 +- 类加载过程(加载,验证,准备,解析,初始化):1 +- 双亲委派优点:2 +- 线程和进程概念:3 +- synchronized和Lock的区别:3 +- synchronized底层实现:2 +- volatile:6 +- HashMap什么时候用到了红黑树:1 +- 红黑树的特点:1 +- synchronized:1 +- AQS:1 +- SpringIoc和AOP:6 +- Spring用到了什么设计模式:2 +- 单例模式:3 +- 对反射的理解:2 +- 乐观锁和悲观锁:2 +- 偏向锁、轻量级锁和重量级锁:1 +- NIO是什么:2 +- CAS原理:2 +- Springboot对Spring优化的原理:1 +- String里的bean线程安全问题:1 +- String里的bean是单例嘛?:1 +- String里的bean的生命周期:1 +- Java基本类型:2 +- Java运行原理:1 +- JVM内存模型:6 +- Spring里autowired注解和resource注解:1 +- 代码中的异常的处理过程:1 +- String和StringBuffer区别,存储位置是否相同:1 +- hashmap、hashtable 实现:1 +- 公平锁和非公平锁如何实现:1 +- yield sleep结合状态转换讲一下:1 +- TreeMap:1 +- 死锁的四个条件:1 +- Java的四种特性吗?:1 +- ArrayList为什么是快速失败的?:1 +- String、StringBuffer与StringBuilder的区别?分别用在什么场景?:1 +- 多线程的wait和sleep的区别:2 +- 可重入锁:1 +- ThreadLocal了解吗,怎么实现的:2 +- OOM如何分析(通过jmap,jstack等命令):1 +- CopyOnWrite 思想和实现:1 +- 类加载器有哪些:1 +- countdownlatch的底层:1 + + +## MySQL +- ACID:1 +- 聊一下事务:6 +- 事务的隔离:7 +- 索引数据结构:2 +- 为什么用B+树而不用hash和B-Tree:7 +- InooDB和MyISAM的区别:1 +- 回表,联合索引查询:1 +- 最左匹配:3 +- 独占所,共享锁,乐观锁:1 +- mysql分库分表:1 +- sql优化:1 +- 聚集索引和非聚集索引:2 +- 数据库范式:1 +- MySQL慢查询怎么看?:1 +- InnoDB的索引是什么形式的?:1 +- SQL语句的优化器什么的?:1 +- SQL的explain,type什么的?:2 +- Count(*)、count(1)与count(列属性)三者的区别?:1 +- sql查询某个人的所有成绩,两个表,学生表和成绩表:1 +- 问MySQL的innodb的行锁是什么?锁的是什么?:1 +- 一个学生表,选出成绩排名第三的学生和成绩分数第三的学生,怎么写sql?:1 +- 姓名课程成绩,找出没有不及格课程的学生(group by having min >= 60) + +## Redis +- Redis使用场景:1 +- Redis线程模型?多路复用讲一下,为什么redis很快:1 +- 分布式锁有哪些?:1 +- Redis的五种数据类型:2 +- Redis跳表:1 +- 集群:2 +- 持久化:2 +- setnx指令:1 +- 数据库和缓存的读一致性?:1 +- 数据库和缓存的写一致性?:1 +- Redis都说是单线程的,具体是怎么处理请求的:1 +- Redis实现过分布式锁,那有别的方案可以实现分布式锁吗:1 + + +## 网络 +- 三次握手:3 +- 滑动控制:1 +- 七层模型:1 +- 四次挥手:3 +- 为什么TCP能保证不丢失(滑动窗口,拥塞控制):1 +- HTTP和HTTPS的区别:1 +- GET和POST区别:2 +- 一次url请求的过程:3 +- cookie和session区别,怎么用:1 +- https实现过程:2 +- 对称加密和非对称加密?:1 +- TCP和UDP的区别:1 +- DNS解析过程 + + +## 其他 +- 操作系统里进程状态及转换:1 +- 4G内存读取10G日志,统计其中出现频次最高的十个word,如何实现,如何使得速度更快:1 +- 5个赛道25匹马,选出最快的5个:1 + +## 代码 +- 二分查找:4 +- 反转链表:2 +- 反转字符串:1 +- 顺时针填充数组:1 +- 字符串去重:3 +- 查找二进制数据里出现1的次数:1 +- 常用的排序算法哪些,最优时间复杂度,最差时间复杂度,平均时间复杂度,空间复杂度:1 +- 二叉树的两个最远节点的距离:1 +- 字符串转为数字:2 +- 链表环路:1 +- 爬楼梯:1 +- 两个链表的第一个公共节点:1 +- 实现一个阻塞队列:1 +- 两个栈实现队列:1 +- 约瑟夫环问题:1 +- BlockingQueue的生产者和消费者模型:1 +- 一个数组找出出现次数最多的一个数,如果多个数出现的次数相同则输出第一个:1 +- 栈里的数降序排序,额外空间只能再用一个栈:1 +- 链表相加:1 + + +## RocketMQ +- 数据存在哪里 +- 会不会丢失数据 +- 幂等操作 +- 生产者提交数据的流程 +- 如何实现高并发消费,分片 + +## MyBatis +- `#`和`$`符号有什么区别 +- ORM框架的作用 + diff --git "a/Java/mianjing/\350\205\276\350\256\257\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\350\205\276\350\256\257\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..1f9323d1 --- /dev/null +++ "b/Java/mianjing/\350\205\276\350\256\257\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,189 @@ + + + +## 算法 + +- 判断树是否对称:1 +- 在一个大数组里求第100大的数字:1 +- 找出A[1000]、B[1000]、C[1000]中的重合的数字:1 +- 给出25亿个QQ号,找出其中重复的?如果不用bitmap且只有一台机器怎么做?:1 +- 你了解哪些排序?:1 +- 快排什么时候复杂度不好?:1 +- 红黑树的特点?如果给红黑树插入数据,红黑树是怎么做的?:1 +- 写一个栈:1 +- 两个特别大的数相乘:1 +- 求两个已排序数组合并后的中位数:1 +- 设计栈,使得getMin()和getMax()的时间复杂度为O(1):1 +- 一个数组中只有一个数字出现了奇数次,其他数字出现了偶数次,找到出现了奇数次的那个数:1 +- 100层楼和2个玻璃杯,怎样用最少的次数找出杯子在哪一层会碎:1 +- 哈希表是如何实现的?:1 +- 1TB的数据如何进行排序:1 +- 对100TB的数据进行排序?(拆分多个数据段进行排序,然后归并):1 +- 判断链表有环,找环入口:2 +- 100万个数找最小的10个(大顶堆):1 +- 旋转数组找出最小值:1 +- 找链表中间节点:1 +- 找链表倒数第k节点:1 +- 微信发红包 m块钱发给n个人 你怎么设计算法:1 +- 很多数中有俩数重复了咋判断:1 +- 单调栈问题:1 +- 分组反转单向链表:1 +- 非递归实现后序遍历:1 +- 把数组排成最小的数:1 +- 找第k大元素:1 +- 链表循环右移动k位置:1 +- 如何自己实现一个kv结构的,你有哪几种方式:1 +- 用hash表有什么问题,和解决了什么问题?:1 +- 用树解决了什么问题,红黑树和hash分别是有序还是无序的呢?:1 +- 为什么是有序还是无序?具体怎么实现的呢?:1 + +## MySQl + +- mysql慢查询如何优化?:2 +- 优化器是什么时候起作用的?:1 +- MVCC的原理?:1 +- InnoDB和myISAM的区别?:2 +- InnoDB的聚集索引和MyISAM的非聚集索引的区别?:1 +- B+树、B树、红黑树的区别:2 +- 辅助索引的叶子上有什么内容?辅助索引和主键索引性能差距在哪里?:1 +- 数据库事务性质,并发一致性问题:1 +- 数据库存储过程,视图,函数的使用,几种连接方式:1 +- 索引:2 +- 事务的ACID:2 +- 数据库的join操作实际上是MySQL底层是怎么做的呢:1 +- limit a,b是什么意思,会有什么性能上的问题:1(limit之前的数据先查出来、a代表起点、b代表数量,如果a很大的话,那么MySQL需要先去遍历前a条数据而不是直接定位,所以这里存在性能问题) +- 数据库容灾方面问题,数据库挂了怎么办:1 +- 数据库集群怎么实现数据一致性:1 + + +## java +- 从java代码到.class文件,中间经历实现你了哪些过程?:1 +- Java数据类型,大小:1 +- instanceof和getClass:1 +- JMM:2 +- 介绍项目中的SpringIOC DI MVC AOP:1 +- JVM数据区:1 +- GC相关:1 +- OOM情况:1 +- 多态的实现:1 +- Java类的分类:1 +- 普通类,接口,抽象类的区别:1 +- Java创建线程的方式:1 +- 线程的状态:1 +- 单例模式:1 +- 类加载过程:1 +- 双亲委派模型:1 +- Java会出现内存泄露吗:1 +- 如何定位和解决OOM:1 +- Java的GC中什么场景下使用CMS和G1:1 +- hashmap:1 +- JVM参数调优:1 +- Minor GC和Full GC:1 +- 线程池:1 +- spring bean的什么周期:2 +- spring 单例的bean是否线程安全:1 +- 有状态的bean和无状态的bean区别:1 +- spring事务了解么:1 +- 如何实现HashMap取出的顺序和放入顺序一致?:1 +- HashMap的扩容的时间复杂度,如何优化?:1 +- JDK8添加了哪些特性?:1 +- java为什么说它即是解释型语言,又是编译型语言:1 +- 面向对象和面向过程的区别?:1 +- java的类和c++的类有什么区别:1 +- java语言的三大特性:1 +- 怎么拼接多个string:1 +- 讲讲异常:1 +- 深拷贝和浅拷贝:1 +- java的包装类的了解?为啥要有包装类:1 +- Java的集合:1 +- 乐观锁和悲观锁:1 +- synchronize和Lock区别:1 +- synchronized的底层是怎么实现的?:1 +- ReentrantLock的区别:1 +- ThreadLocal的原理是什么:1 +- CountdownLatch:1 + +## redis + +- redis在你项目中怎么用的?防止重复提交是怎么做到的?:1 +- Redis五种数据类型:1 +- Hash底层:1 +- redis跳表的优势:1 +- reactor模式:1 +- redis持久化:1 + + +## 计网 + +- HTTP还是HTTPS请求?有什么区别?:3 +- HTTP过程的四次挥手?:2 +- TIME_WAIT的作用?:2 +- cookie的作用?:1 +- 腾讯和百度两个网页能获取对方的cookie吗?:1 +- 在百度里搜索abc的过程?:1 +- 搜索的时候,数据包是怎么交给网卡的?(7层 5层网络模型)层层封包都加的是什么内容?:1 +- 网卡怎么知道数据是发送给百度服务器的,怎么找到服务器的?:1 +- 计算机网络分层,各层的协议:1 +- http流量控制,拥塞避免如何实现:1 +- 多少种请求方式,get和post区别:1 +- Https端口443以及怎么的实现流程:1 +- Session和Cookie区别:1 +- TCP和UDP的区别:1 +- TCP三次握手:1 +- CLOSE_WAIT:1 +- DNS解析的过程:1 +- HTTP长连接和短连接:1 +- TCP拥塞控制:1 +- 了解粘包吗,怎么设置不粘包:1 +- sql注入如何防范:1 +- xss如何防范:1 +- udp的最大包长度,为什么这么大?:1 +- 傻瓜窗口了解吗?怎么解决?:1 +- socket去写的时候你会怎么写?考虑什么?:1 +- http常见头部信息有哪些:1 +- 你知道https的非对称加密和对称加密使用了哪些算法么:1 + +## os +- 内核态和用户态的区别?:1 +- 用户态通过什么样的接口调用内核? +- 进程在内存中是如何分配的?(段页式及其细节、数据段、栈段、代码段):1 +- 操作系统进程同步方式:1 +- 进程怎样通信:2 +- 套接字:1 +- 线程通信机制:1 +- CPU密集型任务适合多进程还是多线程?:1 +- 进程的同步机制,你在什么时候用:1 +- 向一个进程发出kill信号接下来发生什么?:1 +- 共享内存的坏处:1 +- 操作系统中中断的分类:1 +- 操作系统页置换算法:1 +- 僵尸进程问题:1 +- 页式和段式的区别,优缺点,应用场景。:1 +- 讲一下虚拟内存、页表:1 +- 为什么顺序io比随机io快?:1 +- 随机io的过程是什么?:1 +- 用户态和内核态的区别?如何切换?:1 +- 原子操作的意义:1 +- i++为什么不是原子操作,如何去保证是原子操作?:1 + +## 分布式 + +- 高并发系统大量请求如何优化:1 +- 分布式系统CAP:1 +- 秒杀系统解决超卖问题,(数据库排它锁和乐观锁CAS版本号机制):1 +- base理论:1 +- 两阶段提交:1 +- redis分布式锁的注意事项,实现过程?:1 + +## 亮点 +- Netty,NIO通信框架:1 +- BIO、NIO、AIO:1 +- NIO又可以分为三种,基于轮询、基于多路复用、基于事件回调:1 +- 如何指定使用哪种方式:1 +- 知道他底层怎么实现的吗:1 +- Netty底层Buffer的实现:1 +- 日志清理如何解决:1 +- 日志合并如何解决:1 +- reactor模式:1 +- 讲讲netty的io模型:1 +- 讲讲多路复用机制,你觉得什么时候多路复用性能会比较好? \ No newline at end of file diff --git "a/Java/mianjing/\350\277\234\346\231\257\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\350\277\234\346\231\257\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..6021baf1 --- /dev/null +++ "b/Java/mianjing/\350\277\234\346\231\257\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,25 @@ +## java +- springboot了解多少:2 +- Spring MVC:2 +- bean生命周期:1 +- 为什么用MQ:1 +- JVM内存区域:2 +- 双亲委派模型:1 +- sychronized:1 +- volatile:1 +- ArrayList和LinkedList的区别 + + +## Redis +- Redis的数据结构:1 +- 缓存穿透:1 +- 缓存雪崩:1 +- Redis为什么快:1 +- IO多路复用:1 + + + +## 计算机网络 +- 三次握手:1 +- TCP和UDP:2 +- nginx负载均衡的几种方式:1 \ No newline at end of file diff --git "a/Java/mianjing/\351\230\277\351\207\214\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/Java/mianjing/\351\230\277\351\207\214\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 00000000..aad72cbd --- /dev/null +++ "b/Java/mianjing/\351\230\277\351\207\214\346\211\200\346\234\211\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,154 @@ + +## Java + +- JVM分区 +- 垃圾收集算法 +- synchronized原理 +- 双亲委派机制 +- 线程池参数 +- newFixedTheadPool底层,优缺点 +- springmvc +- @Autowired原理 +- springboot原理 +- @OnConditionalClass实现原理 +- @OnMissingBean没有bean会怎么样,会classNotFound +- HashMap 底层数据结构 +- HashMap 什么时候扩容?装载因子和临界值默认是多少?扩容成多大?为什么容量是2的幂次方? +- 线程安全的Map?分段锁是如何实现的?JDK 1.8之后有哪些优化? +- Lock和Synchronized区别 +- AQS实现 +- 锁优化 +- String StringBuilder StringBuffer区别,特点,场景 +- ConcurrentMap 源码 +- CMS +- 线程生命周期(状态) +- ReentrantLock与AQS同步框架 +- CAS原理是什么? +- synchronize原理是什么? +- springboot 和 spring 的区别是什么? +- spring中bean的初始化过程 +- 谈一谈异常机制 +- Spring IOC和AOP +- Arrays.sort()底层原理 + +## 场景题 +- 一个8G的服务器,堆的大小应该设置成多少 +- 海量数据求频率最多的100个 +- spring一个事务中调用另外一个事务,另一个- 事务发生异常会怎么样 +- 一个父类加载器能不能加载一个子类加载器,为什么 +- select * from A where id in (select id from B)怎么优化 +- 一个16G的内存堆分配多少,采用什么垃圾收集器,为什么用cms不用g1,为什么(面试官一直问为什么使用cms或者使用g1,回答了这两个的优缺点之后还是不满意) +- 多线程解析一个超大文件怎么处理,如果文件切分的时候关键信息被分到了不同的解析线程中怎么办 +- hashset是如何判断两个对象相等的 +- 如何要两个对象相等equals和hashcode这两个方法要怎么重写 +- hash算法(最开始讲hash冲突算法,面试官说不是这个,我又说对hash值对质数取余,面试官也说不是这个,不知道他要我回答啥。。。) +- 不使用任何优化,直接访问数据库,如何优化 (提示 redo、undo log的开销)(这里应该还可以答:批处理、连接池等) +- 行锁锁的是索引,如果一个表没有主键怎么加锁?(锁表) +- hashmap 为什么不直接用红黑树,还要用链表? +- 红黑树的特性?各种操作的时间复杂度?最多旋转几次达到平衡? +- finally 中 return 会发生什么? +- 如何破坏双亲委派 +- 浏览器中用户信息和密码的安全有没有考虑过 +- 两个线程如何交替打印A和B +- 100万个数,数据类型是int,给你8核CPU,8G内存,如何求数组的和? +- 很多个不重复的数,假设用的数组来装,我希望你实现一个方法,每次调用这个方法给我随机返回100个不重复,记住是随机的。 +- 四个引用的具体应用场景 +- 垃圾回收器具体应用场景 +- 可达性分析算法具体应用场景 +- 垃圾回收器参数调优(给具体场景 大概就是并发低延时) +- 高并发多连接jvm 线程池参数调优(顺着来) +- 高并发系统调优(再顺一下) +- 队列与栈的 应用场景 +- 点赞高并发怎么办? +- 消息中间件用过么?怎么自己设计一个消息中间件? +- 假设catch,final,catch中有return,那么final还会不会执行 +- 假设1亿的11位的手机号,运行空间128M,如果要进行排序,那么要怎么设计 +- jvm的设计角度,gc你怎么优化? +- 你怎么设计md5函数? +- cpu一直被占满,怎么排查? +- 让你自己设计一个垃圾回收器,尽可能提高回收效率,你会怎么设计?(ZGC和香浓多,整就完事了) +- MD5加密算法,不使用hashmap和映射,你自己来设计一个同类型的,给定字符串可以生成64位随机数的,你怎么设计 +- 利用java现有的东西,让你设计一个对象,实现类似synchronize的功能,使得多个线程不冲突,你如何设计?(ThreadLocal玩起来) +- synchronize锁定.class和锁定一个实例有什么区别? +- explain中的key字段的值等于ref时,有没有触发索引? +- 如何实现单点登录,如何实现权限控制,用户密码泄露后如何保证安全性 +- spring中循环依赖如何解决?如果让你来实现你怎么做? +- 如果我的服务器ip地址变了,客户端如何感知到呢? +- 轮询的负载均衡的缺点是什么?如何改进? +- 让你来实现真正的负载均衡,你如何做?(我回答记录每台服务器在最近的一段时间内接收的请求的数量,每次都把请求发送给数量最小的服务器,后来面试官提醒我还应当考虑每个请求耗费的时间) +- 秒杀项目中静态资源CDN怎么做? +- css文件能放到CDN上吗? +- 秒杀缓存如何与数据库的数据保持一致性? +- 通过广播的方式去更新每个节点的缓存,如果某个节点的缓存更新失败,那么如何排查是哪个节点呢? +- 消费者消费失败是怎么处理的 +- 如何保证消息消费的顺序性 +- 如何保证消息不被重复消费 +- 项目中要进行分布式扩展应该怎么做 +- 缓存和mysql数据一致性如何保证 +- 微信步数排行,假设百万级用户,怎么排序,实时更新(我说排序就是海量数据排序的方式分组排序再归并,然后实时更新的话也是组内更新组内排序,但是每组的分度值可能不一样,比如0-2000可能一组,但2000-4000可能要分五组之类的,因为步数特别少和特别多的都是少数。) +- 发生了OOM,应该怎么去分析解决?jvm调优 +- 什么时候发生线程的上下文切换? +- CAS是硬件实现还是软件实现 +- 除了wait和notifyall,还有什么办法实现类似的功能 +- 微信抢红包设计(只讲了类似多线程抢、Semphore,缓存)、海量文件找重复次数最多的个数(分治) + +## MySQL +- 索引怎么优化 +- 为什么用B+树 +- Innodb 和 Myisam 的区别 +- 聚集索引和非聚集索引 创建以后的文件大小 +- 为什么不用哈希索引? +- 有几种事务隔离级别? +- Innodb 行锁是如何实现的? +- mysql怎么分页 +- 索引为什么查找快 +- 慢查询如何优化 +- mvcc原理 + +## Redis +- redis基本数据类型 +- redis集群?切片 +- 为什么用Redis,Redis宕机了怎么办,数据呢?怎么保持热点数据?过期机制,淘汰机制? +- redis是单线程么?为什么快?io模型什么怎么样的,具体是怎么个流程? +- 缓存穿透、雪崩,redis持久化机制 + +## 计网 +- tcp三次握手 +- HTTP 和RPC区别?还有呢? +- JWT流程,安全?Session? +- 盐有单独保存起来么 +- HTTP 和 HTTPS的区别 +- https的加密证书怎么获取 +- 加密后客户端保留的是公钥还是私钥 +- 讲一下ftp +- cookies +- http以及https 以及加密手段 +- 浏览器输入url后的一系列操作 +- HTTP状态码 +- 为什么用Jwt,cookie,session有什么好处? +- TCP怎么确保可靠性的?丢包和串包怎么办? +- DNS说一下? + +## 操作系统 +- 上下文切换 +- 内核态用户态 +- 多路复用IO模型,select,poll,epoll + +## 分布式 +- 怎么实现分布式锁 +- redis分布式锁有什么缺点,怎么解决 +- 单体应用和微服务区别,为什么要有微服务?好处?还有呢? +- 微服务流程? +- 分布式缓存? +- 分布式session原理 +- SpringCloud及其组件你了解哪些? + + +## 算法 +- n个线程顺序循环打印0-100 +- 手写LinkedList的数据结构并写出add和remove算法 +- 微信红包算法实现 +- 链表如何找环 +- 有序链表合并 +- 共计9个苹果,有2只猴子,一个猴子每次拿2个苹果,一个猴子每次拿3个苹果,如果剩余的苹果不够猴子每次拿的数量,则2只猴子停止拿苹果,请用java多线程模拟上面的描述,要求性能尽可能高效(这个题开始是用可重入锁写的,结束之后自己本地测试发现程序不会自动结束,后来改成用AtomicInteger和cas来实现了) +- 设计一个多线程打印程序,第i个线程只打印i-1数字,比如第1个线程打印数字0,第2个线程只打印数字1,依次类推。任意给定一个数字序列,比如3382019835830,能够使用该程序打印出来。 diff --git a/Java/mind/README.md b/Java/mind/README.md new file mode 100644 index 00000000..49880ea5 --- /dev/null +++ b/Java/mind/README.md @@ -0,0 +1,13 @@ +- [总体架构](https://www.processon.com/view/link/5e170217e4b0bcfb733ce553) **这边就不放图了,放图的字体小,放大可能模糊。该图还在持续总结中...** +- [Java常见基础问题](https://www.processon.com/view/link/5e457c32e4b05d0fd4e94cad) **常见的基础问题,这是必须要掌握。** +- [Java常见集合问题]() **还没总结,后续总结...** +- [Java常见多线程问题](https://www.processon.com/view/link/5e4ab92de4b0996b2ba505bf) **常见的多线程问题,也是必须掌握...** +- [JVM常见问题](https://www.processon.com/view/link/5e4c0704e4b00aefb7e74f44) **常见的JVM要掌握的点...** +- [Spring常见问题](https://www.processon.com/view/link/5e846de9e4b07b16dcdb63f0) **常见的Spring面试的问题...** +- [Mybatis常见问题](https://www.processon.com/view/link/5e4e3b7ae4b0369b916b2e71) **常见的Mybatis面试的问题...** +- [MySQL常见问题](https://www.processon.com/view/link/5e9b0cb15653bb1a686e17ea) **常见的MySQL面试的问题...** +- [Redis常见问题](https://www.processon.com/view/link/5ea2da5907912948b0d89a0a) **常见的Redis面试的问题...** +- [计算机网络常见问题](https://www.processon.com/view/link/5eb8c93be401fd16f42b5f77) **常见的计算机网络面试的问题...** +- [Dubbo常见问题](https://www.processon.com/view/link/5eb8c9715653bb6f2aff7c11) **常见的Dubbo的问题...** +- [RocketMQ常见问题](https://www.processon.com/view/link/5ecf208f7d9c08156c6c37e3) **常见的RocketMQ的问题...** +- [微服务班车在线预约系统](https://github.com/DreamCats/SchoolBus) 个人撸的项目是基于微服务架构的班车预约系统,采用**springboot+mybatis+dubbo+rocketmq+mysql+redis等**。当然,该项目也是前后端分离,前端采用比较流行的vue框架。 \ No newline at end of file diff --git a/Java/mode/README.md b/Java/mode/README.md new file mode 100644 index 00000000..633732eb --- /dev/null +++ b/Java/mode/README.md @@ -0,0 +1,6 @@ +- [单例](mode/单例模式.md) +- [工厂](mode/工厂模式.md) +- [代理](mode/代理模式.md) +- [模板方法](mode/模板方法模式.md) +- [观察者](mode/观察者模式.md) +- [装饰器](mode/装饰器模式.md) \ No newline at end of file diff --git "a/Interview/codes/modes/\344\273\243\347\220\206\346\250\241\345\274\217.md" "b/Java/mode/\344\273\243\347\220\206\346\250\241\345\274\217.md" similarity index 100% rename from "Interview/codes/modes/\344\273\243\347\220\206\346\250\241\345\274\217.md" rename to "Java/mode/\344\273\243\347\220\206\346\250\241\345\274\217.md" diff --git "a/Interview/codes/modes/\345\215\225\344\276\213\346\250\241\345\274\217.md" "b/Java/mode/\345\215\225\344\276\213\346\250\241\345\274\217.md" similarity index 100% rename from "Interview/codes/modes/\345\215\225\344\276\213\346\250\241\345\274\217.md" rename to "Java/mode/\345\215\225\344\276\213\346\250\241\345\274\217.md" diff --git "a/Interview/codes/modes/\345\267\245\345\216\202\346\250\241\345\274\217.md" "b/Java/mode/\345\267\245\345\216\202\346\250\241\345\274\217.md" similarity index 100% rename from "Interview/codes/modes/\345\267\245\345\216\202\346\250\241\345\274\217.md" rename to "Java/mode/\345\267\245\345\216\202\346\250\241\345\274\217.md" diff --git "a/Interview/codes/modes/\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" "b/Java/mode/\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" similarity index 100% rename from "Interview/codes/modes/\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" rename to "Java/mode/\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" diff --git "a/Interview/codes/modes/\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217.md" "b/Java/mode/\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217.md" similarity index 100% rename from "Interview/codes/modes/\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217.md" rename to "Java/mode/\350\243\205\351\245\260\345\231\250\346\250\241\345\274\217.md" diff --git "a/Interview/codes/modes/\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" "b/Java/mode/\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" similarity index 100% rename from "Interview/codes/modes/\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" rename to "Java/mode/\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" diff --git "a/Java/spring-books/MyBatis\346\241\206\346\236\266\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230.md" "b/Java/spring-books/MyBatis\346\241\206\346\236\266\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230.md" new file mode 100644 index 00000000..717ee102 --- /dev/null +++ "b/Java/spring-books/MyBatis\346\241\206\346\236\266\351\235\242\350\257\225\345\270\270\350\247\201\351\227\256\351\242\230.md" @@ -0,0 +1,154 @@ +## 引言 +> 项目中,经常用到MyBatis,面试也需要掌握滴! + + +## 基本概念 + +### 数据持久化 + +**数据持久化是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中的数据模型的统称。例如,文件的存储、数据的读取等都是数据持久化操作。数据模型可以是任何数据结构或对象的模型、XML、二进制流等。** + +**当我们编写应用程序操作数据库,对表数据进行增删改查的操作的时候就是数据持久化的操作。** + +### Mybatis框架简介 + +- **MyBatis框架是一个开源的数据持久层框架。** +- **它的内部封装了通过JDBC访问数据库的操作,支持普通的SQL查询、存储过程和高级映射,几乎消除了所有的JDBC代码和参数的手工设置以及结果集的检索。** +- **MyBatis作为持久层框架,其主要思想是将程序中的大量SQL语句剥离出来,配置在配置文件当中,实现SQL的灵活配置。** +- **这样做的好处是将SQL与程序代码分离,可以在不修改代码的情况下,直接在配置文件当中修改SQL。** + +### ORM + +**ORM(Object/Relational Mapping)即对象关系映射,是一种数据持久化技术。它在对象模型和关系型数据库直接建立起对应关系,并且提供一种机制,通过JavaBean对象去操作数据库表的数据。** + +**MyBatis通过简单的XML或者注解的方式进行配置和原始映射,将实体类和SQL语句之间建立映射关系,是一种半自动(之所以说是半自动,因为我们要自己写SQL)的ORM实现。** + +## MyBatis框架的优缺点及其适用的场合 + +### 优点 + +1. 与JDBC相比,减少了50%以上的代码量。 +2. MyBatis是嘴加单的持久层框架,小巧并且简单易学。 +3. MyBatis相当灵活,不会对应用程序或者数据库的现有设计强加任何影响,SQL写在XML文件里,从程序代码中彻底分离,降低耦合度,便于统一的管理和优化,并可重用。 +4. 提供XML标签,支持编写动态的SQL,满足不同的业务需求。 +5. 提供映射标签,支持对象与数据库的ORM字段关系映射。 + +### 缺点 + +1. SQL语句的编写工作量较大,对开发人员编写SQL的能力有一定的要求。 +2. SQL语句依赖于数据库,导致数据库不具有好的移植性,不可以随便更换数据库。 + +### 适用场合 + +**MyBatis专注于SQL自身,是一个足够灵活的DAO层解决方案。对性能的要求很高,或者需求变化较多的项目,例如Web项目,那么MyBatis是不二的选择。** + +## MyBatis与Hibernate有哪些不同? + +1. Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。 +2. Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。 +3. Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate开发可以节省很多代码,提高效率。 + +## **#{}和${}的区别是什么?** + +1. \#{} 是预编译处理,${}是字符串替换。 +2. Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值; +3. Mybatis在处理\${}时,就是把\${}替换成变量的值。 + +4. 使用#{}可以有效的防止SQL注入,提高系统安全性。 + +## 当实体类中的属性名和表中的字段名不一样 ,怎么办 ? + +1. 第1种: 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。 +2. 第2种: 通过 `` 来映射字段名和实体类属性名的一一对应的关系。 + +## 模糊查询like语句该怎么写? + +1. 第1种:在Java代码中添加sql通配符。 +2. 第2种:在sql语句中拼接通配符,会引起sql注入 + +## 通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗? + +**Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。** +**Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中每`