最新消息: USBMI致力于为网友们分享Windows、安卓、IOS等主流手机系统相关的资讯以及评测、同时提供相关教程、应用、软件下载等服务。

【秋招之Java基础】

IT圈 admin 2浏览 0评论

【秋招之Java基础】

秋招之Java基础

` 提示:2023秋招八股之Java基础部门。


提示:这就开始了...

文章目录

  • 秋招之Java基础
  • 前言
  • 一、Java基础
    • 1.Java语言的跨平台性
    • 2.Java中的基本数据类型
    • 3.static关键字
    • 4.Obeject类
    • 5.面向对象与面向过程
    • 6.编译型语言与解释型语言
    • 7.Java三大特性
      • 多态的表现?
    • 8.Enumeration和Iterator的区别
    • 9.包装类
    • 10.访问修饰符
    • 11.代码编译过程?
    • 12.Java中类的生命周期
    • 13.类加载
      • 类加载过程
      • 类初始化时机
      • 类实例化过程
      • 双亲委托机制
        • 双亲委托机制好处
    • 14.final关键字
    • 15.抽象类和接口
      • 抽象类
      • 接口
      • 抽象类与接口的区别
    • 16.静态编译与动态编译
    • 17.动态加载
    • 18.深拷贝和浅拷贝
    • 19.Java中IO方式
      • IO流
      • 同步/异步,阻塞非阻塞(关于BIO/NIO/AIO)
    • 20.Java中的设计模式
  • 二、Java重点
    • 1.Java/Linux中进程通信方式
    • 2.Java中线程同步的方式
    • 3.怎么创建进程
    • 4.Java中的集合
      • 所有容器
      • HashMap
        • 什么是hash表?
        • 什么是hash?什么是hashcode?
        • 为什么使用hashcode?
        • Equals和hashcode的关系?
        • ==与equals
        • 为什么重写equals方法必须重写hashcode方法
        • 为什么先进行hashcode的对比?
        • HashMap的容量?加载因子值?设置这个值的原因?
        • HashMap扩容为原来的2倍原因?
        • HashMap具体的扩容计算?
        • HashMap的懒加载
        • 链表什么时候会变为红黑树?
        • 为什么要使用红黑树?
        • 为什么要判断等大于8之后才变成红黑树?
        • hash冲突后链表使用的头插法or尾插?
        • Object的hashcode()方法中为什么要使用31?
        • 位移、加减运算的效率比乘除运算的效率更高?
        • hashmap底层的hash值计算方法?
        • 为什么要无符号右移16位?
        • 解决hash冲突的方法
        • hashmap不同版本都会出现什么线程安全问题?
        • hashmap两个版本的区别?
        • ArrayList?
        • HashMap,ArrayMap,SparseArray的区别?
        • HashMap,LinkedHhashMap,HashTable,ConcurrentHashmap的区别?
    • 5.线程安全问题
      • 多线程如何实现线程安全?
      • 并发编程的三个概念?线程安全设计的几个概念?
      • volatile
      • synchronized
      • lock锁
      • synchronized与volatile的区别?
      • synchronized 和Reentrantlock(lock)锁的区别?
      • 类锁与对象锁?
      • 乐观锁与悲观锁?
      • CAS锁
      • CAS的缺陷
    • 6.Java中的内部类
    • 7.泛型
      • 泛型
      • 泛型擦除及原因
      • 限定通配符与非限定通配符
      • <? extends T>和<? super T>读写方面的不同
      • 编译时类型与运行时类型
    • 8.反射
      • 反射
      • 获取class对象
      • 反射常用的api
    • 9.动态代理
      • 静态代理
      • 什么是动态代理
      • Java中动态代理的实现方式
        • 两种动态代理方法的区别:
        • 为什么proxy.newProxyInstance(classloader—类加载器,interface—要实现的接口,invocationhandler—invocationhandler对象)需要传入classloader?
        • 反射缓慢的原因
    • 10.注解
      • 注解Annotation分为三类:
      • 系统注解
      • 元注解
      • 自定义注解
      • APT(Annotation Processing Tool)
    • 11.插桩
    • 13.序列化与反序列化
      • 序列的作用
      • Serializable与Externalizable
      • serializable与Parcelable
      • Serializable UID
      • JDK默认的序列化方式缺陷
      • 实际使用的序列化方式
    • 13.线程
      • 线程的5种状态
      • Wait,sleep,join,yield,interrupt
      • Java线程终止
      • Java线程创建
      • Runnable和Callable的区别
      • Future和Futuretask
      • Thread类中start()和run)()方法的区别
      • Try-catch-finally的返回值问题
      • Throw和throws的区别
      • Java中的异常
      • StackOverFlow
      • 线程池
        • 四种常见的线程池
        • 重要的参数
        • 核心线程与普通线程
        • 阻塞队列
        • 拒绝策略
        • 线程池的submit()与excute()
        • 关闭线程池
    • 11.JVM
      • 主要分为那几个区域并详细介绍
      • 常量池
      • 为什么将字符串常量池从方法区放到了堆中?
      • String、StringBuffer与StringBuilder
      • 为什么用元空间代永久代
      • GC垃圾回收
        • 如何判断一个对象是否可以被回收
        • 垃圾回收方法
        • 什么是大对象
        • 为什么有大对象要进行分配担保?
        • 为什么要分为eden区和survivor区?
        • 为什么要分为两个survivor区?
        • Java中对象的状态
        • finalize关键字的作用
        • 避免使用finalize关键字原因
        • gc操作
        • 垃圾收集器
        • 关于Minor GC与Major GC/Full GC?
        • Java中对象的引用
      • Java内存模型
  • 总结


前言

提示:这里可以添加本文要记录的大概内容:

例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

一、Java基础

1.Java语言的跨平台性

(1)Java语言的跨平台原理:Java程序通过编译为字节码,字节码文件可以在具有JVM的计算机上运行,JVM(Java虚拟机)中的解释器负责将字节码解释为特定的机器码进行运行。
(2)java语言跨平台的好处,一次编译多处运行(java在不同平台上不需要重新编译)。

2.Java中的基本数据类型

Byte、boolean(1个字节);
char、short(2个字节);
int、float(4个字节);
long、double(8个字节);

3.static关键字

static是一个修饰符,可以用来修饰成员方法、成员变量,另外还可以修饰代码块来优化代码性能,被static修饰的变量和方法不依赖对象,可以直接通过类名进行访问。
(1)static修饰的方法为静态方法,静态方法只能直接访问类的静态方法和静态变量,不可以直接访问类的非静态变量和非静态方法。
(2)Static修饰的变量被该类的所有对象共享。
(3)static修饰的代码块的特性是,只会在类初次被加载的时候执行一次(优化程序性能)。

4.Obeject类

Java中object类是所有类的父类,也就是说java中所有的类都继承了object类,所有的子类都可以使用object类的方法。
object类中常用的方法
clone()浅复制 ;
getClass()用于返回class类型的对象 ;
finalize()释放资源;
equals()判断对象内容是否相等;
hashCode()用于哈希查找;
toString();
Notify();
Notifyall();

5.面向对象与面向过程

面向过程主要以功能开发的函数为主,面向对象主要是抽象出类、属性及方法,然后通过实例化来进行操作;
面向过程封装的是功能,面向对象封装的是数据和功能;
面向对象还有继承性和多态性,更易扩展和复用;
面向过程:性能比较高(C)
面向对象:易扩展、易复用。(封装继承多态)(c++,python,java)

6.编译型语言与解释型语言

编译型语言

编译型语言是指程序在执行之前需要一个专门的编译过程,把程序源文件编译为机器语言的文件,运行时不需要重新编译,执行效率高,但缺点是,编译型语言依赖编译器,跨平台性差。
举例:C、C++

解释型语言

解释型语言是指源代码不需要预先进行编译,在运行时,要先进行解释再运行;解释型语言执行效率低,但跨平台性好。
举例:java(半编译半解释,底层是用C++写的),python(底层使用C写的)
解释型语言和编译型语言根本区别在于:编译型语言会将原文件通过编译生成目标程序,而解释型语言不会将整个源文件编译生成目标程序,解释型语言进行解释执行。

7.Java三大特性

封装
封装是把彼此相关数据和操作包围起来,抽象成为一个对象,变量和函数就有了归属,想要访问对象的数据只能通过已定义的接口、
继承
多态

Java 实现多态的 3 个必要条件:继承、重写和向上转型;
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:父类引用指向子类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。

多态的表现?

(编译时多态)重载与(运行时多态)重写;

重写与重载

重载:重载发生在同一个类中,在该类中如果存在多个同名方法,但是方法的参数类型、个数、顺序不一样,那么说明该方法被重载了。 (注意与权限修饰符和返回值类型无关;如果可以根据返回值类型来区分方法重载,那在仅仅调用方法不获取返回值的使用场景,JVM 就不知道调用的是哪个返回值的方法了。)
重写:重写发生在子类继承父类的关系中,父类中的方法被子类继承,方法名,参数完全一样,但是方法体不一样,那么说明父类中的该方法被子类重写了。
两同:方法名、参数列表相同;两小:返回值类型和抛出的异常范围要比父类更小;一大:子类的访问权限要比父类的访问权限大; link)

8.Enumeration和Iterator的区别

Enumeration只能读取集合数据,而不能对数据进行修改;Iterator除了读取外,也能对数据进行增删操作。
1)Enumeration 是 JDK 1.0 添加的接口。使用到它的函数包括 Vector、Hashtable等类,Enumeration 存在的目的就是为它们提供遍历接口。
2)Iterator 是 JDK 1.2 才添加的接口,它是为了HashMap、ArrayList 等集合提供的遍历接口。Iterator 是支持 fail-fast 机制的。
Fail-fast 机制是指 Java 集合 (Collection) 中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。例如:当某个线程 A 通过Iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程 A 访问集合时,就会抛出ConcurrentModificationException 异常,产生 fail-fast 事件。

9.包装类

(自动装箱——自动拆箱) Integer表示的是一个对象(存储的是引用的地址),int存储的是数值;
①基本数据类型计算的速度要远远快于拆箱装箱运算(所以要尽量避免拆箱装箱) ;
②对象是不能直接进行计算的,要先拆箱计算再装箱;
③装箱拆箱与强制类型转换效率差不多,属于同一个量级的;

10.访问修饰符

Public:可以被其他所有的类使用
Protected:可以被同包和子类使用
Default:可以被同包内使用
Private:只能被同类使用(就算实例化这个类还是不能调用这个类中的方法:实例名.方法名)
顺序:Public>Protected>Default>Private

11.代码编译过程?

12.Java中类的生命周期

1)类加载
2)连接
3)初始化
4)使用对象实例化:执行类中构造函数的内容,如果该类中存在父类JVM会先执行父类的构造函数,在堆内存中为父类的实例开辟空间并赋予默认的初始值,然后根据构造函数将真正的值赋予实例变量本身;
垃圾回收;
5)类卸载

13.类加载

类加载过程

链接: link
1)加载 ①. 将class文件中的类信息加载到方法区中 ②.在堆中实例化一个java.lang.class对象作为这个类的信息的入口(注意这一过程是在JVM之外实现的,是由类加载器实现的,类加载器会给每个java文件创建一个class对象,用描述类——作为类的信息入口);
2)链接
验证:确定类是否符合java语言规范;
准备:为静态变量分配内存并设置初始值(注意这里用static final修饰的会直接赋值,如static final b=10,那么默认就是10);
解析:把常量池中的符号引用转为直接引用(类加载之前,javac会将源代码编译为.class文件,这个时候javac是不知道被编译的类所引用的类、方法或者变量他们的引用地址在哪,所以只能用符号引用来表示)(直接引用则是具有引用地址的指针,被引用的类、方法或者变量已经被加载到内存中 link);
3)初始化:在类的初始化阶段将类的变量在准备阶段设置的默认值,修改成正确的初始值以及执行 方法的过程。 Java 虚拟机会通过加锁来确保 方法仅被执行一次。 只有当初始化完成之后,类才正式成为可执行的状态;

类初始化时机

link

1)创建类的实例(new xxxClass() Class.newInstance() constructor.newInstance())。
2)访问类中的某个静态变量,或者对静态变量进行赋值。
3)调用类的静态方法。
4)反射class.forName(“全限定类名”)。
5)子类的初始化,需要先对其父类进行初始化(接口除外)。
6)该类是程序引导人口(main入口或者test入口)。

类实例化过程

1、为新的对象分配内存。
2、为实例变量赋于默认值。
3、为实例变量赋于正确的初始值。

双亲委托机制

当某个特定类加载器在接收到加载类的请求时,首先从自己已经加载过的类中查询此类是否已经被加载过,如果已经加载则直接返回已经加载的类,否则将类加载任务委托给父类加载器,如果父类加载器还存在其父类加载器,则依次递归,如果父加载器可以完成加载就返回成功;如果父加载器无法完成此加载任务,就自己去加载。

启动类加载器—>扩展类加载器—>应用程序加载器—>自定义类加载器

双亲委托机制好处

链接: link
1)避免了重复加载;
2)保证程序安全,避免核心类被篡改(假设通过网络传递一个名为java.lang.Integer或者java.lang.objec的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改);

14.final关键字

final用于修饰类,方法、变量和引用形变量
类:被修饰的类,不能被继承 ;
方法: 被修饰的方法 ,不能被重写 ;
变量:被修饰的变量,不能被重新赋值 ;
引用型变量:当修饰变引用类型时,该变量指向的地址不能变,但是地址中的内容是可以变的;
Final修饰的变量可以先声明不设置初值,这也称为final空白,无论什么情况,编译器都可以保证final在使用之前必须被初始化;
Static final的值在类加载的“链接”的“准备”阶段被初始化;

15.抽象类和接口

抽象类

抽象类是为了被继承而存在
1)抽象类的访问修饰符为public或protected(缺省情况下默认为public);
2)抽象类不能用于创建对象(在抽象类中可以有构造方法,只是不能直接创建抽象类的实例对象,但实例化子类的时候,就会初始化父类,不管父类是不是抽象类都会调用父类的构造方法,初始化一个类,先初始化父类);
3)如果一个类继承于抽象类,则子类必须实现父类的抽象方法,如果子类没有实现父类的抽象方法,则必须将子类也定义为抽象类;

接口

接口作用:

1)实现代码间的耦合 ;
2)接口可以实现多继承,一个类可以实现多个接口;
3)接口可以使项目分离,面向接口开发,提高效率;

抽象类与接口的区别

相同点:
(1)他们都不能被实例化;
(2)接口的实现类和抽象类的子类都只有实现了他们抽象方法后才能被实例化;
不同点:
(1)一个类只能继承一个抽象类,却可以实现多个接口;
(2)接口实现的关键字为implements,而抽象类中的关键字为extends;
(3)接口中的变量只能为public static final,而抽象类中的变量可以被其他访问修饰符修饰;
(4)接口中方法只能用public abstract修饰,(在JDK8版本之后,接口可以实现默认方法和静态方法),抽象类可以有默认方法和静态方法;
总的来说:抽象类更偏向于提供一些默认的实现,接口更偏向于提供一种多继承;

16.静态编译与动态编译

link
静态编译 编译器在编译可执行文件的时候,将可执行文件需要调用的静态库中的内容提出来链接到可执行文件中,所以文件的运行不依赖于链接库;
优点:代码装载速度快,执行速度略快;
缺点:使用静态链接可执行文件体积较大。
动态编译 在编译时仅把部分需要的文件编译进可执行文件中,在运行的过程中还要依赖于动态链接库;
优点:可执行文件体积更小,加快编译速度,内容有改变时不需要重新编译;
缺点:依赖庞大的动态链接库;

17.动态加载

加载不存于程序本身的可执行文件;

Android中按照动态加载技术可以分为两种

1)动态加载so库
Android的NDK中其实就是使用了动态加载.so库并通过JNI调用其封装的方法,后者一般是由C/C++编译而成,运行在Native层效率比在虚拟机执行的java代码高很多,所以Android经常加载动态.so库来完成一些对性能要求比较高的工作,如T9搜索,Bitmap的解码,图片的高斯模糊等。
2)动态加载dex/jar/apk文件
Android项目中,所有的java代码都会被编译成dex文件,Android应用运行时,就是通过执行dex文件里的业务逻辑来工作的。

18.深拷贝和浅拷贝

1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,其中一个对象修改该值,不会影响另外一个,深浅拷贝的效果一致;
2)对于引用类型,浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间,改变其中一个,会对另外一个也产生影响;

实现深拷贝的方法?

1)实现cloneable接口并重写Object类的clone()方法;
2)实现Serialiabl接口,通过对象的序列化和反序列化实现clone;

19.Java中IO方式

IO流

Java中的IO流分为两类,一类是字节流,一类是字符流,Java中其他的流也是由它们演变而来;
字节流:inputstream、outoutstream(可以处理所有的二进制对象,但它不能直接处理unicode字符);
字符流:Reader,Writer(只能处理字符流);

同步/异步,阻塞非阻塞(关于BIO/NIO/AIO)

I/O输入输出的对象可以是文件、网络、进程之间的管道等,在linux中都用文件描述符来表示;
事件包括:
——可读事件:当文件描述符关联的内核缓冲区可读,则触发可读事件;
——可写事件:当文件描述符关联的内核缓冲区可写,则触发可写事件;

link
BIO同步阻塞

BIO:一个连接一个线程(处理数量较少且固定的连接);


NIO同步并非阻塞

一个连接处理多个请求,客户端的请求都会被注册到多路复用器,多路复用器轮询有I/O请求就会进行处理(处理连接数据较多但连接较短;聊天服务器,弹幕、服务器通讯等);

NIO有三大核心部分:channel(管道)、buffer(缓冲区)、selector(选择器),在该种模式下,如果数据还不可读,线程可以去做其他事情;


AIO异步并非阻塞

当有有效请求时才启动线程,等操作系统完成后才通知服务端程序启动线程去处理(连接数据较多且较长;相册服务器充分调用OS参与并发);

多路复用IO

多路复用: link、link
Linux 下有三种提供 I/O 多路复用的API,分别是: selectpollepoll

select 和 poll并没有本质区别,它们内部都是使用「线性结构」来存储进程关注的事件集合。 在使用的时候,首先需要把关注的文件描述符集合通过select/poll系统调用从用户态拷贝到内核态,然后由内核检测事件,当有事件产生时,内核需要遍历进程关注文件描述符集合,找到对应的文件描述符,并设置其状态为可读/可写,然后把整个集合从内核态拷贝到用户态,用户态还要继续遍历整个集合找到可读/可写的文件描述符,然后对其处理。 select 和 poll 的缺陷在于,当关注的事件集合越大,集合的遍历和拷贝会带来很大的开销。

epoll 在内核里使用「红黑树」来关注进程所有待检测的 文件描述符,红黑树是个高效的数据结构,增删查一般时间复杂度是(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个文件描述符集合,减少了内核和用户空间大量的数据拷贝和内存分配。epoll使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的文件描述符集合传递给应用程序,不需要像 select/poll那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。而且,epoll 支持边缘触发和水平触发的方式,而select/poll 只支持水平触发,一般而言,边缘触发的方式会比水平触发的效率高。

水平触发与边缘触发

水平触发
1)对于读操作,只要缓冲区内容不空,就会返回读就绪;
2)对于写操作,只要缓冲区内容不满,就会返回写就绪;
边缘触发
1)对于读操作,当缓冲区由不可读变为可读时,才会触发;
2)对于写操作,当缓冲区由不可写变为可写时,才会触发;

20.Java中的设计模式

link
常用设计模式有:
创建:单例模式、建造者模式、工厂模式
行为:观察者模式、策略模式
构造:适配器模式、代理模式、外观模式、责任链模式

二、Java重点

1.Java/Linux中进程通信方式

管道(Pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信(本质是一个内核缓冲区);
信号量(semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问(信号量有无名信号量和有名信号量,无名信号量一般用于共享内存空间的线程间的通信,而有名信号量可通过名字访问进程,实现进程间通信);
消息(Message)队列:消息队列是内核中存放消息的链表,又消息队列标识符进行标识,可以提供一种全双工通信连接;
文件共享:两个进程通过同一个文件来交换数据,文件共享通过对数据同步要求不高的进程通信(sharedpreference多进程情况下是不安全的,会出现数据丢失,sharedpreference在一个进程中会有自己的缓存);
内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它;
套接口(Socket):更为一般的进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。可用于不同机器之间的进程间通信;

2.Java中线程同步的方式

线程之间的通信主要用于线程同步,所以线程没有像进程通信中用于数据交换的通信机制;

互斥锁:同一时间是由一个线程可以获取锁,其他线程进入阻塞、休眠或者直接返回错误。
自旋锁:当线程尝试获取一个已经加锁的自旋锁时,线程不会进入休眠,而是一直处于忙等,等待锁被释放(用于锁被线程的持有的时间很短,比一个线程重新调度切换带来的成本小)。
读写锁:频繁使用互斥锁,线程只能串行读取数据,影响系统的并发性;读写锁有三种状态:读加锁、写加锁、解锁;读写锁保证了同一时间只有一个线程对共享资源进行修改,但可以允许多个线程对共享数据进行读操作(适应:读操作远多于写次数的场景)。
条件变量:(互斥锁的问题,当生产者变量还未满足消费者条件时,消费者先线程也会进行循环的加锁、解锁进行条件检测,耗费cpu资源)条件变量常与互斥锁一起使用,当满足条件时发送信号通知线程。

3.怎么创建进程

使用fork()可以创建进程,pid标记一个进程,pid=0为子进程,pid>0为父进程;
1)父进程死亡后,子进程将变成孤儿进程,由(init进程—守护进程)1号进程领养;
2)子进程死亡,但是父进程还没有死亡(此时没有进程给子进程回收资源),子进程成为僵尸进程(处理:将父进程kill,子进程将交给init进程);

4.Java中的集合

所有容器

Java中容器分为collections(list,set,queue)和map:
线程安全的有HashTable、vector、stack、concurrenthashmap、copyonwritehashset等;
线程不安全的有ArrsyList、linkedlist、hashmap、hashtable、arraymap、hashset、treeset等;

HashMap

什么是hash表?

hash表又称为散列表,它可以把关键字映射到表中的一个位置,来进行访问,加快查找的速度。

什么是hash?什么是hashcode?

Hash就是一个函数,通过一系列算法来得到一个值也就是hashcode,就对应这个元素在数组中的位置。

为什么使用hashcode?

主要就是为了查找的快捷性,他可以将一个元素定位到一个位置,我们可以直接根据对象确定它的位置,查找起来时间复杂度很低。

Equals和hashcode的关系?

1)先进行hashcode的比较,如果hashcode相同在进行equals比较两个对象。
2)如果equals为true,则两个对象hashcode一定相等。
3)如果两个对象的hashcode相等,不代表equals也为true,只能说明他们再散列表中存放在一个位置。

==与equals

为什么重写equals方法必须重写hashcode方法

判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。
在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。

为什么先进行hashcode的对比?

因为hashcode的一般是integer或string类型,它们不可变,创建之后直接缓存在内存中,比较起来效率更高。

HashMap的容量?加载因子值?设置这个值的原因?

HashMap的capacity 容量默认为16。 loadFactor 加载因子,默认是0.75(时间效率和空间效率综合考虑)。
threshold 阈值,阈值=容量*加载因子。默认12,当元素数量超过阈值时便会触发扩容。
HashMap扩容时一般是变为原来的二倍,所以它的长度一般是2的倍数。

HashMap扩容为原来的2倍原因?

1)hashmap计算key映射到的index时的计算公式为index=hashcode(key)&(length-1),所以hashmap的容量为2的倍数可以将key的hash值均匀的分布在数组上,减少hash冲突。
2)HashMap的初始容量是2的n次幂,扩容也是以2倍的形式进行扩容,可以使hashmap的容量始终是2的n次幂。

HashMap具体的扩容计算?

JDK7新建一个大小为原来2倍的数组,然后对每个元素计算hash值并插入(头插法) ;
JDK8中原位置的节点只有两种调整:
要么:保持原位置不动;
要么:原位置+扩容大小;

HashMap的懒加载

HashMap使用的是懒加载,构造完HashMap对象后,只要不进行put方法插入元素之前,HashMap并不会去初始化或者扩容table。而当首次调用put方法时,HashMap会发现table为空然后调用resize方法进行初始化,当添加完元素后,如果HashMap发现size(元素总数)大于threshold(阈值),则会调用resize方法进行扩容。

链表什么时候会变为红黑树?

链表长度超过8之后,还要判断当前容量的大小是否小于64,如果小于64要进行扩容而不是转化为红黑树。

为什么要使用红黑树?

红黑树为平衡二叉树,以加快检索速度,时间复杂度由原来的O(N),变为了O(logN)。

为什么要判断等大于8之后才变成红黑树?

红黑树的平均查找长度为logn,log8=3,当链表长度小于等于8时,链表的平均查找长度也是3,使用链表效率高,因为红黑树的维护成本比较高,插入一个元素要左旋或者右旋,当长度大于8时,红黑树的查找效率就高于链表。

hash冲突后链表使用的头插法or尾插?

jdk7中当元素冲突时采用的是头插法;(原因是效率更高,否则还要遍历冲突链然后插在尾部)
jdk8中当元素冲突时使用的是尾插法;(因为要遍历链表查看链表长度是否超过8,如果超过长度要将其变为红黑树)

Object的hashcode()方法中为什么要使用31?

——原因:如果使用较小的质数,hash范围会很小,很容易造成hash冲突;如果使用较大的质数,很有可能会溢出,所以经过多次测试选择使用了31。
——在JVM中最有效的方式就是位运算了,31*i相等于(i<<5)-i,计算效率更高。

位移、加减运算的效率比乘除运算的效率更高?

加减和位移运算都是运算器最基本的操作,它们的时间基本相同,都只需要一个时钟周期,但是乘法和除法一般需要多个时钟周期,但是编译器优化的时候可以将乘除法用移位运算来代替。

hashmap底层的hash值计算方法?

hashcode()是Object类中提供的方法;
JDK8中底层通过调用hashcode()方法生成初始hash值h1,h1无符号右移16位得到h2,之后将h1与h2进行异位或运算最终得到hash值h3,最后将h3与(length-1)进行按位与得到hash表的索引;

为什么要无符号右移16位?

因为数组的大小一般比较小,比如数组长度为16时,只有低4位在进行运算,如果产生的hashcode值高位变化很大,而低位变化很小,有很大概率会产生hash冲突,所以为了更好的散列,将hash值的高位也利用起来。

解决hash冲突的方法

重哈希法
链地址法
开放地址法(线性探测再散列、二次探测再散列、伪随机探测再散列)

hashmap不同版本都会出现什么线程安全问题?

hashmap本身是多线程不安全的,所以无论是JDK7还是JDK8都是多线程不安全的;
在JDK7中的头插法是在扩容时会出现,环形链表或元素丢失;
在JDK8中使用尾插法,扩容时可能出现元素被覆盖丢失红黑树成环;

hashmap两个版本的区别?

ArrayList?

HashMap,ArrayMap,SparseArray的区别?

ArrayMap

link ArrayMap中有两个数组,一个用于存放key的hashcode值,另一个用于存放键值对key和value,而且Arraymap是有序的,每次插入删除都会保证它的有序性,算是以时间换空间了。但在大量数据时,使用hashmap的效率更高一点,因为arraymap使用的是数组,扩容或者删除的时候效率都很低。
arraymap是二分查找和实时扩容机制的,以时间换时间; arraymap解决hash冲突的方式是追加,把数据全部后移一位;
arraymap达到oldsize大小会进行扩容,完成扩容需要将老数组拷贝在新数组中,并释放原来的内存;arraymapy是非安全的类;

SparseArray

Sparsearray中的key为int类型(避免了装箱和拆箱),value是object类型,key和value分别存放在两个数组中,且位置对应,key数组的int值是按照顺序排列的,查找的时候采用的是二分查找,效率很高。Add的时候会移位,remove的时候不一定会进行移位,把某个值标记为delete,如果下次有符合的值直接放到该位置。
android内部是推荐使用这种数据结构的,由于android对应的设备内存相对来说比较小,而sparsearray恰好是存储的内存相对来说较小,其内部对数据采取了压缩的方式来表示稀疏数组的数据,从而节约空间。

HashMap,LinkedHhashMap,HashTable,ConcurrentHashmap的区别?

hashmap
key与value值可以为null;
LinkedHhashMap
HashMap的低层实现在java1.7 中是数组+链表,java1.8中是数组+链表+红黑树
1)LinkedHashMap在HashMap的基础上加了双向链表,实现有序性;
2)LinkedHashMap是非线程安全的,HashMap也是非线程安全的;
3)LinkedHashMap中允许key值和value值都为null;
HashTable
1)key与value值不能为null;
2)使用Synchronized 实现多线程安全;
特点:在修改数据时需要锁住整个HashTable,所以效率比较低;
ConcurrentHashmap
1)key与value值不能为null;
2)多线程安全
——如何保证多线程安全?
JDK7中使用的分段锁,锁用的是segment+reentrantlock;
JDK8中使用的是node+CAS+synchronized锁;
——“锁升级”的原因?
使用CAS+synchronized的方式加锁的对象是每个链条的头结点,也就是锁定的是冲突的链条,所以提高了并发程度;
使用reentrantlock则需要节点继承AQS来获取同步支持,增加了内存开销;synchronized是JVM直接支持的,JVM能够在运行时做出响应的优化策略,锁粗话、锁消除、锁自旋;
——什么时候使用CAS锁,什么时候使用synchronized锁?
读时(get操作):
没有使用同步机制,也没有使用CAS方法,支持并发操作(这是因为node成员使用volatile修饰的,所以可以直接get;注意concurrenthashmap的数组也是用volatile修饰的,主要是为了保证在数组扩容的时候保证可见性);
写时(put操作):
(1)当发现当前节点元素为空,则通过CAS来存储该元素;
(2)当发现当前节点元素不为空,则使用synchronized关键字来锁住当前节点,并进行值的设置;

5.线程安全问题

多线程如何实现线程安全?

①线程间不要跨线程访问共享变量; ②使共享变量是final类型的; ③将共享变量的操作加上锁;

并发编程的三个概念?线程安全设计的几个概念?

①可见性(volatile); ②原子性(lock和synchronized); ③有序性(volatile和synchronized);

volatile

原子性

对于写操作:对变量更改完之后,会立即写回到内存中;
对于读操作:对于变量的读取要从内存中读取,而不是缓存;
原理:
volatile关键字会开启总线的mesi的缓存一致性协议,多个线程从主内存读取一个数据到各自的高速缓存,当其中某个线程改版了缓存中的数据,该数据会马上同步回主内存,其他前程通过总线嗅探机制,感知数据的变化从而更新数据;

有序性

Volatile禁止指令重排优化,主要是通过在操作前后加入内存屏障实现的,内存屏障再jvm中可以分为4类:

重排的原因?

为了提高性能(或者说为了提高CPU的利用率吧)
一般有三种重排序:
①编译器优化的重排序;②指令级并行的重排序;③内存系统中的重排序;

重排的规则?

数据依赖性
as_if_serial(指的是不管怎么重排序,单线程程序的执行结果都不能被改变)

什么是happen_before?

先行发生原则,当A操作发先行发生于B操作,则在生B操作的时候,操作A产生的影响可以被操作B感知到,“影响”包括内存中的共享变量的值,发送的消息,调用的方法等。

synchronized

原理

Synchronized底层是monitor监视器;
对于同步代码块,会在代码块的前后产生一个monitorenter和moniterexit指令,来标识这是一个同步代码块。
对于同步方法,方法上还多了ACC_SYNCHRONIZED标识符,JVM根据该标识符实现方法的同步,当方法被调用时,调用指令会检查方法该方法的ACC_SYNCHRONIZED是否被设置,如果被设置了,执行先后才能将先获取Monitor获取成功之后才能执行方法体,方法执行之后再释放Monitor。

synchronized锁优化

link
无锁;
偏向锁:它会偏向第一个访问锁的线程,在运行过程中只有一个线程访问同步锁,不存在多线程竞争情况。(使用CAS获取偏向锁失败,则说明有竞争,此时锁会升级为轻量级锁。)
轻量级锁:当发生锁竞争时就会升级为轻量级锁,轻量级锁为自旋锁,减少线程的频繁切换,减少上下文切换的开销。(自旋等待一定次数后,或者说自旋的开销超过进行上下文切换的开销,锁会升级为重量级锁。)
重量级锁:synchronized升级为重量级锁之后,线程再竞争资源失败,就需要进行上下文切换并进入阻塞状态。

为什么要进行锁优化?锁优化的方法?

因为监视器锁(Monitor)是依赖于操作系统的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到内核态,状态转换需要花费很多的处理器时间。
自旋锁和适应自旋锁(自旋锁避免了线程频繁的挂起和恢复,因为线程的挂起和恢复都需要从用户态进入到内核态,这个过程是比较慢的(耗费资源),所以通过自选的方式)(适应自旋锁自旋的此时不再是固定的值,而是一个动态可以改变的值,这个值会用过自旋锁获取锁的状态来决定自旋的次数)
锁消除 锁粗化
无锁、偏向锁、轻量级锁、重量级锁

lock锁

AQS机制是众多锁的基础,ReentrantLock、Semaphore、CountdownLatch的实现都依赖AQS。
AQS其实就是AbstractQueuedSynchronized,多个线程竞争一个state,这个state用volatile修饰,保持state对各个线程的可见性,AQS使用一个FIFO的队列进行线程管理,线程可以通过CAS去改变state,当state为0,则说明当前对象锁已经被获取,其他线程只能进入自旋等待

synchronized与volatile的区别?

①volatile是只能用来修饰变量,synchronized可以用来修饰变量、方法、代码块。
②Volatile可以保证可见性但不能原子性,synchronized可以保证操作的可见性与原子性。
③volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。(因为synchronized只有一个线程可以获取对象的锁,其他线程阻塞)

synchronized 和Reentrantlock(lock)锁的区别?

①synchronized是java内置关键字在jvm层面,Reentrantlock(lock是一个接口)是个java类;
②synchronized会自动化释放锁,Reentrantlock(lock)需要在finally手动释放锁;
③synchronized的锁可重入、不可中断,为非公平锁,而ReentrantLock(lock)锁可重入、可中断,默认为非公平锁但是可以设置为公平锁。

关于synchronized与reentrantlock(基于AQS自旋锁实现)的性能比较?

在低并发时,由于synchronized可以进行锁优化,所以synchronized的性能更高;
在高并发状态时,synchronized会频繁进行线程的切换,而线程切换需要操作系统的帮助,需要从内核态到用户态以及用户态到内核态的切换,资源耗费更多。

可重入?公平锁?

可重入:指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
公平锁与不公平锁:公平锁会进行频繁的线程切换,耗费资源更多;非公平锁可能出现“饥饿”情况。

类锁与对象锁?

①类锁是对静态方法使用synchronized关键字后,无论是多线程访问单个对象还是多个对象的sychronized块,都是同步的。
②对象锁是实例方法使用synchronized关键字后,如果是多个线程访问同个对象的sychronized块,才是同步的,但是访问不同对象的话就是不同步的。

public class SychronyzedDemo {  
public synchronized  void demo1(){}  //方式1:锁对象方法  
public void emo2(){synchronized (this){}  //方式2:锁对象代码块  
public synchronized static void demo3(){}  //方式3:锁类方法
public  void emo4(){synchronized (SychronyzedDemo.class){}} }//方式4:锁类代码块  

乐观锁与悲观锁?

乐观锁认为并发状态下不会出现冲突,只有在提交修改的时候才进行比较对状态进行检测;
悲观锁认为在并发状态下会出现并发冲突,所以进行各种操作之前都会获取对象锁,阻塞其他线程,直到锁被释放。
常见的乐观锁有CAS和AQS;悲观锁有synchronized;

CAS锁

CAS(compare and swap)比较并交换,是一种实现并发算法时常用到的技术,CAS具体包括三个参数:当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。
CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时 使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并开始自旋等待。

CAS的缺陷

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题:
1)ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A,就会变成1A-2B-3A。
2)循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
3)只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就需要用锁。

6.Java中的内部类

主要分为四类:
①成员内部类(成员内部类可以调用外部类的方法和属性,但是外部类想要访问成员内部类的方法和属性需要先实例化成员内部类)
②静态内部类(静态内部类只能直接访问外部类的静态属性和变量)(最大的优点就是:在创建静态内部类时,不需要外部类对象的引用;)
③匿名内部类(匿名内部类就是没有名字的内部类,使用内部类有一个前提条件:必须继承一个父类或者实现接口,匿名内部类也是唯一一个没有构造器的类)link
④局部内部类(定义在代码块中的类,它的作用范围就是他所在的代码块中,最多可以用final修饰)

内部类的作用:

①间接实现了多继承 ;
②将有一定逻辑关系的类组织在一起,可读性;

7.泛型

泛型

泛型就是指数据类型参数化;

泛型擦除及原因

在进入JVM之前,与泛型相关的信息都会被擦除,这些信息被擦除之后相应的类型就会变成泛型类型的上限,如果没有指定就是Object;
原因:避免创建太多类而造成的运行时过渡消耗;

限定通配符与非限定通配符

限定通配符有两种:<? extends T>和<? super T>其中:

<? extends T>确保泛型只能T本身或者T的子类或者子类的子类,以此来设定泛型的上界; <? super T>表示只能是T的父类,以此来设定类型的下界; 非限定通配符即<?>可以是任何类型(其实相当于List<? extends Object>)

<? extends T>和<? super T>读写方面的不同

链接: link

<? extends T>只能读不能写;(因为编译器只知道它是T或者T的子类,具体是什么类型它不知道)。 <? super T>只能存不能读;(因为元素是T的基类,所以存粒度比T小的都可以存放),不影响往里存,但往外取只能放在Object对象里。

编译时类型与运行时类型

编译时类型由声明该变量的类型决定,运行时类型由该变量指向的对象决定;
例如:Animal a = new Bird();
Animal 就是编译时类型,Bird就是运行时类型,这就是多态的一种表现;
当使用该对象引用进行调用时,规则为:对象调用编译时类型的属性和运行时类型的方法;

8.反射

反射

反射就是再运行过程中,对于任意一个类都可以知道这个类的属性和方法;对于任意一个对象,都可以调用它的属性和方法;

获取class对象

一个class对象表示一个运行中的class字节码文件,class对象是在类加载时JVM自动创建的,一个类在JVM中只会有一个class对象。


三种方法的不同:

相同:得到的都是该类的java.lang.Class对象,它是类加载的产物;
不同:
——类名.class:JVM将使用类加载器,将类装入内存(如果类还没有转入到内存中的话),不做类的初始化,返回class对象(编译时)
——Class.forName(“类名字符串”)将类转入内存,并做类的静态初始化,返回Class的对象(编译时)
——对象.getClass()对类进行静态初始化、非静态初始化;返回引用运行时真正的对象所属类的class对象(运行时)

反射常用的api

Field:提供类的属性以及访问类的属性的接口;
Method :提供类的方法信息以及访问类的方法的接口;
Constructor:提供类的构造方法的信息以及访问构造方法的接口;
Proxy:提供用于创建动态代理类和实例的静态方法;

clazz.getConstructor()  获取共有构造方法
clazz.getDeclaredConstructor()  获取私有构造方法
setAccessible(true);  //设置访问权限,忽略修饰符;

9.动态代理

静态代理

静态代理:代理类为其它对象提供一种代理来控制这个对象的访问。Proxy保存一个引用使得代理类可以访问实体,并提供一个与subject接口相同的接口,这样代理类就可以用来代替实体;
缺点:proxy与realsubject的功能本质上是相同的,proxy只是起到中介作用,导致系统类规模增大不易维护;

什么是动态代理

动态代理就是在程序运行期间,创建目标对象的代理对象,并对目标对象的方法中进行功能增强的一种技术。

Java中动态代理的实现方式

动态代理:link
1)JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在具体方法前调用invocationhandler的invoke()方法来转发对方法的处理;(创建快,但是运行慢)
2)CG LiB动态代理:使用ASM开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理;(创建慢,运行快)

两种动态代理方法的区别:

(1)JDK动态代理是基于接口实现的,不需要添加依赖包,可以平滑的支持JDK版本的升级;
(2)CGLIB不需要实现接口,可以直接代理普通类,需要添加依赖包,性能更好;


为什么proxy.newProxyInstance(classloader—类加载器,interface—要实现的接口,invocationhandler—invocationhandler对象)需要传入classloader?

(1)需要校验传入的接口是否可以被当前类的类加载器加载,假如无法加载,就证明这个接口与类加载器不是同一个;
(2)需要类加载器根据生成的类的字节码去通过defineClass方法生成类的class文件,也就是说,没有类加载器的话是无法生成代理类的;

反射缓慢的原因

链接: link、link
1)反射涉及到动态加载的类型,无法进行优化;
2)需要检查方法可见性,校验参数等;
3)变长参数导致的Object数组,基本数据类型的装箱拆箱等;

10.注解

注解用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联,为程序元素(类、方法、成员变量)加上更直观的说明。

注解Annotation分为三类:

1)JDK内置系统注解
2)元注解
3)自定义注解

系统注解

@Override 是一个标记类型注解,用于提示子类要复写父类中被 @Override 修饰的方法。
@Deprecated 也是一个标记类型注解,用于标记过时的元素。比如如果开发人员正在调用一个过时的方法、类或成员变量时,可以用该注解进行标注。
@SuppressWarnings 并不是一个标记类型注解,它可以阻止警告的提示。它有一个类型为 String[] 的成员,其值为被禁止的警告名。
@SafeVarargs 是一个参数安全类型注解。它的目的是提醒开发人员,不要用参数做一些不安全的操作。它的存在会阻止编译器产生 unchecked 的警告。

元注解

用于修饰注解的注解,通常用在注解的定义上。

@Documented-注解是否将包含在JavaDoc中

@Retention-什么时候使用该注解 ·
@Retention(RetentionPolicy.SOURCE) //做一些检查性的操作,注解仅存在源码级别,在编译的时候丢弃该注解
@Retention(RetentionPolicy.CLASS) //要在编译时进行一些预处理操作,注解会在class文件中存在 @Retention(RetentionPolicy.RUNTIME) //注解会在class字节码文件中存在,jvm加载时可以通过反射获取到该注解的内容 如:retrofit;

@Target-注解用于什么地方 ·
@Target(ElementType.TYPE) // 接口、类、枚举、注解 ·
@Target(ElementType.FIELD) // 属性、枚举的常量 ·
@Target(ElementType.METHOD)// 方法 ·
@Target(ElementType.PARAMETER) // 方法参数 ·
@Target(ElementType.CONSTRUCTOR) // 构造函数 ·
@Target(ElementType.LOCAL_VARIABLE)// 局部变量 ·
@Target(ElementType.ANNOTATION_TYPE)// 该注解使用在另一个注解上 ·
@Target(ElementType.PACKAGE) // 包

@Inherited-是否允许子类继承该注解

自定义注解

1)自定义注解使用@interface关键字定义
2)自动继承java.lang.annotation.Annotation接口
3)配置参数只能为八大基本数据类型、string、class、annotation及相应组合
4)配置参数声明的格式如下: 类型 变量名()[default 默认值];
5)定义中定义了默认值的参数可以不指定值,但是没有默认值的一定要指定值;
6)自定义注解的作用是增强反射效果,在反射中会获取带有自定义注解的元素,根据注解的值决定怎么处理,(这里一般说的是运行时注解,一般都是采用反射处理。

APT(Annotation Processing Tool)

link、link
APT是javac提供的一种工具,它在编译时对代码进行检测,根据注解自动生成代码,这段代码是根据用户编写的注解处理逻辑去生成的,最终将生成的新的源文件和原来的源文件共同编译(APT并不能对源文件进行修改,只能生成新的文件,例如往原来的类中添加方法)。

APT处理有三个元素:注册处理器+注解处理器+代码生成;
(1)在META-INF注册(为什么要在META-INF中注册,在编译时Java编译器Javac会去META-INF中查找实现了abstractprocesser的子类,并且调用该类的process函数,最终生成.java文件);
(2)创建一个继承abstractprocesser类就可以,并在process方法中获取我们需要处理哪些注解;
(3)使用stringbuilder或者javapoet生成代码;

11.插桩

插桩:通过某种策略在一段代码中插入或替换另一段代码。这里的代码可以分为源码和字节码,我们一般所说的就是字节码插桩,就是在.class文件转为.dex文件之前就修改;
应用场景
——通过插桩,扫描每一个class文件,并针对特定规则进行字节码修改从而达到监控每个方法耗时的目的;
——无痕埋点,性能监控;
插桩的实现步骤
——AMS提供了两种API来生成和转换已编译类;一个是核心API,基于事件形式来表示类;另一个是树API,基于对象形式来表示类;
——Transform API,在android在将class转换为dex之前给我们留个接口,在这个接口中通过插件方式来修改class文件;

13.序列化与反序列化

序列化:将java对象转为字节序列;
反序列化:将字节序列转为java对象;

序列的作用

1)java中对象保存在“堆”中,堆是一个内存空间,不能长期保持,程序关闭对象就有可能被回收,但是我们有时候保存对象里面的信息的,这个时候就需要对对象进行持久化;
2)各个服务之间调用对象就需要把对象序列化用于网络上传输;
3)Java对象本质上是class字节码,很多机器不能根据这个字节码识别这个java对象,但是序列化是序列化成二进制流。

Serializable与Externalizable

1)serializable序列化不会调用默认的构造器,Externalizable会调用默认的构造器;
2)Serializable会把对象所有的属性都序列化和反序列化来进行保存和传递;Externalizable需要通过接口中的方法(writeExternal()和readExternal())指定需要序列化的属性;
3)Serializable如果某字段被transient修饰则不会被序列化;Externalizable中在方法(writeExternal()和readExternal())中指定的属性即使被transient修饰还是会被序列化;

serializable与Parcelable

1)serialable是java提供的可序列化接口,parcelable是android中提供的可序列化接口
2)Serializable在反序列化的过程中使用了反射机制,所有会产生大量的临时变量,从而导致频发的GC,并且它是通过IO流的形式将数据写入到硬盘或传到网络上。Parcelable使用binder机制在内存中直接读写,效率更高。

Serializable UID

用于序列化对象的版本控制,如果新版本中这个值修改了,新版本就不兼容旧版本,反序列化时就会抛出异常。

JDK默认的序列化方式缺陷

链接: link
1)无法跨语言(java序列化为java语言的私有协议,其他语言不支持,但是在跨进程的服务中经常会使用c++或其他语言,所以阻碍了它的应用)。
2)序列化之后的码流太大。
3)JDK默认的序列化的性能较低。

实际使用的序列化方式

链接: link
Gson、xml、protobuf
Xml通用的重量级的数据交换格式,以文本结构存储。
Json是一种通用和轻量级的数据交换格式,以文本结构存储;相比来说,gson格式为压缩的,体积更小,传输速度更快,描述性比xml更差一点。)
Protobuf是一种独立和轻量级的数据数据交换格式,以二进制进行存储可读性不强,序列化后数据大小更小,速度更快。
Xml多用于配置文件,json用于数据交互。

13.线程

线程的5种状态

Java线程具有五中基本状态:
①新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = newMyThread();
②就绪状态(Runnable) :当调用线程对象的start方法,线程即进入就绪状态。此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start此线程立即就会执行;
③运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
④阻塞状态(Blocked) :处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1).等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2).同步阻塞―线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3).其他阻塞―通过调用线程的sleep0或join()或发出了IO请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
⑤死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。

Wait,sleep,join,yield,interrupt

①Wait,释放cpu资源,会释放锁,需要被拥有对象锁的线程唤醒(notify或notifyall);
②sleep,释放cpu资源,不释放同步锁(类锁/对象锁),等待指定时间后自动醒来;
③join,释放cpu资源,不释放所持对象锁(object),等待调用join方法的线程结束才继续执行本线程。
④yield,释放cpu资源,不释放对象锁,使线程进入就绪状态;

wait

线程notify()唤醒之后,是从上次阻塞的代码继续往下执行还是从头开始执行? 链接: link
从上次阻塞的位置开始往下执行,也就是从wait()方法之后开始执行的。

join


关于Join: link/ link
①Join使用了synchronized锁,会使主线程持有子线程的对象锁,join内部使用了wait()和notifyall(),wait()会使主线程会释放thread线程对象的锁,进入等待状态。最后,threadA线程执行结束,JVM会调用lock.notify_all(thread);唤醒持有threadA这个对象锁的线程,也就是主线程,所以主线程会继续往下执行.
②但是该线程持有的obj对象不会被释放;

Java线程终止

链接: link
①程序运行结束,线程终止。
②使用退出标志,退出线程。
③interrupt方法来中断线程,可以分为两种情况:
a 线程处于阻塞状态: 当线程使用了sleep/wait/socket中的receiver或accept等方法时,此时调用线程的interrupt方法,会抛出interruptException;阻塞中的方法抛出异常,通过代码捕获,然后break跳出循环状态,才能正常结束run方法。
b 线程是未阻塞状态:使用isinterrupt()方法判断线程的中断标志来退出循环;使用interrupt方法,会把中断标志设置为true,和使用自定义标志来控制循环是一样的。
④stop 暴力终止线程。释放锁,容易造成数据的不一致。这个方法是不安全的,不推荐使用。

使用stop方法不安全的原因?

调用stop 方法后,会使线程释放所有的锁;一般任何进行加锁的代码块,都是为了保证数据的一致性。
如果使用stop方法导致,子线程释放了所有锁,被保护的数据可能会不一致。

Java线程创建

1)继承Thread类的方式创建线程;
2)使用Runnable、Callable接口创建线程(注意:Java中真正能创建新线程的只有Thread类对象,它实现Runnable的方式,最终还是通过Thread类对象来创建线程,)
3)使用线程池(提前创建好多个线程,放入线程池中,使用时直接获取,使用完毕后放入线程池,可以避免频繁的创建、销毁操作)

Runnable和Callable的区别

Callable任务执行任务之后可以返回值,runnable执行任务之后没有返回值;
Callable方法抛出异常,runnable不能抛出异常。

Future和Futuretask

Callable 接口相比于 Runnable 的一大优势是可以有返回结果,那这个返回结果怎么获取呢? 就可以用 Future 类的 get方法来获取 。因此,Future 相当于一个存储器,它存储了 Callable 的 call 方法的任务结果。除此之外,我们还可以通过Future 的 isDone 方法来判断任务是否已经执行完毕了,还可以通过 cancel 方法取消这个任务,或限时获取任务的结果等,总之Future是主线程可以跟踪进度以及其他线程结果的一种方式。

Futuretask实现了Future和Runable,并将它们的功能结合在一起;FutureTask表示一个可以取消的异步运算,它有启动和取消、查询运算是否完成和取回运算结果等方法。在主线程执行比较耗时任务时,但又不想阻塞主线程时,可以把这些作业交给后台的Futuretask对象完成:

Thread类中start()和run)()方法的区别

Start()方法用于启动新创建的线程,start()内部调用了run()方法,直接调用run()方法则还是在原来的线程中运行。

Try-catch-finally的返回值问题

1)无论try、catch中有无异常,finally块中的代码都会执行;
2)如果finally中没有return语句,若try中有异常,则返回catch中的return值,反之,则返回try中的异常,在这种情况下,return语句要在finally后执行。
3)无论try、catch中有无异常,如果finally块中有return语句,最后返回的是finally的return值。
什么情况下不会执行finally语句?
——如果在try或catch语句中执行了system.exit(0);
——在执行finally之前jvm崩溃了
——try语句中有死循环等

Throw和throws的区别

1)throw 在方法体内使用,throws 在方法声明上使用;
2)throw 后面接的是异常对象,只能接一个;throws后面接的是异常类型,可以接多个,多个异常类型用逗号隔开;
3)throw 是在方法中出现不正确情况时,手动来抛出异常,结束方法的,执行了throw 语句一定会出现异常。而 throws是用来声明当前方法有可能会出现某种异常的,如果出现了相应的异常,将由调用者来处理,声明了异常不一定会出现异常。

Java中的异常

Throwable分为Exception和Error;
Error为内存溢出,系统崩溃即OOM或者StackOverflowError等;
Execptiont又分为Checkedexecption和RuntimeException;
Checkedexecption:Java中认为Checkedexecption都是可以被处理的异常,所以java程序必须显示处理Checkedexecption,如果程序没有处理checked异常,该程序编译时会发生错误无法编译。
处理Checkedexecption主要有两种方法:
1)当前方法知道如何处理异常,则用try…catch来处理。
2)当前方法不知道如何处理,则在定义该方法时声明抛出异常。

StackOverFlow

递归循环时可能出现StackOverFlow。

一个栈大概有多大,为什么?

可以通过CreateThread函数中的dwStackSize参数为新线程指定的栈空间大小。 Linux平台的栈默认大小应该是8192KB,
Windows平台的栈默认大小应该是1024KB, 栈太小可能会导致空间不足, 分配失败。

线程池

Java 给我们提供了 threadpoolExecutor 接口来使用线程池。

四种常见的线程池

SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
scheduleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
FixedThreadPool:一个有指定的线程数的线程池,里面有固定的线程数量,响应的速度快。有核心线程,核心线程的即为最大的线程数量,没有非核心线程。
cachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。

重要的参数

corePoolSize:核心线程池的大小
maximumPoolSize:最多线程量
workQueue:阻塞队列
keepAliveTime:空闲线程的存活时间
RejectedExecutionHandler拒绝策略

核心线程与普通线程

核心线程即使处于空闲状态也不会被回收;普通线程的空闲时间超过keepalivetime后就会被回收。

阻塞队列

ArrayBlockingQueue:基于数组结构的阻塞队列(有界);
LinkedBlockingQueue:基于链表结构的阻塞队列(无界); PriorityBlockingQuene:支持线程的优先级排序;
synchronousQueue:不存储任何元素;当一个线程调用了put方法时,发现队列中没有take线程,那么put线程就会阻塞,当take线程进来时发现有阻塞的put线程,那么他们两个就会匹配上,然后take线程获取到put线程的数据,两个线程都不阻塞。反之一个线程调用take方法也会阻塞线程,当一个调用put方法的线程进来后也会与之匹配。链接: link

拒绝策略

AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出异常 RejectedExecutionException (属于RuntimeException),让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
CallerRunsPolicy,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
CallerRunsPolicy策略更佳原因: 第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。 第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。

线程池的线程数怎么确定?

要看任务的性质:链接: link
1)cpu密集型任务(大量计算、对视频进行高清解码等全靠cpu计算)
线程数=cpu总核心数+1(+1是为了利用空闲等待)(数量少减少线程的频繁切换)
2)IO密集型任务(网络请求、数据库数据获取、磁盘数据加载等) 线程数=cpu总核心数*2+1(或者2~3倍都可以)

线程池的submit()与excute()

submit最终还是调用excute()方法去执行任务的,不同的是submit会返回future对象;future对象可以用于获取一个结果,也可以取消执行;

关闭线程池

Shutdown()或者shutdownNow()
shutdown不会关闭正在执行任务的线程;
shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。

11.JVM

主要分为那几个区域并详细介绍


Java栈:存放的是一个个的栈帧,每个栈帧对应一个被调用的方法(包含局部变量、返回信息、中间结果等)。
本地方法栈:与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。
:Java中的堆是用来存储对象(实例化对象以及class对象)或数组。
程序计数寄存器:它保存的是程序当前执行的指令的地址。
方法区:存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

常量池

为什么将字符串常量池从方法区放到了堆中?

因为永久代的回收率很低,一般只有在老年代、永久代不足时才会触发full GC进行回收,所以这就导致字符串常量回收效率不高,而我们在开发过程中会有大量的字符串被创建,放到堆里,能及时回收内存。

String、StringBuffer与StringBuilder

Java中同一个字符串常量在堆中只创建一个,可以减少字符串的重复创建。

为什么用元空间代永久代

元空间不在虚拟机内存中,而是使用的本地内存,因此,在默认情况下,元空间的大小仅受本地内存限制。

GC垃圾回收

垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。

如何判断一个对象是否可以被回收

1)引用计数法(任何引用计数为0的对象实例可以被当作垃圾收集)
2)可达性分析算法(通过判断对象的引用链是否可达来决定对象是否可以被回收)
可达性分析算法中以GC Roots作为起始点,可作为GC Roots的对象包括:
①java栈帧中引用的对象
②静态属性引用的对象
③常量引用的对象
④本地方法栈中引用的对象
⑤synchronized持有的对象

垃圾回收方法

1)标记-清除法
2)复制法
3)分代收集法
新生代:老年代=1:2;老年代的特点是只有少量对象需要回收,新生代中有大量对象需要回收,对于老年代使用标记-清除法,对于新生代使用复制法进行回收。
新生代:局部变量、临时变量等
老年代:单例对象、缓存对象
永久代(JDK1.8):字符串常量池、加载过的类信息;
新生代又分为eden区与survivor1区,Eden区与survivor0和survivor1的比例是8:1:1,两个survivor区总有一个是空闲的,每次只使用eden区和一个survivor,一次GC操作把eden和survivor区中存活的对象放到另一个空闲的survivor中,然后清除原来的eden和survivor区。当对象的年纪>15会被放入老年代。
当survivor区不够用的时候(有大对象)会进行分配担保,把新生代的对象提前转移到老年代中。

什么是大对象

需要占用大量连续的内存空间的java对象为大对象,比如很长的字符串和数组,数据库查询不使用分页查询导致结果集很大等。

为什么有大对象要进行分配担保?

链接: link
1)如果大对象在新生代,新生代是由eden区和survivor区配合来完成的,大对象在两个survivor区频繁复制耗费资源;
2)如果没有大对象直接进入老年代,那么MinorGC会因为Survivor区大对象过多提前发生MinorGC,而且FullGC会因为年轻代过早进入老年代而提前触发。

为什么要分为eden区和survivor区?

链接: link
如果没有survivor区,对象回收之后立即就被放到老年代,老年代将很快被填满,触发full GC,又由于老年代的内存空间要远大于新生代,所以进行full GC消耗的时间比minor GC长的多。

为什么要分为两个survivor区?

设置两个survivor区主要是解决了内存碎片化问题。新创建的对象在eden中,一旦触发Minor GC,存活的对象就会被存放在survivor区,这样当下次垃圾回收的时候,eden和survivor区都有存活对象,如果直接将eden区的存活对象放入survivor区,这两部分对象锁占用的内存是不连续的,就导致了内存的碎片化。

Java中对象的状态

①可触及状态:只有还有引用变量引用它,它就处于可触及状态;
②可复活状态:当不再有引用变量引用该对象时,这个对象就会变为可复活状态,垃圾回收器会准备释放它占用的内存,在释放之前,会调用其他处于可复活状态的对象的finalize方法,这些finalize方法可能使该对象重新转为可触及状态;
③不可触及状态:当java虚拟机执行完所有可复活对象的finalize方法后,如果该对象还没有转为可触及状态,垃圾回收器才会真正回收其所占用的内存。

finalize关键字的作用

首先进行可达性分析,如果这个对象时不可达的,然后进行筛选,当对象没有覆盖finalize方法或者finalize方法在此之前已经被虚拟机调用过,这个对象被判断为没有必要执行finalize(),则整个对象将被回收;
如果对象被判断为有必要执行finalize()方法,则这个对象会被放在一个名为F-queue的队列之中,稍后虚拟机会自动创建一个优先级较低的finalizer线程去执行,在执行前该对象一直在堆区中,对象执行完毕之后,将这些finalizer对象从队列中移除,java虚拟机看到对象没有引用,就进行垃圾回收;

避免使用finalize关键字原因

1)finalize方法的调用时机具有不确定性,从一个对象变的不可达开始,到finalize()方法被执行,所花费的时间是任意长的。所以我们并不能依赖finalize()方法及时回收占用的资源,可能出现的情况是在我们资源耗尽之前,gc却仍未触发。
2)重写finalize()方法意味着延长了回收对象时需要更多的操作,从而延长了对象回收的时间。

gc操作

System.gc()和runtime.gc()用于提示jvm进行垃圾回收,立即开始回收还是延迟回收回收取决于jvm。

垃圾收集器

Serial和Serial Old垃圾回收器:分别用于回收新生代和老年代。
单线程运行,垃圾回收的时候会停止系统中的其他线程,让系统卡死不动,然后执行垃圾回收(stop the world)。
Par New和CMS垃圾回收器:分别用于回收新生代和老年代。
Par New是serial收集器的多线程版本,除了使用多线程外与serial收集器没有区别。
CMS收集器是一种获取最短停顿时间为目标收集器,它实现了垃圾回收进程与用户进程(基本上)同时工作。
G1垃圾回收器:统一收集新生代和老年代。 G1收集器是一款面向服务端应用的收集器,它能充分利用多cpu,多核环境,如下图所示L:

关于Minor GC与Major GC/Full GC?

新生代GC(Minor GC):发生在新生代的垃圾收集动作,Java对象大多朝生夕灭,所以Minor GC非常频繁,回收速度也比较快。
老年代GC(Major GC):只发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC,Major GC的速度一般会比Minor GC慢10倍以上(老年代垃圾回收的时间比较长,因为老年代的空间大,对象多)。
Full GC:收集整个java堆以及方法区的垃圾。

Java中对象的引用

1)强引用:要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
2)软引用:在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。
3)弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
4)虚引用:为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。

引用队列ReferenceQueue

1)reference对象已经不具有价值时,需要进行垃圾对象回收,避免内存占用。
2)当引用所引用的对象被回收后引用对象本身就会被加入引用队列。

Java内存模型

JMM(JAVA 内存模型)规定了所有的变量都存储在主内存中,每个线程都有一个私有的本地内存(local
memory),本地内存中存储了该线程以读/写共享变量的副本。不同线程无法直接访问对方工作内存中的变量。


总结

提示:这里对文章进行总结:

例如:秋招Java基础知识小总结吧。

【秋招之Java基础】

秋招之Java基础

` 提示:2023秋招八股之Java基础部门。


