Java基础 - 易错知识点整理(待更新)

Java基础 - 易错知识点整理(待更新)

Note:这里根据 CSDN Java技能树 整理的Java易错题(不带),以及摘录了博主"爱编程的大李子"“哪吒”JavaGuide博客Java面试题整理(带),学习过程中可以参考Java8 API在线文档

Java进阶知识点 参考 Java进阶 - 知识点整理(待更新)

一、Java简介

更多内容整理 参考《我要进大厂》- Java基础夺命连环18问,你能坚持到第几问?(基础概念 | 基本语法 | 基本数据类型)

二、操作符,基本类型,结构语句 与 函数

更多内容整理 参考《我要进大厂》- Java基础夺命连环18问,你能坚持到第几问?(基础概念 | 基本语法 | 基本数据类型)

三、类、对象和泛型

更多内容整理 参考

四、字符串

更多内容整理 参考《我要进大厂》- Java基础夺命连环14问,你能坚持到第几问?(Object类 | String类)

五、异常处理

更多内容整理 参考

六、集合

更多内容整理 参考

七、IO(传统IO,即BIO)

更多内容整理 参考 《我要进大厂》- Java基础夺命连环9问,你能坚持到第几问?(反射 | 注解 | IO )

八、数据库连接

九、NIO

十、网络编程

