u盘无法挂载问题

不正常拔出U盘后,下次插入可能出现

Error mounting /dev/sdc1 at /media/my/XXX: Command-line `mount -t “ntfs” -o……

terminal下,通过以下指令解决:

sudo apt-get install ntfs-3g
sudo ntfsfix /dev/sdc1

/dev/sdc1/ 对应mounting后的位置

老大难的 Java ClassLoader,到了该彻底理解它的时候了

转载自:http://blog.itpub.net/31561269/viewspace-2222522/

ClassLoader 是 Java 届*为神秘的技术之一,无数人被它伤透了脑筋,摸不清门道究竟在哪里。网上的文章也是一篇又一篇,经过本人的亲自鉴定,*大部分内容都是在误导别人。本文我带读者彻底吃透 ClassLoader,以后其它的相关文章你们可以不必再细看了。

ClassLoader 做什么的?

顾名思义,它是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

 

%title插图%num

 

有很多字节码加密技术就是依靠定制 ClassLoader 来实现的。先使用工具对字节码文件进行加密,运行时使用定制的 ClassLoader 先解密文件内容再加载这些解密后的字节码。

每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。

  1. class Class<T> {
  2.   …
  3.   private final ClassLoader classLoader;
  4.   …
  5. }

延迟加载

JVM 运行并不是一次性加载所需要的全部类的,它是按需加载,也就是延迟加载。程序在运行的过程中会逐渐遇到很多不认识的新类,这时候就会调用 ClassLoader 来加载这些类。加载完成后就会将 Class 对象存在 ClassLoader 里面,下次就不需要重新加载了。

比如你在调用某个类的静态方法时,首先这个类肯定是需要被加载的,但是并不会触及这个类的实例字段,那么实例字段的类别 Class 就可以暂时不必去加载,但是它可能会加载静态字段相关的类别,因为静态方法会访问静态字段。而实例字段的类别需要等到你实例化对象的时候才可能会加载。

各司其职

JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的服务地址来加载。

JVM 中内置了三个重要的 ClassLoader,分别是 BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader。

BootstrapClassLoader 负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.*、java.io.*、java.nio.*、java.lang.* 等等。这个 ClassLoader 比较特殊,它是由 C 代码实现的,我们将它称之为「根加载器」。

ExtensionClassLoader 负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包。

AppClassLoader 才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。

那些位于网络上静态文件服务器提供的 jar 包和 class文件,jdk 内置了一个 URLClassLoader,用户只需要传递规范的网络路径给构造器,就可以使用 URLClassLoader 来加载远程类库了。URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式。ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库。

AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到,它就是我们所说的「系统类加载器」,我们用户平时编写的类代码通常都是由它加载的。当我们的 main 方法执行的时候,这*个用户类的加载器就是 AppClassLoader。

ClassLoader 传递性

程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象。前面我们提到每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。

因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。

双亲委派

前面我们提到 AppClassLoader 只负责加载 Classpath 下面的类库,如果遇到没有加载的系统类库怎么办,AppClassLoader 必须将系统类库的加载工作交给 BootstrapClassLoader 和 ExtensionClassLoader 来做,这就是我们常说的「双亲委派」。

 

%title插图%num

 

AppClassLoader 在加载一个未知的类名时,它并不是立即去搜寻 Classpath,它会首先将这个类名称交给 ExtensionClassLoader 来加载,如果 ExtensionClassLoader 可以加载,那么 AppClassLoader 就不用麻烦了。否则它就会搜索 Classpath 。

而 ExtensionClassLoader 在加载一个未知的类名时,它也并不是立即搜寻 ext 路径,它会首先将类名称交给 BootstrapClassLoader 来加载,如果 BootstrapClassLoader 可以加载,那么 ExtensionClassLoader 也就不用麻烦了。否则它就会搜索 ext 路径下的 jar 包。

这三个 ClassLoader 之间形成了级联的父子关系,每个 ClassLoader 都很懒,尽量把工作交给父亲做,父亲干不了了自己才会干。每个 ClassLoader 对象内部都会有一个 parent 属性指向它的父加载器。

  1. class ClassLoader {
  2.   …
  3.   private final ClassLoader parent;
  4.   …
  5. }

值得注意的是图中的 ExtensionClassLoader 的 parent 指针画了虚线,这是因为它的 parent 的值是 null,当 parent 字段是 null 时就表示它的父加载器是「根加载器」。如果某个 Class 对象的 classLoader 属性值是 null,那么就表示这个类也是「根加载器」加载的。

Class.forName

当我们在使用 jdbc 驱动时,经常会使用 Class.forName 方法来动态加载驱动类。

Class.forName("com.mysql.cj.jdbc.Driver");

其原理是 mysql 驱动的 Driver 类里有一个静态代码块,它会在 Driver 类被加载的时候执行。这个静态代码块会将 mysql 驱动实例注册到全局的 jdbc 驱动管理器里。

  1. class Driver {
  2.   static {
  3.     try {
  4.        java.sql.DriverManager.registerDriver(new Driver());
  5.     } catch (SQLException E) {
  6.        throw new RuntimeException(“Can’t register driver!”);
  7.     }
  8.   }
  9.   …
  10. }

forName 方法同样也是使用调用者 Class 对象的 ClassLoader 来加载目标类。不过 forName 还提供了多参数版本,可以指定使用哪个 ClassLoader 来加载

Class<?> forName(String name, boolean initialize, ClassLoader cl)

通过这种形式的 forName 方法可以突破内置加载器的限制,通过使用自定类加载器允许我们自由加载其它任意来源的类库。根据 ClassLoader 的传递性,目标类库传递引用到的其它类库也将会使用自定义加载器加载。

自定义加载器

ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。

loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。下面我使用伪代码表示一下基本过程

  1. class ClassLoader {
  2.   // 加载入口,定义了双亲委派规则
  3.   Class loadClass(String name) {
  4.     // 是否已经加载了
  5.     Class t = this.findFromLoaded(name);
  6.     if(t == null) {
  7.       // 交给双亲
  8.       t = this.parent.loadClass(name)
  9.     }
  10.     if(t == null) {
  11.       // 双亲都不行,只能靠自己了
  12.       t = this.findClass(name);
  13.     }
  14.     return t;
  15.   }
  16.   // 交给子类自己去实现
  17.   Class findClass(String name) {
  18.     throw ClassNotFoundException();
  19.   }
  20.   // 组装Class对象
  21.   Class defineClass(byte[] code, String name) {
  22.     return buildClassFromCode(code, name);
  23.   }
  24. }
  25. class CustomClassLoader extends ClassLoader {
  26.   Class findClass(String name) {
  27.     // 寻找字节码
  28.     byte[] code = findCodeFromSomewhere(name);
  29.     // 组装Class对象
  30.     return this.defineClass(code, name);
  31.   }
  32. }

自定义类加载器不易破坏双亲委派规则,不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库。在使用自定义加载器时,要明确好它的父加载器是谁,将父加载器通过子类的构造器传入。如果父类加载器是 null,那就表示父加载器是「根加载器」。

  1. // ClassLoader 构造器
  2. protected ClassLoader(String name, ClassLoader parent);

双亲委派规则可能会变成三亲委派,四亲委派,取决于你使用的父加载器是谁,它会一直递归委派到根加载器。

Class.forName vs ClassLoader.loadClass

这两个方法都可以用来加载目标类,它们之间有一个小小的区别,那就是 Class.forName() 方法可以获取原生类型的 Class,而 ClassLoader.loadClass() 则会报错。

  1. Class<?> x = Class.forName(“[I”);
  2. System.out.println(x);
  3. x = ClassLoader.getSystemClassLoader().loadClass(“[I”);
  4. System.out.println(x);
  5. ———————
  6. class [I
  7. Exception in thread “main” java.lang.ClassNotFoundException: [I

钻石依赖

项目管理上有一个著名的概念叫着「钻石依赖」,是指软件依赖导致同一个软件包的两个版本需要共存而不能冲突。

 

%title插图%num

 

我们平时使用的 maven 是这样解决钻石依赖的,它会从多个冲突的版本中选择一个来使用,如果不同的版本之间兼容性很糟糕,那么程序将无法正常编译运行。Maven 这种形式叫「扁平化」依赖管理。

 

使用 ClassLoader 可以解决钻石依赖问题。不同版本的软件包使用不同的 ClassLoader 来加载,位于不同 ClassLoader 中名称一样的类实际上是不同的类。下面让我们使用 URLClassLoader 来尝试一个简单的例子,它默认的父加载器是 AppClassLoader

  1. $ cat ~/source/jcl/v1/Dep.java
  2. public class Dep {
  3.     public void print() {
  4.         System.out.println(“v1”);
  5.     }
  6. }
  7. $ cat ~/source/jcl/v2/Dep.java
  8. public class Dep {
  9.   public void print() {
  10.     System.out.println(“v1”);
  11.   }
  12. }
  13. $ cat ~/source/jcl/Test.java
  14. public class Test {
  15.     public static void main(String[] args) throws Exception {
  16.         String v1dir = “file:///Users/qianwp/source/jcl/v1/”;
  17.         String v2dir = “file:///Users/qianwp/source/jcl/v2/”;
  18.         URLClassLoader v1 = new URLClassLoader(new URL[]{new URL(v1dir)});
  19.         URLClassLoader v2 = new URLClassLoader(new URL[]{new URL(v2dir)});
  20.         Class<?> depv1Class = v1.loadClass(“Dep”);
  21.         Object depv1 = depv1Class.getConstructor().newInstance();
  22.         depv1Class.getMethod(“print”).invoke(depv1);
  23.         Class<?> depv2Class = v2.loadClass(“Dep”);
  24.         Object depv2 = depv2Class.getConstructor().newInstance();
  25.         depv2Class.getMethod(“print”).invoke(depv2);
  26.         System.out.println(depv1Class.equals(depv2Class));
  27.    }
  28. }

在运行之前,我们需要对依赖的类库进行编译

  1. $ cd ~/source/jcl/v1
  2. $ javac Dep.java
  3. $ cd ~/source/jcl/v2
  4. $ javac Dep.java
  5. $ cd ~/source/jcl
  6. $ javac Test.java
  7. $ java Test
  8. v1
  9. v2
  10. false

在这个例子中如果两个 URLClassLoader 指向的路径是一样的,下面这个表达式还是 false,因为即使是同样的字节码用不同的 ClassLoader 加载出来的类都不能算同一个类

depv1Class.equals(depv2Class)

我们还可以让两个不同版本的 Dep 类实现同一个接口,这样可以避免使用反射的方式来调用 Dep 类里面的方法。

  1. Class<?> depv1Class = v1.loadClass(“Dep”);
  2. IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance();
  3. depv1.print()

ClassLoader 固然可以解决依赖冲突问题,不过它也限制了不同软件包的操作界面必须使用反射或接口的方式进行动态调用。Maven 没有这种限制,它依赖于虚拟机的默认懒惰加载策略,运行过程中如果没有显示使用定制的 ClassLoader,那么从头到尾都是在使用 AppClassLoader,而不同版本的同名类必须使用不同的 ClassLoader 加载,所以 Maven 不能完美解决钻石依赖。

如果你想知道有没有开源的包管理工具可以解决钻石依赖的,我推荐你了解一下 sofa-ark,它是蚂蚁金服开源的轻量级类隔离框架。

分工与合作

这里我们重新理解一下 ClassLoader 的意义,它相当于类的命名空间,起到了类隔离的作用。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader 是类名称的容器,是类的沙箱。

 

%title插图%num

 

不同的 ClassLoader 之间也会有合作,它们之间的合作是通过 parent 属性和双亲委派机制来完成的。parent 具有更高的加载优先级。除此之外,parent 还表达了一种共享关系,当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类可以认为是所有子 ClassLoader 共享的。这也是为什么 BootstrapClassLoader 被所有的类加载器视为祖先加载器,JVM 核心类库自然应该被共享。

Thread.contextClassLoader

如果你稍微阅读过 Thread 的源代码,你会在它的实例字段中发现有一个字段非常特别

  1. class Thread {
  2.   …
  3.   private ClassLoader contextClassLoader;
  4.   public ClassLoader getContextClassLoader() {
  5.     return contextClassLoader;
  6.   }
  7.   public void setContextClassLoader(ClassLoader cl) {
  8.     this.contextClassLoader = cl;
  9.   }
  10.   …
  11. }

contextClassLoader「线程上下文类加载器」,这究竟是什么东西?

首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它。你可以使用下面这种方式来显示使用它

Thread.currentThread().getContextClassLoader().loadClass(name);

这意味着如果你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader。那些因为代码上的依赖关系而懒惰加载的类也不会自动使用 contextClassLoader来加载。

其次线程的 contextClassLoader 是从父线程那里继承过来的,所谓父线程就是创建了当前线程的线程。程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是 AppClassLoader。

那这个 contextClassLoader 究竟是做什么用的?我们要使用前面提到了类加载器分工与合作的原理来解释它的用途。

它可以做到跨线程共享类,只要它们共享同一个 contextClassLoader。父子线程之间会自动传递 contextClassLoader,所以共享起来将是自动化的。

如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来。

如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。

如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。

 

线程的 contextClassLoader 使用场合比较罕见,如果上面的逻辑晦涩难懂也不必过于计较。

JDK9 增加了模块功能之后对类加载器的结构设计做了一定程度的修改,不过类加载器的原理还是类似的,作为类的容器,它起到类隔离的作用,同时还需要依靠双亲委派机制来建立不同的类加载器之间的合作关系。

NFS服务器不能挂载问题终*解决办法

大部分NFS服务器不能挂载的问题都是windows端的NFS服务出现了问题,现在环境如下,win7下的NFS服务,虚拟机Ubuntu和开发板liunx,同时使用NFS服务进行挂载,开始使用正常,突然有一天不能使用了,挂载不上了,应该就是win7下的NFS服务出现了问题。无论出现什么问题,可以用以下两种方法解决:

1. 单击win按钮,找到haneWIN软件,或者到该软件的安装目录,默认在C:\Program Files (x86)\nfsd 目录下,右键以管理员身份重启所有服务。

%title插图%num

%title插图%num %title插图%num

咱们从头到尾说一次 Java 垃圾回收

转载自:https://mp.weixin.qq.com/s/7d1-xEvMKSav1Gcmla2u_g

%title插图%num

⬆️ 图片来源于网络

 

之前上学的时候有这个一个梗,说在食堂里吃饭,吃完把餐盘端走清理的,是 C++ 程序员,吃完直接就走的,是 Java 程序员。?

 

确实,在 Java 的世界里,似乎我们不用对垃圾回收那么的专注,很多初学者不懂 GC,也依然能写出一个能用甚至还不错的程序或系统。但其实这并不代表 Java 的 GC 就不重要。相反,它是那么的重要和复杂,以至于出了问题,那些初学者除了打开 GC 日志,看着一堆0101的天文,啥也做不了。?

 

今天我们就从头到尾完整地聊一聊 Java 的垃圾回收。
 

什么是垃圾回收


 

垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

 

Java 语言出来之前,大家都在拼命的写 C 或者 C++ 的程序,而此时存在一个很大的矛盾,C++ 等语言创建对象要不断的去开辟空间,不用的时候又需要不断的去释放控件,既要写构造函数,又要写析构函数,很多时候都在重复的 allocated,然后不停的析构。于是,有人就提出,能不能写一段程序实现这块功能,每次创建,释放控件的时候复用这段代码,而无需重复的书写呢?

 

1960年,基于 MIT 的 Lisp 首先提出了垃圾回收的概念,而这时 Java 还没有出世呢!所以实际上 GC 并不是Java的专利,GC 的历史远远大于 Java 的历史!

 

怎么定义垃圾


 

既然我们要做垃圾回收,首先我们得搞清楚垃圾的定义是什么,哪些内存是需要回收的。

 

 

引用计数算法

引用计数算法(Reachability Counting)是通过在对象头中分配一个空间来保存该对象被引用的次数(Reference Count)。如果该对象被其它对象引用,则它的引用计数加1,如果删除对该对象的引用,那么它的引用计数就减1,当该对象的引用计数为0时,那么该对象就会被回收。

String m = new String("jack");

 

先创建一个字符串,这时候”jack”有一个引用,就是 m。
%title插图%num

 

然后将 m 设置为 null,这时候”jack”的引用次数就等于0了,在引用计数算法中,意味着这块内容就需要被回收了。

m = null;

 

%title插图%num

 

引用计数算法是将垃圾回收分摊到整个应用程序的运行当中了,而不是在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。因此,采用引用计数的垃圾收集不属于严格意义上的”Stop-The-World”的垃圾收集机制。

 

看似很美好,但我们知道JVM的垃圾回收就是”Stop-The-World”的,那是什么原因导致我们*终放弃了引用计数算法呢?看下面的例子。

 

  1. public class ReferenceCountingGC {
  2. public Object instance;
  3. public ReferenceCountingGC(String name){}
  4. }
  5. public static void testGC(){
  6. ReferenceCountingGC a = new ReferenceCountingGC(“objA”);
  7. ReferenceCountingGC b = new ReferenceCountingGC(“objB”);
  8. a.instance = b;
  9. b.instance = a;
  10. a = null;
  11. b = null;
  12. }

 

1. 定义2个对象

2. 相互引用
3. 置空各自的声明引用

%title插图%num

我们可以看到,*后这2个对象已经不可能再被访问了,但由于他们相互引用着对方,导致它们的引用计数永远都不会为0,通过引用计数算法,也就永远无法通知GC收集器回收它们。

 

可达性分析算法

可达性分析算法(Reachability Analysis)的基本思路是,通过一些被称为引用链(GC Roots)的对象作为起点,从这些节点开始向下搜索,搜索走过的路径被称为(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连时(即从 GC Roots 节点到该节点不可达),则证明该对象是不可用的。

 

 

%title插图%num

 

通过可达性算法,成功解决了引用计数所无法解决的问题-“循环依赖”,只要你无法与 GC Root 建立直接或间接的连接,系统就会判定你为可回收对象。那这样就引申出了另一个问题,哪些属于 GC Root。

 

Java 内存区域

在 Java 语言中,可作为 GC Root 的对象包括以下4种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象

 

%title插图%num

 

虚拟机栈(栈帧中的本地变量表)中引用的对象
此时的 s,即为 GC Root,当s置空时,localParameter 对象也断掉了与 GC Root 的引用链,将被回收。

  1. public class StackLocalParameter {
  2. public StackLocalParameter(String name){}
  3. }
  4. public static void testGC(){
  5. StackLocalParameter s = new StackLocalParameter(“localParameter”);
  6. s = null;
  7. }

 

方法区中类静态属性引用的对象
s 为 GC Root,s 置为 null,经过 GC 后,s 所指向的 properties 对象由于无法与 GC Root 建立关系被回收。

而 m 作为类的静态属性,也属于 GC Root,parameter 对象依然与 GC root 建立着连接,所以此时 parameter 对象并不会被回收。

  1. public class MethodAreaStaicProperties {
  2. public static MethodAreaStaicProperties m;
  3. public MethodAreaStaicProperties(String name){}
  4. }
  5. public static void testGC(){
  6. MethodAreaStaicProperties s = new MethodAreaStaicProperties(“properties”);
  7. s.m = new MethodAreaStaicProperties(“parameter”);
  8. s = null;
  9. }

 

方法区中常量引用的对象
m 即为方法区中的常量引用,也为 GC Root,s 置为 null 后,final 对象也不会因没有与 GC Root 建立联系而被回收。

  1. public class MethodAreaStaicProperties {
  2. public static final MethodAreaStaicProperties m = MethodAreaStaicProperties(“final”);
  3. public MethodAreaStaicProperties(String name){}
  4. }
  5. public static void testGC(){
  6. MethodAreaStaicProperties s = new MethodAreaStaicProperties(“staticProperties”);
  7. s = null;
  8. }

 

本地方法栈中引用的对象
任何 Native 接口都会使用某种本地方法栈,实现的本地方法接口是使用 C 连接模型的话,那么它的本地方法栈就是 C 栈。当线程调用 Java 方法时,虚拟机会创建一个新的栈帧并压入 Java 栈。然而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不再在线程的 Java 栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

 

%title插图%num

 

 

怎么回收垃圾


 

在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,这里我们讨论几种常见的垃圾收集算法的核心思想。

标记 — 清除算法

%title插图%num

标记清除算法(Mark-Sweep)是*基础的一种垃圾回收算法,它分为2部分,先把内存区域中的这些对象进行标记,哪些属于可回收标记出来,然后把这些垃圾拎出来清理掉。就像上图一样,清理掉的垃圾就变成未使用的内存区域,等待被再次使用。

这逻辑再清晰不过了,并且也很好操作,但它存在一个很大的问题,那就是内存碎片。

上图中等方块的假设是 2M,小一些的是 1M,大一些的是 4M。等我们回收完,内存就会切成了很多段。我们知道开辟内存空间时,需要的是连续的内存区域,这时候我们需要一个 2M的内存区域,其中有2个 1M 是没法用的。这样就导致,其实我们本身还有这么多的内存的,但却用不了。

复制算法

%title插图%num

 

复制算法(Copying)是在标记清除算法上演化而来,解决标记清除算法的内存碎片问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。保证了内存的连续可用,内存分配时也就不用考虑内存碎片等复杂情况,逻辑清晰,运行高效。

 

上面的图很清楚,也很明显的暴露了另一个问题,合着我这140平的大三房,只能当70平米的小两房来使?代价实在太高。

 

标记整理算法

%title插图%num

 

标记整理算法(Mark-Compact)标记过程仍然与标记 — 清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,再清理掉端边界以外的内存区域。

 

标记整理算法一方面在标记-清除算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但从上图可以看到,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。

 

分代收集算法分代收集算法(Generational Collection)严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。对象存活周期的不同将内存划分为几块。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用*适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清理或者标记 — 整理算法来进行回收。so,另一个问题来了,那内存区域到底被分为哪几块,每一块又有什么特别适合什么算法呢?

 

 

内存模型与回收策略


 

 

%title插图%num

 

Java 堆(Java Heap)是JVM所管理的内存中*大的一块,堆又是垃圾收集器管理的主要区域,这里我们主要分析一下 Java 堆的结构。

 

Java 堆主要分为2个区域-年轻代与老年代,其中年轻代又分 Eden 区和 Survivor 区,其中 Survivor 区又分 From 和 To 2个区。可能这时候大家会有疑问,为什么需要 Survivor 区,为什么Survivor 还要分2个区。不着急,我们从头到尾,看看对象到底是怎么来的,而它又是怎么没的。

 

Eden 区

IBM 公司的专业研究表明,有将近98%的对象是朝生夕死,所以针对这一现状,大多数情况下,对象会在新生代 Eden 区中进行分配,当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。

通过 Minor GC 之后,Eden 会被清空,Eden 区中*大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。

 

Survivor 区

Survivor 区相当于是 Eden 区和 Old 区的一个缓冲,类似于我们交通灯中的黄灯。Survivor 又分为2个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(如果 To 区不够,则直接进入 Old 区)。

 

为啥需要?

不就是新生代到老年代么,直接 Eden 到 Old 不好了吗,为啥要这么复杂。想想如果没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有很多对象虽然一次 Minor GC 没有消灭,但其实也并不会蹦跶多久,或许第二次,第三次就需要被清除。这时候移入老年区,很明显不是一个明智的决定。

 

所以,Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。

 

为啥需要俩?

设置两个 Survivor 区*大的好处就是解决内存碎片化。

 

我们先假设一下,Survivor 如果只有一个区域会怎样。Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。问题来了,这时候我们怎么清除它们?在这种场景下,我们只能标记清除,而我们知道标记清除*大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为 Survivor 有2个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责兑换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。

 

这种机制*大的好处就是,整个过程中,永远有一个 Survivor space 是空的,另一个非空的 Survivor space 是无碎片的。那么,Survivor 为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果 Survivor 区再细分下去,每一块的空间就会比较小,容易导致 Survivor 区满,两块 Survivor 区可能是经过权衡之后的*佳方案。

 

Old 区

老年代占据着2/3的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记 — 整理算法。

 

除了上述所说,在内存担保机制下,无法安置的对象会直接进到老年代,以下几种情况也会进入老年代。

 

大对象

大对象指需要大量连续内存空间的对象,这部分对象不管是不是“朝生夕死”,都会直接进到老年代。这样做主要是为了避免在 Eden 区及2个 Survivor 区之间发生大量的内存复制。当你的系统有非常多“朝生夕死”的大对象时,得注意了。

 

长期存活对象

虚拟机给每个对象定义了一个对象年龄(Age)计数器。正常情况下对象会不断的在 Survivor 的 From 区与 To 区之间移动,对象在 Survivor 区中每经历一次 Minor GC,年龄就增加1岁。当年龄增加到15岁时,这时候就会被转移到老年代。当然,这里的15,JVM 也支持进行特殊设置。

 

动态对象年龄

虚拟机并不重视要求对象年龄必须到15岁,才会放入老年区,如果 Survivor 空间中相同年龄所有对象大小的总合大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进去老年区,无需等你“成年”。

 

这其实有点类似于负载均衡,轮询是负载均衡的一种,保证每台机器都分得同样的请求。看似很均衡,但每台机的硬件不通,健康状况不同,我们还可以基于每台机接受的请求数,或每台机的响应时间等,来调整我们的负载均衡算法。

 

本文部分内容参考自书籍:《深入理解Java虚拟机》。

冒泡排序法

冒泡排序法

冒泡排序算法问题:
算法介绍:冒泡排序法是一种简单的排序算法,假设有某一数列 a 1 , a 2 , … a n a_1,a_2,…a_n a
1

,a
2

,…a
n

,算法过程如下。

step1: 首先比较 a 1 a_1 a
1

与 a 2 a_2 a
2

的大小,如果 a 1 > a 2 a_1>a_2 a
1

>a
2

,则令 a 1 a_1 a
1

与 a 2 a_2 a
2

相互交换,接着比较 a 2 a_2 a
2

与 a 3 a_3 a
3

的大小,同样,如果 a 2 > a 3 a_2>a_3 a
2

>a
3

,则令 a 2 a_2 a
2

与 a 3 a_3 a
3

相互交换,以此类推,直至比较 a n − 1 a_{n-1} a
n−1

与 a n a_n a
n

的大小,如果 a n − 1 > a n a_{n-1}>a_n a
n−1

>a
n

,则令 a n − 1 a_{n-1} a
n−1

与 a n a_n a
n

相互交换,经过上述操作, a n a_n a
n

就变成了该数列中*大的元素。

step2: 仍然从 a 1 a_1 a
1

与 a 2 a_2 a
2

开始比较,沿用*步中的方法,不同的是,只需比较到 a n − 2 a_{n-2} a
n−2

与 a n − 1 a_{n-1} a
n−1

,以保证 a n − 1 a_{n-1} a
n−1

是前 n − 1 n-1 n−1个元素中的*大数。

step3: 按照上述步骤step2,即可保证 a i a_i a
i

是前 i i i个元素中的*大数,其中 i = n − 2 , n − 1 , … 1 i=n-2,n-1,…1 i=n−2,n−1,…1,这样,*终所得到的新的数列 a 1 , a 2 , … a n a_1,a_2,…a_n a
1

,a
2

,…a
n

即为原始数列从小到大排列后的结果。

MATLAB程序实现

%Date:2019-11-11
%Writer:无名十三

%%本程序的目的是利用冒泡排序算法对已知某组数据进行排序
function y = bubble_sort(A) %参数A为向量或矩阵
if nargin==0
errordlg(‘未输入参数!’, ‘Error!’)
end
if ischar(A) == 1
errordlg(‘输入数据中含有字符或字符串,无法进行排序!’, ‘Error!’)
error %报错终止程序运行
end

m = size(A); %求出参数A的维数
if m(1)==1 | m(2)==1 %当参数A为向量不是矩阵时
n = length(A); %得到向量A布包含的元素个数
else %参数A为矩阵而非向量时
n = m(1)*m(2); %得到矩阵A中包含的元素个数
A = reshape(A, 1, n); %化矩阵A为向量
end

for i = 1:n-1
for j = 1:n-i
if A(j) > A(j+1)
temp = A(j); %核心代码
A(j) = A(j+1);
A(j+1) = temp;
end
end
end

fprintf(‘经冒泡排序算法排序后的结果如下:\n\n’) %输出排序结果
for i = 1:n
fprintf(‘%f ‘, A(i))
if mod(i,8) == 0
fprintf(‘\n\n’)
end
end
fprintf(‘\n’)
end
%%

示例1:对如下向量A进行排序
>> A = [1 6 9 8 13 6 9 5 20 15 36 27];
>> bubble_sort(A)
经冒泡排序算法排序后的结果如下:

1.000000 5.000000 6.000000 6.000000 8.000000 9.000000 9.000000 13.000000

15.000000 20.000000 27.000000 36.000000

示例2:对如下三阶方阵B进行排序
>> B = [2 3 8; 9 6 7; 15 32 17];
>> bubble_sort(B)
经冒泡排序算法排序后的结果如下:

2.000000 3.000000 6.000000 7.000000 8.000000 9.000000 15.000000 17.000000

32.000000

判断素数:

P判断素数:
说明:本程序首先定义一个判断素数并将其输出的函数 I s P r i m e ( ) IsPrime() IsPrime(),函数包含两个参数 a a a和 b b b,本程序的作用即输出 a a a和 b b b之间的所有素数。
Python程序实现
#Date:2019-11-12
#Writer:无名十三

def IsPrime(a, b): #定义一个判断素数的函数
list_Prime = [] #创建一个空列表,用于接收a与b之间的素数
for i in range(a, b+1):
for j in range(2, i+1):
if j < i:
if i % j == 0:
break
if j == i:
list_Prime.append(i) #将经判断得到的素数放入列表中

print(‘从{}至{}之间的素数如下:\n’.format(a, b))
for k in range(len(list_Prime)):
print(list_Prime[k], end = ‘ ‘)
if (k+1) % 6 == 0:
print(‘\n’)
print(‘\n\n共{}个素数.’.format(len(list_Prime)))

示例:输出1至100之间所有的素数如下:
IsPrime(1, 100) #调用函数
1
程序运行结果如下:
从1至100之间的素数如下:

2 3 5 7 11 13

17 19 23 29 31 37

41 43 47 53 59 61

67 71 73 79 83 89

97

共25个素数.

详解Java8 Collect收集Stream的方法

转载自:https://www.jb51.net/article/138519.htm

这篇文章主要介绍了Java8-Collect收集Stream的方法,提到了收集器的作用,连接收集器的方法,需要的朋友可以参考下

Collection, Collections, collect, Collector, Collectos

Collection是Java集合的祖先接口。
Collections是java.util包下的一个工具类,内涵各种处理集合的静态方法。
java.util.stream.Stream#collect(java.util.stream.Collector<? super T,A,R>)是Stream的一个函数,负责收集流。
java.util.stream.Collector 是一个收集函数的接口, 声明了一个收集器的功能。
java.util.Comparators则是一个收集器的工具类,内置了一系列收集器实现。

收集器的作用

你可以把Java8的流看做花哨又懒惰的数据集迭代器。他们支持两种类型的操作:中间操作(e.g. filter, map)和终端操作(如count, findFirst, forEach, reduce). 中间操作可以连接起来,将一个流转换为另一个流。这些操作不会消耗流,其目的是建立一个流水线。与此相反,终端操作会消耗类,产生一个*终结果。collect就是一个归约操作,就像reduce一样可以接受各种做法作为参数,将流中的元素累积成一个汇总结果。具体的做法是通过定义新的Collector接口来定义的。

预定义的收集器

下面简单演示基本的内置收集器。模拟数据源如下:

final ArrayList<Dish> dishes = Lists.newArrayList(
    new Dish("pork", false, 800, Type.MEAT),
    new Dish("beef", false, 700, Type.MEAT),
    new Dish("chicken", false, 400, Type.MEAT),
    new Dish("french fries", true, 530, Type.OTHER),
    new Dish("rice", true, 350, Type.OTHER),
    new Dish("season fruit", true, 120, Type.OTHER),
    new Dish("pizza", true, 550, Type.OTHER),
    new Dish("prawns", false, 300, Type.FISH),
    new Dish("salmon", false, 450, Type.FISH)
);

*大值,*小值,平均值

// 为啥返回Optional? 如果stream为null怎么办, 这时候Optinal就很有意义了
Optional<Dish> mostCalorieDish = dishes.stream().max(Comparator.comparingInt(Dish::getCalories));
Optional<Dish> minCalorieDish = dishes.stream().min(Comparator.comparingInt(Dish::getCalories));
Double avgCalories = dishes.stream().collect(Collectors.averagingInt(Dish::getCalories));
IntSummaryStatistics summaryStatistics = dishes.stream().collect(Collectors.summarizingInt(Dish::getCalories));
double average = summaryStatistics.getAverage();
long count = summaryStatistics.getCount();
int max = summaryStatistics.getMax();
int min = summaryStatistics.getMin();
long sum = summaryStatistics.getSum();

这几个简单的统计指标都有Collectors内置的收集器函数,尤其是针对数字类型拆箱函数,将会比直接操作包装类型开销小很多。

连接收集器

想要把Stream的元素拼起来?

//直接连接
String join1 = dishes.stream().map(Dish::getName).collect(Collectors.joining());
//逗号
String join2 = dishes.stream().map(Dish::getName).collect(Collectors.joining(", "));

toList

List<String> names = dishes.stream().map(Dish::getName).collect(toList());

将原来的Stream映射为一个单元素流,然后收集为List。

toSet

Set<Type> types = dishes.stream().map(Dish::getType).collect(Collectors.toSet());

将Type收集为一个set,可以去重复。

toMap

Map<Type, Dish> byType = dishes.stream().collect(toMap(Dish::getType, d -> d));

有时候可能需要将一个数组转为map,做缓存,方便多次计算获取。toMap提供的方法k和v的生成函数。(注意,上述demo是一个坑,不可以这样用!!!, 请使用toMap(Function, Function, BinaryOperator))

上面几个几乎是*常用的收集器了,也基本够用了。但作为初学者来说,理解需要时间。想要真正明白为什么这样可以做到收集,就必须查看内部实现,可以看到,这几个收集器都是基于java.util.stream.Collectors.CollectorImpl,也就是开头提到过了Collector的一个实现类。后面自定义收集器会学习具体用法。

自定义归约reducing

前面几个都是reducing工厂方法定义的归约过程的特殊情况,其实可以用Collectors.reducing创建收集器。比如,求和

Integer totalCalories = dishes.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j));
//使用内置函数代替箭头函数
Integer totalCalories2 = dishes.stream().collect(reducing(0, Dish::getCalories, Integer::sum));

当然也可以直接使用reduce

Optional<Integer> totalCalories3 = dishes.stream().map(Dish::getCalories).reduce(Integer::sum);

虽然都可以,但考量效率的话,还是要选择下面这种

int sum = dishes.stream().mapToInt(Dish::getCalories).sum();

根据情况选择*佳方案

上面的demo说明,函数式编程通常提供了多种方法来执行同一个操作,使用收集器collect比直接使用stream的api用起来更加复杂,好处是collect能提供更高水平的抽象和概括,也更容易重用和自定义。

我们的建议是,尽可能为手头的问题探索不同的解决方案,始终选择*专业的一个,无论从可读性还是性能来看,这一般都是*好的决定。

reducing除了接收一个初始值,还可以把*项当作初始值

Optional<Dish> mostCalorieDish = dishes.stream()
        .collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));

reducing

关于reducing的用法比较复杂,目标在于把两个值合并成一个值。

public static <T, U>
  Collector<T, ?, U> reducing(U identity,
                Function<? super T, ? extends U> mapper,
                BinaryOperator<U> op)

首先看到3个泛型,

U是返回值的类型,比如上述demo中计算热量的,U就是Integer。

关于T,T是Stream里的元素类型。由Function的函数可以知道,mapper的作用就是接收一个参数T,然后返回一个结果U。对应demo中Dish。

?在返回值Collector的泛型列表的中间,这个表示容器类型,一个收集器当然需要一个容器来存放数据。这里的?则表示容器类型不确定。事实上,在这里的容器就是U[]。

关于参数:

identity是返回值类型的初始值,可以理解为累加器的起点。

mapper则是map的作用,意义在于将Stream流转换成你想要的类型流。

op则是核心函数,作用是如何处理两个变量。其中,*个变量是累积值,可以理解为sum,第二个变量则是下一个要计算的元素。从而实现了累加。

reducing还有一个重载的方法,可以省略*个参数,意义在于把Stream里的*个参数当做初始值。

public static <T> Collector<T, ?, Optional<T>>
  reducing(BinaryOperator<T> op)

先看返回值的区别,T表示输入值和返回值类型,即输入值类型和输出值类型相同。还有不同的就是Optional了。这是因为没有初始值,而*个参数有可能是null,当Stream的元素是null的时候,返回Optional就很意义了。

再看参数列表,只剩下BinaryOperator。BinaryOperator是一个三元组函数接口,目标是将两个同类型参数做计算后返回同类型的值。可以按照1>2? 1:2来理解,即求两个数的*大值。求*大值是比较好理解的一种说法,你可以自定义lambda表达式来选择返回值。那么,在这里,就是接收两个Stream的元素类型T,返回T类型的返回值。用sum累加来理解也可以。

上述的demo中发现reduce和collect的作用几乎一样,都是返回一个*终的结果,比如,我们可以使用reduce实现toList效果:

//手动实现toListCollector --- 滥用reduce, 不可变的规约---不可以并行
List<Integer> calories = dishes.stream().map(Dish::getCalories)
    .reduce(new ArrayList<Integer>(),
        (List<Integer> l, Integer e) -> {
          l.add(e);
          return l;
        },
        (List<Integer> l1, List<Integer> l2) -> {
          l1.addAll(l2);
          return l1;
        }
    );

关于上述做法解释一下。

<U> U reduce(U identity,
         BiFunction<U, ? super T, U> accumulator,
         BinaryOperator<U> combiner);

U是返回值类型,这里就是List

BiFunction<U, ? super T, U> accumulator是是累加器,目标在于累加值和单个元素的计算规则。这里就是List和元素做运算,*终返回List。即,添加一个元素到list。

BinaryOperator<U> combiner是组合器,目标在于把两个返回值类型的变量合并成一个。这里就是两个list合并。
这个解决方案有两个问题:一个是语义问题,一个是实际问题。语义问题在于,reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变归约。相反,collect方法的设计就是要改变容器,从而累积要输出的结果。这意味着,上面的代码片段是在滥用reduce方法,因为它在原地改变了作为累加器的List。错误的语义来使用reduce方法还会造成一个实际问题:这个归约不能并行工作,因为由多个线程并发修改同一个数据结构可能会破坏List本身。在这种情况下,如果你想要线程安全,就需要每次分配一个新的List,而对象分配又会影响性能。这就是collect适合表达可变容器上的归约的原因,更关键的是它适合并行操作。

总结:reduce适合不可变容器归约,collect适合可变容器归约。collect适合并行。

分组

数据库中经常遇到分组求和的需求,提供了group by原语。在Java里, 如果按照指令式风格(手动写循环)的方式,将会非常繁琐,容易出错。而Java8则提供了函数式解法。

比如,将dish按照type分组。和前面的toMap类似,但分组的value却不是一个dish,而是一个List。

Map<Type, List<Dish>> dishesByType = dishes.stream().collect(groupingBy(Dish::getType));

 

这里

public static <T, K> Collector<T, ?, Map<K, List<T>>>
  groupingBy(Function<? super T, ? extends K> classifier)

参数分类器为Function,旨在接收一个参数,转换为另一个类型。上面的demo就是把stream的元素dish转成类型Type,然后根据Type将stream分组。其内部是通过HashMap来实现分组的。groupingBy(classifier, HashMap::new, downstream);

除了按照stream元素自身的属性函数去分组,还可以自定义分组依据,比如根据热量范围分组。

既然已经知道groupingBy的参数为Function, 并且Function的参数类型为Dish,那么可以自定义分类器为:

private CaloricLevel getCaloricLevel(Dish d) {
  if (d.getCalories() <= 400) {
   return CaloricLevel.DIET;
  } else if (d.getCalories() <= 700) {
   return CaloricLevel.NORMAL;
  } else {
   return CaloricLevel.FAT;
  }
}

 

再传入参数即可

Map<CaloricLevel, List<Dish>> dishesByLevel = dishes.stream()
    .collect(groupingBy(this::getCaloricLevel));

多级分组

groupingBy还重载了其他几个方法,比如

public static <T, K, A, D>
  Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                     Collector<? super T, A, D> downstream)

泛型多的恐怖。简单的认识一下。classifier还是分类器,就是接收stream的元素类型,返回一个你想要分组的依据,也就是提供分组依据的基数的。所以T表示stream当前的元素类型,K表示分组依据的元素类型。第二个参数downstream,下游是一个收集器Collector. 这个收集器元素类型是T的子类,容器类型container为A,reduction返回值类型为D。也就是说分组的K通过分类器提供,分组的value则通过第二个参数的收集器reduce出来。正好,上个demo的源码为:

public static <T, K> Collector<T, ?, Map<K, List<T>>>
  groupingBy(Function<? super T, ? extends K> classifier) {
    return groupingBy(classifier, toList());
  }

 

将toList当作reduce收集器,*终收集的结果是一个List<Dish>, 所以分组结束的value类型是List<Dish>。那么,可以类推value类型取决于reduce收集器,而reduce收集器则有千千万。比如,我想对value再次分组,分组也是一种reduce。

//多级分组
Map<Type, Map<CaloricLevel, List<Dish>>> byTypeAndCalory = dishes.stream().collect(
  groupingBy(Dish::getType, groupingBy(this::getCaloricLevel)));
byTypeAndCalory.forEach((type, byCalory) -> {
 System.out.println("----------------------------------");
 System.out.println(type);
 byCalory.forEach((level, dishList) -> {
  System.out.println("\t" + level);
  System.out.println("\t\t" + dishList);
 });
});

验证结果为:

----------------------------------
FISH
  DIET
    [Dish(name=prawns, vegetarian=false, calories=300, type=FISH)]
  NORMAL
    [Dish(name=salmon, vegetarian=false, calories=450, type=FISH)]
----------------------------------
MEAT
  FAT
    [Dish(name=pork, vegetarian=false, calories=800, type=MEAT)]
  DIET
    [Dish(name=chicken, vegetarian=false, calories=400, type=MEAT)]
  NORMAL
    [Dish(name=beef, vegetarian=false, calories=700, type=MEAT)]
----------------------------------
OTHER
  DIET
    [Dish(name=rice, vegetarian=true, calories=350, type=OTHER), Dish(name=season fruit, vegetarian=true, calories=120, type=OTHER)]
  NORMAL
    [Dish(name=french fries, vegetarian=true, calories=530, type=OTHER), Dish(name=pizza, vegetarian=true, calories=550, type=OTHER)]

总结:groupingBy的核心参数为K生成器,V生成器。V生成器可以是任意类型的收集器Collector。

比如,V生成器可以是计算数目的, 从而实现了sql语句中的select count(*) from table A group by Type

Map<Type, Long> typesCount = dishes.stream().collect(groupingBy(Dish::getType, counting()));
System.out.println(typesCount);
-----------
{FISH=2, MEAT=3, OTHER=4}

sql查找分组*高分select MAX(id) from table A group by Type

Map<Type, Optional<Dish>> mostCaloricByType = dishes.stream()
    .collect(groupingBy(Dish::getType, maxBy(Comparator.comparingInt(Dish::getCalories))));

这里的Optional没有意义,因为肯定不是null。那么只好取出来了。使用collectingAndThen

Map<Type, Dish> mostCaloricByType = dishes.stream()
  .collect(groupingBy(Dish::getType,
    collectingAndThen(maxBy(Comparator.comparingInt(Dish::getCalories)), Optional::get)));

到这里似乎结果出来了,但IDEA不同意,编译黄色报警,按提示修改后变为:

Map<Type, Dish> mostCaloricByType = dishes.stream()
  .collect(toMap(Dish::getType, Function.identity(),
    BinaryOperator.maxBy(comparingInt(Dish::getCalories))));

 

是的,groupingBy就变成toMap了,key还是Type,value还是Dish,但多了一个参数!!这里回应开头的坑,开头的toMap演示是为了容易理解,真那么用则会被搞死。我们知道把一个List重组为Map必然会面临k相同的问题。当K相同时,v是覆盖还是不管呢?前面的demo的做法是当k存在时,再次插入k则直接抛出异常:

java.lang.IllegalStateException: Duplicate key Dish(name=pork, vegetarian=false, calories=800, type=MEAT)
  at java.util.stream.Collectors.lambda$throwingMerger$0(Collectors.java:133)

 

正确的做法是提供处理冲突的函数,在本demo中,处理冲突的原则就是找出*大的,正好符合我们分组求*大的要求。(真的不想搞Java8函数式学习了,感觉到处都是性能问题的坑)

继续数据库sql映射,分组求和select sum(score) from table a group by Type

Map<Type, Integer> totalCaloriesByType = dishes.stream()
  .collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));

然而常常和groupingBy联合使用的另一个收集器是mapping方法生成的。这个方法接收两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收集起来。其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接收特定类型元素的收集器适应不同类型的对象。我么来看一个使用这个收集器的实际例子。比如你想得到,对于每种类型的Dish,菜单中都有哪些CaloricLevel。我们可以把groupingBy和mapping收集器结合起来,如下所示:

Map<Type, Set<CaloricLevel>> caloricLevelsByType = dishes.stream()
  .collect(groupingBy(Dish::getType, mapping(this::getCaloricLevel, toSet())));

这里的toSet默认采用的HashSet,也可以手动指定具体实现toCollection(HashSet::new)

分区

分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称为分区函数。分区函数返回一个布尔值,这意味着得到的分组Map的键类型是Boolean,于是它*多可以分为两组:true or false. 例如,如果你是素食者,你可能想要把菜单按照素食和非素食分开:

Map<Boolean, List<Dish>> partitionedMenu = dishes.stream().collect(partitioningBy(Dish::isVegetarian));

当然,使用filter可以达到同样的效果:

List<Dish> vegetarianDishes = dishes.stream().filter(Dish::isVegetarian).collect(Collectors.toList());

 

分区相对来说,优势就是保存了两个副本,当你想要对一个list分类时挺有用的。同时,和groupingBy一样,partitioningBy一样有重载方法,可以指定分组value的类型。

Map<Boolean, Map<Type, List<Dish>>> vegetarianDishesByType = dishes.stream()
  .collect(partitioningBy(Dish::isVegetarian, groupingBy(Dish::getType)));
Map<Boolean, Integer> vegetarianDishesTotalCalories = dishes.stream()
  .collect(partitioningBy(Dish::isVegetarian, summingInt(Dish::getCalories)));
Map<Boolean, Dish> mostCaloricPartitionedByVegetarian = dishes.stream()
  .collect(partitioningBy(Dish::isVegetarian,
    collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get)));

作为使用partitioningBy收集器的*后一个例子,我们把菜单数据模型放在一边,来看一个更加复杂也更为有趣的例子:将数组分为质数和非质数。

首先,定义个质数分区函数:

private boolean isPrime(int candidate) {
  int candidateRoot = (int) Math.sqrt((double) candidate);
  return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i == 0);
}

 

然后找出1到100的质数和非质数

Map<Boolean, List<Integer>> partitionPrimes = IntStream.rangeClosed(2, 100).boxed()
  .collect(partitioningBy(this::isPrime));

服务器nas目录挂载

1. 重启后,如果df -h看不见挂载点,请尝试直接cd /nfsc/NAS卷名(autofs,无访问不显示);
2. 如果不能cd到挂载点里,请检查主机IP到NAS服务器2049端口是否联通, telnet NAS服务器IP 2049。
3. 如果可以telnet NAS服务器IP 2049通,进行showmount -e 加nas服务器ip 看nas卷授权情况
4. 如果端口也是通的,nas卷授权也存在,仍无法挂载成功。

NFSC挂载先查询
%title插图%num

[appdeploy@CNSZ22PL0215:/]$df -h |grep ‘HRSS’
172.16.31.10:/HRSS_GPS_SIT
976M  2.0M  907M   1% /nfsc/HRSS_GPS

%title插图%num

当前能访问的NFSC
[appdeploy@DockerHost_10_203_225_26 ~]$showmount -e 172.16.31.10 | grep ‘HRSS’
/HRSS_GPS_STG                 10.202.154.201,10.202.154.200
/HRSS_GPS_SIT                 10.202.74.201,10.202.74.200

10张图带你深入理解Docker容器和镜像

这篇文章希望能够帮助读者深入理解Docker的命令,还有容器(container)和镜像(image)之间的区别,并深入探讨容器和运行中的容器之间的区别。
%title插图%num

当我对Docker技术还是一知半解的时候,我发现理解Docker的命令非常困难。于是,我花了几周的时间来学习Docker的工作原理,更确切地说,是关于Docker统一文件系统(the union file system)的知识,然后回过头来再看Docker的命令,一切变得顺理成章,简单*了。

题外话:就我个人而言,掌握一门技术并合理使用它的*好办法就是深入理解这项技术背后的工作原理。通常情况下,一项新技术的诞生常常会伴随着媒体的大肆宣传和炒作,这使得用户很难看清技术的本质。更确切地说,新技术总是会发明一些新的术语或者隐喻词来帮助宣传,这在初期是非常有帮助的,但是这给技术的原理蒙上了一层砂纸,不利于用户在后期掌握技术的真谛。

Git就是一个很好的例子。我之前不能够很好的使用Git,于是我花了一段时间去学习Git的原理,直到这时,我才真正明白了Git的用法。我坚信只有真正理解Git内部原理的人才能够掌握这个工具。

Image Definition

镜像(Image)就是一堆只读层(read-only layer)的统一视角,也许这个定义有些难以理解,下面的这张图能够帮助读者理解镜像的定义。
%title插图%num

从左边我们看到了多个只读层,它们重叠在一起。除了*下面一层,其它层都会有一个指针指向下一层。这些层是Docker内部的实现细节,并且能够在主机(译者注:运行Docker的机器)的文件系统上访问到。统一文件系统(union file system)技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统。我们可以在图片的右边看到这个视角的形式。

你可以在你的主机文件系统上找到有关这些层的文件。需要注意的是,在一个运行中的容器内部,这些层是不可见的。在我的主机上,我发现它们存在于/var/lib/docker/aufs目录下。


sudo tree -L 1 /var/lib/docker/

/var/lib/docker/├── aufs├── containers├── graph├── init├── linkgraph.db├── repositories-aufs├── tmp├── trust└── volumes7 directories, 2 files

 

Container Definition

容器(container)的定义和镜像(image)几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的*上面那一层是可读可写的。
%title插图%num

细心的读者可能会发现,容器的定义并没有提及容器是否在运行,没错,这是故意的。正是这个发现帮助我理解了很多困惑。

要点:容器 = 镜像 + 读写层。并且容器的定义并没有提及是否要运行容器。

接下来,我们将会讨论运行态容器。

Running Container Definition

一个运行态容器(running container)被定义为一个可读写的统一文件系统加上隔离的进程空间和包含其中的进程。下面这张图片展示了一个运行中的容器。
%title插图%num

正是文件系统隔离技术使得Docker成为了一个前途无量的技术。一个容器中的进程可能会对文件进行修改、删除、创建,这些改变都将作用于可读写层(read-write layer)。下面这张图展示了这个行为。
%title插图%num

我们可以通过运行以下命令来验证我们上面所说的:

docker run ubuntu touch happiness.txt

即便是这个ubuntu容器不再运行,我们依旧能够在主机的文件系统上找到这个新文件。


find / -name happiness.txt

/var/lib/docker/aufs/diff/860a7b…889/happiness.txt

 

Image Layer Definition

为了将零星的数据整合起来,我们提出了镜像层(image layer)这个概念。下面的这张图描述了一个镜像层,通过图片我们能够发现一个层并不仅仅包含文件系统的改变,它还能包含了其他重要信息。
%title插图%num

元数据(metadata)就是关于这个层的额外信息,它不仅能够让Docker获取运行和构建时的信息,还包括父层的层次信息。需要注意,只读层和读写层都包含元数据。
%title插图%num

除此之外,每一层都包括了一个指向父层的指针。如果一个层没有这个指针,说明它处于*底层。
%title插图%num

Metadata Location:
我发现在我自己的主机上,镜像层(image layer)的元数据被保存在名为”json”的文件中,比如说:

/var/lib/docker/graph/e809f156dc985.../json

e809f156dc985…就是这层的id

一个容器的元数据好像是被分成了很多文件,但或多或少能够在/var/lib/docker/containers/<id>目录下找到,<id>就是一个可读层的id。这个目录下的文件大多是运行时的数据,比如说网络,日志等等。

全局理解(Tying It All Together)

现在,让我们结合上面提到的实现细节来理解Docker的命令。
docker create <image-id>

%title插图%num

docker create 命令为指定的镜像(image)添加了一个可读写层,构成了一个新的容器。注意,这个容器并没有运行。
%title插图%num

 

docker start <container-id>

%title插图%num

Docker start命令为容器文件系统创建了一个进程隔离空间。注意,每一个容器只能够有一个进程隔离空间。
docker run <image-id>

%title插图%num

看到这个命令,读者通常会有一个疑问:docker start 和 docker run命令有什么区别。
%title插图%num

从图片可以看出,docker run 命令先是利用镜像创建了一个容器,然后运行这个容器。这个命令非常的方便,并且隐藏了两个命令的细节,但从另一方面来看,这容易让用户产生误解。

题外话:继续我们之前有关于Git的话题,我认为docker run命令类似于git pull命令。git pull命令就是git fetch 和 git merge两个命令的组合,同样的,docker run就是docker create和docker start两个命令的组合。
docker ps

%title插图%num

docker ps 命令会列出所有运行中的容器。这隐藏了非运行态容器的存在,如果想要找出这些容器,我们需要使用下面这个命令。
docker ps –a

%title插图%num

docker ps –a命令会列出所有的容器,不管是运行的,还是停止的。
docker images

%title插图%num

docker images命令会列出了所有顶层(top-level)镜像。实际上,在这里我们没有办法区分一个镜像和一个只读层,所以我们提出了top-level镜像。只有创建容器时使用的镜像或者是直接pull下来的镜像能被称为顶层(top-level)镜像,并且每一个顶层镜像下面都隐藏了多个镜像层。
docker images –a

%title插图%num

docker images –a命令列出了所有的镜像,也可以说是列出了所有的可读层。如果你想要查看某一个image-id下的所有层,可以使用docker history来查看。
docker stop <container-id>

%title插图%num

docker stop命令会向运行中的容器发送一个SIGTERM的信号,然后停止所有的进程。
docker kill <container-id>

%title插图%num

docker kill 命令向所有运行在容器中的进程发送了一个不友好的SIGKILL信号。
docker pause <container-id>

%title插图%num

docker stop和docker kill命令会发送UNIX的信号给运行中的进程,docker pause命令则不一样,它利用了cgroups的特性将运行中的进程空间暂停。具体的内部原理你可以在这里找到:https://www.kernel.org/doc/Doc … m.txt,但是这种方式的不足之处在于发送一个SIGTSTP信号对于进程来说不够简单易懂,以至于不能够让所有进程暂停。
docker rm <container-id>

%title插图%num

docker rm命令会移除构成容器的可读写层。注意,这个命令只能对非运行态容器执行。
docker rmi <image-id>

%title插图%num

docker rmi 命令会移除构成镜像的一个只读层。你只能够使用docker rmi来移除*顶层(top level layer)(也可以说是镜像),你也可以使用-f参数来强制删除中间的只读层。
docker commit <container-id>

%title插图%num

docker commit命令将容器的可读写层转换为一个只读层,这样就把一个容器转换成了不可变的镜像。
%title插图%num

 

docker build

%title插图%num

docker build命令非常有趣,它会反复的执行多个命令。
%title插图%num

我们从上图可以看到,build命令根据Dockerfile文件中的FROM指令获取到镜像,然后重复地1)run(create和start)、2)修改、3)commit。在循环中的每一步都会生成一个新的层,因此许多新的层会被创建。
docker exec <running-container-id>

%title插图%num

docker exec 命令会在运行中的容器执行一个新进程。
docker inspect <container-id> or <image-id>

%title插图%num

docker inspect命令会提取出容器或者镜像*顶层的元数据。
docker save <image-id>

%title插图%num

docker save命令会创建一个镜像的压缩文件,这个文件能够在另外一个主机的Docker上使用。和export命令不同,这个命令为每一个层都保存了它们的元数据。这个命令只能对镜像生效。
docker export <container-id>

%title插图%num

docker export命令创建一个tar文件,并且移除了元数据和不必要的层,将多个层整合成了一个层,只保存了当前统一视角看到的内容(译者注:expoxt后的容器再import到Docker中,通过docker images –tree命令只能看到一个镜像;而save后的镜像则不同,它能够看到这个镜像的历史镜像)。
docker history <image-id>

%title插图%num

docker history命令递归地输出指定镜像的历史镜像。

不错的MySQL重要知识点

转载自:https://mp.weixin.qq.com/s/S9jiO_e-_CKRgNnzAU5Z0Q

标题有点标题党的意思,但希望你在看了文章之后不会有这个想法——这篇文章是作者对之前总结的 MySQL 知识点做了完善后的产物,可以用来回顾MySQL基础知识以及备战MySQL常见面试问题。

 

什么是MySQL?

 

MySQL 是一种关系型数据库,在Java企业级开发中非常常用,因为 MySQL 是开源免费的,并且方便扩展。阿里巴巴数据库系统也大量用到了 MySQL,因此它的稳定性是有保障的。MySQL是开放源代码的,因此任何人都可以在 GPL(General Public License) 的许可下下载并根据个性化的需要对其进行修改。MySQL的默认端口号是3306

 

事务相关

 

什么是事务?

 

事务是逻辑上的一组操作,要么都执行,要么都不执行。

 

事务*经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。

事物的四大特性(ACID)介绍一下?

%title插图%num

  • 原子性: 事务是*小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  • 一致性: 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
  • 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  • 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

并发事务带来哪些问题?

 

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对统一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题:

 

  • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
  • 失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在*个事务中修改了这个数据后,第二个事务也修改了这个数据。这样*个事务内的修改结果就被丢失,因此称为丢失修改。例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,*终结果A=19,事务1的修改被丢失。
  • 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在*个事务中的两次读数据之间,由于第二个事务的修改导致*个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
  • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,*个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

 

不可重复度和幻读区别:

 

不可重复读的重点是修改,幻读的重点在于新增或者删除。

 

例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复读。

 

例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读。

事务隔离级别有哪些?MySQL的默认隔离级别是?

 

SQL 标准定义了四个隔离级别:

  • READ-UNCOMMITTED(读取未提交): *低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化): *高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

 

 

隔离级别 脏读 不可重复读 幻影读
READ-UNCOMMITTED
READ-COMMITTED ×
REPEATABLE-READ × ×
SERIALIZABLE × × ×

 

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过SELECT @@tx_isolation;命令来查看

mysqlSELECT @@tx_isolation;+-----------------+| @@tx_isolation  |+-----------------+| REPEATABLE-READ |+-----------------+

这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的SERIALIZABLE(可串行化)隔离级别。

 

因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容):,但是你要知道的是InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读)并不会有任何性能损失。

 

InnoDB 存储引擎在 分布式事务 的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。

 

索引相关

为什么索引能提高查询速度

以下内容整理自:《数据库两大神器【索引和锁】》作者 :Java3y

先从 MySQL 的基本存储结构说起

MySQL的基本存储结构是页 (记录都存在页里边) :

%title插图%num

%title插图%num

  • 各个数据页可以组成一个双向链表
  • 每个数据页中的记录又可以组成一个单向链表
- 每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录- 以其他列(非主键)作为搜索条件:只能从*小记录开始依次遍历单链表中的每条记录。

所以说,如果我们写select * from user where indexname = ‘xxx’这样没有进行任何优化的sql语句,默认会这样做:

  1. 定位到记录所在的页:需要遍历双向链表,找到所在的页
  2. 从所在的页内中查找相应的记录:由于不是根据主键查询,只能遍历所在页的单链表了

很明显,在数据量很大的情况下这样查找会很慢!这样的时间复杂度为O(n)。

 

索引做了些什么可以让我们查询加快速度呢?其实就是将无序的数据变成有序(相对):

 

%title插图%num

 

要找到id为8的记录简要步骤:

%title插图%num

很明显的是:没有用索引我们是需要遍历双向链表来定位对应的页,现在通过 “目录” 就可以很快地定位到对应的页上了!(二分查找,时间复杂度近似为O(logn))

其实底层结构就是B+树,B+树作为树的一种实现,能够让我们很快地查找出对应的记录。

以下内容整理自:《Java工程师修炼之道》

什么是*左前缀原则?

 

MySQL中的索引可以以一定顺序引用多列,这种索引叫作联合索引。如User表的name和city加联合索引就是(name,city),而*左前缀原则指的是,如果查询的时候查询条件精确匹配索引的左边连续一列或几列,则此列就可以被用到。如下:

select * from user where name=xx and city=xx ; //可以命中索引select * from user where name=xx ; // 可以命中索引select * from user where city=xx ; // 无法命中索引

这里需要注意的是,查询的时候如果两个条件都用上了,但是顺序不同,如 city= xx and name =xx,那么现在的查询引擎会自动优化为匹配联合索引的顺序,这样是能够命中索引的。

 

由于*左前缀原则,在创建联合索引时,索引字段的顺序需要考虑字段值去重之后的个数,较多的放前面。ORDER BY子句也遵循此规则。

注意避免冗余索引

 

冗余索引指的是索引的功能相同,能够命中就肯定能命中 ,那么 就是冗余索引如(name,city )和(name )这两个索引就是冗余索引,能够命中后者的查询肯定是能够命中前者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。

 

MySQLS.7 版本后,可以通过查询 sys 库的 schema_redundant_indexes 表来查看冗余索引

Mysql如何为表字段添加索引?

 

1.添加PRIMARY KEY(主键索引)

ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` )