提示:这就开始了...

文章目录

  • 秋招之Java基础
  • 前言
  • 一、Java基础
    • 1.Java语言的跨平台性
    • 2.Java中的基本数据类型
    • 3.static关键字
    • 4.Obeject类
    • 5.面向对象与面向过程
    • 6.编译型语言与解释型语言
    • 7.Java三大特性
      • 多态的表现?
    • 8.Enumeration和Iterator的区别
    • 9.包装类
    • 10.访问修饰符
    • 11.代码编译过程?
    • 12.Java中类的生命周期
    • 13.类加载
      • 类加载过程
      • 类初始化时机
      • 类实例化过程
      • 双亲委托机制
        • 双亲委托机制好处
    • 14.final关键字
    • 15.抽象类和接口
      • 抽象类
      • 接口
      • 抽象类与接口的区别
    • 16.静态编译与动态编译
    • 17.动态加载
    • 18.深拷贝和浅拷贝
    • 19.Java中IO方式
      • IO流
      • 同步/异步,阻塞非阻塞(关于BIO/NIO/AIO)
    • 20.Java中的设计模式
  • 二、Java重点
    • 1.Java/Linux中进程通信方式
    • 2.Java中线程同步的方式
    • 3.怎么创建进程
    • 4.Java中的集合
      • 所有容器
      • HashMap
        • 什么是hash表?
        • 什么是hash?什么是hashcode?
        • 为什么使用hashcode?
        • Equals和hashcode的关系?
        • ==与equals
        • 为什么重写equals方法必须重写hashcode方法
        • 为什么先进行hashcode的对比?
        • HashMap的容量?加载因子值?设置这个值的原因?
        • HashMap扩容为原来的2倍原因?
        • HashMap具体的扩容计算?
        • HashMap的懒加载
        • 链表什么时候会变为红黑树?
        • 为什么要使用红黑树?
        • 为什么要判断等大于8之后才变成红黑树?
        • hash冲突后链表使用的头插法or尾插?
        • Object的hashcode()方法中为什么要使用31?
        • 位移、加减运算的效率比乘除运算的效率更高?
        • hashmap底层的hash值计算方法?
        • 为什么要无符号右移16位?
        • 解决hash冲突的方法
        • hashmap不同版本都会出现什么线程安全问题?
        • hashmap两个版本的区别?
        • ArrayList?
        • HashMap,ArrayMap,SparseArray的区别?
        • HashMap,LinkedHhashMap,HashTable,ConcurrentHashmap的区别?
    • 5.线程安全问题
      • 多线程如何实现线程安全?
      • 并发编程的三个概念?线程安全设计的几个概念?
      • volatile
      • synchronized
      • lock锁
      • synchronized与volatile的区别?
      • synchronized 和Reentrantlock(lock)锁的区别?
      • 类锁与对象锁?
      • 乐观锁与悲观锁?
      • CAS锁
      • CAS的缺陷
    • 6.Java中的内部类
    • 7.泛型
      • 泛型
      • 泛型擦除及原因
      • 限定通配符与非限定通配符
      • <? extends T>和<? super T>读写方面的不同
      • 编译时类型与运行时类型
    • 8.反射
      • 反射
      • 获取class对象
      • 反射常用的api
    • 9.动态代理
      • 静态代理
      • 什么是动态代理
      • Java中动态代理的实现方式
        • 两种动态代理方法的区别:
        • 为什么proxy.newProxyInstance(classloader—类加载器,interface—要实现的接口,invocationhandler—invocationhandler对象)需要传入classloader?
        • 反射缓慢的原因
    • 10.注解
      • 注解Annotation分为三类:
      • 系统注解
      • 元注解
      • 自定义注解
      • APT(Annotation Processing Tool)
    • 11.插桩
    • 13.序列化与反序列化
      • 序列的作用
      • Serializable与Externalizable
      • serializable与Parcelable
      • Serializable UID
      • JDK默认的序列化方式缺陷
      • 实际使用的序列化方式
    • 13.线程
      • 线程的5种状态
      • Wait,sleep,join,yield,interrupt
      • Java线程终止
      • Java线程创建
      • Runnable和Callable的区别
      • Future和Futuretask
      • Thread类中start()和run)()方法的区别
      • Try-catch-finally的返回值问题
      • Throw和throws的区别
      • Java中的异常
      • StackOverFlow
      • 线程池
        • 四种常见的线程池
        • 重要的参数
        • 核心线程与普通线程
        • 阻塞队列
        • 拒绝策略
        • 线程池的submit()与excute()
        • 关闭线程池
    • 11.JVM
      • 主要分为那几个区域并详细介绍
      • 常量池
      • 为什么将字符串常量池从方法区放到了堆中?
      • String、StringBuffer与StringBuilder
      • 为什么用元空间代永久代
      • GC垃圾回收
        • 如何判断一个对象是否可以被回收
        • 垃圾回收方法
        • 什么是大对象
        • 为什么有大对象要进行分配担保?
        • 为什么要分为eden区和survivor区?
        • 为什么要分为两个survivor区?
        • Java中对象的状态
        • finalize关键字的作用
        • 避免使用finalize关键字原因
        • gc操作
        • 垃圾收集器
        • 关于Minor GC与Major GC/Full GC?
        • Java中对象的引用
      • Java内存模型
  • 总结