十一、类型信息(反射 和 动态代理)

  • 【问】何为反射?(在运行时通过字符串,动态获取一个类(Class<?> cls = Class.forName("java.util.ArrayList"))、该类所实现的接口(cls.getInterfaces())、动态创建该类的对象(cls.newInstance();),调用该类变量(Field name = cls.getDeclaredField("name"))或方法(Method addMethod = cls.getMethod("add", Object.class); addMethod.invoke(obj, "Hello");)),可参考类加载器(3个) & 类加载机制 & 反射
    Note
    • 反射的概念:反射是指在运行时获取一个类的变量和方法信息,然后通过获取到的信息来创建对象,调用方法的一种机制。
    • 获取Class类对象:
      • 通过类的class属性、或者运行时的对象、或者字符串来获取一个类对象
    	// 方法1:使用类的class属性来获取该类对应的Class对象
    	Class<Student> c1 = Student.class;
    	System.out.println(c1);
    	// 方法2:调用对象的getClass()方法,返回该对象所属类对应的Class对象
    	Class<? extends Student> c2 = new Student().getClass();
    	System.out.println(c2);
    	// 方法3:使用Class类中的静态方法forName(String className)
    	Class<?> c3 = Class.forName("Student");
    	System.out.println(c3);
    
    • 通过Class类对象获取构造器:参考Java 反射篇——获取构造方法
      • 通过getConstructors()返回一个包含Constructor对象的数组,不包含私有构造;
      • 通过getConstructor(Class<?>… parameterTypes)返回一个指定的Constructor对象,不包含私有构造;
      • 通过getDeclaredConstructors()返回一个包含Constructor对象的数组,包含私有构造
        • 如果不重写public无参构造器,该构造器默认为default(仅次于private),如果使用getConstructor()会抛出NoSuchMethodExcepton
        • 如果使用getDeclaredConstructor(),访问default不需要 constructor.setAccessible(true),而访问private就需要;
      • Constructor对象通过newInstance()创建对象
      Class<Student> c = Student.class;
      // 获取所有公开的构造方法
      Constructor<?>[] constructors = c.getConstructors();
      for (Constructor<?> constructor : constructors) {
      	System.out.println(constructor);
      }
      System.out.println("--------------------");
      
      // 获取指定参数且公开的构造方法
      Constructor<Student> constructor = c.getConstructor(String.class, int.class, String.class);
      System.out.println(constructor);
      System.out.println("--------------------");
      
      // 获取所有权限的构造方法
      Constructor<?>[] declaredConstructors = c.getDeclaredConstructors();
      for (Constructor<?> declaredConstructor : declaredConstructors) {
      	System.out.println(declaredConstructor);
      }
      
      // 使用类对象
      Class<?> myClass = Class.forName("com.zhang.reflect.Student");
      // new Student()   无参构造方法
      Object myObject = myClass.getDeclaredConstructor().newInstance();
      // 获取有参数的构造方法
      Object o = myClass.getDeclaredConstructor(String.class, String.class).newInstance("张三", "Tom");
      // 获取所有的构造方法
      Constructor<?>[] declaredConstructors = myClass.getDeclaredConstructors();
      // 获取类的路径名字
      System.out.println("类的全类名: " + myClass.getName());
      System.out.println("类的简单类名: " + myClass.getSimpleName());
      
    • 通过Class类对象获取成员变量:
      • 通过getFields()返回一个包含Field对象的数组,不包含私有变量
      • getField(String name)返回一个指定的Field对象,不包含私有变量
      • getDeclaredField(String name)返回一个指定的Field对象,包含私有变量
      • Field对象通过setAccessible(true)可以无视Java语言访问检查当前使用的反射对象,通过set实现成员变量的设置;
    • 通过Class类对象获取成员方法:
      • 通过getMethods()返回一个包含Method对象的数组,不包含私有成员方法
      • 通过getMethod(String name, Class<?>… parameterTypes)返回一个包含Method对象,不包含私有成员方法;
      • Method对象通过setAccessible(true)可以无视Java语言访问检查当前使用的反射对象,通过invoke()执行成员方法
    • 综合案例
      public class Main {
      	public static void main(String[] args) throws Exception {
      		// 获取学生类类对象
      		Class<Student> c = Student.class;
      		// 通过无参构造创建
      		Constructor<Student> constructor = c.getConstructor();
      		Student newStudent = constructor.newInstance();
      		System.out.println(newStudent);
      		System.out.println("--------------------");
      
      		// 反射设置成员变量
      		Field name = c.getDeclaredField("name");
      		name.setAccessible(true);
      		name.set(newStudent, "张三丰");
      		Field age = c.getDeclaredField("age");
      		age.setAccessible(true);
      		age.set(newStudent, 55);
      		Field address = c.getDeclaredField("address");
      		address.setAccessible(true);
      		address.set(newStudent, "武当山");
      		System.out.println(newStudent);
      		System.out.println("--------------------");
      
      		// 反射执行成员方法
      		Method getName = c.getDeclaredMethod("getName");
      		getName.setAccessible(true);
      		getName.invoke(newStudent);
      		Method setAge = c.getDeclaredMethod("setAge", int.class);
      		setAge.setAccessible(true);
      		setAge.invoke(newStudent, 60);
      		System.out.println(newStudent);
      	}
      }
      
  • 【问】反射机制优缺点?
    Note
    • 优点:增加代码的灵活性
    • 缺点:增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过对于框架来说实际是影响不大的。Java Reflection: Why is it so slow?
  • 【问】反射的应用场景
    Note
    • Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射
      比如下面是通过 JDK 实现动态代理的示例代码,先通过java.lang.reflect.ProxynewProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)创建代理对象(该方法会返回实现了指定接口的代理类实例),其中代理对象在执行方法时就使用了反射类 Method 来调用指定的方法
    • 如下代码中,为UserDao创建了一个代理对象,该代理对象既包含了UserDao类信息也包含了接口信息,代理对象在调用UserDao原对象方法(add())时,会自动调用MyInvocationHandler对象的invoke(),进而调用原对象(target)的add()方法,而在MyInvocationHandler对象的invoke()中可以增加其他逻辑实现功能增强(下面会说明为什么会自动调用handler对象的invoke())。
      import java.lang.reflect.InvocationHandler;
      import java.lang.reflect.Method;
      import java.lang.reflect.Proxy;
      
      interface UserDao {
      	public abstract void add();
      
      	public abstract void delete();
      
      	public abstract void update();
      
      	public abstract void find();
      }
      
      class UserDaoImpl implements UserDao {
      	@Override
      	public void add() {
      		System.out.println("添加功能");
      	}
      
      	@Override
      	public void delete() {
      		System.out.println("删除功能");
      	}
      
      	@Override
      	public void update() {
      		System.out.println("修改功能");
      	}
      
      	@Override
      	public void find() {
      		System.out.println("查找功能");
      	}
      }
      
      class MyInvocationHandler implements InvocationHandler {
      	private Object target;
      
      	public MyInvocationHandler(Object target) {
      		this.target = target;
      	}
      
      	@Override
      	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      		System.out.println("权限校验");
      		Object result = method.invoke(target, args);
      		System.out.println("日志记录");
      		System.out.println();
      		return result;
      	}
      }
      
      public class Main {
      	public static void main(String[] args) throws Exception {
      		UserDao ud1 = new UserDaoImpl();
      		ud1.add();
      		ud1.delete();
      		ud1.update();
      		ud1.find();
      		System.out.println("----------");
      
      		UserDao ud2 = new UserDaoImpl();
      		MyInvocationHandler handler = new MyInvocationHandler(ud2);
      		UserDao ud2Proxy = (UserDao) Proxy.newProxyInstance(ud2.getClass().getClassLoader(), ud2.getClass().getInterfaces(), handler);
      		ud2Proxy.add();
      		ud2Proxy.delete();
      		ud2Proxy.update();
      		ud2Proxy.find();
      	}
      }
      
    • 另外,像 Java 中的一大利器 注解 的实现也用到了反射。在使用 Spring 的时候 ,一个@Component注解就声明了一个类为 Spring BeanIOC容器最基本的技术就是“反射(Reflection)”编程),通过一个 @Value 注解就读取到配置文件中的值,其实背后是基于反射分析类,然后获取到类/属性/方法/方法参数上的注解
  • 【问】怎么实现动态代理?
    Note:参考上面的动态代理代码
    • 在理解newProxyInstance(ud2.getClass().getClassLoader()时,可以对比反射中的newInstance,容易将newProxyInstance理解成通过constructor对象反射创建了代理对象
    • 第一步:在底层代码Class<?> cl = getProxyClass0(loader, intfs);中,通过传入的加载的类loader和接口intfs来构建出相应的代理类,这样代理类继承了UserDemoImpl类、实现了UserDao接口,拥有add()
    • 第二步:代理类的构造器为final Constructor<?> cons = cl.getConstructor(constructorParams);,传入的参数是一个数组即{InvocationHandler.class} ,因此handler对象作为代理对象的成员属性
    • 第三步:在调用代理对象的add()时,会先调用handler对象的invoke(),而handler对象中存放着原始UserDao对象target,在执行targetadd()时会通过反射来调用。
  • 【问】动态代理是什么?有哪些应用?,可参考JDK 动态代理(AOP)使用及实现原理分析
  • 对象类型判断(obj.getClass()的使用)
  • 关于Class对象的描述,可参考类加载器(3个) & 类加载机制 & 反射Java 中的Class.getClassLoader(委托机制)
  • 关于Instanceof的描述(注意继承关系)Instanceof 和 isInstance的区别,可参考instanceof关键字详解(如果无法强转,则抛ClassCastException)
  • 关于反射的描述反射例子(假设没有重写equals方法),可参考类加载器(3个) & 类加载机制 & 反射
  • 关于动态代理的描述动态代理例子,可参考JDK动态代理和cglib动态代理以及区别JDK 动态代理(AOP)使用及实现原理分析
  • Java的空(null)对象(没有赋值,但在内存中存在)

更多内容整理 参考 《我要进大厂》- Java基础夺命连环9问,你能坚持到第几问?(反射 | 注解 | IO )

十二、注解

十三、并发编程(Thread,ThreadPool,Synchronized,AQS,ThreadLocal)

参考java线程多线程和线程池并发编程面试题(2020最新版)

  • 【问】如何让 Java 的线程彼此同步?(volatilesynchronized,JUC(java.util.concurrent)工具包等)

  • 【问】创建线程的四种方法?(主要分两大类:Thread类和ExecutorService接口,而Thread有3种实现方法),可参考多线程 - Thread、Executor
    Note

    • 小总结

      • 创建一个线程的核心是ThreadRunnable,如果需要用到线程返回值(异步调用)则考虑Callable
      • FutureTask为了创建线程需要实现Runnable,为了实现异步调用则需要实现Callable
      • 实现RunnableCallable接口只是对线程业务逻辑的定义,创建线程仍然需要使用new Thread(Runnable)来初始化线程对象,接着通过start()来提醒JVM该线程准备运行,JVM才会去调用run()
    • Thread 类进行派生并覆盖 run方法(Thread t1 = new MyThread(); t1.start(); )。

    • 实现 Runnable接口,重写 run 方法(Thread t2 = new Thread(Runnable); t2.start(); );当你打算多重继承时,优先选择实现Runnable比实例化Thread的派生类更灵活有效(直接将A实体类变成一个Runnable实例,即可以通过A类对象创建一个线程对象)。

      class ThreadType implements Runnable{ 
           public void run(){ 
               …… 
           } 
      } 
      Runnable rb = new ThreadType (); 
      Thread td = new Thread(rb);   //通过 Runnable 的实例创建一个线程对象
      td.start(); 
      
    • 实现 Callable 接口:利用Callable实现类创建FutureTask对象,FutureTask实现了FutureRunnable接口,因此可利用new Thread(Runnable)将任务装配到线程中,进而创建线程;通过futuretask.get()获取线程的返回值

      class MyCallable implements Callable<Integer> {
          @Override
          public Integer call() {
              System.out.println(Thread.currentThread().getName() + "Callable  call()方法");
              return 1;
          }
      }
      
      public class Main {
       
          public static void main(String[] args) {
              //2.以myCallable为参数创建FutureTask对象(call返回值为Integer)
              FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
              //3.将FutureTask作为参数创建Thread对象
              Thread thread = new Thread(futureTask);
              thread.start();  //4.执行
       
              try {
                  Thread.sleep(1000);
                  //5.通过futuretask可以得到myCallable的call()的运行结果: futuretask.get();
                  System.out.println("MyCallable:" + futureTask.get());
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } catch (ExecutionException e) {
                  e.printStackTrace();
              }
              System.out.println(Thread.currentThread().getName() + " main方法执行完成");
          }
       
      }
      
    • ExecutorService线程池的抽象接口,使用 Executors 工具类提供创建线程池的工厂方法,创建简化版的线程池,也可以使用ThreadPoolExecutor创建旗舰版的线程池。下面分别给出ExecutorsExecutorServiceThreadPoolExecutorExecutorService的继承关系。

      • ThreadPoolExecutorExecutorExecutorService的关系:
        • Executor是一个顶层接口,在它里面只声明了一个方法void execute(Runnable)
        • ExecutorService接口继承了Executor接口,并声明了一些方法:submitinvokeAllinvokeAny以及shutDown等,是对线程池的抽象
        • 抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法(实现submit()用于任务的提交);
        • ThreadPoolExecutor继承了类AbstractExecutorService,实现execute()用于任务的提交。
      • Executors只继承于Object类,这个包中定义的Executor、ExecutorService、ScheduledExecutorService、ThreadFactory和Callable类的工厂方法和实例方法。这个类支持以下类型的方法:

        Executors创建的SingleThreadExecutor,CachedThreadPool,FixedThreadPoolScheduledThreadPool均通过ThreadPoolExecutor创建。

      关于 ExecutorsThreadPoolExecutor的详细使用,以下会详细说明。

  • 【问】Thread和Runnable的区别?(Thread实现了Runnable,Thread是对线程的唯一抽象)
    Note

    • Thread 才是 Java 里对线程的唯一抽象thread.start()告诉JVM启动线程,JVM才会调用run()方法执行任务),Runnable 只是对任务(业务逻辑)的抽象Thread实现了Runnable接口:public class Thread implements Runnable{} ,因此自定义的MyThread在重写run()时其实是对Runnable.run()的重写)。Thread 可以接受任意一个 Runnable 的实例并执行。
    • Thread 自身实现了Runnable接口
    • 一般情况下使用Runnable方式创建线程。除非你需要重写Thread类除了run()方法外的其他方法来自定义线程,否则不建议使用继承Thread的方式来创建
  • 【问】Future和FutureTask的区别?
    Note

    • Future只是一个接口,可以理解是返回结果<T>的封装类,用于同步/异步返回结果<T>
    • FutureTask通过实现了RunnableFuture,进而实现了FutureRunnable两接口;
  • 【问】为什么要使用线程池?(降低创建和销毁线程对象的次数,因此有了"池化资源"技术比如数据库连接池,线程池)
    Note:线程池的作用

    • 降低资源消耗(线程创建和销毁需要完成用户态和核心态的切换,有开销);
    • 提高响应速度,当任务到达时,任务可以不需要等到线程创建就可以立即执行
    • 使用线程池可以对线程进行统一分配、调优和监控
  • 【问】Java 中线程池的四个基本组成部分(SingleThreadExecutorCachedThreadPoolFixedThreadPoolThreadPoolExecutor
    Note

    • 1)线程池管理器ThreadPool):用于创建并管理线程池。包含 创建线程池,销毁线程池,加入新任务(线程池初始化时,poolsize为0);
    • 2)工作线程PoolWorker):线程池中线程,在没有任务时处于等待状态。能够循环的运行任务;
    • 3)任务接口Task):每一个任务必须实现的接口,以供工作线程调度任务的运行。它主要规定了任务的入口,任务运行完后的收尾工作,任务的运行状态等(是直接交给工作线程执行,还是放入BlockingQueue)。
    • 4)任务队列taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
  • 【问】ThreadPoolExecutor 有几个核心构造参数?(7大参数,排队策略,拒绝策略),参考带你了解下SynchronousQueue
    Note

    • ThreadPoolExecutor提供了4个构造器,其余3个都是对下面这个构造器的调用

      // Java线程池的完整构造函数
      public ThreadPoolExecutor(
      	  int corePoolSize, // 线程池长期维持的最小线程数,即使线程处于Idle状态,也不会回收。
      	  int maximumPoolSize, // 线程数的上限
      	  long keepAliveTime, // 线程最大生命周期。
      	  TimeUnit unit, //时间单位                                 
      	  BlockingQueue<Runnable> workQueue, //任务队列。当线程池中的线程都处于运行状态,而此时任务数量继续增加,则需要一个容器来容纳这些任务,这就是任务队列。
      	  ThreadFactory threadFactory, // 线程工厂。定义如何启动一个线程,可以设置线程名称,并且可以确认是否是后台线程等。
      	  RejectedExecutionHandler handler // 拒绝任务处理器。由于超出线程数量和队列容量而对继续增加的任务进行处理的程序。
      )
      
      • corePoolSize(阈值1): 默认情况下,在创建了线程池后,线程池中的线程数(poolsize为0),当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目(poolsize)达到corePoolSize后,就会把到达的任务放到缓存队列当中;
      • maximumPoolSize(阈值2):线程池最大线程数;
      • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止(销毁线程释放资源)。默认情况下,只有当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止。
      • unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性
      • workQueue:一个阻塞队列(BlockingQueue<Runnable>,用来存储等待执行的任务,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue; LinkedBlockingQueue; SynchronousQueue; 阻塞队列提供了可阻塞的 puttake 方法,它们与同步队列 offerpoll 是等价的。如果队列满则 put阻塞,如果队列是空的则take方法阻塞。
        • 1)ArrayBlockingQueue : 有界的数组队列(长度至少为1
        • 2)LinkedBlockingQueue : 可支持有界/无界的队列,使用链表实现(默认大小为Integer.MAX_VALUE
        • 3)PriorityBlockingQueue : 优先队列,可以针对任务排序(无界)
        • 4)SynchronousQueue : 同步队列长度为1(与其说是队列,不如说是个锁),不能peek()查看队列元素;和Array有点区别就是:client thread提交到block queue会是一个阻塞过程,直到有一个worker thread连接上来poll task。
      • threadFactory:线程工厂,主要用来创建线程
      • handler:表示当拒绝处理任务时的策略,有以下四种取值(abort,discard,caller):
        ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
        ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
        ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
        ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
        

      ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关

  • 【问】ThreadPoolExecutor逻辑结构? 或者说ThreadPoolExecutor线程池的执行流程(corePoolSizemaximumPoolsize双阈值控制poolsize
    Note

    • 第一步:初始的poolSize < corePoolSize,提交的Runnable任务,会直接通过new Thread(Runnable),创建并执行线程。
    • 第二步:当提交的任务数超过了corePoolSize,就进入了第二步操作。会将当前的Runnable提交到一个block queue(使用不同的阻塞队列实现排队策略)。
    • 第三步:如果block queue是个有界队列,当队列满了之后就进入了第三步。如果poolSize < maximumPoolsize时,会尝试new Thread(Runnable)进行救急处理,立马执行对应的Runnable任务。
    • 第四步:如果第三步救急方案也无法处理了(poolSize > maximumPoolsize),就会走到第四步执行reject操作(使用任务拒绝策略,abort / discard / caller)。
  • 【问】ThreadPoolExecutor的任务排队策略和拒绝策略(见前2问解析)

  • 【问】ThreadPoolExecutor类的execute()和submit()的区别?(submit()会调用的execute()
    Note

    • execute()方法: 实际上Executor中声明的方法,在ThreadPoolExecutor进行了实现,通过这个方法ThreadPoolExecutor可以向线程池提交一个任务,交由线程池去执行
    • submit()方法: 是ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,用来向线程池提交任务submit()会调用的execute(),只不过它利用了Future来获取任务执行结果。
    • shutdown()shutdownNow()是用来关闭线程池的。
  • 【问】ThreadPoolExecutor线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?(利用new Thread(Runnable)创建 + 排队策略 + 拒绝策略;双阈值控制poolsize,见上几问解析)
    Note

    • 默认情况下,在使用ThreadPoolExecutor创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程
    • 在实际中如果需要,可以通过以下两个方法,在线程池创建之后立即创建线程
      prestartCoreThread():初始化一个核心线程;
      prestartAllCoreThreads():初始化所有核心线程
      
  • 【问】如何在 Java 线程池中提交线程?(execute()submit()submit()有返回结果,见前2问解析)

  • 【问】ThreadPoolExecutor线程池的关闭?(ThreadPoolExecutor提供shutdown()shutdownNow()用于线程池的关闭)
    Note

    • shutdown()不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
    • shutdownNow()立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
  • 【问】ThreadPoolExecutor线程池容量如何动态调整?(setCorePoolSize()和setMaximumPoolSize()

  • 【问】既然提到可以通过配置不同参数创建出不同的线程池,那么 Java 中默认实现好的线程池又有哪些呢?请比较它们的异同?(Executors类提供的创建线程的工厂方法)
    Note

    • Executors是一个提供了一系列用于创建线程池的工厂方法的类,线程池都通过ThreadPoolExecutor来创建,并返回一个ExecutorService接口
      • Executors.newSingleThreadExecutor()只有一个线程的线程池,该线程永不超时(没有keepAliveTime),当有多个任务需要处理时,会将它们放置到一个无界阻塞队列中逐个处理;
      • Executors.newCachedThreadPool():建立了一个线程池,而且线程数量是没有限制的(当然,不能超过Integer的最大值),新增一个任务即有一个线程处理(复用或new),线程空闲时长超过keepAliveTime则终止。
      • Executors.newFixedThreadPool():固定线程数量的线程池,初始化线程的最大数量,若任务数超过线程的处理能力,则建立阻塞队列容纳多余的任务
    • 阿里巴巴编码规范里面提到:线程池最好不要使用Executors去创建(线程池简化版),而是通过ThreadPoolExecutor(线程池旗舰版)的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险Executors各个方法的弊端:
      • 1)newFixedThreadPoolnewSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM
      • 2)newCachedThreadPoolnewScheduledThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM
  • 【问】Synchronized 用过吗,其原理是什么?(特性:可重入性,不可中断性;原理:在编译成字节码时,会通过字节码命令或者标志位判断告诉JVM,要访问的对象的monitor(管程)是否被其它线程占用),可参考深入理解Java并发之synchronized实现原理
    Note

    • Synchronized的3种应用方式:
      • 1)修饰实例方法:为实例对象加锁(实例对象锁,一个对象配一个monitor
      • 2)修饰静态方法:为当前类对象加锁(class对象锁,一个对象配一个monitor
      • 3)修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
    • Synchronized的实现原理:
      • 在JVM中,对象在内存中的布局分为三块区域:对象头(Mark word,klass word)、实例数据和对齐填充

      • 重量级锁(即Java1.6之前未引入轻量级锁和偏向锁概念的synchronized对象锁),锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象(Object)都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式:

        • monitor可以与对象一起创建销毁
        • 线程试图获取对象锁(synchronized)时自动生成monitor,但当一个 monitor 被某个线程持有后,它便处于锁定状态

        在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

      • monitor对象存在于每个Java对象的对象头中(存储的指针的指向)synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。

      • Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorentermonitorexit 指令,即同步代码块)还是隐式同步(通过ACC_SYNCHRONIZED标志实现,即同步方法)都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法并不是由 monitorentermonitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。

        • synchronized 代码块原理:(monitorentermonitorexit字节码命令,在JVM中能保证monitorentermonitorexit配对执行,即获取到锁,执行完毕或异常后会释放锁;monitor.count = 0获取锁成功;可重入monitor.count ++;释放锁monitor.count = 0
          public class SyncCodeBlock {
          
             public int i;
          
             public void syncTask(){
                 //同步代码库
                 synchronized (this){
                     i++;
                 }
             }
          }
          
          • 1)当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,objectrefmonitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功
          • 2)如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时(访问当前实例对象的其它synchronized方法)计数器的值也会加 1
          • 3)倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor
          • 4)为了保证在方法异常完成时 monitorentermonitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit指令。
        • synchronized 方法原理:通过方法表结构中的 ACC_SYNCHRONIZED标识(flag)来判断monitor是否被其它线程占用。
          方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor
    • Synchronized的特性:
      • 可重入性:访问当前实例对象的其它synchronized方法(包括当前实例对象的父类的同步方法)
      • 不可中断性:对于线程的中断,需要在Thread.run(){while(true){ if (this.isInterrupted()){break;} }}中进行中断判断,否则在main中通过thread.interrupt()也无法得到响应(详细代码参考深入理解Java并发之synchronized实现原理)
        线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。
  • 【问】多线程中 synchronized 锁升级的原理是什么?,参考synchronized的偏向锁、轻量级锁和重量级锁Synchronized偏向锁、轻量级锁、重量级锁详解Java程序员装X必备词汇之Mark Word!对象头由两部分组成:mark word和klass word
    Note

    • 对象头介绍: 对象头是堆中对象的头结构,它由两个部分组成:mark wordklass word
      在这里插入图片描述

      1. mark word
        在这里插入图片描述

        • mark word的大小为64bit,在对象的5种状态中(无锁,偏向锁,轻量级锁,重量级锁,GC标记),mark word的结构有所不同
        • 无锁 状态时,mark word的前56bit存储对象的hashcode信息。(前25bit为未使用,后31bit存储hashcode);后三位001表示无锁状态
        • 偏向锁 状态时,mark word的前54bit存储获取锁的线程相关信息;后三位101表示偏向锁
        • 轻量级锁 状态时,mark word的前62bit存储线程的栈的指针;后2位00表示轻量级锁
        • 重量级锁 状态时,markword的前62bit存储一个monitor对象信息;后2位10表示重量级锁
          可以通过JOL查看对象在堆内存的存储布局(前8位object headermark word,后4位object header为指针压缩后的klass word):
          在这里插入图片描述
      2. klass word

        • klassword 的32位或者64位代表元数据的指针。
        • klassword的大小为64bit,如果开启了指针压缩,大小为32bit

      其中mark word是后面学习并发编程,了解各种锁的基础。

    • 三种锁的比较(分别对应不同的并发场景,锁的升级是动态自适应的,可以动态适应不同场景):

      • 偏向锁:适合对象没有竞争的场景,表示该对象目前属于某个线程所有,过段时间后偏向锁可能会偏向其他线程,主要通过比较mark word记录的线程ID是否与第一次CAS时的一致来实现。只有存在竞争或者撤销次数达到阈值时才会解锁偏向锁,升级为轻量级锁。
      • 轻量级锁:只能处理线程之间交替加锁(没有竞争)的场景(T1加锁A解锁A,T2加锁B解锁B,T1加锁B解锁B),通过查看Mark Wordlock Record,以及synchronized解决。
      • 重量级锁:处理线程之间竞争互斥的场景,即JDK1.6之前的synchronized实现(最高级别的锁,更新Mark Word状态,但不会操作lock Record),底层原理为monitor管程,阻塞队列(竞争的线程)和等待队列(wait()被挂起)。

      synchronized锁的升级只能从低级(偏向锁)升级为高级(重量级锁),主要通过mark word锁状态和lock record锁记录(线程ID地址),来监控当前对象是否存在线程间的竞争关系。

    • 偏向锁不会主动进行解锁,出现竞争或撤销次数达到阈值时才会解锁101 → \rightarrow 001),这样做的目的是下一次同一个线程来获取锁时,直接检查mark word的锁记录就可以了。

      • JDK1.6之后默认使用偏向锁,即第一次使用CAS将线程的ID放置在访问对象的Mark word时,下一次如果发现访问的线程ID仍然是自己,则不再使用CAS,该对象由该线程所有,偏向锁状态设置成功(101);偏向锁会在当前线程的栈帧中创建锁记录Lock Record),使这个锁记录指向锁对象
      • 如果发现该对象锁释放之后,通过锁记录查看到有其它线程指向锁对象(获得锁资源),则将该对象的对象头(Mark word)设置为无锁状态(001),从偏向锁升级为轻量锁,即00(并发情况中等)
      • 如果通过锁记录发现该对象锁还没被释放就有其它线程来抢占资源,则将该对象的对象头(Mark word)设置为无锁状态(001),再从偏向锁升级为重量级锁,即10(并发情况严重)
      • 调用 wait/notify方法时,偏向锁直接升级为重量级锁(10,因为只有重量级锁才有该方法。
      • 关于偏向锁的批量重偏向
        如果一个类的大量对象被一个线程T1执行了同步操作,也就是大量对象先偏向了T1,T1同步结束后,另一个线程也将这些对象作为锁对象进行操作,会导致偏向锁重偏向的操作。
      • 关于偏向锁的撤销
        • 在B线程获取偏向锁时,查看mark word的线程id不是自己的,那么B线程就会向VM的线程队列发送一个撤销偏向锁的任务,VM线程会不断检测是否有任务要执行,当检测到这个任务后,就需要在安全点去执行(安全点时,JVM内的所有线程都会被阻塞,只有VM线程处于运行状态,它可以执行一些特殊的任务,如full gc就是此时执行)
        • 待全局安全点(在这个时间点上没有正在运行的字节码)。它会首先暂停拥有偏向锁的线程,然后检测这个线程是否存活。如果不存活的话,那么就先将对象头设置为无锁状态,并偏向提交撤销锁的那个线程
        • 如果存活且存在竞争(线程交替加锁),那么就先将对象头设置为无锁状态,并升级为轻量级锁
        • 当偏向锁的撤销次数超过40次后,会直接升级为轻量级锁
    • 轻量级锁:只能处理线程之间交替加锁的场景,通过Mark Wordlock Record监控,通过synchronized代码块(用法仍然是synchronized,但没有用到monitor)实现。

      • 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。如果直接升级成重量级锁(解决互斥) 的话,是没有必要的。
      • 轻量锁的操作步骤:
        • 创建锁记录(Lock Record)对象存放线程ID地址,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

        • 让锁记录中 Object reference 指向锁对象,并尝试用 CAS 替换 ObjectMark Word,将 Mark Word 的值存入锁记录;如果 CAS 失败(内存快照A != 内存当前值V),有两种情况:

          • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
          • 如果是自己执行了synchronized 锁重入,那么再添加一条Lock Record作为重入的计数
        • 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录(线程地址引用已被当前线程替换,之前的引用被解除),表示有重入,这时重置锁记录,表示重入计数减一

        • 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用CASMark Word的值(内存快照A )恢复给对象头

          • 成功,则解锁成功。
          • 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
      • 轻量级锁的作用:
        • 轻量级锁并不提供线程的互斥性,它的指定条件是多个线程交替去获取;
        • 偏向锁存在竞争的时候,会先撤销到无锁的状态,之后升级为轻量级锁,而轻量级锁升级时直接升级成重量级锁
    • 重量级锁:即JDK1.6以前的synchronized,底层使用Monitor管程对象(看上面synchronized底层原理),处于竞争的线程在等待队列或阻塞队列中

      • 刚开始 Monitor 中 Owner 为 null。
      • Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个Owner。
      • Thread-2上锁的过程中,如果 Thread-3Thread-4Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED
      • Thread-2 执行完同步代码块的内容,然后唤醒EntryList 中等待的线程来竞争锁,竞争时是非公平的。
      • 图中 WaitSet 中的 Thread-0Thread-1 是之前获得过锁,但条件不满足进入调用wait()方法进入 WAITING 状态的线程。当调用notifyAll()方法之后,WaitSet中的 Thread-0Thread-1 进入EntryList BLOCKED
  • 【问】为什么代码会重排序?(为了提供性能,处理器和JIT编译器常常会对指令进行重排序,需要满足以下两个条件:1)在单线程环境下不能改变程序运行的结果;2)存在数据依赖关系的不允许重排序)

  • 【问】什么是自旋?(因为线程阻塞涉及到用户态和内核态切换的问题,不如让线程忙循环(自旋)等待锁的释放,如果做了多次循环发现还没有获得锁,再阻塞)

  • 【问】volatile 关键字的作用?(工作内存中的操作结果立刻写回主存中),可参考volatile关键字最全总结
    Note

    • 在Java的内存模型中分为主内存(物理内存) 和 工作内存(高速缓存),Java内存模型规定所有的变量存储在主内存中,每条线程都有自己的工作内存。
      结合CAS算法就很好理解,volatile作用是禁止指令重排序,能够将线程在工作内存(新值B)中的操作结果立刻写回主存(内存值V)中,其他线程每次在读取时都能访问最新的值,但不能保证原子性而且volatile只能作用于变量
    • 线程的三个概念
      • 原子性:对基本数据类型的变量的读取和赋值操作是原子性操作
      • 可见性:当一个共享变量被volatile修饰时,他会保证修改的值会立刻被更新到主存,当以后其他线程需要读取时,它会去内存中读取新值。
      • 有序性:在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的运行,却会影响到多线程并发执行的正确性。
    • 应用场景
      • volatile作为一个轻量级同步锁,可用的场景较少,要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
        • 对变量的写操作不依赖于当前值(加锁)
        • 该变量没有包含在具有其他变量的不变式中
      • 单例模式的双重锁需要加volatile
          public class TestInstance{
          	private volatile static TestInstance instance;
          	public static TestInstance getInstance(){        
          		if(instance == null){   //1                     
          			 synchronized(TestInstance.class){    //2    
          			 		if(instance == null){   //3             
          			 				instance = new TestInstance();   //4
          			 		} //5
          			 }
          		}
          	  return instance;                             
          	}//6
          }
        
        在并发情况下,如果没有volatile关键字,在第5行会出现问题。instance = new TestInstance();可以分解为3行伪代码:
        	a. memory = allocate() //分配内存
        	b. ctorInstanc(memory) //初始化对象
        	c. instance = memory //设置instance指向刚分配的地址
        
        上面的代码在编译运行时,可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时,B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序,即先执行了ac没有执行b 那么由于A线程执行了c导致instance指向了一段地址,所以B线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。因此需要使用volatile修饰instance。
  • 【问】JVM 对 Java 的原生锁做了哪些优化?
    Note

    • 线程阻塞时,自旋锁自旋次数默认10次自适应自旋锁自旋次数不固定,由前一次自旋次数和锁的拥有者的状态决定;
    • 锁消除:在动态编译同步代码块的时候,JVM中的JIT编译器借助逃逸分析技术来判断对象是否存在线程同步的情况,是否只被一个线程访问,若不存在同步情况则可以取消锁,省去了加锁解锁的开销。
      逃逸分析是指当前加锁的对象是否会逃出它的作用域?比如如下代码:
      • 在代码1中,append方法用了 synchronized关键字,如果只有一个线程对这个StringBuffer对象进行操作时(不存在同步),在线程内部可以把StringBuffer当做局部变量使用StringBuffer仅在方法内作用域有效,因此是线程安全的,可以进行锁消除
        @Override
        public synchronized StringBuffer append(String str) {
            toStringCache = null;
            super.append(str);
            return this;
        }
        
      • 在代码2中,sBuf会逃出当前线程,作为外部的全局变量使用(存在同步操作),因而是线程不安全的,不能对sBufappend()进行锁消除。
        public static String createStringBuffer(String str1, String str2) {
            StringBuffer sBuf = new StringBuffer();
            sBuf.append(str1);// append方法是同步操作
            sBuf.append(str2);
            return sBuf.toString();
        }
        
      • 对于第一段代码,可以通过JIT编译器将其优化,将锁消除,前提是Java必须运行在server模式,同时必须开启逃逸分析
        -server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
        
        其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。
        
    • 锁粗化:锁的请求、同步、释放都会消耗一定的系统资源,如果高频的锁请求反而不利于系统性能的优化。当JIT编译器发现一系列的操作都对同一个对象反复加锁解锁,甚至加锁操作出现在循环中,此时会将加锁同步的范围粗化到整个操作系列的外部
      目的是让能够一次性执行完的代码不要多次对同一个变量加锁执行。
    • 锁粒度:不要锁住一些无关的代码
  • 【问】为什么说 Synchronized 是非公平锁?(公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,而Synchronized锁是非公平锁,属于抢占式)

  • 【问】什么是锁消除和锁粗化?(见上两问)

  • 【问】为什么说 Synchronized 是一个悲观锁?(不管是否产生竞争,任何数据的操作都必须加锁)

  • 【问】乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?乐观锁一定就是好的吗?,可参考乐观锁之CAS算法
    Note

    • Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。Java1.6Synchronized做了优化,增加了从偏向锁轻量级锁再到重量级锁的过度(其中也用到了CAS

    • 原子操作类是Atomic开头的包装类(AtomicBooleanAtomicIntegerAtomicLong)在进行自增时,性能比Synchronized好,其原理是使用CAS机制。Lock系列的底层实现也用到了CAS

    • CAS机制即比较并交换,CAS机制当中使用了3个基本操作数:内存地址V(共享内存空间),旧的预期值A(此前的内存快照),要修改的新值B(线程的操作)。线程1在将B写入内存时,会比较此刻内存中的值V和A是否相等如果相等,则写入如果不等,则重新获取内存中的值V,再重新尝试操作,此过程为自旋

    • CAS优缺点:
      优点:

      • CAS无锁和非阻塞,性能好,没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销。

      缺点:

      • CPU开销大:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,不断的进行自旋,循环往复,会给CPU带来很大的压力。
      • 不能保证整个代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性;比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
      • 存在ABA问题:如果以提款存款为例子,假设此时存款100,线程1和线程2都在提款50(A都为100),线程1提款成功(V为50),此时又有另一个线程在存款50(A为50)将V被修改为100,此时线程2开始运行时发现自己的A=此时的V,把V修改为50,但却没有把钱吐出来,导致用户账号里少钱了。
    • ABA的解决方法:更为严谨的CAS算法应该加一个版本号,用于比较前后V相同时,版本号是否相同,如果两者都相同,才进行写入,否则自旋。

    • AtomicInteger自增方法incrementAndGet源码如下:

      public final int incrementAndGet() {
          for (;;) {
              int current = get();
              int next = current + 1;
              if (compareAndSet(current, next))
                  return next;
          }
      }
      private volatile int value;
      public final int get() {
          return value;
      }
      

      其中的compareAndSet也是原子操作,底层是通过JVM调用的后门程序unsafe,实现硬件层面的原子操作

    • Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守,比如比如mysql的行锁,表锁,读锁和写锁CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。比如版本控制软件git,svn,cvs。

  • 【问】请对比下 volatile,Synchronized,CAS(乐观锁,非阻塞)的异同?,参考并发编程面试题(2020最新版)

  • 【问】跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不同?(在使用synchronized时,其锁对象monitor内部有一个计数器,在monitorenter()monitorexit()内部如果当前线程访问则计数器+1
    Note

    • ReentrantLock使用内部类Sync来管理锁,所以真正的获取锁是由Sync的实现类控制的。Sync有两个实现,分别为NonfairSync非公公平锁)和FairSync公平锁)。
      // Sync继承于AQS
      abstract static class Sync extends AbstractQueuedSynchronizer {
        ...
      }
      // ReentrantLock默认是非公平锁
      public ReentrantLock() {
              sync = new NonfairSync();
       }
      // 可以通过向构造方法中传true来实现公平锁
      public ReentrantLock(boolean fair) {
          sync = fair ? new FairSync() : new NonfairSync();
      }
      
    • Sync通过继承AQS实现,在AQS中维护了一个private volatile int state计算重入次数,避免频繁的持有释放操作带来的线程问题。
      • 当一个线程在获取锁过程中,先判断state的值是否为0,如果是表示没有线程持有锁,就可以尝试获取锁。
      • state的值不为0时,表示锁已经被一个线程占用了,这时会做一个判断current==getExclusiveOwnerThread(),这个方法返回的是当前持有锁的线程,如果是自己,那么将state的值+1,表示重入返回即可。
  • 【问】synchronized 和 ReentrantLock 区别是什么?
    Note

    • 1)相似点:它们都是阻塞式的同步,也就是说一个线程获得了对象锁,进入代码块,其它访问该同步块的线程都必须阻塞在同步代码块外面等待,而进行线程阻塞和唤醒的代码是比较高的。
    • 2)功能区别
      • Synchronized是java语言的关键字,是原生语法层面的互斥,需要JVM实现ReentrantLockJDK1.5之后提供的API层面的互斥锁,需要lockunlock()方法配合try/finally代码块来完成。
      • Synchronized使用较ReentrantLock 便利一些;
      • 锁的细粒度和灵活性:ReentrantLock强于Synchronized
    • 3)性能区别
      • Synchronized引入偏向锁,自旋锁之后,两者的性能差不多,在这种情况下,官方建议使用Synchronized
      • 两者的重入区别看上一问
      • ReentrantLockjava.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有如下三项:
        • 等待可中断:持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized避免出现死锁的情况。通过lock.lockInterruptibly()来实现这一机制;
        • 公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁Synchronized锁是非公平锁;ReentrantLock默认也是非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好
        • 锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个对象。ReentrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像Synchronized要么随机唤醒一个线程,要么唤醒全部线程。
  • 【问】那么请谈谈 AQS 框架是怎么回事儿?,可参考并发编程面试题(2020最新版)
    Note

    • java的Lock体系其实是基于AQS框架实现的Lock体系中的锁对象其实一个资源(独占/共享),在加锁和释放锁是通过CAS算法对state+1state-1实现。在使用SemaphoreReentrantlock锁资源时,需要配合try-catch手动释放锁资源,而CountDownLatchCyclicBarrier不需要。

    • AQS全称是AbstractQueuedSynchronizer(抽象队列同步器),是一个独占锁/共享锁的实现框架(ReentrantLockCountDownLatchCyclicBarrierReadWriteLock),其核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中

    • CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。AQS原理图如下:
      在这里插入图片描述

    • AQS使用一个int成员变量state来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

      private volatile int state;//共享变量,使用volatile修饰保证线程可见性
      

      状态信息通过protected类型的getStatesetStatecompareAndSetState进行操作

      //返回同步状态的当前值
      protected final int getState() {  
              return state;
      }
       // 设置同步状态的值
      protected final void setState(int newState) { 
              state = newState;
      }
      //原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
      protected final boolean compareAndSetState(int expect, int update) {
              return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
      }
      
    • AQS定义两种资源共享方式

      • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁
        • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
        • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
      • Share(共享):多个线程可同时执行,如Semaphore、CountDownLatch、CyclicBarrier、ReadWriteLock

      ReentrantReadWriteLock可以看成是组合式(共享 + 独占),因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读

    • 不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。

    • AQS底层使用了模板方法设计模式
      同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

      • 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
      • AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
      isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
      tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
      tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
      tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
      tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
      

      默认情况下,每个方法都抛出 UnsupportedOperationException。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final,所以无法被其他类使用只有这几个方法可以被其他类使用

    • ReentrantLock为例,state初始化为0,表示未锁定状态A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()state=0(即释放锁)为止,其它线程才有机会获取该锁。
      当然,释放锁之前,A线程自己是可以重复获取此锁的(锁重入时state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到0的。

    • 再以CountDownLatch以例,任务分为N个子线程去执行state也初始化为N注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会通过CAS减1(N不大,CAS开销也不大)。
      等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作

    • 一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

  • 【问】除了 ReetrantLock,你还接触过 JUC 中的哪些并发工具?,参考多线程工具类:CountDownLatch、CyclicBarrier、Semaphore
    Note:具体代码参考以上链接

    • CountDownLatch:假如有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以(countDownLatch.await();CountDownLatch是JDK为我们提供的一个计数器,核心是通过countDown()实现减1操作,它主要方法如下:
      //构造方法,接收计数器的数量	
      public CountDownLatch(int count)	
      //持续等待计数器归零	
      public void await()	
      //最多等待unit时间单位内timeout时间	
      public boolean await(long timeout, TimeUnit unit)	
      //计数器减1	
      public void countDown()	
      //返回现在的计数器数量	
      public long getCount()
      
    • CyclicBarrierCyclicBarrier(循环栅栏)可以完成CountDownLatch的全部功能,但是相比CountDownLatch,它可以一次性执行多个线程,接着通过await()等待一次性释放锁资源,即一组线程相互等待。常用方法如下:
      //构造方法,第一个参数为栅栏的长度,第二个是Runnable对象	
      public CyclicBarrier(int parties, Runnable barrierAction)	
      public CyclicBarrier(int parties)	
      //获取现在的数量	
      public int getParties()	
      //持续等待栅栏归零	
      public int await()	
      //最多等待unit时间单位内timeout时间	
      public int await(long timeout, TimeUnit unit)
      
      CyclicBarrier的任务执行完一轮(5个thread)以后,如果构造时传入了Runnable对象,则先执行Runnable对象,然后在完成瞬间释放所有任务的锁,接着再加入新的任务执行。
    • Semaphore:允许多个线程同时访问,相比于CyclicBarrierSemaphore(信号量)并没有限制一次性只能执行N个线程并一次性释放N锁资源synchronizedReentrantLock 是独占锁,同一时刻只允许一个线程访问,Semaphore(信号量)是共享锁。
      • Semaphore基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,通过 acquire()release()方法来获得和释放临界资源。经实测,Semaphone.acquire()方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()方法中断synchronized则不允许线程中断)。
      • 此外,Semaphore 也实现了可轮询的锁请求定时锁的功能,除了方法名 tryAcquiretryLock 不同,其使用方法与ReentrantLock几乎一致。Semaphore也提供了公平与非公平锁的机制,也可在构造函数中进行设定。常用方法如下:
        //创建具有给定许可数的信号量	
        Semaphore(int permits):构造方法,创建	
        //拿走1个许可	
        void acquire()	
        //拿走多个许可	
        void acquire(int n)	
        //释放一个许可	
        void release()	
        //释放n个许可	
        void release(int n):	
        //当前可用的许可数	
        int availablePermits()
  • 【问】请谈谈 ReadWriteLock 和 StampedLock?,可参考ReadWriteLock和StampedLock
    Note

    • ReadWriteLock 是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLockReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的ReentrantReadWriteLock的构造函数如下:
      public class ReentrantReadWriteLock
          implements ReadWriteLock, java.io.Serializable {
          public ReentrantReadWriteLock() {
              this(false);
          }  
          public ReentrantReadWriteLock(boolean fair) {
              sync = fair ? new FairSync() : new NonfairSync();
              readerLock = new ReadLock(this);
              writerLock = new WriteLock(this);
          }
      }
      
      ReadLock的加锁方法是基于AQS同步器的共享模式。
      public void lock() {
          sync.acquireShared(1);
      }
      
      WriteLock的加锁方法是基于AQS同步器的独占模式。
      public void lock() {
          sync.acquire(1);
      }
      
    • StampedLock邮戳锁不可重入):但是读写锁ReadWriteLock容易引起饥饿写的问题。饥饿写即在使用读写锁的时候,读线程的数量要远远大于写线程的数量,导致锁对象(this)长期被读线程持有,写线程无法获取锁对象(this)的写操作权限而进入饥饿状态。因此JDK1.8引入了StampedLock
      • StampedLock获取锁的时候会返回一个long型的数据戳,该数据戳用于稍后的锁释放参数,如果返回的数据戳为0则表示锁获取失败
      • StampedLock不可重入的,即使当前线程持有了锁再次获取锁还是会返回一个新的数据戳,所以要注意锁的释放参数,使用不小心可能会导致死锁。
  • 【问】什么是 Java 的内存模型(JMM),Java 中各个线程是怎么彼此看到对方的变量的?(见volatile解析)

  • 【问】Java8开始ConcurrentHashMap,为什么舍弃分段锁?(ConcurrentHashMap的原理是引用了内部的 Segment ( ReentrantLock ) 分段锁,但在Java8使用synchronized+CAS,原因是加入多个分段锁浪费内存空间)

  • 【问】ThreadLocal 是什么?有哪些使用场景?,参考史上最全ThreadLocal 详解拼多多面试官没想到ThreadLocal我用得这么溜,人直接傻掉
    Note

    • ThreadLocal是线程变量,是每个线程执行时的局部变量表,通过每个线程的ThreadLocalMap来存储,在Java8中key为ThreadLocal,value为值;

    • ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个线程使用该变量的线程都会初始化一个完全独立的实例副本ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收

    • ThreadLocalMap没有实现Map接口,而是基于数组实现,其中数组中的每个元素表示一个ThreadLocal副本,用Entry<ThreadLocal,Object>来存储,因此ThreadLocalMap中不同的threadlocal可以用于存储不同类型的对象(Entry<ThreadLocal,Integer>,Entry<ThreadLocal,Character>等)

    • ThreadLocalMap在处理冲突时:1)会先通过 key.threadLocalHashCode & (len-1);计算当前ThreadLocal的hash值,接着到数组中查找该位置是否为空:2)如果为空,则直接初始化Entry并插入;3)如果非空,则比较当前位置上的key是否为当前的ThreadLocal对象,如果是,则完成value的更新;如果不是则通过线性探测法搜索下一个未冲突的位置。

    • ThreadLocal 适用于如下两种场景

      • 1、每个线程需要有自己单独的实例(线程A不能访问线程B的工作内存,具有隔离性
      • 2、实例需要在多个方法中共享,但不希望被多线程共享

      场景包括:
      1)数据库连接:每个线程通过ThreadLocal 创建一个JDBC Connection,线程A不能close()线程B的connection);
      2)处理数据库事务
      3)用户session管理
      4)Spring使用ThreadLocal解决线程安全问题

  • 【问】请谈谈 ThreadLocal 是怎么解决并发安全的?与SychronizedLock的比较
    Note

    • 在处理并发问题时,SynchronizedLock时间换空间的方式,让一个线程执行,其他线程等待,使不同线程串行执行;
    • ThreadLocal通过创建线程局部变量,用空间换时间的方式,让不同线程并发执行(实现简单,很多开源项目比如Spring都是用ThreadLocal来处理并发问题)。
  • 【问】很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?,参考ThreadLocal 你真的用不上吗?
    Note

    • 线程之间的threadLocal变量是互不影响的;
    • 使用private final static进行修饰,防止多实例时内存的泄露问题
    • 线程池环境下使用后将threadLocal变量remove掉或设置成一个初始值
  • 【问】什么是上下文切换?(时间片轮转,线程3个状态)
    Note

    • 多线程编程中Thread的创建个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
  • 【问】什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?

  • 【问】为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?,参考wait、notify、notifyAll的理解与使用
    Note

    • 在调用 wait()notifynotifyAll时,线程必须要获得该对象的对象监视器锁,否则会抛出 IllegalMonitorStateException异常;
    • notifyAll() 使所有原来在该对象上 wait 的线程退出 WAITTING 状态,使得他们全部从等待队列中移入到同步队列中去(阻塞状态转就绪状态
  • 【问】object的wait,notify,notifyAll与Condition的await,signal,signalAll的区别,参考用lock condition实例,与await区别,await为何必须用在lock()里面
    Note

    • Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll()
    • Condition需要在共享资源中创建:
      public class Car {
          private boolean waxStatus = false;//车的上蜡状态
          private Lock lock = new ReentrantLock();
          Condition conditionC = lock.newCondition();//消费者的condition
          Condition conditionP = lock.newCondition();//生产者的condition
      
          ....
         }
      
    • 使用synchronized/wait()只有一个阻塞队列notifyAll唤起所有阻塞队列下的线程,而使用lock/condition可以实现多个阻塞队列signalAll只会唤起某个阻塞队列下的阻塞线程
  • 【问】Thread 类中的 yield 方法有什么作用?,参考Thread.yield()详解
    Note

    • yield 是一个静态的原生(native)方法;
    • Thread.yield();当前线程从运行状态 转为 就绪状态(不是等待状态),把运行机会交给线程池中/其他拥有相同优先级的线程;
    • 无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中
  • 【问】Java 如何实现多线程之间的通讯和协作?(生产者消费者问题)
    Note
    Java中线程通信协作的最常见的两种方式:

    • 1)synchronized加锁的线程,通过Object类的wait()/notify()/notifyAll()完成线程间的通讯;只能随机唤醒一个线程(notify)或者唤醒所有线程(notifyAll)
    • 2)ReentrantLock类加锁的线程的,通过Condition类的await()/signal()/signalAll()完成线程间的通讯;可以分组唤醒需要唤醒的线程;
    • 通过管道进行线程间通信:1)字节流;2)字符流
  • 【问】Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比synchronized它有什么优势?即ReentrantLock与Synchronized的区别?
    Note

    • Locksynchronized 的扩展版,Lock 提供了无条件的、可轮询的(lock.tryLock 方法)、定时的(lock.tryLock 带参方法)、可中断的(lock.lockInterruptibly)、多条件阻塞队列的(lock.newCondition方法)锁操作。
    • Lock 的实现类基本都支持非公平锁(默认)和公平锁synchronized 只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择
  • 并发的定义

  • Java多线程,可参考线程基本定义 & java创建线程的4种方法常见的线程安全问题
    Notet.setDaemon(true)将线程转换成守护线程。守护线程的唯一用途是为其他线程提供服务。比如说,JVM的垃圾回收、内存管理等线程都是守护线程。

  • 线程状态(sleep不释放,wait主动释放,yield将该线程从运行转入到就绪状态),可参考线程5个状态的转换

  • 生产者消费者,可参考Java多种方式解决生产者消费者问题(十分详细)

  • DeadLock例子(多核无法避免死锁),可参考多核/单核 死锁 问题

  • 集合线程安全,可参考Collections.synchronizedCollection 该集合线程安全java中哪些集合是线程安全的,哪些是线程不安全的

  • 计数器(volatile,lock,synchronized),可参考并发编程面试题(2020最新版)Java中18把锁

  • Future(实现类为FutureTask) 和 线程池 的使用,可参考多线程和线程池(Future,Executor,ThreadPoolExecutor)java线程池详解(corePoolSize, maximumPoolSize, workQueue 解析线程池执行流程)

