2019-04-18

Java常见面试题

JVM虚拟机

1.简述Java运行时数据区分

image.png
  1. PC寄存器/程序计数器

    • 严格来说是一个数据结构,用于保存当前正在执行的程序的内存地址,由于Java是支持多线程执行的,所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。
  2. Java栈 Java Stack

    • Java栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的Java栈,在这个Java栈中又会包含多个栈帧(Stack Frame),这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量、操作栈和方法返回值等信息 。每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用,当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧被创建,这个新创建的栈帧被放到Java栈的栈顶,变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用,当这个栈帧中所有指令都完成时,这个栈帧被移除Java栈,刚才的那个栈帧变为活动栈帧,前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。

    • 由于Java栈是与线程对应起来的,Java栈数据不是线程共有的,所以不需要关心其数据一致性,也不会存在同步锁的问题。

    • 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。在Hot Spot虚拟机中,可以使用-Xss参数来设置栈的大小。栈的大小直接决定了函数调用的可达深度。

  3. 堆 Heap

    • 概念

      • 堆是JVM所管理的内存中国最大的一块,是被所有Java线程锁共享的,不是线程安全的,在JVM启动时创建。堆是存储Java对象的地方,这一点Java虚拟机规范中描述是:所有的对象实例以及数组都要在堆上分配。Java堆是GC管理的主要区域,从内存回收的角度来看,由于现在GC基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代再细致一点有Eden空间、From Survivor空间、To Survivor空间等。
    • 方法区Method Area

      • 概念

        • 方法区是堆中的一部分,就是我们通常所说的Java堆中的永久区 (Permanet Generation),大小可以通过参数来设置,可以通过-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。

        • 方法区存放了要加载的类的信息(名称、修饰符等)、类中的静态常量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当在程序中通过Class对象的getName.isInterface等方法来获取信息时,这些数据都来源于方法区。

        • 方法区是被Java线程锁共享的,不像Java堆中其他部分一样会频繁被GC回收,它存储的信息相对比较稳定,在一定条件下会被GC,当方法区要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

      • 常量池Constant Pool

        • 常量池本身是方法区中的一个数据结构。常量池中存储了如字符串、final变量值、类名和方法名常量。

        • 常量池在编译期间就被确定,并保存在已编译的.class文件中。

        • 一般分为两类:

          • 字面量:就是字符串、final变量等。

          • 引用量:类名和方法名属于引用量。最常见的是在调用方法的时候,根据方法名找到方法的引用,并以此定为到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符。

  4. 本地方法栈Native Method Stack

    • 本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

2.JVM内存模型JMM

image.png
  • 为什么会划分这样的模型?

代码执行时对象的生命周期不同,所以在我们heap中划分成了三个代,并且三个代大小根据gc分配的大小不同,其实也是避免浪费空间

在1.8之后,Meta Space替代了永久代(永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError:),Meta Space存在内存中,而且大小是可变的(就像ArrayList),缺点是如果Meta Space无限扩大,会占用内存中其他资源空间,也会导致内存溢出。

3.什么样的对象会被gc

  • 判断算法

引用计数法:对象被引用时在对象中计数,当对象不被应用时,计数为0,被回收。当遇到循环引用(即A引用B,B引用A)时,及时相互不引用是,也无法回收,这也是该算法最后被gc淘汰的原因

可达性分析: 依赖gcRoot,gc root 没有指向对象时该对象被回收。可成为gcRoot的对象有:①虚拟机栈中本地变量表引用的对象;②方法区中,类静态变量引用的对象③方法区中,常量引用的对象;④本地方法中jni引用的对象。原因:①④在线程运行时一定存在的,②③是一直存在在。

不可达是不是就一定会被回收?

答:不一定的,finalize()可以使失去引用的对象重新可达,但只能调一次。

线程