前言

提示:这里可以添加本文要记录的大概内容:

例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

一、Java基础

1.Java语言的跨平台性

(1)Java语言的跨平台原理:Java程序通过编译为字节码,字节码文件可以在具有JVM的计算机上运行,JVM(Java虚拟机)中的解释器负责将字节码解释为特定的机器码进行运行。
(2)java语言跨平台的好处,一次编译多处运行(java在不同平台上不需要重新编译)。

2.Java中的基本数据类型

Byte、boolean(1个字节);
char、short(2个字节);
int、float(4个字节);
long、double(8个字节);

3.static关键字

static是一个修饰符,可以用来修饰成员方法、成员变量,另外还可以修饰代码块来优化代码性能,被static修饰的变量和方法不依赖对象,可以直接通过类名进行访问。
(1)static修饰的方法为静态方法,静态方法只能直接访问类的静态方法和静态变量,不可以直接访问类的非静态变量和非静态方法。
(2)Static修饰的变量被该类的所有对象共享。
(3)static修饰的代码块的特性是,只会在类初次被加载的时候执行一次(优化程序性能)。

4.Obeject类

Java中object类是所有类的父类,也就是说java中所有的类都继承了object类,所有的子类都可以使用object类的方法。
object类中常用的方法
clone()浅复制 ;
getClass()用于返回class类型的对象 ;
finalize()释放资源;
equals()判断对象内容是否相等;
hashCode()用于哈希查找;
toString();
Notify();
Notifyall();