Note

  • Runnable 不会返回结果,无法抛出返回结果的异常;Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到。
  • 阿里巴巴编码规范里面提到:线程池最好不要使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
  • 线程与协程的区别(协程是在线程基础上进一步的抽象,之前线程是和程序绑定在一起的,多个程序的运行会轮流使用线程池中的线程,虽然线程池可以避免线程的创建和销毁次数,但是每一次创建难免存在上下文切换开销大,而多了协程这层抽象之后,用户在逻辑层面上将一个程序与一个协程绑定,而对线程池的管理由之前的用户管理交给底层去实现,如果线程执行完毕的话,多个协程可以同时使用同一个线程执行(协程与线程为多对多关系),少去了由用户来完成线程间的上下文切换,线程的切换在底层的核心态内来完成,而协程只起到与线程的绑定作用)。 参考总结:协程与线程协程与线程的区别多线程和线程池Java 19 正式发布,虚拟线程来了!
    • 当我们的程序是 IO 密集型时(如 web 服务器、网关等),为了追求高吞吐,有两种思路:

      • 为每个请求开一个线程处理,为了降低线程的创建开销,可以使用线程池技术,理论上线程池越大,则吞吐越高,但线程池越大,CPU花在切换上的开销也越大
      • 使用异步非阻塞的开发模型,用一个进程或线程接收请求,然后通过 IO 多路复用让进程或线程不阻塞,省去上下文切换的开销

      这两个方案,优缺点都很明显:方案1实现简单,但性能不高;方案2性能非常好,但实现起来复杂。

    • 协程需要解决线程遇到的几个问题:

      • 内存占用要小,且创建开销要小

        • 用户态的协程,可以设计的很小,可以达到 KB 级别。是线程的千分之一。
        • 线程栈空间通常是MB级别, 协程栈空间最小KB级别。
      • 减少上下文切换的开销

        • 可执行的线程尽量少,这样切换次数必然会少
        • 让线程尽可能的处于运行状态,而不是阻塞让出时间片
          • 多个协程多个协程绑定一个或者多个线程上
          • 当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上(分时复用)。
            即使有协程阻塞,该线程的其他协程也可以被 runtime 调度,转移到其他可运行的线程上。
      • 降低开发难度

        • goroutine是golang中对协程的实现
        • goroutine底层实现了少量线程干多事,减少切换时间等,程序员可以轻松创建协程,无需去关注底层性能优化的细节
    • 协程和线程的区别

      • 线程是操作系统的资源,由OS创建切换和停止,而协程是用户级线程,由编程语言本身来实现
      • 协程是异步机制,而线程和进程是同步机制;
      • 线程抢占式,协程非抢占式(由用户释放);
      • 线程开辟数量限制在千的级别,而协程可以达到上万级别