1.什么是JMM(Java Memory Model)?

  • 概念

    • Java内存模型的主要目标是定义程序中各个变量的访问规则,即在JVM中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量与Java编程里面的变量有所不同步,它包含了实例字段、静态字段和构成数组对象的元素,但不包含局部变量和方法参数,因为后者是线程私有的,不会共享,当然不存在数据竞争问题(如果局部变量是一个reference引用类型,它引用的对象在Java堆中可被各个线程共享,但是reference引用本身在Java栈的局部变量表中,是线程私有的)。为了获得较高的执行效能,Java内存模型并没有限制执行引起使用处理器的特定寄存器或者缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。
  • 主内存和工作内存:

    • JMM规定了所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
  • 线程1和线程2要想进行数据的交换一般要经历下面的步骤:

    1. 线程1把工作内存1中的更新过的共享变量刷新到主内存中去。

    2. 线程2到主内存中去读取线程1刷新过的共享变量,然后copy一份到工作内存2中去。

  • Java内存模型是围绕着并发编程的三个特征来建立的

    1. 原子性

      • 概念:一个操作不能被打断,要么全部执行完毕,要么不执行。

      • 基本类型数据的访问大都是原子操作,longdouble类型的变量是64位,但是在32位JVM中,32位的JVM会将64位数据的读写操作分为2次32位的读写操作来进行,这就导致了long、double类型的变量在32位虚拟机中是非原子操作,数据有可能会被破坏,也就意味着多个线程在并发访问的时候是线程非安全的。32位的JVM中,要想保证对long、double类型数据的操作的原子性,可以对访问该数据的方法进行同步

    2. 可见性

      • 概念:一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量这种修改(变化)。

      • Java内存模型是通过将在工作内存中的变量修改后的值同步到主内存,在读取变量前从主内存刷新最新值到工作内存中,这种依赖主内存的方式来实现可见性的

    3. 有序性

      • 在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。

      • 保证多线程之间操作的有序性

        • volatile关键字本身通过加入内存屏障来禁止指令的重排序

        • synchronized关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现

  • happens-before原则:

    Java内存模型中定义的两项操作之间的次序关系,如果说操作A先行发生于操作B,操作A产生的影响能被操作B观察到,“影响”包含了修改了内存中共享变量的值、发送了消息、调用了方法等。

    下面是Java内存模型下一些”天然的“happens-before关系,这些happens-before关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随意地重排序。

    a.程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。

    b.管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面“是指时间上的先后顺序。

    c.volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。

    d.线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。

    e.线程终于规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。

    f.线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。

    g.对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。

    g.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

    一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生 “呢?也是不成立的,一个典型的例子就是指令重排序。所以时间上的先后顺序与happens-before原则之间基本没有什么关系,所以衡量并发安全问题一切必须以happens-before 原则为准

2.并发中如何实现信息通信和数据同步的?

  1. Java语言中是采用共享内存模型实现的

  2. 信息通信是隐式的

    • 线程之间通过共享程序公共的状态,通过读-写内存中公共状态的方式来进行隐式的通信。
  3. 数据同步是显示的

    • 指的是程序在控制多个线程之间执行程序的相对顺序的机制,在共享内存模型中,同步是显式的,程序员必须显式指定某个方法/代码块需要在多线程之间互斥执行。

3.volatile,synchronized,Lock区别

  • volatile

    1. 关键字,用来修饰共享可变变量,对volatile变量的读写操作都是从高速缓存或者主内存中读取。

    2. 作用

      1. 保证此变量对所有的线程的(可见性)

        • volatile 变量,JVM 保证了每次读变量都从主内存中读变量本身,跳过工作内存这一步。

        • 非volatile 变量,每次读变量都从工作内存中读变量的副本

      2. 禁止指令重排序优化(有序性)

    3. volatile的原理和实现机制

      观察加入 volatile关键字和没有加入 volatile关键字时所生成的汇编代码发现,加入 volatile关键字时,会多出一个lock前缀指令。Iock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能

      1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置。也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成

      2. 它会强制将对缓存的修改操作立即写入主存

      3. 如果是写操作,它会导致其他CPU中对应的缓存行无效

    4. volatile的性能

      • volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令。
    5. volatile的使用条件

      • 在某些场景下,因为不会引起线程的上下文切换,volatile相当于轻量级的synchronized,但使用volatile必须满足两个条件:

        1. 对变量的写操作不依赖当前值
volatile int a = 1;
//多线程下执行a++,虽然可以保证可见性,但因为非原子操作,无法保证原子性。
        2.  **该变量没有包含在具有其它变量的不变式中**

            例:

            