5.面向对象与面向过程

面向过程主要以功能开发的函数为主,面向对象主要是抽象出类、属性及方法,然后通过实例化来进行操作;
面向过程封装的是功能,面向对象封装的是数据和功能;
面向对象还有继承性和多态性,更易扩展和复用;
面向过程:性能比较高(C)
面向对象:易扩展、易复用。(封装继承多态)(c++,python,java)

6.编译型语言与解释型语言

编译型语言

编译型语言是指程序在执行之前需要一个专门的编译过程,把程序源文件编译为机器语言的文件,运行时不需要重新编译,执行效率高,但缺点是,编译型语言依赖编译器,跨平台性差。
举例:C、C++

解释型语言

解释型语言是指源代码不需要预先进行编译,在运行时,要先进行解释再运行;解释型语言执行效率低,但跨平台性好。
举例:java(半编译半解释,底层是用C++写的),python(底层使用C写的)
解释型语言和编译型语言根本区别在于:编译型语言会将原文件通过编译生成目标程序,而解释型语言不会将整个源文件编译生成目标程序,解释型语言进行解释执行。

7.Java三大特性

封装
封装是把彼此相关数据和操作包围起来,抽象成为一个对象,变量和函数就有了归属,想要访问对象的数据只能通过已定义的接口、
继承
多态