2.添加UNIQUE(唯一索引)

ALTER TABLE `table_name` ADD UNIQUE ( `column` )

3.添加INDEX(普通索引)

ALTER TABLE `table_name` ADD INDEX index_name ( `column` )

4.添加FULLTEXT(全文索引)

ALTER TABLE `table_name` ADD FULLTEXT ( `column`)

5.添加多列索引

ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` )

 

存储引擎

一些常用命令

 

查看MySQL提供的所有存储引擎

mysql> show engines;

%title插图%num

 

从上图我们可以查看出 MySQL 当前默认的存储引擎是InnoDB,并且在5.7版本所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。

 

查看MySQL当前默认的存储引擎

 

我们也可以通过下面的命令查看默认的存储引擎。

mysql> show variables like '%storage_engine%';

 

查看表的存储引擎

show table status like "table_name" ;

%title插图%num

MyISAM和InnoDB区别

 

MyISAM是MySQL的默认数据库引擎(5.5版之前)。虽然性能*佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,而且*大的缺陷就是崩溃后无法安全恢复。不过,5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。

 

大多数时候我们使用的都是 InnoDB 存储引擎,但是在某些情况下使用 MyISAM 也是合适的比如读密集的情况下。(如果你不介意 MyISAM 崩溃回复问题的话)。

两者的对比:

  1. 是否支持行级锁 : MyISAM 只有表级锁(table-level locking),而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
  2. 是否支持事务和崩溃后的安全恢复:MyISAM 强调的是性能,每次查询具有原子性,其执行比InnoDB类型更快,但是不提供事务支持。但是InnoDB 提供事务支持事务,外部键等高级数据库功能。具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
  3. 是否支持外键: MyISAM不支持,而InnoDB支持。
  4. 是否支持MVCC :仅 InnoDB 支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作;MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。
  5. ……

 

《MySQL高性能》上面有一句话这样写到:

不要轻易相信“MyISAM比InnoDB快”之类的经验之谈,这个结论往往不是*对的。在很多我们已知场景中,InnoDB的速度都可以让MyISAM望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。

一般情况下我们选择 InnoDB 都是没有问题的,但是某事情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择MyISAM也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。

 

 

乐观锁与悲观锁的区别

悲观锁

 

总是假设*坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

 

总是假设*好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

两种锁的使用场景

 

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

 

乐观锁常见的两种实现方式

乐观锁一般会使用版本号机制或CAS算法实现。

1. 版本号机制

 

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

 

举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

 

  1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
  2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
  3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
  4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

 

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

2. CAS算法

 

compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

 

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试

 

乐观锁的缺点

ABA 问题是乐观锁一个常见的问题

1 ABA 问题

 

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 “ABA”问题。

 

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2 循环时间长开销大

 

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,*它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3 只能保证一个共享变量的原子操作

 

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

 

锁机制与InnoDB锁算法

 

MyISAM和InnoDB存储引擎使用的锁:

  • MyISAM 采用表级锁(table-level locking)。
  • InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁

 

表级锁和行级锁对比:

  • 表级锁: Mysql中锁定 粒度*大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度*大,触发锁冲突的概率*高,并发度*低,MyISAM和 InnoDB引擎都支持表级锁。
  • 行级锁: Mysql中锁定 粒度*小 的一种锁,只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度*小,并发度高,但加锁的开销也*大,加锁慢,会出现死锁。

 

InnoDB存储引擎的锁的算法有三种:

  • Record lock:单个行记录上的锁
  • Gap lock:间隙锁,锁定一个范围,不包括记录本身
  • Next-key lock:record+gap 锁定一个范围,包含记录本身

 

相关知识点:

  • innodb对于行的查询使用next-key lock
  • Next-locking keying为了解决Phantom Problem幻读问题
  • 当查询的索引含有唯一属性时,将next-key lock降级为record key
  • Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
  • 有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数innodb_locks_unsafe_for_binlog设置为1

大表优化

 

当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:

 

1. 限定数据的范围

务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;

 

2. 读/写分离

经典的数据库拆分方案,主库负责写,从库负责读;

 

3. 垂直分区

根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。

简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。 如下图所示,这样来说大家应该就更容易理解了。

%title插图%num

  • 垂直拆分的优点: 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
  • 垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;

4. 水平分区

 

保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。水平拆分可以支撑非常大的数据量。

 

水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。

%title插图%num

水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分*好分库 。

 

水平拆分能够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨节点Join性能较差,逻辑复杂。《Java工程师修炼之道》的作者推荐 尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。

 

下面补充一下数据库分片的两种常见方案:

 

  • 客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
  • 中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。