//要求:下界总是小于或等于上界
            public class NumberRange {
            private volatile int lower, upper;
            public int getLower() { return lower; }
            public int getUpper() { return upper; }
            public void setLower(int value) {
            if (value > upper)
            throw new IllegalArgumentException(...);
            lower = value;
            }
            public void setUpper(int value) {
            if (value < lower)
            throw new IllegalArgumentException(...);
            upper = value;
            }
            }
假设初始状态 lower=1 , upper=9 ,同时A,B两个线程同时进行操作,A执行setLower(8),B执行setUpper(3), 结果为lower=8 , upper=3,这是不合理的 6. **volatile的使用场景** 1. **状态标志**
volatile boolean shutdownRequested;
        ...
        public void shutdown(){
        shutdownRequested = true;
        }
        public void doWork() {
        while (!shutdownRequested) {
        // do stuff
        }
        }
可以在循环外部--其他线程--来终止循环,然而,使用 synchronized 块编写循环要比使用volatile 状态标志编写麻烦很多。由于 volatile 简化了编码,并且状态标志并不依赖于程序内任何其他状态,因此此处非常适合使用 volatile。 2. **双重检查模式 (DCL)**
public class Singleton {
        private volatile static Singleton instance = null;
        private Singleton(){};
        public static Singleton getInstance() {
        if (instance == null) {
        synchronized(this) {
        if (instance == null) {
        instance = new Singleton();
        }
        }
        }
        return instance;
        }
        }
  • synchronized

    1. 关键字,可用于修饰代码块,方法,对象,类。

    2. Synchronized经过编译,会在同步块的前后分别形成 monitorenter和 monitorexit这个两个宇节码指令。在执行 monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行 monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。

    3. 获取该锁的线程释放锁的两种情况(自动释放)

      • 获取锁的对象执行完改代码块,然后释放对锁的占有

      • 线程执行发生异常,此时jvm会让线程自动释放锁

  • Lock(常用 ava. util. concurrent包下提供的一套互斥锁Reentrantlock)

    1. 等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized来说可以避兔出现死锁的情况。

    2. 公平锁,多个线程等待同个锁时,必须按照申请锁的时间顺序获得锁, Synchronized锁非公平锁, Reentrantlocke认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。

    3. 锁绑定多个条件,个 Reentrantlock时象可以同时绑定对个对象

    4. 获取该锁的线程需要手动释放锁,如不释放,可能导致死锁现象

  • 场景对比synchronized和Lock

    1. 当线程中有IO或者其他原因(比如调用 sleep方法)被阻塞了,但是又没有释放锁,其他线程只能等待,严重影响效率

      • 因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待定的时间或者能够响应中断),通过Lock就可以办到
    2. 当有多个线程读写文件时,读操作和写操作会发生沖突现象,写操作和写操作会发生冲突现象,但是读操作和谈操作不会发生冲突现象

      • 当采用 asynchronized关键字来实现同步的话,就会导致一个问题,如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作,因此就要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到
    3. 通过Lock可以知道线程有没有成功获取到锁,这个是 synchronized无法办到的。