Java 实现多态的 3 个必要条件:继承、重写和向上转型;
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:父类引用指向子类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。

多态的表现?

(编译时多态)重载与(运行时多态)重写;

重写与重载

重载:重载发生在同一个类中,在该类中如果存在多个同名方法,但是方法的参数类型、个数、顺序不一样,那么说明该方法被重载了。 (注意与权限修饰符和返回值类型无关;如果可以根据返回值类型来区分方法重载,那在仅仅调用方法不获取返回值的使用场景,JVM 就不知道调用的是哪个返回值的方法了。)
重写:重写发生在子类继承父类的关系中,父类中的方法被子类继承,方法名,参数完全一样,但是方法体不一样,那么说明父类中的该方法被子类重写了。
两同:方法名、参数列表相同;两小:返回值类型和抛出的异常范围要比父类更小;一大:子类的访问权限要比父类的访问权限大; link)

8.Enumeration和Iterator的区别

Enumeration只能读取集合数据,而不能对数据进行修改;Iterator除了读取外,也能对数据进行增删操作。
1)Enumeration 是 JDK 1.0 添加的接口。使用到它的函数包括 Vector、Hashtable等类,Enumeration 存在的目的就是为它们提供遍历接口。
2)Iterator 是 JDK 1.2 才添加的接口,它是为了HashMap、ArrayList 等集合提供的遍历接口。Iterator 是支持 fail-fast 机制的。
Fail-fast 机制是指 Java 集合 (Collection) 中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。例如:当某个线程 A 通过Iterator 去遍历某集合的过程中,若该集合的内容被其他线程所改变了,那么线程 A 访问集合时,就会抛出ConcurrentModificationException 异常,产生 fail-fast 事件。

9.包装类

(自动装箱——自动拆箱) Integer表示的是一个对象(存储的是引用的地址),int存储的是数值;
①基本数据类型计算的速度要远远快于拆箱装箱运算(所以要尽量避免拆箱装箱) ;
②对象是不能直接进行计算的,要先拆箱计算再装箱;
③装箱拆箱与强制类型转换效率差不多,属于同一个量级的;

10.访问修饰符

Public:可以被其他所有的类使用
Protected:可以被同包和子类使用
Default:可以被同包内使用
Private:只能被同类使用(就算实例化这个类还是不能调用这个类中的方法:实例名.方法名)
顺序:Public>Protected>Default>Private

11.代码编译过程?

12.Java中类的生命周期

1)类加载
2)连接
3)初始化
4)使用对象实例化:执行类中构造函数的内容,如果该类中存在父类JVM会先执行父类的构造函数,在堆内存中为父类的实例开辟空间并赋予默认的初始值,然后根据构造函数将真正的值赋予实例变量本身;
垃圾回收;
5)类卸载

13.类加载

类加载过程

链接: link
1)加载 ①. 将class文件中的类信息加载到方法区中 ②.在堆中实例化一个java.lang.class对象作为这个类的信息的入口(注意这一过程是在JVM之外实现的,是由类加载器实现的,类加载器会给每个java文件创建一个class对象,用描述类——作为类的信息入口);
2)链接
验证:确定类是否符合java语言规范;
准备:为静态变量分配内存并设置初始值(注意这里用static final修饰的会直接赋值,如static final b=10,那么默认就是10);
解析:把常量池中的符号引用转为直接引用(类加载之前,javac会将源代码编译为.class文件,这个时候javac是不知道被编译的类所引用的类、方法或者变量他们的引用地址在哪,所以只能用符号引用来表示)(直接引用则是具有引用地址的指针,被引用的类、方法或者变量已经被加载到内存中 link);
3)初始化:在类的初始化阶段将类的变量在准备阶段设置的默认值,修改成正确的初始值以及执行 方法的过程。 Java 虚拟机会通过加锁来确保 方法仅被执行一次。 只有当初始化完成之后,类才正式成为可执行的状态;

类初始化时机

link

1)创建类的实例(new xxxClass() Class.newInstance() constructor.newInstance())。
2)访问类中的某个静态变量,或者对静态变量进行赋值。
3)调用类的静态方法。
4)反射class.forName(“全限定类名”)。
5)子类的初始化,需要先对其父类进行初始化(接口除外)。
6)该类是程序引导人口(main入口或者test入口)。

类实例化过程

1、为新的对象分配内存。
2、为实例变量赋于默认值。
3、为实例变量赋于正确的初始值。

双亲委托机制

当某个特定类加载器在接收到加载类的请求时,首先从自己已经加载过的类中查询此类是否已经被加载过,如果已经加载则直接返回已经加载的类,否则将类加载任务委托给父类加载器,如果父类加载器还存在其父类加载器,则依次递归,如果父加载器可以完成加载就返回成功;如果父加载器无法完成此加载任务,就自己去加载。

启动类加载器—>扩展类加载器—>应用程序加载器—>自定义类加载器

双亲委托机制好处

链接: link
1)避免了重复加载;
2)保证程序安全,避免核心类被篡改(假设通过网络传递一个名为java.lang.Integer或者java.lang.objec的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改);

14.final关键字

final用于修饰类,方法、变量和引用形变量
类:被修饰的类,不能被继承 ;
方法: 被修饰的方法 ,不能被重写 ;
变量:被修饰的变量,不能被重新赋值 ;
引用型变量:当修饰变引用类型时,该变量指向的地址不能变,但是地址中的内容是可以变的;
Final修饰的变量可以先声明不设置初值,这也称为final空白,无论什么情况,编译器都可以保证final在使用之前必须被初始化;
Static final的值在类加载的“链接”的“准备”阶段被初始化;

15.抽象类和接口

抽象类

抽象类是为了被继承而存在
1)抽象类的访问修饰符为public或protected(缺省情况下默认为public);
2)抽象类不能用于创建对象(在抽象类中可以有构造方法,只是不能直接创建抽象类的实例对象,但实例化子类的时候,就会初始化父类,不管父类是不是抽象类都会调用父类的构造方法,初始化一个类,先初始化父类);
3)如果一个类继承于抽象类,则子类必须实现父类的抽象方法,如果子类没有实现父类的抽象方法,则必须将子类也定义为抽象类;

接口

接口作用:

1)实现代码间的耦合 ;
2)接口可以实现多继承,一个类可以实现多个接口;
3)接口可以使项目分离,面向接口开发,提高效率;

抽象类与接口的区别

相同点:
(1)他们都不能被实例化;
(2)接口的实现类和抽象类的子类都只有实现了他们抽象方法后才能被实例化;
不同点:
(1)一个类只能继承一个抽象类,却可以实现多个接口;
(2)接口实现的关键字为implements,而抽象类中的关键字为extends;
(3)接口中的变量只能为public static final,而抽象类中的变量可以被其他访问修饰符修饰;
(4)接口中方法只能用public abstract修饰,(在JDK8版本之后,接口可以实现默认方法和静态方法),抽象类可以有默认方法和静态方法;
总的来说:抽象类更偏向于提供一些默认的实现,接口更偏向于提供一种多继承;

16.静态编译与动态编译

link
静态编译 编译器在编译可执行文件的时候,将可执行文件需要调用的静态库中的内容提出来链接到可执行文件中,所以文件的运行不依赖于链接库;
优点:代码装载速度快,执行速度略快;
缺点:使用静态链接可执行文件体积较大。
动态编译 在编译时仅把部分需要的文件编译进可执行文件中,在运行的过程中还要依赖于动态链接库;
优点:可执行文件体积更小,加快编译速度,内容有改变时不需要重新编译;
缺点:依赖庞大的动态链接库;

17.动态加载

加载不存于程序本身的可执行文件;

Android中按照动态加载技术可以分为两种

1)动态加载so库
Android的NDK中其实就是使用了动态加载.so库并通过JNI调用其封装的方法,后者一般是由C/C++编译而成,运行在Native层效率比在虚拟机执行的java代码高很多,所以Android经常加载动态.so库来完成一些对性能要求比较高的工作,如T9搜索,Bitmap的解码,图片的高斯模糊等。
2)动态加载dex/jar/apk文件
Android项目中,所有的java代码都会被编译成dex文件,Android应用运行时,就是通过执行dex文件里的业务逻辑来工作的。

