# JAVA ## 高级特性 ### JDK8特性 - Lamdba表达式:带有参数变量的表达式,是一段可以传递的代码,可以被一次或多次执行,主要用于内部匿名类和各种回调 - Stream流:添加了一个新的抽象方法,可以用来以一种声明的方式处理数据, - 其元素是特定类型的对象,形成一个队列,不会存储元素,而是按需计算 - 其数据源,可以是集合、数组、io流,生成器(generator)等 - 有类似sql语句一样的操作,filter,map等 - 中间的操作都会返回流对象本身,这样多个操作可以串联成一个管道,流式风格 - stream提供了内部迭代的方式,通过访问者模式(Visitor)实现 - Base64:java8中内置了Base64编码发编码器和解码器,使base64编码成为java类库的标准(base64是网络上最常见的用于传输8bit字节代码的编码方式之一,也用于在http环境下传递较长的标识信息) - 方法引用:通过方法的名字来指向一个方法,使用一对冒号来标识`::` - 新的时间api:最主要优化了时区的处理,避免时区产生的问题,也可以指定时区 ### Java作用域 - public:可以杯其他任何类访问 - private:私有,只能在类内部访问 - protected:只能被该类的子类、以及子类的子类访问 - package:包作用域,一个类允许访问同一个package的没有public、private修饰的类,以及没有以上三种修饰的字段和方法 ### Java面向对象的特征 - 封装:增强代码可维护性 - 隐藏了类的内部实现机制,可以在不影响外界的情况下改变类的内部结构,同时保护了数据 - 属性的封装:只能通过事先制定好的方法来访问数据,可以更方便的加入控制逻辑,限制对属性的不合理操作 - 方法的封装:使用者按照既定方法调用,不必关心方法的内部实现,便于使用和修改 - 继承:使得系统模型简练、清晰 - 从已有的类种派生出新的类,新的类具备已有类的数据属性和行为,并能进行扩展。本质上是一般-->特殊的关系,子类继承父类,表明子类是一种特殊的父类;从多种实现类中抽象出一个基类,使其具备共同特性。 - 子类使用extends关键字继承父类,父类中private定义的方法和变量不会被继承 - 多态 - 同一个行为具有多个不同的表现形式的能力。即同一个接口,使用不同的实例而执行不同的操作 - 三种实现的方式:①重写②接口③抽象类和抽象方法 ### 重写(Override)与重载(Overload) - 重写: - 是子类对父类运行访问的方法的实现,并进行重新编写,返回值和形参都不能改变。即**外壳不变核心重写** - 不能抛出新的检查异常或者比父类方法更宽泛的异常 - 访问权限不能比父类中被重写的方法访问权限更低 - 声明为static和final的不能被重写,但是static可以被再次声明 - 构造方法不能重写 - 当在子类中调用父类被重写的方法时,需要使用super关键字 - 重载: - 在一个类里面,方法名字相同,参数不同,每个重载的方法都必须有一个独一无二的参数类型列表,如:构造器重载。 - 重载的方法可以改变访问修饰符,可以声明新的或更广的检查异常 - 不能用返回值的类型来作为重载函数的区分标准 ### 接口(Interface)与抽象类(Abstract Class) 不同 - 抽象类中可以定义构造器,接口中不能定义 - 抽象类中可以定义抽象方法和具体方法,接口中全都是抽象方法 - 接口中只能使用public访问修饰符,抽象类无限制 - 抽象类可以定义成员变量,接口中的成员变量实际都是常量 - 抽象类可以包含静态方法,接口编写 - 有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法 - 一个类只能继承一个抽象类,但可以实现多个接口 相同 - 都不能够实例化 - 都能作为引用类型 - 一个类如果继承了某个抽象类或者实现了某个接口,都需要对其中的抽象方法全部进行实现,否则该类仍需要被声明为抽象类 ### 用final修饰一个类,不能被继承 ### 为什么java语言编译与解释并存 - 编译型:编译型语言会通过编辑器将源代码一次性翻译成可以倍平台执行的机器码,一般来说执行速度较快,开发效率较低 - 解释型:解释型语言会通过解释器一句一句的将代码解析为机器代码后再执行,开发效率快,执行速度较慢 java语言这两种特征都有,java程序需要先经过编译的步骤,生成.class字节码文件,这种字节码通过解释器来解释执行 ### 静态方法为什么不能调用 静态方法是属于类的,在类加载时会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,需要对象实例化后才存在,需要通过实例对象去访问 在类的非静态成员方法不存在的时候,静态成员就已经存在了,此时调用内存中还不存在的非静态成员,属于非法操作。 ### 泛型 在编译时检测非法的类型,本质是将一个数据类型指定为参数 java的泛型的伪泛型,在java的运行期间,所有的泛型信息都会被擦除 一般有三种使用方式 - 泛型类:在类后添加泛型,实例化此类时,必须指定具体的类型 - 泛型接口:在接口后添加泛型,继承接口时可以不指定类型,实现接口时必须指定 - 泛型方法:在方法上添加泛型,可以使用不指定类型的参数 常用通配符为:T,E,K,V - ? 表示不确定的jJava类型 - T(type)表示具体的一个Java类型 - K V(key value)表示Java键值对中的KeyValue - E(element)代表Element ### 自动装箱与拆箱 - 装箱:将基本类型用他们对应的引用类型包装起来 - 将包装类型转换为引用类型 - Integer i = 10 等价于 Integer i = Integer.valueOf(10) - int n = i 等价于 int n = i. intValue() ### 包装类型的常量池 Byte,Short,Integer,Long这四个包装类默认创建了-128,127的缓存数据, Character创建了数值在0,127范围的缓存数据, Boolean直接返回True和False Float和Double没有缓存常量池 **所有整型包装类对象之间值的比较,全部使用 equals 方法比较** ### Object类的方法 11种,其中wait重载了两种,方法名有8种 native -- java调非java的方法时添加的关键字 - public final native Class<?> **getClass()** - 返回当前运行对象的class对象,不能被重写 - public native int **hashCode()** - 用于返回对象的hashCode码 - public boolean **equals(Object obj)** - 比较两个对象是否相等 - protected native Object **clone()** throws CloneNotSupportedException - 用于创建并返回当前对象的一份拷贝。 - 一般情况下,对于任何对象 x,表达式 x.clone() != x 为true,x.clone().getClass() == x.getClass() 为true。 - Object本身没有实现Cloneable接口,所以不重写clone方法并且进行调用的话会发生CloneNotSupportedException异常。 - public String **toString()** - 将对象转为字符串形式,方便展示 - public final native void **notify()** - 唤醒一个在此对象监视器上等待的线程,如果有多个随机唤起一个 - public final native void **notifyAll()** - 唤醒所有在此对象监视器上等待的线程。 - public final native void **wait(long timeout)** throws InterruptedException - 暂停线程的执行 - sleep没有释放锁,wait释放了锁 - public final native void **wait(long timeout,int nanos)** throws InterruptedException - 添加了微毫秒参数 - public final void **wait() **throws InterruptedException - 一直等待,不会超时 - protected void **finalize()** throws Throwable - 实例被垃圾回收器回收时触发 ### 深拷贝和浅拷贝 - 浅拷贝:在堆上创建一个新的对象;如果是引用类型的话,会直接复制引用地址,公用一个内部对象 - 深拷贝:完全复制这个对象,包括内部对象 ### 异常 <img src="https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12/Java%E5%BC%82%E5%B8%B8%E7%B1%BB%E5%B1%82%E6%AC%A1%E7%BB%93%E6%9E%84%E5%9B%BE.png" alt="img" style="zoom: 67%;" /> - Exceptions:程序可以处理的异常,通过trycatch来捕获 - Check Exceptions:受检查的异常,必须捕获或抛出 - Uncheck Exceptions:可以不处理 - Errors:程序无法处理的错误,如Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 ,一般会直接终止线程 ***如果try和finally种都有return,那么finally中的return会覆盖try中的*** ### 序列化 - 序列化:将数据结构或对象转为二进制字节流的过程 - 反序列化:将序列化生成的字节流,转换回数据结构或对象 transient 可以避免对象被序列化 static对象不属于object,无法被序列化 ### 反射 #### 机制 反射机制是在**运行状态中**,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制 #### 作用 1. 在运行时判断任意一个对象所属的类 2. 在运行时构造任意一个类的对象 3. 在运行时判断一个类所具有的成员变量和方法 4. 在运行时调用任意一个对象的方法 #### Class类 class对象的由来是将class文件读入内存,并为之创建一个class对象,是反射机制的起源和入口 - 用于获取与类相关的各种信息,提供了获取类信息的相关方法 - Class类是继承自Object类 - Class类是所有类的共同图纸 - 每个类有自己的对象,同时每个类也可以看做是一个对象,有共同的图纸Class,存放类的结构信息,能够通过相应方法取出相应的信息:类的名字、属性、方法、构造方法、父类和接口 #### 获取反射入口的方式 - 通过Class.forName("全类名") - 通过类名.class - 对象.getClass() ## 集合 ### 常见集合类 - Collection 类 - List - ArrayList:Object [ ] 数组 - Vector:Object [ ] 数组,线程安全,但不推荐 - LinkedList: 双向链表 - Set - HashSet(无序,唯一):基于HashMap实现,底层用HashMap保存元素 - LinkedHashSet:是HashSet的子类,内部通过LinkedHashMap实现 - TreeSet(无序,唯一):黑白数 - Queue - PriorityQueue:数组实现二叉堆 - ArrayQueue:数组+双指针 - Map类 - <a href="#preface">HashMap</a> 点击跳转 - LinkedHashMap : 继承自HashMap:底层依旧是基于拉链式散列结构,在此基础上增加了一条双向链表 - HashTable:数组+链表结构,数组是HashTable的主体,链表是解决Hash冲突用,线程安全但效率不高 - TreeMap:自平衡二叉树 ### ArrayList和LinkedList的区别 #### ArrayList 是基于索引的数据接口,底层是数组,适合查,可以以O(1)时间复杂度对元素进行随机访问,增删改时最差需要O(n)的时间复杂度,而且需要提前预留空间 #### linkedList 是基于链表存储的,适合增删改,自由性高,可以动态变化,而查找某个元素的时间复杂度是O(n),增删改仅为O(1),但是存储时需要更多的内存,在存储实际数据时,还需要存储两个指针 ### Array.sort是什么排序 多种排序组合 <img src="https://img2018.cnblogs.com/i-beta/1701765/201911/1701765-20191126153931611-191817306.png" alt="img" style="zoom: 67%;" /> ### <a id="preface">HashMap原理是什么</a> 根据键的hashCode值存储数据,底层是数组+链表实现,数组的每个元素都是链表,大多数情况下可以直接定位到值,时间复杂度O(1),但遍历顺序不确定,且只能有一个键位null;hashMap的线程是不安全的,如果需要满足,可以用Collections的synchronizedMap,或使用ConcurrentHashMap方法获得线程安全的HashMap,默认大小16,0.75的阈值,增长方式是2的指数倍 #### jdk1.7的HashMap HashMap里面每一个元素是一个单向链表,每个链表节点存放了Entry的实例,包含四个属性:key,value,hash值和单向链表的next ##### jdk1.7的HashMap为什么线程不安全 在多线程下进行扩容,调用了transfer函数,在某个线程执行过程中被挂起,而其他线程已经完成了数据迁移。等CPU资源释放后,被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环,导致数据丢失,已在1.8版本修复 #### jdk1.8的HashMap 1.8采用了链表+红黑树的组成,当链表的元素超过了8个以后,会将链表转换为红黑树,可以将链表的查询时间复杂度将为O(logN) ##### jdk1.8的HashMap为什么线程不安全 多线程对HashMap进行put操作时,两个线程计算的下标相同,当A线程时间片耗尽被挂起后,B线程成功插入了元素,由于之前在该下标处插入了元素,也进行过hash碰撞的判断,所以会直接插入,这样就会丢失一个数据 ### Queue和Deque - Queue是单端队列,只能从一端插入元素,另一端删除,遵循先进先出的原则 - Deque扩展了Queue,是双端队列,可以在两端同时插入删除,可用于模拟栈 ### ConcurrentHashMap 和 Hashtable 的区别 - HashTable:使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低 - ConcurrentHashMap : - 1.7版本时,采用ReentrantLock+Segment(分段锁)+HashEntry对整个桶数据进行了分段,一把锁只锁住一部分数据,多线程访问不同分段的数据时,不会存在锁竞争 - 1.8版本,实现了原子操作,采用synchronized+CAS+HashEntry+红黑树,只锁定当前链表或红黑树的首节点,只要不发生hash冲突,就不会有并发问题 - 两个同样不支持key为null ### == 和equals - == 的作用: - 基本类型:比较值是否相同 - 引用类型:比较地址值是否相同 - equals 的作用: 引用类型:默认情况下,比较地址值是否相同,如果重写,按重写的来。 ### 为什么要重写hashCode和equals 在object中的equals比较是内存地址,hashCode()由native修饰,返回字符串的哈希码 加入两个key类的id都是1,将key1及value存在hashmap中,因为两个key的id相同,所以希望key2能获取key1的value 如果没有重写hashcode,那么会调用object的hashcode,从而获取真是的内存哈希,如果重写了,可以获取key类的id的hash值,因为key1和2的id都是1,所以hashcode相同; 但是key2还是获取不到,因为equals没有重写,那么默认比较的是两个key的内存地址,重写后,比较key具体的值。相同 而equals方法必须要满足以下几个特性   1.自反性:x.equals(x) == true,自己和自己比较相等   2.对称性:x.equals(y) == y.equals(x),两个对象调用equals的的结果应该一样   3.传递性:如果x.equals(y) == true y.equals(z) == true 则 x.equals(z) == true,x和y相等,y和z相等,则x和z相等   4.一致性 : 如果x对象和y对象有成员变量num1和num2,其中重写的equals方法只有num1参加了运算,则修改num2不影响x.equals(y)的值   5.对于任何非空引用x,x.equals(null)应该返回false。 ## 字符串 ### String类型是什么 string类型本质是一个用final 修饰的char数组,但是普通的数组是可以变的,final修饰的是地址不可变,不限制地址内数组的元素 ~~~ java final char[] value={'a','b','c'}; value[2]='z'; // 这时value={'a','b','z'} ~~~ 想要维持不变,在后面调用String的方法里没有动Array里的元素,不暴露内部字段。而其Array 设为private完全私有访问,避免被继承后破坏 ### 为什么String不可变 - 什么是不可变类 - 一个类的对象再通过构造方法创建后,如果状态不会被改变,那就是一个不可变类(immutable),所有成员变量的赋值仅在构造方法中完成,不会提供任何set方法供外部修改 - 八个基本类型的包装类和String类都属于不可变类 - 把String设置为不可变有什么好处 1. 常量池的需要:当创建一个String时,如果已存在相同的,就不会再创建,而是直接引用,减少JVM内存开销 2. hashCode:因为字符串不可变,所有创建的时候,hashCode就被缓存了,因此非常适合做哈希值,不需要频繁调方法来获取 3. 线程安全,因为String不可变,可以在多线程之间共享,不需要同步处理 ## IO模型 ### 简介 从计算机的角度看,I/O描述了计算机系统与外包设备之间通信的过程 从应用程序看,程序在用户空间向系统内核发起io调用,操作系统负责内核执行具体的操作。所以发起io调用后,会经历两个步骤:①内核等待io设备准备好数据,②内核将数据从内核空间拷贝到用户空间 ### Linux的IO ![图片](https://mmbiz.qpic.cn/mmbiz_png/mQlO20PgUDLJyNAPpmHXFWjrXZ2uXvSeqeQfjNIVKzKA4lJUFPDKUic0FiayuEXticzTtnFPN74Y7poNjZbV0DygQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1) - 阻塞式io:进程或线程等待某个条件直到满足后,才进行下一步操作 - 非阻塞式io:进程与内核交互,未达到目的之前,不再一味等待,而是直接返回,然后通过轮询的方式,不停检查内核数据是否准备好,某一次发现准备好后,再将数据拷贝到用户空间 - 信号驱动io:进程预先向内核注册一个信号处理函数,然后进程返回,当内核数据就绪时发送信号给进程,进程接收到信号后进行拷贝 - io复用:增加select函数,多个进程的io可以注册到同一个select上,当用户进程调用该select时,会监听所有注册好的io,如果所有的io需要的数据都没好,select调用进程会发生阻塞,当任意一个io准备好后,select会返回,然后进行再通过recvfrom来拷贝 - 异步io模型:当进程发送异步io请求后,给内核传递一系列的参数,告诉内核处理完操作后,如何通知进程。当内核收到请求后,会立即返回,然后开始等待数据准备,准备好后直接拷贝到指定位置,在通知进程io完成 ### Java中的io - BIO:同步阻塞io模型,同上 - NIO(Non-blocking):多路复用io,支持面向缓存,基于通道的io操作 - 使用了选择器(selector)也可以称作多路复用器,只需一个线程便可以管理多个客户端连接,当数据到了后,才会为其服务 - AIO(Asynchronous):异步io,java7引入,基于事件和回调机制实现的,后台处理完成,操作系统通知相应的线程进行后续操作 ## 线程池 ### 七大参数 - corePoolSize:线程池中**常驻核心线程数**。近似理解为值班的线程数量,当任务数达到设定的数值后,会把到达的任务放到缓存队列中,没任务时也不会销毁 - maximumPoolSize:线程池能够容纳同时执行的最大线程数,核心线程数没有空闲时,允许临时扩容的线程数量 - keepAliveTime:临时扩容的空闲线程存活时间 - unit:空闲线程存活时间的单位 - workQueue:工作队列,新任务被提交后,先进入到工作队列中,任务调度时再从队列中取出任务,jdk中提供了四种工作队列 - ArrayBlockingQueue:基于数组的有界阻塞队列,新任务进来后,会放到队尾,有界数组可以防止资源耗尽的问题,如果队列已满,创建一个新的,如果线程数达到最大,会执行拒绝策略 - LinkedBlockingQueue:基于链表的无界阻塞队列,先入先出。由于队列近似无界,当线程池中线程数达到最大值后直接抛出异常,使用该队列时,maxPoolSize不起作用 - SynchronousQueue:不缓存任务的阻塞队列,生产者放入一个任务后必须等待消费者取出这个任务,也就是说任务进来直接调度执行该任务,如果没有可用线程,创建新线程,达到maxPoolSize后,执行拒绝策略 - PriorityBlockingQueue:具有优先级的无界阻塞队列,优先级通过参数Comparator实现 - threadFactory:线程工厂,创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon线程等 - handler:拒绝策略,当队列满了后执行的策略 - AboryPolicy(默认):直接抛出异常阻止系统正常运行 - CallerRunsPolicy:该策略不会抛弃任务和异常,而是将某些任务回退到调用者,从而降低新任务的流量 - DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务 - DiscardPolicy:直接丢弃任务,不处理也不抛出异常 ### 工作原理 ![img](https://img-blog.csdnimg.cn/2018122411100636) ## Java多线程 ### 初识线程 > 线程开启不一定执行,由cpu调度执行 > > 多个线程同时使用一个资源的情况下,线程不安全,出现并发问题 > > 采用了静态代理的设计模式 - 创建线程方式1:继承Thread类后,重写run方法,完成多线程内容的编写,调用start方法,开启线程 - 创建线程方式2:实现runnable接口,重写run方法,执行线程需要丢入runnable接口实现类,调用start方法(推荐使用,避免单继承局限性,方便灵活,同一个对象可以被多个线程使用) - 创建线程方式3:实现callable接口,启动分四步(优点,有返回值 ) - 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(3); //创建线程池,这里设最大为3 - 提交执行:Future<Boolean> r1 = ser.submit([这里填入callbale接口的实现类]); - 获取结果:因为callable有返回值,所以需要获取:Boolean res = r1.get(); - 关闭服务:ser.shutdownNow(); - 方式4:通过线程池的方式创建线程,基于实现Runnable或Callable接口方法创建。 ### 线程状态 Thread.start线程状态 new(线程初始化)=>start(线程就绪状态)=>run(线程运行状态)=>sleep,wait(线程阻塞状态)=>死亡 1. new(尚未启动的线程)=> 2. runnable(虚拟机中执行的线程)=> 3. blocked(被阻塞等待监视器锁定的线程)=> 4. waiting(正在等待另一个线程执行特定动作的线程)=> 5. timed_waiting(正在等待另一个线程执行动作达到指定的时间)=> 6. terminated(已退出的线程) #### 常见方法 - setPriority(int newPriority) 更改线程优先级 getPriority 获得线程优先级 - 优先级1-10,优先级只是增加权重,优先级高不一定先执行 - sleep(long millis) 在指定的毫秒数让当前正在执行的线程休眠 - 模拟网络延时,放大问题的发生性 - 存在异常interruptedException中断异常 - 时间达到后线程进入就绪状态 - 每个对象都有一把锁,sleep不会释放锁 - join() 插队,等待该线程执行完后,再执行其他线程,其他线程阻塞 - yield() 暂停当前正在执行的对象,并执行其他线程 - 线程礼让,让当前正在执行的线程暂停,但不阻塞 - 将线程从运行状态转为就绪状态 - ***让cpu重新调度,礼让不一定成功*** - interrupt() 中断线程,不建议使用,建议使用标志位让线程自己停 - boolean isAlive() 测试线程是否处于活动状态 - setDaemon() 设置为守护线程 - 线程分用户线程和守护线程 - 虚拟机必须确保用户线程执行完毕,但不用管守护线程 - 如,后台记录操作日志,垃圾回收,内存监控等 ### synchronized #### 特性 - 原子性:在执行操作之前必须先获得类或对象的锁,直到执行完才释放 - 可见性:synchonized加的锁的状态对任何线程都可见,并且释放锁值钱会将变量刷新到主存中,保证可见性 - 有序性:指令重排不影响单线程的顺序,影响多线程并发执行的顺序,而synchonized保证了每个时刻都只有一个线程访问同步代码块,保证了有序性 - 可重入性:和ReentranLock都是可重入锁,当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗来讲就是一个线程拥有了锁仍然可以重复申请锁 #### 锁的实现 基于java对象头和Monitor,对方法上锁和构造同步代码块这两种方法,底层实现原理都一样,进入同步代码之前先获取锁,获取到锁后锁的计数器+1,同步代码执行完锁计数器-1,如果获取失败就阻塞式等待锁的释放。 区别在同步块识别方式上,同步代码块通过monitorenter和monitorexit指令操作,同步方法通过flags标志 - 同步代码块: - 线程访问时通过monitorenter命令尝试获取monitor(监视器)的所有权 - 如果monitor的进入数为1,该线程进入monitro,然后进入数设置为1,该线程为monitor所有者 - 如果线程已经占有该monitor,只是重新进入,进入的monitor数加1 - 如果其他线程占用了monitor,则当前线程进入阻塞状态,知道monitor进入数为0,在重新获取所有权 - 通过执行monitorexit命令解锁,必须是是monitor的所有者,该monitor的进入数-1,如果-1后为0,不再是monitor的所有者,其他线程可以尝试获取这个所有权,如果线程异常也会释放 - wait和notify方法依赖于monitor对象,只有在同步块中才能这些方法 - 同步方法 - 通过常量池中添加acc_synchronized来标识,如果被设置了,先获取monitor对象,之后与同步代码块相似 两种同步方式本质上没有区别,jvm通过操作系统互斥质量来实现,对性能影响较大 #### 1.6后对其的优化 - 自旋锁与自适应自旋锁:在共享数据锁定状态持续时间较短的情况下,切换线程不值得,从而让线程等待锁的释放,不让出cpu。自适应的等待时间不固定,由前一次在同一个锁上的时间jued - 锁消除:对运行的上下文进行扫描,除去不可能存在竞争的锁,节省无意义的时间,如对私有对象加锁 - 锁粗化:扩大锁的范围,避免重复加锁解锁 - 偏向锁:大多数情况下,锁不存在多线程竞争,总是由一个线程多次获得,如果线程获取了锁,那么进入偏向锁模式,当线程再次请求锁时,只需要检查当前线程是不是之前的锁,省去了大量有关锁申请的操作,但不适用竞争激烈的多线程场景 - 轻量级锁:由偏向锁升级而来,运行在一个线程进入同步块时,当第二个线程加入锁竞争时,偏向锁会升级为轻量锁 ### volatile的原理和作用 #### 简介 volatile用来声明变量的值可能随时会被别的线程修改。volatile禁止指令重排,采用添加内存屏障的方法,使用其修饰的变量会强制将修改的值立即写入主存中,而其他线程私有内存中的缓存会失效。 如果是非基础数据类型建议用JUC包中的Atomic,因为其只会刷新引用地址,而非对象本身 <img src="https://img-blog.csdn.net/20180804162254196?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM0ODcxNjI2/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70" alt="img" style="zoom:67%;" /> #### 特性 具有可见性,有序性,不具备原子性 - 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如booleanflag;或者作为触发器,实现轻量级同步 - 属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的 - 提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主存中读取 #### 与synchronized的区别 - volatile只能修饰实例变量和类变量,而synchronized可以修饰方法和代码块 - volatile无法保证原子性,synchronized是排他机制,可以保证 - volatile可以看作轻量版synchronized,如果只赋值的话,赋值本身是原子性,就可以保证线程安全 ### juc并发编程 jds提供的concurrent包,里面为并发方法 #### Lock锁 - 通过显式定义同步锁对象来实现同步,同步锁使用lock对象充当 - 需要手动开关,当比synchronized性能略好 - reentrantLock 可重入锁实现了lock ### 死锁 多个线程各自占用一些资源,并且互相等待其他线程占有的资源才能运行,从而导致两个或两个以上线程都在等待对方释放资源,而停止执行的现象,叫***死锁***,一个同步块同时拥有**两个以上对象的锁**时,就有可能发生 ### 线程通信 ***只能在同步代码块中使用*** - wait() 表示线程会一直等待,直到得到其他线程通知,与sleep不同,他会释放锁 - wait(long time) 指定等待的好秒数 - notify() 唤醒一个处于等待状态的线程 - notifyAll() 唤醒同一个对象上所有调用wait方法的线程,优先级别高的有限调度 ### 高并发中的集合有哪些问题 #### ArrayList - 数组越界异常:因为arraylist添加元素是分两部,当两个线程同时获取到长度,发现不用扩容就能完成添加,但是只有一个参数的大小,两个同时添加就会抛出异常ArrayIndexOutOfBoundsException, - 元素值覆盖和为空:同时往下标为[0]的地址赋值,造成了覆盖,理想状态是size=2,下标0值为a,下标1值为b,结果为下标0结果为b,下标1结果为空 - 在遍历中修改集合中的元素:ConcurrentModificationException:当方法检测到对象的并发修改,但不允许这种修改时,抛出此异常,因为迭代器是依赖于集合存在的,判断成功后,集合中添加了元素,迭代器缺不知道,造成并发修改异常,可以用copyonwriteArraylist解决 #### HashMap 见集合-》hashmap原理 上文ConcurrentModificationException也存在 ### 12306秒杀问题 ![img](https://pic1.zhimg.com/80/v2-f346dde10f4b1f300d28d4e7b3e9e6a0_720w.jpg) ## JVM #### JVM是什么 是java虚拟机,是java运行环境的一部分,屏蔽了与代码与具体操作系统平台的相关信息,使得Java程序只需在虚拟机上运行目标代码,就可以在多种平台不加修改的运行,简答来说是用来解析和运行java程序的 #### Java运行时一个类是什么时候被加载的 sun/orcale公司的HotSpot虚拟机 按需加载 -XX:+TraceClassLoading 监控类加载过程 #### Jvm一个类的加载过程 ①加载,(②验证,③准备,④解析)[统称连接],⑤初始化,⑥使用,⑦卸载 ①类的二进制字节流读进来存放在元空间(逻辑概念对应方法区),在内存中生成一个代表这个类的java.lang.Class对象放入堆中,此阶段可以干预,可以自定义类加载器实现类的加载 ②验证Class文件字节流中信息是否符合约束,保证虚拟机的安全 ③准备,类变量赋默认值,int为0,long为0L,boolean为false,引用类型为null,常量符正式值; ④解析,把符号引用翻译为直接引用 ⑤初始化,当我们new一个类的对象时;访问、修改类的静态属性;调用一个类的静态方法;用反射对一个类进行调用;初始化当前类,其父也会初始化。 ⑥使用就是使用 ⑦卸载,该类所有实例都被GC,也就是jvm中不存在该class的任何实例;加载该类的ClassLoader已被GC;该类的java.lang.Class对象没有任何引用,也没有通过反射来获取时;才能卸载,基本不卸载类 #### 一个类被初始化的过程 静态常量==准备 静态变量==准备阶段赋值为null,初始化阶段赋值为静态变量 静态初始化块==初始化阶段执行 ---此处分割线,父类继承时到此轮到子类执行---- 变量==创建对象的时候赋值 初始化块==创建对象的时候执行 构造器==创建对象的时候执行 #### 继承时父子类的初始化顺序 看上 ### 类加载器 #### 什么是类加载器 在类的加载阶段,通过一个类的全限定名(带包名的全程)来获取该类的二进制字节流的这个动作的代码,被称为**类加载器**(class loader),这个动作是可以自定义实现的 #### jvm有哪些类加载器 站在java虚拟机的角度来看,只有两种类加载器 1,启动类加载器,c++实现,是虚拟机的一部分 2,其他所有的类加载器,由java语言实现,独立存在于虚拟机外部,全部继承抽象类ClassLoader java一直保持三种类加载器结构 启动类加载器(bootstrap classloader)扩展类加载器(Extension ClassLoader)应用程序类加载器(Application ClassLoader) #### jvm中不同的类加载器加载那些文件 - 启动类加载器:根的类加载器,c++实现 加载<JAVA_HOME>\jre\lib\rt.jar、resources.jar、charsets.jar,被-Xbootclasspath参数所指定的路径中存放的类库 - 扩展类加载器 加载<JAVA_HOME>\jre\lib\ext,或被java.ext.dirs系统变量所指定的路径中所有的类库 - 应用程序类加载器:系统的类加载器 自己写的代码和第三方依赖、 #### jvm三层类加载器之间的关系是继承吗 不是继承,都继承自ClassLoader,组合关系 ### 双亲委派 #### jvm双亲委派模型 三层类加载器加上自定义类加载器四层,**自底向上检查是否加载成功,自顶向下尝试加载**,最上层检查是否可以加载,不行的话往下推一层,下层重新此步骤。都找不到,报类加载失败异常 #### 为什么要设计双亲委派模型 - 确保安全,避免java核心类库被修改 - 避免重复加载 - 保证类的唯一性 #### 可以打破双亲委派模型吗,如何打破 可以,想要打破模型,自定义类加载器,重写其中的loadClass方法 #### 如何自定义自己的类加载器 - 继承ClassLoader - 覆盖findClass(String name)方法 或者loadClass()方法(可以打破双拼委派); 大致思路,先获取类的全限定名,找到这个类,获取二进制流文件,调用defineClass拿到class,这个类就加载完成 #### ClassLoader中的loadClass、findClass、defineClass的区别 loadClass主要进行类加载的方法,默认的双亲委派机制就在此实现 findClass根据名称或位置,加载.class文件 defineClass把字节码转化为class #### 加载一个类用Class.forName和ClassLoader有什么区别 forname会把类初始化,ClassLoader不会,访问一下才会实例化即按需加载 #### tomcat的类加载机制 - 三层类加载器 - common类加载器 - share类加载器(tomcat6后没有了) - webapp类加载器(每个web服务一个类加载器,打破双亲委派) - jsp类加载器(每个jsp文件一个类加载器,打破双亲委派) - catalina类加载器(server类加载器)(tomcat6后没有了) 在原来的三层类加载器上新增了3个基础类加载器和web应用类加载器+jsp类加载器 3个基础类加载器在conf/catalina.properties中进行配置,tomcat自定义了webapp类加载器,打破双亲委派,会首先自己尝试加载,找不到再交给父加载器去加载。 #### 为什么tomcat要破坏双亲委派 tomcat是web容器,一个web容器可能需要部署多个应用 - 部署在同一个容器上的两个web所使用的java类库需要相互隔离 - 部署在同一个容器上的两个web所使用的通用类库需要相互共享 - 保证tomcat服务器自身安全不受web程序影响 - 需要支持jsp的页面可以热加载和热部署 ### 运行机制 #### 热加载和热部署是什么,如何自己实现热加载 热加载:在不重启服务的时候让代码生效,基于类加载器实现的,但是不安全。 热部署:在不重启服务的时候重新部署项目,如tomcat热部署就是在程序运行时修改了war包,就会删除之前的,重新解压新的war包,生成新文件夹 - 热加载就是在运行时重新加载class,后台会启动一个线程不断检测你的class是否发生改变 - 热部署是在运行时重新部署整个项目,耗时相对较高 如何实现:实现自己的类加载器,从自己的类加载器中加载要热加载的类,然后不断轮询要热加载的class文件是否更新(可以用map缓存一下之前有没有加载过,用定时任务轮询) #### java代码是如何运行起来的 1. java源文件 2. 通过javac命令获取.class文件 3. 通过java命令运行起来,即加载到jvm里面,jvm是一个软件帮我们屏蔽了底层操作系统层面的细节 #### 画一画jvm运行原理图 ![img](https://upload-images.jianshu.io/upload_images/4469129-fa3f54aecf6ef8a2.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) #### jvm的内存结构划分 线程私有区: * 虚拟机栈:存储方法,局部变量,运行时产生的数据,主要存的**引用** * 本地方法栈:存储native方法 * 程序计数器:存储字节码行号 线程共有区: * 堆:存储所有创建的**对象**,数组 * 方法区:存储虚拟机加载的字节码数据,静态变量、常量、运行时常量池(hotSpot中元空间用来实现方法区) #### jvm运行时数据区,程序计数器的特点及作用 - 程序计数器是一块较小的内存空间,几乎可以忽略 - 是当前线程所执行的字节码的行号指示器 - 多线程时,每个线程都有一个独立的指示器,各条线程互不影响 - 该区域线程私有,每个线程独立村粗 - 无GC回收,不存在内存溢出,线程销毁即销毁 #### jvm运行时数据区虚拟机栈的特点及作用 - 线程私有 - 方法执行会创建栈帧,存储局部变量表等信息 - 方法执行入,执行完成出 - 栈深度过大会报异常 - 栈里面运行方法,存放方法的局部变量名,变量名所指向的值(常量值,对象值等),都存在堆上 - 栈一般不设置大小,默认为1M - 随线程生灭,不会gc回收 ### 垃圾回收 #### jvm运行时数据区 java堆的特点及作用 - 线程共享的一块区域,虚拟机启动时创建 - 是虚拟机所管理的内存中最大的一块区域 - 存放所有实例对象或数组 - GC垃圾收集器的主要管理区域,分新生代和老年代 - 可调整堆大小 - 无法再扩展时会报堆溢出 - 从分配内存的角度看,所有线程共享的java堆中可以划分出多个线程私有的分配缓冲区TLAB,以提升分配时的效率 #### jvm什么情况下会发生堆内存溢出 java堆中用于存储对象,只要不断的创建对象,并且保持GC Roots到对象之间有可达路径来避免垃圾回收机制清理这些对象,那么随着对象数量的增加,总容量达到最大堆内存的容量限制后就会产生内存溢出 首先添加jvm参数,可以把溢出的内存用快照保存成hprof文件在指定位置,再用 MAT工具来分析内存溢出问题 #### jvm如何判断对象可以被回收 java通过基于**可达性分析算法**来判断是否存活,该算法的基本思路为:通过一系列成为“GC roots”的根节点作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC roots之间没有任何引用链,则证明此对象是不能在被使用的,就可以回收 > 哪些对象可以作为GC roots呢 1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法栈中的参数局部变量等所引用的对象 2. 方法区/元空间中类静态属性引用的对象,或常量引用的对象 3. 在本地方法栈中jni引用的对象 4. 再java虚拟机内部引用,如基本数据类型对应的Class对象,一写常驻的异常对象,等,和系统类加载器 #### 谈一谈java中不同的引用类型 java里有不同的引用类型,分别是 强引用:Object object = new Object(); 软引用:SoftReference 内存充足时不回收,不足时回收 弱引用:WeakReference 不管内存是否充足,只要GC运行就回收 虚引用(几乎不用):PhantomReference 形同虚设,像没有引用一样,该引用被GC时会触发一个通知,或触发进一步的操作 #### JVM堆内存分代模型 - 新生代(1/3)Minor GC回收 - - Eden(80%) - FromSurvivor(10%) - ToSurvivor(10%) - 老年代(2/3)Full GC 回收 #### JVM中垃圾回收的过程 jvm中垃圾回收针对的是新生代,老年代和元空间(永久代) 短期存活的,分配在java堆内存之后,迅速使用完就会被回收 长期存活的,通过在新生代S0和S1区来回被回收15次后,进入内存的老年代,称之为对象的年龄为15岁 #### jvm动态年龄判断是什么 在s区的所有年龄的对象占比总和大于50时,比50%大的所有年龄都会移入老年代 #### jvm老年代空间担保机制 - 在新生代准备MinorGC时,会检查老年代可用空间是否大于新生代对象总和, - 大于正常执行, - 小于,则检查老年代可用空间的大小是否大于之前MinorGC对象的平均大小, - 大于 正常执行minorGC - 执行后发现S区放不下,老年代也放不下,也执行FullGC - 小于执行fullGC #### 什么情况下对象会进入老年代 1. 躲过15次GC后进入老年代,可通过参数调整,而CMS回收默认是6次 2. 通过动态对象年龄判断 3. 通过老年代空间担保机制 4. 大对象之间进入老年代 #### 为什么要分新生代和老年代 因为两个区域的特点不同,所以需要采用不同的回收算法 MinorGC/YoungGC:新生代收集 MajorGC/OldGC:老年代收集 FullGC:整堆收集,收集整个java堆和元空间的垃圾收集 MixedGC:混合收集,收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为 #### 介绍下JVM中的垃圾回收算法 - 标记清除法 - 最基础的算法,分为标记和清除两个阶段 - 标记阶段:标记需要回收的对象,在标记完成后统一清除,也可以标记存活的,标记对象的过程就是判断是否属于垃圾的过程,基于可达性分析算法判断 - 清除阶段:堆标记的对象进行回收 - 优点:实现简单,基于最基础的可达性分析算法 - 缺点: - 执行效率不稳定:如果清除的对象数量太多,性能消耗就越多。 - 内存空间碎片化问题严重,如果需要分配较大对象时会不得不触发另一次垃圾回收 - 标记复制法 - 将可用内存按容量分为两块,每次只用一块,这块用完了,将存活的复制到另一块内存,再一次性清除上一块内存 - 优点:实现简单,效率高,解决了标记清除算法导致的碎片问题 - 缺点: - 代价过大,将可用内存缩小了一般,空间浪费大 - 对象存活率较高时就要进行较多的复制,效率降低。 - jvm新生代采用该算法,并进行了改进,改为了8:1:1,使用e区和一块s区,空闲另一块,单次空闲10%,如果单次需要存活超过了10%,就会触发空间担保机制 - 标记整理法 - 是根据老年代特点而产生的 - 标记过程于标记清理算法一致 - 整理过程不是针对可回收对象进行清理,而是根据存活对象进行整理,让存活对象都向一端移动,然后清理边界以外的内存对象 - 移动存活对象并更新所有引用这些对象的引用,是一种比较耗时的操作,这种对象移动操作必须全程暂停程序, - 优点:提高空间利用率,不会产生不连续的内存碎片 - 缺点:效率问题 - 分代收集法 - 是一种思想,即新生代老年代 ### 分割 #### jvm运行时数据区元空间的特点和作用 1. 在jdk8开始才出现元空间的概念,之前叫永久代 2. 元空间和java堆类似,是线程共享的内存区域 3. 存储被加载的类信息,常量,静态变量,常量池,即时编译后的代码数据 4. 元空间采用本地内存,有多少剩余就能扩展多大,也可以设置元空间大小 5. 元空间很少有GC回收,该区域条件苛刻,能回收的很少所以很少回收 6. 元空间内存不足时会排除内存异常错误 #### jvm内存相关的核心参数 -Xms Java堆内存的大小 -Xms java堆内存的最大大小 -Xmn java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小 -Xss 每个线程的栈内存大小 #### 什么是内存泄漏,什么是溢出 内存溢出:OutOfMemory,程序再申请内存时没有足够的内存供其使用, 内存泄漏:MemoryLeak,程序运行后,没有释放所占用的内存问题,一次没事,堆积到一定程度就会产生内存溢出如:单例对象持有外部引用,或资源未关闭 #### 线上jvm设置多大 线上1核2G 栈1m 堆,一半1G,元空间一般128M #### 堆溢出后其他程序也可以运行