4.ThreadLocal

  1. 概念

    • 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

    • 特别注意

      1. ThreadLocal不是用来解决多线程的共享对象访问问题

      2. 它也不是“本地线程”,ThreadLocal并不是一个Thread,而是Thread的局部变量

  2. ThreadLocal要解决的问题

    • 要解决的问题:保证缓存变量(实现线程内变量共享)的线程安全

      • 假设有一个需求,需要缓存一个变量,有人可能会想到静态map,但会有两个问题,其他线程的影响和访问并发。
  3. ThreadLocal使用场景

    1. 日志的打印(同一线程的日志一起打印,或者说一次事务的日志一起打印,因为一般默认一次事务都是由同一个线程执行的,将事务的日志保存在线程局部变量当中,当事务执行完成的时候统一打印)

    2. 数据路连接、事务,事务是和线程绑定起来的,Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection

    3. 调用SimpleDataFormat的工具类建议使用ThreadLocal

    4. Session管理

  4. 示例代码

    public class OperationLogThreadLocal {
    private static ThreadLocal userThreadLocal = new ThreadLocal<>();
    public static void setLog(CreateOperationLogParam logParam) {
    userThreadLocal.set(logParam);
    }
    public static CreateOperationLogParam getLog() {
    return userThreadLocal.get();
    }
    public static void clear() {
    userThreadLocal.remove();
    }
    }

  5. ThreadLocal的原理

    1. 用到的相关类

      • ThreadLocalMap : ThreadLocal类中的静态内部类(实质存储变量的位置,在Thread中有类型为ThreadLocal.ThreadLocalMap类型的属性threadLocals)

      • Entry : ThreadLocalMap中的静态内部类

    2. 操作

      • 存储(存储过程实际是往当前线程的ThreadLocalMap中存储,key为 “this”,不是当前线程)

        [图片上传失败...(image-c3b276-1555545198430)]

        获取线程中的ThreadLocalMap

        [图片上传失败...(image-5d65d0-1555545198430)]

        [图片上传失败...(image-8f02e0-1555545198430)]

      • 取值( 从当前线程中获取ThreadLocalMap,然后从ThreadLocalMap获取值)

        [图片上传失败...(image-bea14c-1555545198430)]

        给ThreadLocalMap初始化值

        [图片上传失败...(image-a5de5-1555545198430)]

        初始化值,可以重写

        [图片上传失败...(image-ae1bbc-1555545198430)]

      • 移除

        [图片上传失败...(image-10c178-1555545198430)]

  6. 使用步骤

    1. 在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。

    2. 在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。

    3. 在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

      public class Student {
      private int age = 0; //年龄

      public int getAge() {
          return this.age;
      }
      
      public void setAge(int age) {
          this.age = age;
      }
      

      }

      public class ThreadLocalDemo implements Runnable {
      //创建线程局部变量studentLocal,在后面你会发现用来保存Student对象
      private final static ThreadLocal studentLocal = new ThreadLocal();

      public static void main(String[] agrs) {
          ThreadLocalDemo td = new ThreadLocalDemo();
          Thread t1 = new Thread(td, "a");
          Thread t2 = new Thread(td, "b");
          t1.start();
          t2.start();
      }
      
      public void run() {
          accessStudent();
      }
      
      /**
       * 示例业务方法,用来测试
       */
      public void accessStudent() {
          //获取当前线程的名字
          String currentThreadName = Thread.currentThread().getName();
          System.out.println(currentThreadName + " is running!");
          //产生一个随机数并打印
          Random random = new Random();
          int age = random.nextInt(100);
          System.out.println("thread " + currentThreadName + " set age to:" + age);
          //获取一个Student对象,并将随机数年龄插入到对象属性中
          Student student = getStudent();
          student.setAge(age);
          System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());
          try {
              Thread.sleep(500);
          }
          catch (InterruptedException ex) {
              ex.printStackTrace();
          }
          System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());
      }
      
      protected Student getStudent() {
          //获取本地线程变量并强制转换为Student类型
          Student student = (Student) studentLocal.get();
          //线程首次执行此方法的时候,studentLocal.get()肯定为null
          if (student == null) {
              //创建一个Student对象,并保存到本地线程变量studentLocal中
              student = new Student();
              studentLocal.set(student);
          }
          return student;
      }
      

      }

  7. 共享问题的对比

    [图片上传失败...(image-ebec84-1555545198437)]