18.深拷贝和浅拷贝

1) 对于基本数据类型的成员对象,因为基础数据类型是值传递的,其中一个对象修改该值,不会影响另外一个,深浅拷贝的效果一致;
2)对于引用类型,浅拷贝只是把内存地址赋值给了成员变量,它们指向了同一内存空间,改变其中一个,会对另外一个也产生影响;

实现深拷贝的方法?

1)实现cloneable接口并重写Object类的clone()方法;
2)实现Serialiabl接口,通过对象的序列化和反序列化实现clone;

19.Java中IO方式

IO流

Java中的IO流分为两类,一类是字节流,一类是字符流,Java中其他的流也是由它们演变而来;
字节流:inputstream、outoutstream(可以处理所有的二进制对象,但它不能直接处理unicode字符);
字符流:Reader,Writer(只能处理字符流);

同步/异步,阻塞非阻塞(关于BIO/NIO/AIO)

I/O输入输出的对象可以是文件、网络、进程之间的管道等,在linux中都用文件描述符来表示;
事件包括:
——可读事件:当文件描述符关联的内核缓冲区可读,则触发可读事件;
——可写事件:当文件描述符关联的内核缓冲区可写,则触发可写事件;

link
BIO同步阻塞

BIO:一个连接一个线程(处理数量较少且固定的连接);


NIO同步并非阻塞

一个连接处理多个请求,客户端的请求都会被注册到多路复用器,多路复用器轮询有I/O请求就会进行处理(处理连接数据较多但连接较短;聊天服务器,弹幕、服务器通讯等);

NIO有三大核心部分:channel(管道)、buffer(缓冲区)、selector(选择器),在该种模式下,如果数据还不可读,线程可以去做其他事情;


AIO异步并非阻塞

当有有效请求时才启动线程,等操作系统完成后才通知服务端程序启动线程去处理(连接数据较多且较长;相册服务器充分调用OS参与并发);

多路复用IO

多路复用: link、link
Linux 下有三种提供 I/O 多路复用的API,分别是: selectpollepoll

select 和 poll并没有本质区别,它们内部都是使用「线性结构」来存储进程关注的事件集合。 在使用的时候,首先需要把关注的文件描述符集合通过select/poll系统调用从用户态拷贝到内核态,然后由内核检测事件,当有事件产生时,内核需要遍历进程关注文件描述符集合,找到对应的文件描述符,并设置其状态为可读/可写,然后把整个集合从内核态拷贝到用户态,用户态还要继续遍历整个集合找到可读/可写的文件描述符,然后对其处理。 select 和 poll 的缺陷在于,当关注的事件集合越大,集合的遍历和拷贝会带来很大的开销。

epoll 在内核里使用「红黑树」来关注进程所有待检测的 文件描述符,红黑树是个高效的数据结构,增删查一般时间复杂度是(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个文件描述符集合,减少了内核和用户空间大量的数据拷贝和内存分配。epoll使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的文件描述符集合传递给应用程序,不需要像 select/poll那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。而且,epoll 支持边缘触发和水平触发的方式,而select/poll 只支持水平触发,一般而言,边缘触发的方式会比水平触发的效率高。

水平触发与边缘触发

水平触发
1)对于读操作,只要缓冲区内容不空,就会返回读就绪;
2)对于写操作,只要缓冲区内容不满,就会返回写就绪;
边缘触发
1)对于读操作,当缓冲区由不可读变为可读时,才会触发;
2)对于写操作,当缓冲区由不可写变为可写时,才会触发;

20.Java中的设计模式

link
常用设计模式有:
创建:单例模式、建造者模式、工厂模式
行为:观察者模式、策略模式
构造:适配器模式、代理模式、外观模式、责任链模式

二、Java重点

1.Java/Linux中进程通信方式

管道(Pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信(本质是一个内核缓冲区);
信号量(semaphore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问(信号量有无名信号量和有名信号量,无名信号量一般用于共享内存空间的线程间的通信,而有名信号量可通过名字访问进程,实现进程间通信);
消息(Message)队列:消息队列是内核中存放消息的链表,又消息队列标识符进行标识,可以提供一种全双工通信连接;
文件共享:两个进程通过同一个文件来交换数据,文件共享通过对数据同步要求不高的进程通信(sharedpreference多进程情况下是不安全的,会出现数据丢失,sharedpreference在一个进程中会有自己的缓存);
内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它;
套接口(Socket):更为一般的进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。可用于不同机器之间的进程间通信;

2.Java中线程同步的方式

线程之间的通信主要用于线程同步,所以线程没有像进程通信中用于数据交换的通信机制;

互斥锁:同一时间是由一个线程可以获取锁,其他线程进入阻塞、休眠或者直接返回错误。
自旋锁:当线程尝试获取一个已经加锁的自旋锁时,线程不会进入休眠,而是一直处于忙等,等待锁被释放(用于锁被线程的持有的时间很短,比一个线程重新调度切换带来的成本小)。
读写锁:频繁使用互斥锁,线程只能串行读取数据,影响系统的并发性;读写锁有三种状态:读加锁、写加锁、解锁;读写锁保证了同一时间只有一个线程对共享资源进行修改,但可以允许多个线程对共享数据进行读操作(适应:读操作远多于写次数的场景)。
条件变量:(互斥锁的问题,当生产者变量还未满足消费者条件时,消费者先线程也会进行循环的加锁、解锁进行条件检测,耗费cpu资源)条件变量常与互斥锁一起使用,当满足条件时发送信号通知线程。

3.怎么创建进程

使用fork()可以创建进程,pid标记一个进程,pid=0为子进程,pid>0为父进程;
1)父进程死亡后,子进程将变成孤儿进程,由(init进程—守护进程)1号进程领养;
2)子进程死亡,但是父进程还没有死亡(此时没有进程给子进程回收资源),子进程成为僵尸进程(处理:将父进程kill,子进程将交给init进程);

4.Java中的集合

所有容器

Java中容器分为collections(list,set,queue)和map:
线程安全的有HashTable、vector、stack、concurrenthashmap、copyonwritehashset等;
线程不安全的有ArrsyList、linkedlist、hashmap、hashtable、arraymap、hashset、treeset等;

HashMap

什么是hash表?

hash表又称为散列表,它可以把关键字映射到表中的一个位置,来进行访问,加快查找的速度。

什么是hash?什么是hashcode?

Hash就是一个函数,通过一系列算法来得到一个值也就是hashcode,就对应这个元素在数组中的位置。

为什么使用hashcode?

主要就是为了查找的快捷性,他可以将一个元素定位到一个位置,我们可以直接根据对象确定它的位置,查找起来时间复杂度很低。

Equals和hashcode的关系?

1)先进行hashcode的比较,如果hashcode相同在进行equals比较两个对象。
2)如果equals为true,则两个对象hashcode一定相等。
3)如果两个对象的hashcode相等,不代表equals也为true,只能说明他们再散列表中存放在一个位置。

==与equals

为什么重写equals方法必须重写hashcode方法

判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。
在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。

为什么先进行hashcode的对比?

因为hashcode的一般是integer或string类型,它们不可变,创建之后直接缓存在内存中,比较起来效率更高。

HashMap的容量?加载因子值?设置这个值的原因?

HashMap的capacity 容量默认为16。 loadFactor 加载因子,默认是0.75(时间效率和空间效率综合考虑)。
threshold 阈值,阈值=容量*加载因子。默认12,当元素数量超过阈值时便会触发扩容。
HashMap扩容时一般是变为原来的二倍,所以它的长度一般是2的倍数。

HashMap扩容为原来的2倍原因?

1)hashmap计算key映射到的index时的计算公式为index=hashcode(key)&(length-1),所以hashmap的容量为2的倍数可以将key的hash值均匀的分布在数组上,减少hash冲突。
2)HashMap的初始容量是2的n次幂,扩容也是以2倍的形式进行扩容,可以使hashmap的容量始终是2的n次幂。

HashMap具体的扩容计算?

JDK7新建一个大小为原来2倍的数组,然后对每个元素计算hash值并插入(头插法) ;
JDK8中原位置的节点只有两种调整:
要么:保持原位置不动;
要么:原位置+扩容大小;

HashMap的懒加载

HashMap使用的是懒加载,构造完HashMap对象后,只要不进行put方法插入元素之前,HashMap并不会去初始化或者扩容table。而当首次调用put方法时,HashMap会发现table为空然后调用resize方法进行初始化,当添加完元素后,如果HashMap发现size(元素总数)大于threshold(阈值),则会调用resize方法进行扩容。

链表什么时候会变为红黑树?

链表长度超过8之后,还要判断当前容量的大小是否小于64,如果小于64要进行扩容而不是转化为红黑树。

为什么要使用红黑树?

红黑树为平衡二叉树,以加快检索速度,时间复杂度由原来的O(N),变为了O(logN)。

为什么要判断等大于8之后才变成红黑树?

红黑树的平均查找长度为logn,log8=3,当链表长度小于等于8时,链表的平均查找长度也是3,使用链表效率高,因为红黑树的维护成本比较高,插入一个元素要左旋或者右旋,当长度大于8时,红黑树的查找效率就高于链表。

hash冲突后链表使用的头插法or尾插?

jdk7中当元素冲突时采用的是头插法;(原因是效率更高,否则还要遍历冲突链然后插在尾部)
jdk8中当元素冲突时使用的是尾插法;(因为要遍历链表查看链表长度是否超过8,如果超过长度要将其变为红黑树)

Object的hashcode()方法中为什么要使用31?

——原因:如果使用较小的质数,hash范围会很小,很容易造成hash冲突;如果使用较大的质数,很有可能会溢出,所以经过多次测试选择使用了31。
——在JVM中最有效的方式就是位运算了,31*i相等于(i<<5)-i,计算效率更高。

位移、加减运算的效率比乘除运算的效率更高?

加减和位移运算都是运算器最基本的操作,它们的时间基本相同,都只需要一个时钟周期,但是乘法和除法一般需要多个时钟周期,但是编译器优化的时候可以将乘除法用移位运算来代替。

hashmap底层的hash值计算方法?

hashcode()是Object类中提供的方法;
JDK8中底层通过调用hashcode()方法生成初始hash值h1,h1无符号右移16位得到h2,之后将h1与h2进行异位或运算最终得到hash值h3,最后将h3与(length-1)进行按位与得到hash表的索引;

为什么要无符号右移16位?

因为数组的大小一般比较小,比如数组长度为16时,只有低4位在进行运算,如果产生的hashcode值高位变化很大,而低位变化很小,有很大概率会产生hash冲突,所以为了更好的散列,将hash值的高位也利用起来。

解决hash冲突的方法

重哈希法
链地址法
开放地址法(线性探测再散列、二次探测再散列、伪随机探测再散列)

hashmap不同版本都会出现什么线程安全问题?

hashmap本身是多线程不安全的,所以无论是JDK7还是JDK8都是多线程不安全的;
在JDK7中的头插法是在扩容时会出现,环形链表或元素丢失;
在JDK8中使用尾插法,扩容时可能出现元素被覆盖丢失红黑树成环;

hashmap两个版本的区别?

ArrayList?

HashMap,ArrayMap,SparseArray的区别?

ArrayMap

link ArrayMap中有两个数组,一个用于存放key的hashcode值,另一个用于存放键值对key和value,而且Arraymap是有序的,每次插入删除都会保证它的有序性,算是以时间换空间了。但在大量数据时,使用hashmap的效率更高一点,因为arraymap使用的是数组,扩容或者删除的时候效率都很低。
arraymap是二分查找和实时扩容机制的,以时间换时间; arraymap解决hash冲突的方式是追加,把数据全部后移一位;
arraymap达到oldsize大小会进行扩容,完成扩容需要将老数组拷贝在新数组中,并释放原来的内存;arraymapy是非安全的类;

SparseArray

Sparsearray中的key为int类型(避免了装箱和拆箱),value是object类型,key和value分别存放在两个数组中,且位置对应,key数组的int值是按照顺序排列的,查找的时候采用的是二分查找,效率很高。Add的时候会移位,remove的时候不一定会进行移位,把某个值标记为delete,如果下次有符合的值直接放到该位置。
android内部是推荐使用这种数据结构的,由于android对应的设备内存相对来说比较小,而sparsearray恰好是存储的内存相对来说较小,其内部对数据采取了压缩的方式来表示稀疏数组的数据,从而节约空间。

HashMap,LinkedHhashMap,HashTable,ConcurrentHashmap的区别?

hashmap
key与value值可以为null;
LinkedHhashMap
HashMap的低层实现在java1.7 中是数组+链表,java1.8中是数组+链表+红黑树
1)LinkedHashMap在HashMap的基础上加了双向链表,实现有序性;
2)LinkedHashMap是非线程安全的,HashMap也是非线程安全的;
3)LinkedHashMap中允许key值和value值都为null;
HashTable
1)key与value值不能为null;
2)使用Synchronized 实现多线程安全;
特点:在修改数据时需要锁住整个HashTable,所以效率比较低;
ConcurrentHashmap
1)key与value值不能为null;
2)多线程安全
——如何保证多线程安全?
JDK7中使用的分段锁,锁用的是segment+reentrantlock;
JDK8中使用的是node+CAS+synchronized锁;
——“锁升级”的原因?
使用CAS+synchronized的方式加锁的对象是每个链条的头结点,也就是锁定的是冲突的链条,所以提高了并发程度;
使用reentrantlock则需要节点继承AQS来获取同步支持,增加了内存开销;synchronized是JVM直接支持的,JVM能够在运行时做出响应的优化策略,锁粗话、锁消除、锁自旋;
——什么时候使用CAS锁,什么时候使用synchronized锁?
读时(get操作):
没有使用同步机制,也没有使用CAS方法,支持并发操作(这是因为node成员使用volatile修饰的,所以可以直接get;注意concurrenthashmap的数组也是用volatile修饰的,主要是为了保证在数组扩容的时候保证可见性);
写时(put操作):
(1)当发现当前节点元素为空,则通过CAS来存储该元素;
(2)当发现当前节点元素不为空,则使用synchronized关键字来锁住当前节点,并进行值的设置;

5.线程安全问题

多线程如何实现线程安全?

①线程间不要跨线程访问共享变量; ②使共享变量是final类型的; ③将共享变量的操作加上锁;

并发编程的三个概念?线程安全设计的几个概念?

①可见性(volatile); ②原子性(lock和synchronized); ③有序性(volatile和synchronized);

volatile

原子性

对于写操作:对变量更改完之后,会立即写回到内存中;
对于读操作:对于变量的读取要从内存中读取,而不是缓存;
原理:
volatile关键字会开启总线的mesi的缓存一致性协议,多个线程从主内存读取一个数据到各自的高速缓存,当其中某个线程改版了缓存中的数据,该数据会马上同步回主内存,其他前程通过总线嗅探机制,感知数据的变化从而更新数据;

有序性

Volatile禁止指令重排优化,主要是通过在操作前后加入内存屏障实现的,内存屏障再jvm中可以分为4类:

重排的原因?

为了提高性能(或者说为了提高CPU的利用率吧)
一般有三种重排序:
①编译器优化的重排序;②指令级并行的重排序;③内存系统中的重排序;

重排的规则?

数据依赖性
as_if_serial(指的是不管怎么重排序,单线程程序的执行结果都不能被改变)

什么是happen_before?

先行发生原则,当A操作发先行发生于B操作,则在生B操作的时候,操作A产生的影响可以被操作B感知到,“影响”包括内存中的共享变量的值,发送的消息,调用的方法等。

synchronized

原理