更多内容整理 参考

十四、行为抽象和Lambda

Note

  • 流(Stream)是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。集合讲的是数据,流讲的是计算
  • stream计算主要包括两个动作:中间动作(filtermapflatMap等)和结束动作(reducecollectforEach等)
  • Sensor::getNum表示Sensor类中的getNum方法
  • filter()传入的是Predicate<? super T>断定式函数接口
  • 匿名内部类中,一定是程序在运行的过程当中没有发生改变的量,即无法捕获在匿名内部类改变的变量,lambda表达式也是如此。
  • Stream流属于管道流该对象只能被消费(使用)一次;第一个stream流调用完毕方法, 数据就会流转到下一个Stream上(streamB),而这时第一个stream流(streamA)已经使用完毕,就会关闭了;如果再次使用streamA,则会抛出:java.lang.IllegalStateException: stream has already been operated upon or closed
  • Optional类是一个对象容器,可以对对象进行进一步封装,可以提示用户该对象是否为null,并使用filterorElse等进行处理,简化if else代码。

十五、设计模式

更多内容整理参考

十六、JVM(类加载,运行时数据区,垃圾回收算法,垃圾回收器,JVM调优)

参考Java虚拟机(JVM)你只要看这一篇就够了!全面阐述JVM原理

  • 【问】说一下 JVM 的主要组成部分?及其作用?(JVM包括类加载子系统、运行时数据区(堆、方法区、虚拟机栈、本地(native)方法栈、程序计数器)、直接内存、垃圾回收器、执行引擎),可参考Java虚拟机(JVM)你只要看这一篇就够了!
    Note

    • JVM包含两个子系统和两个组件,两个子系统为Classloader(类装载)、 Execution engine(执行引擎);
      两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。
    • Class loader(类装载):根据给定的全限定名类名(如: java.lang.Object)来装载class文件到Runtime data area中的method area。
    • Execution engine(执行引擎):执行classes中的指令(JIT编译器)。
    • Native Interface(本地接口):与native libraries交互,是其它编程语 言交互的接口。
    • Runtime data area(运行时数据区域):这就是我们常说的JVM的内存。
  • 【问】说一下 JVM 运行时数据区?(运行时数据区包括堆、方法区、虚拟机栈、本地方法栈、程序计数器)
    Note:堆(动,线程共享),区(静,线程共享),栈(动,线程私有),计数器(线程私有)

    • 对象实例或数组,是垃圾回收器管理的主要区域。(new
    • 方法区:方法区可以认为是堆的一部分,用于存储已被JVM加载的类信息常量、静态变量、即时编译器JIT编译后的代码。(反射,String常量,对象类型数据)
    • 虚拟机栈:栈解决的是程序运行的问题,栈里面存的是栈帧栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息。(方法递归recursive
      • 栈帧每个方法从调用到执行的过程就是一个栈帧在虚拟机栈入栈到出栈的过程。
      • 局部变量表:用于保存函数的参数和局部变量
      • 操作数栈:操作数栈又称操作栈,大多数指令都是从这里弹出数据,执行运算,然后把结果压回操作数栈。
    • 本地方法栈:本地方法栈执行的是本地方法,一个Java调用非Java代码的接口。 (native方法)
    • 程序计数器(PC寄存器):存放的是当前线程所执行的字节码(.class文件)的行数JVM工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。(读取.class文件的指令行)
  • 【问】对象的内存布局?(对象头,实例数据,对齐填充)
    Note

    • 对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为 Mark Word。第二部分是类型指针(Klass word,即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例
      另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。
    • 实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。
    • 对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍
  • 【问】关于对象的访问定位?
    Note

    • 使用对象时,通过栈上的 reference 数据来操作堆上的具体对象。

      • 通过句柄访问:Java 堆中会分配一块内存作为句柄池reference 存储的是句柄地址(二次寻址)。
      • 使用直接指针访问reference中直接存储对象地址(一次寻址)。

      两者比较:使用句柄的最大好处是 reference 中存储的是稳定的句柄地址,在对象移动(GC)时只改变实例数据指针地址reference 自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。
      如果是对象频繁 GC 那么句柄方法好(通过判断对象不可达到GC Roots 进而GC),如果是对象频繁访问则直接指针访问好。

  • 【问】什么是类加载器,类加载器有哪些?(启动类 / 扩展类 / 应用程序类加载器,了解之间的继承关系)
    Note

    • 类加载器的作用.class文件字节码内容加载到内存中,并将这些静态数据转换成方法区运行时的数据,然后在堆中生成一个代表这个类的Java.lang.class对象,作为方法区中类数据的访问入口。
    • JVM有三种类加载器(B,E,A):
      1. 启动类加载器:该类没有父加载器,用来加载Java的核心类,启动类加载器的实现依赖于底层操作系统,属于虚拟机实现的一部分,它并不继承自java.lang.classLoader
      2. 扩展类加载器:它的父类为启动类加载器,扩展类加载器是纯java类,是ClassLoader类的子类,负责加载JRE的扩展目录
      3. 应用程序类加载器:它的父类为扩展类加载器,它从环境变量classpath或者系统属性java.lang.path指定的目录中加载类,它是自定义的类加载器的父加载器。
  • 【问】说一下类加载的执行过程?(加载类对象,包括成员变量,成员方法等) ,参考类加载的过程
    Note
    当程序主动使用某个类时,如果该类.class还未被加载到内存中,JVM主要会通过类加载、类连接、类初始化3个步骤对该类进行类加载。

    • 类加载将类的.class文件读入到内存中,并在堆中为之创建一个java.lang.Class对象,作为类数据的访问入口。类的加载由类加载器完成,类加载器由JVM提供,开发者也可以通过继承ClassLoader基类来创建自己的类加载器。类加载机制包括全盘负责,双亲委派和缓存机制,下面会具体说明。
    • 类连接:当类被加载之后,系统为之生成一个对应的Class对象,接着进入连接阶段,连接阶段负责将类的二进制数据合并到JRE
      • 验证:是连接的第一步,确保 .class 文件的字节流中包含的信息符合当前虚拟机要求,包括:文件格式验证,元数据验证,字节码验证,符号引用验证。如果无法通过符号引用验证将抛出一个 java.lang.IncompatibleClass.ChangeError 异常的子类。如 java.lang.IllegalAccessErrorjava.lang.NoSuchFieldErrorjava.lang.NoSuchMethodError 等。
      • 准备:这个阶段正式为类分配内存设置类变量初始值
      • 解析:这个阶段是JVM 将常量池内的符号引用(常量名)替换为直接引用(地址) 的过程。
    • 类初始化:JVM对类进行初始化

    以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):

    • 遇到 newgetstaticputstaticinvokestatic 这 4 条字节码指令时触发初始化。使用场景:使用 new 关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法
    • 使用 java.lang.reflect包的方法对类进行反射调用的时候。
    • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
    • 当虚拟机启动时,用户需指定一个要加载的主类(包含 main()方法的那个类),虚拟机会先初始化这个主类
    • 当使用 JDK 1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStaticREF_putStaticREF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化
  • 【问】JVM的类加载机制是什么?(全盘负责,双亲委派,缓存机制)
    Note

    • 全盘负责:类加载器加载某个class时,该class依赖的和引用其它的class由该类加载器载入
    • 双亲委派:先让父加载器加载该class,父加载器无法加载时才考虑自己加载。因此如果自定义String
    • 缓存机制:缓存机制保证所有加载过的class都会被缓存,当程序中需要某个class时,先从缓存区中搜索,如果不存在,才会读取该类对应的二进制数据,并将其转换成class对象,存入缓存区中。
  • 【问】什么是双亲委派模型?(如果父加载器存在其父加载器,则进一步向上委托,依次递归,请求将最终到达顶层的启动类加载器;如果父加载器无法完成加载任务,子加载器才会尝试自己去加载;可避免重复加载问题),参考JVM类加载器是否可以加载自定义的String
    Note

    • JVM出于安全性的考虑全限定类名相同String不能被加载的。但是如果加载了,会出现什么样的结果呢?下面分别通过全限定类名不同 和 全限定类名相同做一下实验:
    • 全限定类名不同:自定义一个com.example.demojava.String
      package com.example.demojava.loadclass;
      public class String {
          public static void main(String[] args) {
              System.out.println("我是自定义的String");
          }
      }
      ---
      错误: 找不到或无法加载主类 src.main.java.com.example.demojava.loadclass.String
      
      主要原因是参数String和自定义String冲突,修改如下就可以加载自定义String了:
      package com.example.demojava.loadclass;
        public class String {
            public static void main(java.lang.String[] args) {
                System.out.println("我是自定义的String");
            }
        }
        ---
        我是自定义的String
      
    • 全限定类名相同:自定义包名为java.lang
      package java.lang;
      
      public class String {
          public static void main(java.lang.String[] args) {
              System.out.println("我是自定义的String");
          }
      }
      ---
      Connected to the target VM, address: '127.0.0.1:63569', transport: 'socket'
      错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
         public static void main(String[] args)
      否则 JavaFX 应用程序类必须扩展javafx.application.Application
      
  • 【问】怎么判断对象是否可以被回收?或者GC 对象的判定方法(GC Roots可达判断:GC Roots主要来自栈和区,先删GC Roots,再删除堆对象)
    Note

    • 引用计数算法
      • 判断对象的引用数量:每个对象实例都有一个引用计数器,被引用+1,完成引用-1
        任何引用计数为0的对象实例可以被当做垃圾回收
      • 优缺点:
        • 优点:执行效率高,程序受影响较小;
        • 缺点:无法检测出循环引用的情况,导致内存泄漏
    • 可达性分析算法GC Roots):
      • 通过判断对象的引用链是否可达来决定对象是否可以被回收。对于不可达状态的判断,需要用到GC roots,也就是根对象,如果一个对象无法到达根对象的路径,或者说从根对象无法引用到该对象,该对象就是不可达的
      • 以下三种对象在JVM中被称为GC roots,来判断一个对象是否可以被回收。
        • 1)虚拟机栈的栈帧:每个方法在执行的时候,JVM都会创建一个相应的栈帧(操作数栈、局部变量表、运行时常量池的引用),当方法执行完,该栈帧就从栈中弹出,这样一来,方法中临时创建的独享就不存在了,或者说没有任何GC roots指向这些临时对象,这些对象在下一次GC的时候便会被回收
        • 2)方法区中的静态属性静态属性数据类属性,不属于任何实例,因此该属性自然会作为GC roots。只要这个class在,该引用指向的对象就一直存在,因此class对象也有被回收的时候
          class何时会被回收?
          • 堆中不存在该类的任何实例
          • 加载该类的classLoader已经被回收
          • 该类的java.lang.class对象没有在任何地方被引用,也就是说无法通过反射访问该类的信息
        • 3)本地方法栈引用的对象
  • 【问】java 中都有哪些引用类型?(强引用(不被GC),软引用(内存够不被GC),弱引用,虚引用

  • 【问】说一下 JVM 有哪些垃圾回收算法?,可参考全面阐述JVM原理
    Note

    • 对象是否已死算法:引用计数器算法,可达性分析算法
    • JVM的垃圾回收算法有三种(会用到可达性分析算法):
      • 标记-清除:容易产生内存碎片,进而再一次出发GC
      • 标记-复制:Java堆中新生代的垃圾回收算法,新生代对象一般很少存活,将不回收的对象复制到新内存空间上效率高。
      • 标记-压缩:Java堆中老生代的垃圾回收算法(Major GC),老生代大部分对象会存活,将不回收的对象压缩到内存一端,避免碎片化
  • 【问】说一下 jvm 有哪些垃圾回收器?(单线程(停)/ 多线程(停)/ CMS(不停)/ G1;主要在并发,标记策略上存在不同),可参考全面阐述JVM原理
    Note

    • 串行垃圾回收器
      • JDK1.3之前,单线程回收器是唯一的选择,在它进行垃圾回收的时候,必须暂停其它所有的工作线程(Stop The World,STW),直到它收集完成。
      • 串行的垃圾收集器有两种,Serial(新生代,使用标记-复制算法)和Serial Old(老生代,使用标记-压缩算法),一般两者搭配使用。
      • -XX:+UseSerialGC开启串行垃圾回收器
    • 并行垃圾回收器:(配合CMS收集器使用)
      • 并行垃圾回收器是通过多线程进行垃圾收集的。也会暂停其它所有的工作线程(Stop The World,STW),一般会和JDK1.5之后出现的CMS搭配使用
      • 并行的垃圾回收器有以下几种:
        • ParNewSerial收集器的多线程版本),运行数量可以通过修改ParallelGCThreads设定;
        • Parallel Scavenge: 关注吞吐量,吞吐量优先,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间);用于新生代收集,复制算法。
        • Parllel OldParallel Scavenge的老年代版本,JDK 1.6开始提供的。
    • CMS收集器:(多次标记,一次清除;不用暂停用户的工作线程,配合并行垃圾回收器使用)
      • CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。从名字就能知道它是标记-清除算法的。但是它比一般的标记-清除算法要复杂一些,分为以下4个阶段:
        • 初始标记:标记一下GC Roots能直接关联到的对象,会"Stop The World"。
        • 并发标记GC Roots Tracing,可以和用户线程并发执行。
        • 重新标记:标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,会“Stop The World”。
        • 并发清除:清除对象,可以和用户线程并发执行
      • CMS(Concurrent Mark Sweep) 收集器存在的问题:
        • 由于它是基于标记-清除算法的,那么就无法避免空间碎片的产生
        • CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。 所谓浮动垃圾,在CMS并发清理阶段用户线程还在运行着,伴随程序运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只能留待下一次GC时再清理掉。
    • G1垃圾收集器:JDK 7发布,并在JDK 9中成为了默认的垃圾回收器,G1收集器特性如下:
      - 1)并行与并发:G1收集器能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop The World停顿时间。部分其他收集器原本需要暂停Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。【能充分利用多CPU、多核环境的硬件优势,缩短停顿时间;能和用户线程并发执行】
      • 2)分代收集:虽然G1收集器可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间的对象,熬过多次GC的旧对象以获取更好的收集效果。
      • 3)空间整合:与CMS收集器的“标记-清除”算法不同,G1收集器整体上看采用“标记-整理“算法,局部看采用“复制”算法(两个Region之间),不会有内存碎片,不会因为大对象(full)找不到足够的连续空间而提前触发GC(触发全局GC - Full GC),这点优于CMS收集器;
      • 4)可预测的停顿:这是G1收集器相对于CMS收集器的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超N毫秒,这点优于CMS收集器。
  • 【问】详细介绍一下 CMS 垃圾回收器?(上一问)

  • 【问】新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?(新生代回收器:SerialParNewParallel Scavenge,采用标记-复制算法;老年代回收器:Serial OldParallel OldCMS,采用标记-清除算法)

  • 【问】简述分代垃圾回收器是怎么工作的?(新生代分3个区,老年代)
    Note

    • 分代回收器分为新生代和老年代,新生代大概占1/3老年代大概占2/3
    • 新生代包括EdenFrom SurvivorTo Survivor
      Eden区和两个survivor区的 的空间比例 为8:1:1
    • 垃圾回收器的执行流程:
      • 1)把 Eden + From Survivor 存活的对象放入 To Survivor
      • 2)清空 Eden + From Survivor 分区,From SurvivorTo Survivor 分区交换
      • 3)每熬过一次Minor GC对象年龄就加1的对象年龄+1,到达15,升级为老年代大对象会直接进入老年代
      • 4)老年代中当空间到达一定占比,会触发全局回收(Full GC),老年代一般采取标记-压缩算法
    • Minor GC触发的条件:
      • 1)Eden区域满
      • 2)新创建的对象大小大于Eden区所剩空间大小(如果Minor GC时,对象大小大于To Survivor可用内存,则会进入老年代;如果大于老年代剩余内存,则会Full GC
    • Full GC触发条件:
      • 1)老年代所剩空间不足
      • 2)方法区空间不足;
      • 3)调用System.gc()方法;
      • 4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存;
      • 5)由Eden区、From Survivor区向To Survivor区复制时,对象大小大于To Survivor可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
  • 【问】GC 是什么? 为什么要有 GC?(Gabage Collection垃圾收集,清除掉没用的对象,为新创建的对象腾出空间,看前几问解析)

  • 【问】简述 Java 垃圾回收机制(GC对象的判定方法,3大垃圾回收算法,4大垃圾回收器,看前几问解析)

  • 【问】如何判断一个对象是否存活?(GC 对象的判定方法:引用计数法,GC Roots可达分析法;看前几问解析)

  • 【问】Java 中会存在内存泄漏吗,请简单描述,可参考Java中的内存泄露问题
    Note

    • 内存泄漏是指:对象已经不被使用,但对象仍存在被引用状态,导致垃圾回收器无法将其回收。久而久之,不能被回收的内存越来越多,最终导致内存溢出OOM(OutOfMemoryError)
    • 1)静态类型的对象的引用会导致Java内存泄漏。
      预防方法:我们需要格外注意对关键词static的使用,对任何集合或者是庞大的对象进行static声明都会使其声明周期与JVM的生命周期同步,从而使其无法回收。
    • 2)第二种常见内存泄漏发生在对字符串的操作上,尤其是使用到String.intern()接口时。
      预防方法:
      • 我们一定要记住Interned String是被储存在永久代(Java7的版本)里的,如果我们想要对超大字符串进行操作,我们便需要增大原空间的内存大小
      • 第二种解决方法是使用Java8,永久代被原空间取代了,即使使用interned string也不会发生OOM内存泄漏的情况。
    • 3)忘记关闭流也是一种导致内存泄漏发生的常见情况。 Java7由于引入了try-with-resources以后部分解决了流未能关闭导致的内存泄漏。
    • 4)未关闭连接,例如数据库,FTP服务器等连接会导致内存泄漏。
    • 5)把没有hashCode()equals()的实例对象加到HashSet里面,由于可以把重复的对象添加到集合中,从而导致内存泄漏。
      @Test(expected = OutOfMemoryError.class)
      public void givenMap_whenNoEqualsNoHashCodeMethods_thenOutOfMemory()
        throws IOException, URISyntaxException {
          Map<Object, Object> map = System.getProperties();
          while (true) {
              map.put(new Key("key"), "value");  //Object的`hashCode()`默认用地址求`hash`,
             //导致每次new Key("key")是不一样的对象
          }
      }
      
  • 【问】System.gc() 和 Runtime.gc() 会做什么事情?
    Note

    • java.lang.System.gc()只是java.lang.Runtime.getRuntime().gc()的简写,两者的行为没有任何不同;
    • System.gc()开启回收器,主动通知虚拟机进行垃圾回收,但是回收器不一定会马上回收
  • 【问】串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?(单线程和多线程,Parallel Scavenge关注吞吐量优先;看前几问解析)

  • 【问】简述 Java 内存分配与回收策略以及 Minor GC 和 Major GC。(GC的执行流程,Minor GC 和 Major GC触发条件,见前几问解析)

  • 【问】VM 的永久代中会发生垃圾回收么?(Major GC=Full GC,见前几问解析)

  • 【问】Java 中垃圾收集的方法有哪些?(垃圾收集GC = 垃圾回收,包括对象已死算法;标记-清除,标记-复制,标记-压缩,见前几问解析)

  • 【问】finalize() 方法什么时候被调用?析构函数 (finalization) 的目的是什么?,参考finalize()方法和finalization
    Note

    • 析构函数:是一个对象被撤销时自动调用的,析构与构造函数相反,当对象所在的函数一调用完毕,系统自动执行析构函数,往往用来做"清理善后"的工作;
    • 每个对象的finalize()方法只能被执行一次,第二次就会直接跳过finalize()方法,目的是避免对象无限复活(调用了finalize()又有新的引用)。
    • finalize()执行的时间是不固定的,由GC决定,极端情况下,没有GC就不会执行finalize()方法。由于只能被执行一次,因此不建议使finalize(),交给GC即可。
  • 【问】如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?(不会立即,未达到触发GC的条件(G1垃圾回收器) / 或者说只有当用户线程运行到安全点(safe point)或者安全区域才会扫描对象引用关系(Stop The World, STW,比如serial,parNew,CMS))

  • 【问】JMM和JVM的区别,可参考Java 内存模型(JMM)
    Note

    • JMM是围绕原子性,有序性、可见性展开。JMM描述了线程内的工作内存主存之间的访问情况。
    • JMMJava内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈
  • 【问】说一下 JVM 调优的工具?(JConsole看内存,JProfiler看CPU资源,jmeter是java测试工具),参考面试官:如何进行 JVM 调优(附真实案例)jmeter 入门到精通

  • 【问】常用的 JVM 调优的参数都有哪些?,参考面试官:如何进行 JVM 调优(附真实案例)JVM调优总结 -Xms -Xmx -Xmn -Xss
    Note

    • 通常来说,我们的 JVM 参数配置大多还是会遵循 JVM 官方的建议,例如:
      -XX:NewRatio=2:年轻代:老年代=1:2
      -XX:SurvivorRatio=8:eden:survivor=8:1
      -Xmx3550m:设置JVM最大可用内存为3550M
      -Xms3550m:设置JVM最小内存为3550M
      -Xmn2g:设置年轻代大小为2G。
      -Xss128k:设置每个线程的堆栈大小
      堆内存设置为物理内存的3/4左右
      等等

    • 当然,更重要的是,大部分的应用 QPS 都不到10,数据量不到几万,这种低压环境下,想让 JVM 出问题,说实话也挺难的。大部分同学更常遇到的应该是自己的代码 bug 导致 OOMCPU load高、GC频繁啥的,这些场景也基本都是代码修复即可,通常不需要动 JVM。

    • JVM 有哪些核心指标?合理范围应该是多少?

      这个问题没有统一的答案,因为每个服务对AVG/TP999/TP9999等性能指标的要求是不同的,因此合理的范围也不同。

      为了防止面试官追问,对于普通的 Java 后端应用来说,我这边给出一份相对合理的范围值。以下指标都是对于单台服务器来说:

      • jvm.gc.time:每分钟的GC耗时在1s以内,500ms以内尤佳
      • jvm.gc.meantime:每次YGC耗时在100ms以内,50ms以内尤佳
      • jvm.fullgc.countFGC最多几小时1次,1天不到1次尤佳
      • jvm.fullgc.time:每次FGC耗时在1s以内,500ms以内尤佳

      通常来说,只要这几个指标正常,其他的一般不会有问题,如果其他地方出了问题,一般都会影响到这几个指标。

    • JVM 核心指标配置监控告警:CPU指标,内存指标,GC指标等

更多内容整理参考

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值