5.什么是死锁?如何避免死锁?

  • 什么是死锁

    • 多个进程在运行过程中因争夺资源而造成的一种僵局。当一个进程请求资源时,如果该资源不能立即获得,那么进程就会进入等待状态。如果一个处于等待状态的进程 P1,由于所等待的资源被另一个处于等待状态的进程 p2 所占有,而 p2 所请求的资源又被 p1 占有,这样它们所请求的资源都不会获得,两进程一直处于等待状态,形成死锁
  • 死锁产生的原因

    1. 因为系统资源不足。

    2. 进程运行推进的顺序不合适。

    3. 资源分配不当等。

  • 死锁产生的4个必要条件(缺一不可)

    1. 互斥(Mutual exclusion)。进程对所分配到的资源进行排它性使用,在一段时间内某资源只由一个进程占用。

    2. 持有并等待(Hold and wait)。指某个进程已经持有了一个或多个资源,但是还要请求其他资源,而它请求的资源不能立即获得,需要等待。

    3. 不可抢占(No preemption)。即进程已经获取的资源在使用过程中不能被其他进程抢占,只能在使用完后,由该进程自己释放。

    4. 环路等待(Circular wait)。即形成进程和请求资源之间的环路(T1正在等待T2占用的资源->T2正在等待T3占用的资源->T3正在等待T1占用的资源)

  • 避免死锁

    1. 打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。

    2. 打破不可抢占条件:当一个进程占有一个独占性资源后又申请一个独占性资源而无法满足,则退出原占有的资源。

    3. 打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。

    4. 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源

  • 银行家算法

    • 来源于银行的借贷业务,一定数量的本金要应多个客户的借贷周转,为了防止银行家资金无法周转而倒闭,对每一笔贷款,必须考察其是否能限期归还

    • 系统中有限的资源要供多个进程使用,必须保证得到的资源的进程能在有限的时间内归还资源,以供其他进程使用资源。如果资源分配不得到就会发生进程循环等待资源,则进程都无法继续执行下去的死锁现象。

    • 只要能确保上述四个条件之一不出现,则系统就不会发生死锁。

6.线程池的参数有哪几种?

  • corePoolSize:核心线程数

    • 核心线程会一直存活,即使没有任务需要执行

    • 当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理

    • 设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

  • queueCapacity:任务队列容量(阻塞队列)

    • 当核心线程数达到最大时,新任务会放在队列中排队等待执行
  • maxPoolSize:最大线程数

    • 当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务

    • 当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常

  • keepAliveTime:线程空闲时间

    • 当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize

    • 如果allowCoreThreadTimeout=true,则会直到线程数量=0

  • allowCoreThreadTimeout:允许核心线程超时

  • rejectedExecutionHandler:任务拒绝处理器

    • 两种情况会拒绝处理任务:

      • 当线程数已经达到maxPoolSize,切队列已满,会拒绝新任务

      • 当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务

    • 线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常

    • ThreadPoolExecutor类有几个内部实现类来处理这类情况:

      • AbortPolicy 丢弃任务,抛运行时异常

      • CallerRunsPolicy 执行任务

      • DiscardPolicy 忽视,什么都不会发生

      • DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务

    • 实现RejectedExecutionHandler接口,可自定义处理器

7.线程池的几种类型?

  1. newCachedThreadPool

    • 可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程

    • 特点:

      • 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。

      • 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。

      • 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

    • package test;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      public class ThreadPoolExecutorTest {
      public static void main(String[] args) {
      ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
      for (int i = 0; i < 10; i++) {
      final int index = i;
      try {
      Thread.sleep(index * 1000);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      cachedThreadPool.execute(new Runnable() {
      public void run() {
      System.out.println(index);
      }
      });
      }
      }
      }

  2. newFixedThreadPool

    • 指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。

      FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

      package test;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      public class ThreadPoolExecutorTest {
      public static void main(String[] args) {
      ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
      for (int i = 0; i < 10; i++) {
      final int index = i;
      fixedThreadPool.execute(new Runnable() {
      public void run() {
      try {
      System.out.println(index);
      Thread.sleep(2000);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      }
      });
      }
      }
      }

      因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。 定长线程池的大小最好根据系统资源进行设置如Runtime.getRuntime().availableProcessors()

  3. newSingleThreadExecutor

    • 单线程化的线程池,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

      package test;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      public class ThreadPoolExecutorTest {
      public static void main(String[] args) {
      ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
      for (int i = 0; i < 10; i++) {
      final int index = i;
      singleThreadExecutor.execute(new Runnable() {
      public void run() {
      try {
      System.out.println(index);
      Thread.sleep(2000);
      } catch (InterruptedException e) {
      e.printStackTrace();
      }
      }
      });
      }
      }
      }

  4. newScheduleThreadPool

    • 定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行

    • 延迟3秒执行,延迟执行示例代码如下:

    • package test;
      import java.util.concurrent.Executors;
      import java.util.concurrent.ScheduledExecutorService;
      import java.util.concurrent.TimeUnit;
      public class ThreadPoolExecutorTest {
      public static void main(String[] args) {
      ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
      scheduledThreadPool.schedule(new Runnable() {
      public void run() {
      System.out.println("delay 3 seconds");
      }
      }, 3, TimeUnit.SECONDS);
      }
      }

    • 表示延迟1秒后每3秒执行一次,定期执行示例代码如下:

      package test;
      import java.util.concurrent.Executors;
      import java.util.concurrent.ScheduledExecutorService;
      import java.util.concurrent.TimeUnit;
      public class ThreadPoolExecutorTest {
      public static void main(String[] args) {
      ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
      scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
      public void run() {
      System.out.println("delay 1 seconds, and excute every 3 seconds");
      }
      }, 1, 3, TimeUnit.SECONDS);
      }
      }