Synchronized底层是monitor监视器;
对于同步代码块,会在代码块的前后产生一个monitorenter和moniterexit指令,来标识这是一个同步代码块。
对于同步方法,方法上还多了ACC_SYNCHRONIZED标识符,JVM根据该标识符实现方法的同步,当方法被调用时,调用指令会检查方法该方法的ACC_SYNCHRONIZED是否被设置,如果被设置了,执行先后才能将先获取Monitor获取成功之后才能执行方法体,方法执行之后再释放Monitor。

synchronized锁优化

link
无锁;
偏向锁:它会偏向第一个访问锁的线程,在运行过程中只有一个线程访问同步锁,不存在多线程竞争情况。(使用CAS获取偏向锁失败,则说明有竞争,此时锁会升级为轻量级锁。)
轻量级锁:当发生锁竞争时就会升级为轻量级锁,轻量级锁为自旋锁,减少线程的频繁切换,减少上下文切换的开销。(自旋等待一定次数后,或者说自旋的开销超过进行上下文切换的开销,锁会升级为重量级锁。)
重量级锁:synchronized升级为重量级锁之后,线程再竞争资源失败,就需要进行上下文切换并进入阻塞状态。

为什么要进行锁优化?锁优化的方法?

因为监视器锁(Monitor)是依赖于操作系统的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到内核态,状态转换需要花费很多的处理器时间。
自旋锁和适应自旋锁(自旋锁避免了线程频繁的挂起和恢复,因为线程的挂起和恢复都需要从用户态进入到内核态,这个过程是比较慢的(耗费资源),所以通过自选的方式)(适应自旋锁自旋的此时不再是固定的值,而是一个动态可以改变的值,这个值会用过自旋锁获取锁的状态来决定自旋的次数)
锁消除 锁粗化
无锁、偏向锁、轻量级锁、重量级锁

lock锁

AQS机制是众多锁的基础,ReentrantLock、Semaphore、CountdownLatch的实现都依赖AQS。
AQS其实就是AbstractQueuedSynchronized,多个线程竞争一个state,这个state用volatile修饰,保持state对各个线程的可见性,AQS使用一个FIFO的队列进行线程管理,线程可以通过CAS去改变state,当state为0,则说明当前对象锁已经被获取,其他线程只能进入自旋等待

synchronized与volatile的区别?

①volatile是只能用来修饰变量,synchronized可以用来修饰变量、方法、代码块。
②Volatile可以保证可见性但不能原子性,synchronized可以保证操作的可见性与原子性。
③volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。(因为synchronized只有一个线程可以获取对象的锁,其他线程阻塞)

synchronized 和Reentrantlock(lock)锁的区别?

①synchronized是java内置关键字在jvm层面,Reentrantlock(lock是一个接口)是个java类;
②synchronized会自动化释放锁,Reentrantlock(lock)需要在finally手动释放锁;
③synchronized的锁可重入、不可中断,为非公平锁,而ReentrantLock(lock)锁可重入、可中断,默认为非公平锁但是可以设置为公平锁。

关于synchronized与reentrantlock(基于AQS自旋锁实现)的性能比较?

在低并发时,由于synchronized可以进行锁优化,所以synchronized的性能更高;
在高并发状态时,synchronized会频繁进行线程的切换,而线程切换需要操作系统的帮助,需要从内核态到用户态以及用户态到内核态的切换,资源耗费更多。

可重入?公平锁?

可重入:指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。
公平锁与不公平锁:公平锁会进行频繁的线程切换,耗费资源更多;非公平锁可能出现“饥饿”情况。

类锁与对象锁?

①类锁是对静态方法使用synchronized关键字后,无论是多线程访问单个对象还是多个对象的sychronized块,都是同步的。
②对象锁是实例方法使用synchronized关键字后,如果是多个线程访问同个对象的sychronized块,才是同步的,但是访问不同对象的话就是不同步的。

public class SychronyzedDemo {  
public synchronized  void demo1(){}  //方式1:锁对象方法  
public void emo2(){synchronized (this){}  //方式2:锁对象代码块  
public synchronized static void demo3(){}  //方式3:锁类方法
public  void emo4(){synchronized (SychronyzedDemo.class){}} }//方式4:锁类代码块  

乐观锁与悲观锁?

乐观锁认为并发状态下不会出现冲突,只有在提交修改的时候才进行比较对状态进行检测;
悲观锁认为在并发状态下会出现并发冲突,所以进行各种操作之前都会获取对象锁,阻塞其他线程,直到锁被释放。
常见的乐观锁有CAS和AQS;悲观锁有synchronized;

CAS锁

CAS(compare and swap)比较并交换,是一种实现并发算法时常用到的技术,CAS具体包括三个参数:当前内存值V、旧的预期值A、即将更新的值B,当且仅当预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。
CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时 使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并开始自旋等待。

CAS的缺陷

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题:
1)ABA问题:因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A,就会变成1A-2B-3A。
2)循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
3)只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就需要用锁。

6.Java中的内部类

主要分为四类:
①成员内部类(成员内部类可以调用外部类的方法和属性,但是外部类想要访问成员内部类的方法和属性需要先实例化成员内部类)
②静态内部类(静态内部类只能直接访问外部类的静态属性和变量)(最大的优点就是:在创建静态内部类时,不需要外部类对象的引用;)
③匿名内部类(匿名内部类就是没有名字的内部类,使用内部类有一个前提条件:必须继承一个父类或者实现接口,匿名内部类也是唯一一个没有构造器的类)link
④局部内部类(定义在代码块中的类,它的作用范围就是他所在的代码块中,最多可以用final修饰)

内部类的作用:

①间接实现了多继承 ;
②将有一定逻辑关系的类组织在一起,可读性;

7.泛型

泛型

泛型就是指数据类型参数化;

泛型擦除及原因

在进入JVM之前,与泛型相关的信息都会被擦除,这些信息被擦除之后相应的类型就会变成泛型类型的上限,如果没有指定就是Object;
原因:避免创建太多类而造成的运行时过渡消耗;

限定通配符与非限定通配符

限定通配符有两种:<? extends T>和<? super T>其中:

<? extends T>确保泛型只能T本身或者T的子类或者子类的子类,以此来设定泛型的上界; <? super T>表示只能是T的父类,以此来设定类型的下界; 非限定通配符即<?>可以是任何类型(其实相当于List<? extends Object>)

<? extends T>和<? super T>读写方面的不同

链接: link

<? extends T>只能读不能写;(因为编译器只知道它是T或者T的子类,具体是什么类型它不知道)。 <? super T>只能存不能读;(因为元素是T的基类,所以存粒度比T小的都可以存放),不影响往里存,但往外取只能放在Object对象里。

编译时类型与运行时类型

编译时类型由声明该变量的类型决定,运行时类型由该变量指向的对象决定;
例如:Animal a = new Bird();
Animal 就是编译时类型,Bird就是运行时类型,这就是多态的一种表现;
当使用该对象引用进行调用时,规则为:对象调用编译时类型的属性和运行时类型的方法;

8.反射

反射

反射就是再运行过程中,对于任意一个类都可以知道这个类的属性和方法;对于任意一个对象,都可以调用它的属性和方法;

获取class对象

一个class对象表示一个运行中的class字节码文件,class对象是在类加载时JVM自动创建的,一个类在JVM中只会有一个class对象。


三种方法的不同:

相同:得到的都是该类的java.lang.Class对象,它是类加载的产物;
不同:
——类名.class:JVM将使用类加载器,将类装入内存(如果类还没有转入到内存中的话),不做类的初始化,返回class对象(编译时)
——Class.forName(“类名字符串”)将类转入内存,并做类的静态初始化,返回Class的对象(编译时)
——对象.getClass()对类进行静态初始化、非静态初始化;返回引用运行时真正的对象所属类的class对象(运行时)

反射常用的api

Field:提供类的属性以及访问类的属性的接口;
Method :提供类的方法信息以及访问类的方法的接口;
Constructor:提供类的构造方法的信息以及访问构造方法的接口;
Proxy:提供用于创建动态代理类和实例的静态方法;

clazz.getConstructor()  获取共有构造方法
clazz.getDeclaredConstructor()  获取私有构造方法
setAccessible(true);  //设置访问权限,忽略修饰符;

9.动态代理

静态代理

静态代理:代理类为其它对象提供一种代理来控制这个对象的访问。Proxy保存一个引用使得代理类可以访问实体,并提供一个与subject接口相同的接口,这样代理类就可以用来代替实体;
缺点:proxy与realsubject的功能本质上是相同的,proxy只是起到中介作用,导致系统类规模增大不易维护;

什么是动态代理

动态代理就是在程序运行期间,创建目标对象的代理对象,并对目标对象的方法中进行功能增强的一种技术。

Java中动态代理的实现方式

动态代理:link
1)JDK动态代理:利用反射机制生成一个实现代理接口的匿名类,在具体方法前调用invocationhandler的invoke()方法来转发对方法的处理;(创建快,但是运行慢)
2)CG LiB动态代理:使用ASM开源包,将代理对象类的class文件加载进来,通过修改其字节码生成子类来处理;(创建慢,运行快)

两种动态代理方法的区别:

(1)JDK动态代理是基于接口实现的,不需要添加依赖包,可以平滑的支持JDK版本的升级;
(2)CGLIB不需要实现接口,可以直接代理普通类,需要添加依赖包,性能更好;


为什么proxy.newProxyInstance(classloader—类加载器,interface—要实现的接口,invocationhandler—invocationhandler对象)需要传入classloader?

(1)需要校验传入的接口是否可以被当前类的类加载器加载,假如无法加载,就证明这个接口与类加载器不是同一个;
(2)需要类加载器根据生成的类的字节码去通过defineClass方法生成类的class文件,也就是说,没有类加载器的话是无法生成代理类的;

反射缓慢的原因

链接: link、link
1)反射涉及到动态加载的类型,无法进行优化;
2)需要检查方法可见性,校验参数等;
3)变长参数导致的Object数组,基本数据类型的装箱拆箱等;

10.注解

注解用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联,为程序元素(类、方法、成员变量)加上更直观的说明。

注解Annotation分为三类:

1)JDK内置系统注解
2)元注解
3)自定义注解

系统注解

@Override 是一个标记类型注解,用于提示子类要复写父类中被 @Override 修饰的方法。
@Deprecated 也是一个标记类型注解,用于标记过时的元素。比如如果开发人员正在调用一个过时的方法、类或成员变量时,可以用该注解进行标注。
@SuppressWarnings 并不是一个标记类型注解,它可以阻止警告的提示。它有一个类型为 String[] 的成员,其值为被禁止的警告名。
@SafeVarargs 是一个参数安全类型注解。它的目的是提醒开发人员,不要用参数做一些不安全的操作。它的存在会阻止编译器产生 unchecked 的警告。

元注解

用于修饰注解的注解,通常用在注解的定义上。

@Documented-注解是否将包含在JavaDoc中

@Retention-什么时候使用该注解 ·
@Retention(RetentionPolicy.SOURCE) //做一些检查性的操作,注解仅存在源码级别,在编译的时候丢弃该注解
@Retention(RetentionPolicy.CLASS) //要在编译时进行一些预处理操作,注解会在class文件中存在 @Retention(RetentionPolicy.RUNTIME) //注解会在class字节码文件中存在,jvm加载时可以通过反射获取到该注解的内容 如:retrofit;

@Target-注解用于什么地方 ·
@Target(ElementType.TYPE) // 接口、类、枚举、注解 ·
@Target(ElementType.FIELD) // 属性、枚举的常量 ·
@Target(ElementType.METHOD)// 方法 ·
@Target(ElementType.PARAMETER) // 方法参数 ·
@Target(ElementType.CONSTRUCTOR) // 构造函数 ·
@Target(ElementType.LOCAL_VARIABLE)// 局部变量 ·
@Target(ElementType.ANNOTATION_TYPE)// 该注解使用在另一个注解上 ·
@Target(ElementType.PACKAGE) // 包

@Inherited-是否允许子类继承该注解

自定义注解

1)自定义注解使用@interface关键字定义
2)自动继承java.lang.annotation.Annotation接口
3)配置参数只能为八大基本数据类型、string、class、annotation及相应组合
4)配置参数声明的格式如下: 类型 变量名()[default 默认值];
5)定义中定义了默认值的参数可以不指定值,但是没有默认值的一定要指定值;
6)自定义注解的作用是增强反射效果,在反射中会获取带有自定义注解的元素,根据注解的值决定怎么处理,(这里一般说的是运行时注解,一般都是采用反射处理。

APT(Annotation Processing Tool)

link、link
APT是javac提供的一种工具,它在编译时对代码进行检测,根据注解自动生成代码,这段代码是根据用户编写的注解处理逻辑去生成的,最终将生成的新的源文件和原来的源文件共同编译(APT并不能对源文件进行修改,只能生成新的文件,例如往原来的类中添加方法)。

APT处理有三个元素:注册处理器+注解处理器+代码生成;
(1)在META-INF注册(为什么要在META-INF中注册,在编译时Java编译器Javac会去META-INF中查找实现了abstractprocesser的子类,并且调用该类的process函数,最终生成.java文件);
(2)创建一个继承abstractprocesser类就可以,并在process方法中获取我们需要处理哪些注解;
(3)使用stringbuilder或者javapoet生成代码;

11.插桩

插桩:通过某种策略在一段代码中插入或替换另一段代码。这里的代码可以分为源码和字节码,我们一般所说的就是字节码插桩,就是在.class文件转为.dex文件之前就修改;
应用场景
——通过插桩,扫描每一个class文件,并针对特定规则进行字节码修改从而达到监控每个方法耗时的目的;
——无痕埋点,性能监控;
插桩的实现步骤
——AMS提供了两种API来生成和转换已编译类;一个是核心API,基于事件形式来表示类;另一个是树API,基于对象形式来表示类;
——Transform API,在android在将class转换为dex之前给我们留个接口,在这个接口中通过插件方式来修改class文件;

13.序列化与反序列化

序列化:将java对象转为字节序列;
反序列化:将字节序列转为java对象;

序列的作用

1)java中对象保存在“堆”中,堆是一个内存空间,不能长期保持,程序关闭对象就有可能被回收,但是我们有时候保存对象里面的信息的,这个时候就需要对对象进行持久化;
2)各个服务之间调用对象就需要把对象序列化用于网络上传输;
3)Java对象本质上是class字节码,很多机器不能根据这个字节码识别这个java对象,但是序列化是序列化成二进制流。

Serializable与Externalizable

1)serializable序列化不会调用默认的构造器,Externalizable会调用默认的构造器;
2)Serializable会把对象所有的属性都序列化和反序列化来进行保存和传递;Externalizable需要通过接口中的方法(writeExternal()和readExternal())指定需要序列化的属性;
3)Serializable如果某字段被transient修饰则不会被序列化;Externalizable中在方法(writeExternal()和readExternal())中指定的属性即使被transient修饰还是会被序列化;

serializable与Parcelable

1)serialable是java提供的可序列化接口,parcelable是android中提供的可序列化接口
2)Serializable在反序列化的过程中使用了反射机制,所有会产生大量的临时变量,从而导致频发的GC,并且它是通过IO流的形式将数据写入到硬盘或传到网络上。Parcelable使用binder机制在内存中直接读写,效率更高。

Serializable UID

用于序列化对象的版本控制,如果新版本中这个值修改了,新版本就不兼容旧版本,反序列化时就会抛出异常。

JDK默认的序列化方式缺陷

链接: link
1)无法跨语言(java序列化为java语言的私有协议,其他语言不支持,但是在跨进程的服务中经常会使用c++或其他语言,所以阻碍了它的应用)。
2)序列化之后的码流太大。
3)JDK默认的序列化的性能较低。

实际使用的序列化方式