8.Java线程池中开启线程执行池中的任务submit()和 execute()方法有什么区别

  1. execute方法无返回值。 submit方法可以提供Future < T > 类型的返回值。

  2. excute方法会抛出异常。 sumbit方法不会抛出异常。除非你调用Future.get()。

  3. excute方法入参Runnable submit方法入参可以为Callable,也可以为Runnable

  • 其实,submit方法是先构造出一个RunnableFuture(FutureTask) 然后调用execute方法。不管你submit的时候传入的是Runnable还是Callable最后RunnableFuture(FutureTask)里面都会生成Callable对象。任务调用的时候调用RunnableFuture(FutureTask)的run方法,run方法调用Callable对象的call方法。

9.Java中如何停止一个线程?

  • Java提供了很丰富的API但没有为停止线程提供API。JDK 1.0本来有一些像stop(), suspend(:和 resume()的控制方法但是由于潜在的死锁威胁因此在后续的JDK版本中他们被弃用了,之后Java API的设计者就没有提供一个兼容且线程安全的方法来停止一个线程。当run(:或者 call(:方法执行完的时候线程会自动结束,如果要手动结束一个线程,你可以用volatile 布尔变量来退出run()方法的循环或者是取消任务来中断线程。

数据库

1.事务

  1. 定义:事务(txn)是一系列在共享数据库上执行的行为,以达到更高层次更复杂逻辑的功能。事务是DBMS中最基础的单位,事务不可分割。

  2. ACID

    1. 原子性(Atomicity)

      • 原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
    2. 一致性(Consistency)

      • 一致性是指事务使得系统从一个一致的状态转换到另一个一致状态。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。

      • 例子:对银行转帐事务,不管事务成功还是失败,应该保证事务结束后ACCOUNT表中A和B的存款总额始终为2000元。如果一个人扣100元,一个人得50元,就破坏了一致性。

        事务的一致性决定了一个系统设计和实现的复杂度。事务可以不同程度的一致性:
        强一致性:读操作可以立即读到提交的更新操作。
        弱一致性:提交的更新操作,不一定立即会被读操作读到,此种情况会存在一个不一致窗口,指的是读操作可以读到最新值的一段时间。
        最终一致性:是弱一致性的特例。事务更新一份数据,最终一致性保证在没有其他事务更新同样的值的话,最终所有的事务都会读到之前事务更新的最新值。如果没有错误发生,不一致窗口的大小依赖于:通信延迟,系统负载等。
        其他一致性变体还有:
        单调一致性:如果一个进程已经读到一个值,那么后续不会读到更早的值。
        会话一致性:保证客户端和服务器交互的会话过程中,读操作可以读到更新操作后的最新值。

    3. 隔离性(Isolation)

      • 当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

      • 并行时可能出现的问题:

        - 脏读:事务A修改了一个数据,但未提交,事务B读到了事务A未提交的更新结果,如果事务A提交失败,事务B读到的就是脏数据。

        • 不可重复读:在同一个事务中,对于同一份数据读取到的结果不一致。比如,事务B在事务A提交前读到的结果,和提交后读到的结果可能不同。
        • 幻读:在同一个事务中,同一个查询多次返回的结果不一致。事务A新增了一条记录,事务B在事务A提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录。
    4. 不同的隔离级别:

      - Read Uncommitted:最低的隔离级别,什么都不需要做,一个事务可以读到另一个事务未提交的结果。所有的并发事务问题都会发生。

      • Read Committed:只有在事务提交后,其更新结果才会被其他事务看见。可以解决脏读问题。
      • Repeated Read:在一个事务中,对于同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。可以解决脏读、不可重复读。
      • Serialization:事务串行化执行,隔离级别最高,牺牲了系统的并发性。可以解决并发事务的所有问题。
    5. [图片上传失败...(image-8ba184-1555545198430)]

  3. 持久性(Durability)

    • 持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。

    • 例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。

MongoDB

1.事物

Redis

1.redis高性能的原理

2.五种数据类型

3.除了做缓存,还可以做什么

RabbitMQ

1.可靠性(如何处理消息丢失的问题)?

  • 生产者

    • 可能情况:生产者将数据发送到rabbitmq的时候,因为网络或者其他问题,半路给搞丢了。

    • 两种解决方案

      • rabbitmq提供的事务功能(同步)

        • 就是生产者发送数据之前开启rabbitmq事务(channel.txSelect),然后发送消息,如果消息没有成功被rabbitmq接收到,那么生产者会收到异常报错,此时就可以回滚事务(channel.txRollback),然后重试发送消息;如果收到了消息,那么可以提交事务(channel.txCommit)。但是rabbitmq事务机会降低吞吐量,因为太耗性能。
      • confirm模式(异步)

        • 所以一般是开启confirm模式,在生产者那里设置开启confirm模式后,每次写消息都会分配一个唯一的id,然后如果写入了rabbitmq中,rabbitmq会回传一个ack消息,告诉你说这个消息ok了。如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。
  • rabbitmq丢失数据

    • 开启RabbitMQ的持久化。RabbitMQ 的消息默认存放在内存上面,如果不特别声明设置,消息不会持久化保存到硬盘上面的,如果节点重启或者意外crash掉,消息就会丢失。必须满足以下三个条件,缺一不可

      1) Exchange 设置持久化

      2)Queue 设置持久化(RabbitMQ持久化queue的元数据,但是不会持久化queue里的数据)

      3)Message持久化发送:发送消息设置发送模式deliveryMode=2,(此时rabbitmq就会将消息持久化到磁盘上去)

    • 而且持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,rabbitmq挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。

  • 消费者

    • 用rabbitmq提供的ack机制,简单来说,就是关闭RabbitMQ自动ack,可以通过一个api来调用,然后每次代码里确保处理完的时候,再程序里ack一把。这样的话,如果还没处理完,就没有ack,那rabbitmq就认为你还没处理完,这个时候rabbitmq会把这个消费分配给别的consumer去处理,消息是不会丢的。

2.顺序性

  • 拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点;或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理

3.高可用

  1. 单节点模式:最简单的情况,非集群模式,节点挂了,消息就不能用了。业务可能瘫痪,只能等待。

  2. 普通集群模式:多台机器部署,每个机器放一个rabbitmq实例,但是创建的queue只会放在一个rabbitmq实例上,每个实例同步queue的元数据。如果消费时连的是其他实例,那个实例会从queue所在实例拉取数据。这就会导致拉取数据的开销,如果那个放queue的实例宕机了,那么其他实例就无法从那个实例拉取,即便开启了消息持久化,让rabbitmq落地存储消息的话,消息不一定会丢,但得等这个实例恢复了,然后才可以继续从这个queue拉取数据,这就没什么高可用可言,主要是提供吞吐量,让集群中多个节点来服务某个queue的读写操作。

  3. 镜像集群模式:queue的元数据和消息都会存放在多个实例,每次写消息就自动同步到多个queue实例里。这样任何一个机器宕机,其他机器都可以顶上,但是性能开销太大,消息同步导致网络带宽压力和消耗很重,另外,没有扩展性可言,如果queue负载很重,加机器,新增的机器也包含了这个queue的所有数据,并没有办法线性扩展你的queue。此时,需要开启镜像集群模式,在rabbit管理控制台新增一个策略,将数据同步到指定数量的节点,然后你再次创建queue的时候,应用这个策略,就会自动将数据同步到其他的节点上去了

你可能感兴趣的:(2019-04-18)