链接: link
Gson、xml、protobuf
Xml通用的重量级的数据交换格式,以文本结构存储。
Json是一种通用和轻量级的数据交换格式,以文本结构存储;相比来说,gson格式为压缩的,体积更小,传输速度更快,描述性比xml更差一点。)
Protobuf是一种独立和轻量级的数据数据交换格式,以二进制进行存储可读性不强,序列化后数据大小更小,速度更快。
Xml多用于配置文件,json用于数据交互。

13.线程

线程的5种状态

Java线程具有五中基本状态:
①新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = newMyThread();
②就绪状态(Runnable) :当调用线程对象的start方法,线程即进入就绪状态。此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start此线程立即就会执行;
③运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。
④阻塞状态(Blocked) :处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用以进入到运行状态。
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1).等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2).同步阻塞―线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3).其他阻塞―通过调用线程的sleep0或join()或发出了IO请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
⑤死亡状态(Dead):线程执行完了或者因异常退出了run方法,该线程结束生命周期。

Wait,sleep,join,yield,interrupt

①Wait,释放cpu资源,会释放锁,需要被拥有对象锁的线程唤醒(notify或notifyall);
②sleep,释放cpu资源,不释放同步锁(类锁/对象锁),等待指定时间后自动醒来;
③join,释放cpu资源,不释放所持对象锁(object),等待调用join方法的线程结束才继续执行本线程。
④yield,释放cpu资源,不释放对象锁,使线程进入就绪状态;

wait

线程notify()唤醒之后,是从上次阻塞的代码继续往下执行还是从头开始执行? 链接: link
从上次阻塞的位置开始往下执行,也就是从wait()方法之后开始执行的。

join


关于Join: link/ link
①Join使用了synchronized锁,会使主线程持有子线程的对象锁,join内部使用了wait()和notifyall(),wait()会使主线程会释放thread线程对象的锁,进入等待状态。最后,threadA线程执行结束,JVM会调用lock.notify_all(thread);唤醒持有threadA这个对象锁的线程,也就是主线程,所以主线程会继续往下执行.
②但是该线程持有的obj对象不会被释放;

Java线程终止

链接: link
①程序运行结束,线程终止。
②使用退出标志,退出线程。
③interrupt方法来中断线程,可以分为两种情况:
a 线程处于阻塞状态: 当线程使用了sleep/wait/socket中的receiver或accept等方法时,此时调用线程的interrupt方法,会抛出interruptException;阻塞中的方法抛出异常,通过代码捕获,然后break跳出循环状态,才能正常结束run方法。
b 线程是未阻塞状态:使用isinterrupt()方法判断线程的中断标志来退出循环;使用interrupt方法,会把中断标志设置为true,和使用自定义标志来控制循环是一样的。
④stop 暴力终止线程。释放锁,容易造成数据的不一致。这个方法是不安全的,不推荐使用。

使用stop方法不安全的原因?

调用stop 方法后,会使线程释放所有的锁;一般任何进行加锁的代码块,都是为了保证数据的一致性。
如果使用stop方法导致,子线程释放了所有锁,被保护的数据可能会不一致。

Java线程创建

1)继承Thread类的方式创建线程;
2)使用Runnable、Callable接口创建线程(注意:Java中真正能创建新线程的只有Thread类对象,它实现Runnable的方式,最终还是通过Thread类对象来创建线程,)
3)使用线程池(提前创建好多个线程,放入线程池中,使用时直接获取,使用完毕后放入线程池,可以避免频繁的创建、销毁操作)

Runnable和Callable的区别

Callable任务执行任务之后可以返回值,runnable执行任务之后没有返回值;
Callable方法抛出异常,runnable不能抛出异常。

Future和Futuretask

Callable 接口相比于 Runnable 的一大优势是可以有返回结果,那这个返回结果怎么获取呢? 就可以用 Future 类的 get方法来获取 。因此,Future 相当于一个存储器,它存储了 Callable 的 call 方法的任务结果。除此之外,我们还可以通过Future 的 isDone 方法来判断任务是否已经执行完毕了,还可以通过 cancel 方法取消这个任务,或限时获取任务的结果等,总之Future是主线程可以跟踪进度以及其他线程结果的一种方式。

Futuretask实现了Future和Runable,并将它们的功能结合在一起;FutureTask表示一个可以取消的异步运算,它有启动和取消、查询运算是否完成和取回运算结果等方法。在主线程执行比较耗时任务时,但又不想阻塞主线程时,可以把这些作业交给后台的Futuretask对象完成:

Thread类中start()和run)()方法的区别

Start()方法用于启动新创建的线程,start()内部调用了run()方法,直接调用run()方法则还是在原来的线程中运行。

Try-catch-finally的返回值问题

1)无论try、catch中有无异常,finally块中的代码都会执行;
2)如果finally中没有return语句,若try中有异常,则返回catch中的return值,反之,则返回try中的异常,在这种情况下,return语句要在finally后执行。
3)无论try、catch中有无异常,如果finally块中有return语句,最后返回的是finally的return值。
什么情况下不会执行finally语句?
——如果在try或catch语句中执行了system.exit(0);
——在执行finally之前jvm崩溃了
——try语句中有死循环等

Throw和throws的区别

1)throw 在方法体内使用,throws 在方法声明上使用;
2)throw 后面接的是异常对象,只能接一个;throws后面接的是异常类型,可以接多个,多个异常类型用逗号隔开;
3)throw 是在方法中出现不正确情况时,手动来抛出异常,结束方法的,执行了throw 语句一定会出现异常。而 throws是用来声明当前方法有可能会出现某种异常的,如果出现了相应的异常,将由调用者来处理,声明了异常不一定会出现异常。

Java中的异常

Throwable分为Exception和Error;
Error为内存溢出,系统崩溃即OOM或者StackOverflowError等;
Execptiont又分为Checkedexecption和RuntimeException;
Checkedexecption:Java中认为Checkedexecption都是可以被处理的异常,所以java程序必须显示处理Checkedexecption,如果程序没有处理checked异常,该程序编译时会发生错误无法编译。
处理Checkedexecption主要有两种方法:
1)当前方法知道如何处理异常,则用try…catch来处理。
2)当前方法不知道如何处理,则在定义该方法时声明抛出异常。

StackOverFlow

递归循环时可能出现StackOverFlow。

一个栈大概有多大,为什么?

可以通过CreateThread函数中的dwStackSize参数为新线程指定的栈空间大小。 Linux平台的栈默认大小应该是8192KB,
Windows平台的栈默认大小应该是1024KB, 栈太小可能会导致空间不足, 分配失败。

线程池

Java 给我们提供了 threadpoolExecutor 接口来使用线程池。

四种常见的线程池

SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
scheduleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
FixedThreadPool:一个有指定的线程数的线程池,里面有固定的线程数量,响应的速度快。有核心线程,核心线程的即为最大的线程数量,没有非核心线程。
cachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。

重要的参数

corePoolSize:核心线程池的大小
maximumPoolSize:最多线程量
workQueue:阻塞队列
keepAliveTime:空闲线程的存活时间
RejectedExecutionHandler拒绝策略

核心线程与普通线程

核心线程即使处于空闲状态也不会被回收;普通线程的空闲时间超过keepalivetime后就会被回收。

阻塞队列

ArrayBlockingQueue:基于数组结构的阻塞队列(有界);
LinkedBlockingQueue:基于链表结构的阻塞队列(无界); PriorityBlockingQuene:支持线程的优先级排序;
synchronousQueue:不存储任何元素;当一个线程调用了put方法时,发现队列中没有take线程,那么put线程就会阻塞,当take线程进来时发现有阻塞的put线程,那么他们两个就会匹配上,然后take线程获取到put线程的数据,两个线程都不阻塞。反之一个线程调用take方法也会阻塞线程,当一个调用put方法的线程进来后也会与之匹配。链接: link

拒绝策略

AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出异常 RejectedExecutionException (属于RuntimeException),让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
CallerRunsPolicy,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
CallerRunsPolicy策略更佳原因: 第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。 第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。

线程池的线程数怎么确定?

要看任务的性质:链接: link
1)cpu密集型任务(大量计算、对视频进行高清解码等全靠cpu计算)
线程数=cpu总核心数+1(+1是为了利用空闲等待)(数量少减少线程的频繁切换)
2)IO密集型任务(网络请求、数据库数据获取、磁盘数据加载等) 线程数=cpu总核心数*2+1(或者2~3倍都可以)

线程池的submit()与excute()

submit最终还是调用excute()方法去执行任务的,不同的是submit会返回future对象;future对象可以用于获取一个结果,也可以取消执行;

关闭线程池

Shutdown()或者shutdownNow()
shutdown不会关闭正在执行任务的线程;
shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。

11.JVM

主要分为那几个区域并详细介绍


Java栈:存放的是一个个的栈帧,每个栈帧对应一个被调用的方法(包含局部变量、返回信息、中间结果等)。
本地方法栈:与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。
:Java中的堆是用来存储对象(实例化对象以及class对象)或数组。
程序计数寄存器:它保存的是程序当前执行的指令的地址。
方法区:存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

常量池

为什么将字符串常量池从方法区放到了堆中?

因为永久代的回收率很低,一般只有在老年代、永久代不足时才会触发full GC进行回收,所以这就导致字符串常量回收效率不高,而我们在开发过程中会有大量的字符串被创建,放到堆里,能及时回收内存。

String、StringBuffer与StringBuilder

Java中同一个字符串常量在堆中只创建一个,可以减少字符串的重复创建。

为什么用元空间代永久代

元空间不在虚拟机内存中,而是使用的本地内存,因此,在默认情况下,元空间的大小仅受本地内存限制。

GC垃圾回收

垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。

如何判断一个对象是否可以被回收

1)引用计数法(任何引用计数为0的对象实例可以被当作垃圾收集)
2)可达性分析算法(通过判断对象的引用链是否可达来决定对象是否可以被回收)
可达性分析算法中以GC Roots作为起始点,可作为GC Roots的对象包括:
①java栈帧中引用的对象
②静态属性引用的对象
③常量引用的对象
④本地方法栈中引用的对象
⑤synchronized持有的对象

垃圾回收方法

1)标记-清除法
2)复制法
3)分代收集法
新生代:老年代=1:2;老年代的特点是只有少量对象需要回收,新生代中有大量对象需要回收,对于老年代使用标记-清除法,对于新生代使用复制法进行回收。
新生代:局部变量、临时变量等
老年代:单例对象、缓存对象
永久代(JDK1.8):字符串常量池、加载过的类信息;
新生代又分为eden区与survivor1区,Eden区与survivor0和survivor1的比例是8:1:1,两个survivor区总有一个是空闲的,每次只使用eden区和一个survivor,一次GC操作把eden和survivor区中存活的对象放到另一个空闲的survivor中,然后清除原来的eden和survivor区。当对象的年纪>15会被放入老年代。
当survivor区不够用的时候(有大对象)会进行分配担保,把新生代的对象提前转移到老年代中。

什么是大对象

需要占用大量连续的内存空间的java对象为大对象,比如很长的字符串和数组,数据库查询不使用分页查询导致结果集很大等。

为什么有大对象要进行分配担保?

链接: link
1)如果大对象在新生代,新生代是由eden区和survivor区配合来完成的,大对象在两个survivor区频繁复制耗费资源;
2)如果没有大对象直接进入老年代,那么MinorGC会因为Survivor区大对象过多提前发生MinorGC,而且FullGC会因为年轻代过早进入老年代而提前触发。

为什么要分为eden区和survivor区?

链接: link
如果没有survivor区,对象回收之后立即就被放到老年代,老年代将很快被填满,触发full GC,又由于老年代的内存空间要远大于新生代,所以进行full GC消耗的时间比minor GC长的多。

为什么要分为两个survivor区?

设置两个survivor区主要是解决了内存碎片化问题。新创建的对象在eden中,一旦触发Minor GC,存活的对象就会被存放在survivor区,这样当下次垃圾回收的时候,eden和survivor区都有存活对象,如果直接将eden区的存活对象放入survivor区,这两部分对象锁占用的内存是不连续的,就导致了内存的碎片化。

Java中对象的状态

①可触及状态:只有还有引用变量引用它,它就处于可触及状态;
②可复活状态:当不再有引用变量引用该对象时,这个对象就会变为可复活状态,垃圾回收器会准备释放它占用的内存,在释放之前,会调用其他处于可复活状态的对象的finalize方法,这些finalize方法可能使该对象重新转为可触及状态;
③不可触及状态:当java虚拟机执行完所有可复活对象的finalize方法后,如果该对象还没有转为可触及状态,垃圾回收器才会真正回收其所占用的内存。

finalize关键字的作用

首先进行可达性分析,如果这个对象时不可达的,然后进行筛选,当对象没有覆盖finalize方法或者finalize方法在此之前已经被虚拟机调用过,这个对象被判断为没有必要执行finalize(),则整个对象将被回收;
如果对象被判断为有必要执行finalize()方法,则这个对象会被放在一个名为F-queue的队列之中,稍后虚拟机会自动创建一个优先级较低的finalizer线程去执行,在执行前该对象一直在堆区中,对象执行完毕之后,将这些finalizer对象从队列中移除,java虚拟机看到对象没有引用,就进行垃圾回收;

避免使用finalize关键字原因

1)finalize方法的调用时机具有不确定性,从一个对象变的不可达开始,到finalize()方法被执行,所花费的时间是任意长的。所以我们并不能依赖finalize()方法及时回收占用的资源,可能出现的情况是在我们资源耗尽之前,gc却仍未触发。
2)重写finalize()方法意味着延长了回收对象时需要更多的操作,从而延长了对象回收的时间。

gc操作

System.gc()和runtime.gc()用于提示jvm进行垃圾回收,立即开始回收还是延迟回收回收取决于jvm。

垃圾收集器

Serial和Serial Old垃圾回收器:分别用于回收新生代和老年代。
单线程运行,垃圾回收的时候会停止系统中的其他线程,让系统卡死不动,然后执行垃圾回收(stop the world)。
Par New和CMS垃圾回收器:分别用于回收新生代和老年代。
Par New是serial收集器的多线程版本,除了使用多线程外与serial收集器没有区别。
CMS收集器是一种获取最短停顿时间为目标收集器,它实现了垃圾回收进程与用户进程(基本上)同时工作。
G1垃圾回收器:统一收集新生代和老年代。 G1收集器是一款面向服务端应用的收集器,它能充分利用多cpu,多核环境,如下图所示L:

关于Minor GC与Major GC/Full GC?

新生代GC(Minor GC):发生在新生代的垃圾收集动作,Java对象大多朝生夕灭,所以Minor GC非常频繁,回收速度也比较快。
老年代GC(Major GC):只发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC,Major GC的速度一般会比Minor GC慢10倍以上(老年代垃圾回收的时间比较长,因为老年代的空间大,对象多)。
Full GC:收集整个java堆以及方法区的垃圾。

Java中对象的引用

1)强引用:要强引用还存在,垃圾收集器就永远不会回收掉被引用的对象。
2)软引用:在系统内存不够用时,这类引用关联的对象将被垃圾收集器回收。
3)弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
4)虚引用:为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。

引用队列ReferenceQueue

1)reference对象已经不具有价值时,需要进行垃圾对象回收,避免内存占用。
2)当引用所引用的对象被回收后引用对象本身就会被加入引用队列。

Java内存模型

JMM(JAVA 内存模型)规定了所有的变量都存储在主内存中,每个线程都有一个私有的本地内存(local
memory),本地内存中存储了该线程以读/写共享变量的副本。不同线程无法直接访问对方工作内存中的变量。


总结

提示:这里对文章进行总结:

例如:秋招Java基础知识小总结吧。

与本文相关的文章

发布评论

评论列表 (0)

  1. 暂无评论