Java面筋

(一)基础

JDK1.8的新特性

  1. Lambda表达式 本质上是一段匿名内部类,也可以是一段可以传递的代码,代码比较简洁。
  2. 函数式接口:是为了让Lambda表达式的使用提供更好的支持,简单的来说,就是只定义一个抽象方法的接口,并且还提供@FuncationInteface
    常见的函数式接口有四种:Consumer 消费型接口,有参无返回值 Presumer 供给型接口,无参有返回值 Function<>函数型接口 有参有返回值 Predicate<>断言型接口 有参有返回值,返回的是boolean型
  3. 方法引用和构造器引用 方法引用是Lambda表达式的另一种形式表现,比lambda表达式更简洁
    方法引用有三种方式:1.对象::实例方法 2.类::静态方法名 3.类::实例方法名
    构造器引用:className::new 数组引用Type[]::new
  4. Stream API 其中有三个步骤:1.创建stream 2.中间操作(过滤、map) 3.终止操作
  5. 接口的默认方法和静态方法 在接口中可以使用default和static关键字来修饰接口中定义的普通方法
    当一个类继承父类又实现接口时,若后者两者方法名相同,则优先继承于父类中的同名方法,即“类优先”,如果实现两个同名方法的接口,则要求实现类必须手动声明默认实现哪个接口中的方法。
  6. 新时间API LocalDate| LocalTime |LocalDateTime 新的日期api都是不可变的,更实用于多线程

面向对象的特征

  • 封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。
  • 继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类;得到继承信息的类被称为子类。
  • 多态:是指允许不同子类型的对象对同一消息作出不同的响应。多态性分为编译时的多态和运行时的多态。
    方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写 (override)实现的是运行时的多态性(也称为后绑定)。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。
  • 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

JDK和JRE的区别

  • JDK是Java语言的软件开发工具包,是整个Java开发的核心,包括了Java运行环境、Java工具和Java基础类库
  • JRE是Java运行时环境,包括Java虚拟机、Java核心类库和支持文件
  • JDK = JRE + Java开发工具
  • JRE = JVM + Java核心类库

重载与重写的区别

  1. 概念上
  • 重载:不同函数使用相同的函数,但函数的形参不同,调用时根据形参进行区别。
  • 重写:子类对父类的方法进行重新实现,方法名、形参列表都相同,方法体不同。
  1. 规则上
  • 重载:必须有不同的形参列表、可以有不同的访问修饰符、可以抛出不同的异常。
  • 重写:参数列表必须完全相同、返回类型与被重写方法相同、访问修饰符的限制一定要大于被重写的方法、重写的方法一定不能抛出新的检查异常或比重写方法申明更加宽泛的检查异常。
  1. 多态性
  • 重写Overriding是父类与子类之间多态性的一种表现。
  • 重载Overloading是一个类中多态性的一种表现。

==和equals的区别

  1. 功能不同
    "=="是判断两个变量或者实例是不是指向同一个内存空间
    "equals"是判断两个变量或者实例所指向的内存空间的值是不是相同
  2. 定义不同
    "equals"在Java中是一个方法
    "=="在JAVA中只是一个运算符

两个对象的hashCode()相同,则equals()也一定ture,对吗?

  • equals()相等的两个对象他们的hashCode()肯定相等,用equals()是绝对可靠的
  • hashCode()相等的两个对象他们的equals()不一定相等,用hashCode()不是绝对可靠的
  • 由于生成hash值的公式可能存在问题,不同对象生成hashcode有时会相等,所以hashCode()不是绝对可靠
  • 需要进行对比时,一般先用hashCode()进行对比,如果hashCode()不相同,则表示这两个对象肯定不相等;
    如果hashCode()相等,再用equals()进行对比

为什么重写equals()方法为什么要重写hashCode()方法

  • 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode方法都必须始终返回同一个值。在一个应用程序与另一个应用程序的执行过程中,执行hashCode方法所返回的值可以不一致。

  • 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中的hashCode方法都必须产生同样的整数结果

  • 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中的hashCode方法,则不一定要求hashCode方法必须产生不同的结果。但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表的性能。

如果重写了equals方法而没有重写hashCode方法的话,就违反了第二条规定。相等的对象必须拥有相等的hashcode。

final在Java中的作用

final关键字可以用于三个地方。用于修饰类、类属性和类方法
被final关键字修饰的类不能被继承,被final关键字修饰的类属性和类方法不能被重写
对于被final关键字修饰的类属性而言,子类不能给他重新赋值

扩展:修饰符

  • 访问控制修饰符
    default(默认,不写):在同一包内可见,不使用任何修饰符,使用对象:类、接口、变量、方法
    private:在同一类内可见,使用对象:变量、方法。注意:不能修饰类(外部类)
    public:对所有类可见,使用对象:类、接口、变量、方法
    protected:对同一包内的类和所有子类可见,使用对象:变量、方法。注意:不能修饰类(外部类)
  • 非访问控制修饰符
    static:用来修饰类方法和类变量
    final:用来修饰类、方法和变量
    abstract:用来创建抽象类和抽象方法
    synchronized和volatile:主要用于线程的编程

final、finally、finalize的区别?

final:

当final修饰类时,表明该类不能被其他类所继承。当我们需要一个类永远不能被继承时,此时就可以使用final修饰,但要注意:

final类中所有的成员方法都会隐式的定义为final方法

final修饰方法时,表明此方法不能被重写

注意:若父类中final方法的访问权限为private,将导致子类中不能直接继承该方法,因此,此时可以在子类中定义相同方法名的函数,此时不会与重写final的矛盾,而是在子类中重新地定义了新方法。

final修饰成员变量,表示常量,只能被赋值一次,赋值后其值不再改变

**final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。

finally:

finally是在异常处理时提供finally块来执行任何清除操作。不管有没有异常被抛出、捕获,finally块都会被执行。try块中的内容是在无异常时执行到结束。catch块中的内容,是在try块内容发生catch所声明的异常时,跳转到catch块中执行。finally块则是无论异常是否发生,都会执行finally块的内容,所以在代码逻辑中有需要无论发生什么都必须执行的代码,就可以放在finally块中。

finalize:

finalize是方法名。java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者被执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

java中的 Math.round(-1.5) 等于多少?

  • Math.round表示四舍五入,算法为Math.floor(x+0.5),即将原来的数字加上0.5后再向下取值,因此Math.round(-1.5)=-1
  • Math.ceil()向上取整
  • Math.floor()向下取整
  • Math.rint()返回于参数最接近的整数

String 属于基础的数据类型吗?

基础数据类型包括:byte、short、int、long、float、double、boolean、char
String属于类,因此String不属于基础的数据类型

java 中操作字符串都有哪些类?它们之间有什么区别?

  • String:由final修饰,String类的方法都是返回new String。也就是说String对象的任何改变都不影响到原对象,对字符串的修改操作都会生成新的对象。
  • StringBuffer:对象能够被多次的修改,并且不产生新的未使用对象,对字符串操作的方法都加了synchronized,保证线程安全。
  • StringBuilder:不保证线程安全,在方法体内需要进行字符串的修改操作,可以new StringBuilder对象,调用StringBuilder对象的append、replace、delete等方法修改字符串。

String str="i"与 String str=new String(“i”)一样吗?

String str = “i”; java虚拟机会将其分配到常量池中,而常量池中没有重复的元素
String str = new String(“i”); java虚拟机会将其分配到堆内存中
因此不一样(参考JVM)

如何将字符串反转?

  1. 自己实现:获取字符串长度,定义char数组,反转赋值,创建新的String

  2. 使用StringBuilder的reverse()方法:return new StringBuilder(str).reverse().toString();

String 类的常用方法都有那些?

  • indexOf() 返回指定字符得索引
  • charAt() 返回指定索引处得字符
  • repalce() 字符串替换
  • trim() 去除字符串两端的空白
  • split() 分割字符串 返回分割后的字符串数组
  • getBytes() 返回字符串的byte类型数组
  • length() 返回字符串的长度
  • toLowerCase() 字符串转小写
  • toUpperCase() 字符串转大写
  • substring() 截取字符串
  • equals() 字符串比较

抽象类必须要有抽象方法吗?

  • 抽象类必须有关键字abstract来修饰。
  • 抽象类可以不含有抽象方法
  • 如果一个类包含抽象方法,则该类必须是抽象类
  • 抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
  • 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
  • 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。

普通类和抽象类有哪些区别?

  • 普通类中不可含有抽象方法,可以被实例化
  • 抽象类中所有的方法自动被认为是抽象方法,没有实现过程,不可被实例化;
    • 抽象类的子类,除非也是抽象类,否则必须实现该抽象类声明的方法

抽象类能使用 final 修饰吗?

不能,抽象类必须被继承才可使用,而使用final修饰的类不可修改、不可继承

接口和抽象类有什么区别?

  • 抽象类要被子类继承,接口要被类实现
  • 接口只能做方法声明,抽象类中可以作方法声明,也可以做方法实现
  • 接口里定义的变量只能是公共的静态常量,抽象类中的变量是普通变量
  • 接口是设计的结果,抽象类是重构的结果
  • 抽象类和接口都是用来抽象具体对象的,但接口的抽象级别最高
  • 抽象类可以有具体的方法和属性,接口只能有抽象方法和不可变常量
  • 抽象类主要用来抽象类别,接口主要用来抽象功能

如何实现对象克隆?

  • 实现Cloneable接口,重写clone()方法,不实现Cloneable接口,会报CloneNotSupportedException异常

  • Object的clone()方法是 浅拷贝即如果类中属性有自定义引用类型,只拷贝引用,不拷贝引用指向的对象

  • 深拷贝实现方法:

    • 对象的属性的Class也实现Cloneable接口,在克隆对象时也手动克隆属性
    • 结合序列化,完成深拷贝

深拷贝和浅拷贝区别是什么?

  • 浅拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量和变量指向堆内存中的对象的指针,不复制堆内存中的对象。
  • 深拷贝:复制基本类型的属性;引用类型的属性复制,复制栈中的变量和变量指向堆内存中对象的指针和堆内存中的对象。

异常

-RuntimeException类型的异常,在程序中是不会捕获的,也不会在方法体中声明抛出的异常。它是表示程序中出现了编程错误,需要找出错误修改程序。
-其他异常都会在正确的运行中,会去捕获。

erro和exception的异常

异常是能被程序本身处理的,而错误是无法处理的。

异常处理机制

-抛出异常:当一个方法出现错误引发异常时,方法会创建异常对象并交给运行时的系统,异常对象包含了异常类型和异常出现时程序状态等信息。
-捕获异常:当一个方法抛出异常之后,运行时系统会尝试去寻找合适的处理异常处理器。try{}catch{}通常使用catch去捕获异常。

常见的RuntimeException

1.数组索引越界异常 2.算术异常 3.非法参数异常 4. 空指针异常 5.数组长度为负异常

throw和throws的区别

1.throws出现在方法头上,而throw出现在代码块中,try{}catch{}
2.Throws表示出现异常的一种可能性,可能发生,可能不发生;而一旦执行throw的话,表示异常一定会发生。

(二)容器

java 容器都有哪些?

  • Set、List、Map
    • Set:HashSet、LinkedHashSet、TreeSet
    • List:ArrayList(基于数组实现的,其特点是查询快,增删慢)、LinkedList(基于链表实现的,相比于ArrayList其特点是查询慢,增删快)、Vector
    • Map:HashMap、LinkedHashMap、HashTable、TreeMap

Collection 和 Collections 有什么区别?

Collection是集合的接口
Collections是集合的工具类,定义了许多操作集合的静态方法(实现对各种集合的搜索、排序、线程安全等),不能被实例化

List、Set、Map之间的区别?

  • List:有序集合,允许重复
  • Set:不重复集合,LinkedSet按照插入顺序排序,SortedSet可排序,HashSet无序
  • Map:键值对集合,存储键、值和之间的映射,Key无序、唯一,Value不要求有序、允许重复

HashMap 和 Hashtable 有什么区别?

  • 同步性:HashTable是同步的、HashMap不是同步的
  • 对null的支持:HashTable不允许null值,HashMap允许null值
  • 遍历方式:HashTable使用Enumeration遍历,HashMap使用Iterator遍历
  • 父类不同:
  • 初始大小和扩容方式:HashTable初始大小为11,每次扩容变为原来的2n+1,HashMap初始大小为16,每次扩容为原来的2倍
  • 计算hash的方法不同
  • 补充

如何决定使用HashMap还是TreeMap

查询使用HashMap、增加、快速创建的时候使用TreeMap
原因:HashMap的Key值实现散列hashCode(),分布是散列的均匀的,不支持排序;TreeMap迭代时默认按照Key值升序排列

HashMap 的实现原理?

  • HashMap是基于哈希表的Map接口实现。HashMap使用数组加链表实现,每个数组中存储着链表。通过put()和get()方法存储和获取对象。

  • 使用数组的好处,寻址容易,缺点在于插入和删除困难。使用链表的好处插入和删除容易,缺点在于寻址困难。因此将数组和链表组合可以提高查询效率又便于频繁修改。

  • 哈希表的思想是通过哈希算法,将不定长度的Key映射为数组下标,访问Key的数据时再次通过哈希算法计算出数组下标,访问下标对应的数据项。

  • 由于在实现哈希函数时存在着哈希冲突的问题,就得处理哈希冲突。常见的算法有:开放寻址法、公共溢出区、拉链法(链地址法)

    • 开放寻址法:如果出现了哈希冲突,就重新找一个空闲的位置,将数据插入。比较简单的是线性探测,当发生哈希冲突时,就从当前位置往后找,直到找到空闲位置。另外还有二次探测和双重哈希。
    • 公共溢出区:将哈希表分为主表和溢出表两部分,所有溢出的数据均存入溢出表中。
    • 拉链法:将每个桶作为链表的头部,所有哈希地址相同的元素构成一个同义词链表,每次添加数据都添加到对应桶链表的尾部。(HashMap)在JDK1.8中,当链表长度大于8时,把链表换成红黑树。
  • HashMap使用的是拉链法,如果分桶方式合理,数据能够被均匀分布到所有桶中,在数据量不是特别巨大的前提下,查询效率接近于O(1),插入或删除效率接近于O(1)

  • HashMap在链表中存储的是键值对,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。

  • 当HashMap需要进行扩容时,会重新进行哈希和分桶,因此不能保证插入顺序不变。由于需要重建数组,开销巨大,当知道数据量时可以设定HashMap的容量。

  • 另外一个重要参数是装载因子(默认0.75),当存储数据量/数组容量>装载因子时需要扩容。

  • 装载因子越大,则空间利用率提升,但发生冲突的概率也就变大,查找的成本变高;装载因子越小,则发生冲突的概率变小了,但空间浪费了。

  • HashMap的三个构造函数

    • HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。
    • HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。
    • HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。

HashMap是线程安全的嘛?

HashMap不是线程安全的,在并发过程中容易出现死循环,主要是因为链表的插入操作不是原子性的,每一个HashEntry都是一个链表,假设我们要插入一个数据,可能已经设置了后节点,但是实际上并没有插入,反而插入到了后节点后面的位置,这样就出现了环,破坏了链表结构,所以他不是线程安全的。

那有什么解决方法,避免线程安全问题了?

–首先可以考虑HashTable,因为它是使用Synchronized关键字来保证线程安全的,但是在线程竞争比较激烈的情况下HashTable的处理速度就变的很缓慢,这主要是因为Synchronized关键字使代码块或者同步方法进入临界区,如果一个线程想进入临界区,而其他线程也想,但同一时刻进入临界区的线程只有一个,其他线程必须在外面等待进入阻塞状态,锁只有一个,但是想获取锁的线程却在增加,这样就导致HashTable在处理多线程的时候效率低下。
–所以我们会选用CocurrentHashMap,它是利用锁分段技术,将数据分成很多段,每一段都独立拥有一把锁,从而想争取锁的线程数目得到了控制,并且每一段之间竞争锁是不影响的。与HashTable主要不同的是CocurrentHashMap的get方法是不需要用到锁的,共享变量是由volatile关键字修饰了,这就保证了共享变量在内存的可见性,使得每一次获取的都是最新值,同时还避免了指令重排序。

那么CocurrentHashMap的底层原理你知道吗

在JDK1.7的时候,CocurrentHashMap的底层是由Segment和数组实现的。其中Segment是继承于Reentranlock的,和HashTable非常相似,但是不会像HahTable不管是put方法还是get方法都使用锁机制保证线程安全,不同的地方就是核心数据value和链表是由volatile修饰的,保证了内存可见性。而且每一个线程占用锁访问Segment,是相互独立的。
–put()方法 首先通过要根据插入的key的hash值定位到Segment,再通过Hash定位到具体的Hashenrty上,如果HashEntry不为空,就比较要插入的key与已经存在的key是否相等,如果相等,就替换,如果不等,就以链表的结构插入。如果HashEntry为空,就新建一个HashEntry到Segment中,但前提要需要判断扩容。
–get() 先通过key的hash值定位到具体的Segment上,再通过hash值定位到具体的HashEntry上,如果没有就返回空,如果有,就直接返回。由于Hashentry中的value是由volatile修饰的,保证了内存的可见性,每一次获取都是最新值。

在jdk1.8的时候,CocurrentHashMap将HashEntry数组替换成Node[]数组,其中val,next都是由volatile修饰的,保证了可见性。
–put()方法 首先根据key计算出hash值,定位到具体的node上,判断Node是否为空,如果为空,表示当前可以写入数据,利用CAS尝试写入,如果失败,能够自旋成功。如果不为空,就先当前位置hashcodemoved-1,就需要扩容。如果都不满足,就使用Synchronized写入数据,如果达到一定阈值时,就改用红黑树进行存储,这样的话,查找的效率会高很多。
–get() 首先计算key的hash值,根据hash值来寻址,如果正好在table上,就直接返回。如果是红黑树就按数的查找方式寻找,如果是链表结构,就按链表的方式查找。
jdk1.8在1.7的版本上作出了很大的改动,将链表在一定的条件下改用红黑树进行查找,保证了查询效率,而且还将Reentranlock替换成了Synchronized。

HashSet的实现原理

基于HashMap实现,只使用Key,value使用一个static final的Object对象标识。

ArrayList 和 LinkedList 的区别是什么?

Arraylist:底层是基于动态数组,根据下标随机访问数组元素的效率高,向数组尾部添加元素的效率高;但是,删除数组中的数据以及向数组中间添加数据效率低,因为需要移动数组。
Linkedlist:基于链表的动态数组,数据添加删除效率高,只需要改变指针指向即可,但是访问数据的平均效率低,需要对链表进行遍历。

ArrayList和Vector的区别?

同步性:Vector是线程安全的,方法之间是线程同步的;ArrayList是线程不安全的,方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好使用ArrayList。
数据增长:Vector增长为原来的两倍,ArrayList增长为原来的1.5倍。Vector可设置初始的空间大小。

Array和ArrayList的区别?

Array可以包含基本数据类型和对象类型,ArrayList只能包含对象类型。
Array大小固定,ArrayList大小动态变化。
ArrayList提供了更多的方法和特性。

Queue 中 poll()和 remove()有什么区别?

offer()和add()都是向队列添加一个元素,当队列满时,offer()返回false,add()会抛出一个 unchecked 异常
poll()和remove()都将移除并返回队头,但在队空时,poll()返回null,remove()会抛出NoSuchElementException异常
peek()和element()都将在不移除的情况下返回队头,但在队空时,peek()返回null,element()抛出NoSuchElementException异常

CopyOnWrite容器

  • 在JDK1.5之后,java.util.concurrent引入了两个CopyOnWrite容器,分别是CopyOnWriteArrayList, CopyOnWriteArraySet.
  • 顾名思义,CopyOnWrite就是在write操作之前,对集合进行Copy,针对容器的任意改操作(add,set,remove之类),都是在容器的副本上进行的。并且在修改完之后,将原容器的引用指向修改后的副本。
  • 如果线程A得到容器list1的iterator之后,线程B对容器list1加入了新的元素,由于线程A获得list1的iterator时候在线程B对list1进行修改前,所以线程A是看不到线程B对list1进行的任何修改。
  • 应用场景
    • 经常用在读多写少的场景,比如EventListener的添加,网站的category列表等偶尔修改,但是需要大量读取的情景。
  • 缺点
  1. 数据一致性的问题。
    • 因为读操作没有用到并发控制,所以可能某个线程读到的数据不是实时数据。
  2. 内存占用问题。
    • 因为写操作会进行数据拷贝,并且旧有的数据引用也可能被其他线程占有一段时间,这样针对数据比较大的情况,可能会占用相当大的内存。并且由于每次写操作都会占用额外的内存,最后进行的GC时间也可能相应的增加。

哪些集合类是线程安全的?

Vector、HashTable、ConcurrentHashMap、Stack

迭代器 Iterator 是什么?

迭代器是一种设计模式,它是一个对象,可以遍历并选择序列中的对象。Java中的Iterator功能比较简单,只能单向移动。

Iterator 怎么使用?有什么特点?

  • 使用方法iterator()要求容器返回一个Iterator。第一次调用Iterator的next()方法时,它返回序列的第一个元素。
    • 注意:iterator()方法时java.lang.Iterable接口,被Collection继承。
  • 使用next()获取下一个元素。
  • 使用hasNext()检查序列中是否还有元素。
  • 使用remove()将迭代器新返回的元素删除。

Iterator 和 ListIterator 有什么区别?

  • 所属关系:ListIterator是一个Iterator的子类型。
  • 局限:ListIterator只能应用于各种List类的访问。
  • 优势:Iterator只能向前移动,而ListIterator可以双向移动。
  • ListIterator 有 add() 方法,可以向 List 中添加对象,而 Iterator 不能。

怎么确保一个集合不能被修改?

  • 采用Collections包下的unmodifiableMap方法,通过这个方法返回的map,是不可以修改的。他会报 java.lang.UnsupportedOperationException错。
  • 同理:Collections包也提供了对list和set集合的方法。
    • Collections.unmodifiableList(List)
    • Collections.unmodifiableSet(Set)
  • 不能使用final修饰符,因为final修饰的集合的内容是可以改变的。

2.多线程

并行和并发有什么区别?

并行是指两个或者多个事件在同一时刻发生,并发是指两个或者多个事件在同一时间间隔发生。

进程、线程和协程的区别?

进程是资源分配的最小单位,线程是CPU调度的最小单位。由于线程的上下文切换,需要操作系统内核进行,十分消耗系统资源,因此在线程中可以拥有多个协程,多个协程的切换完全是在用户态下执行的。

根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位

在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

什么是守护线程?

  • java里线程分为守护线程和用户线程。
  • 守护线程:专门用于服务其他的线程,如果用户自定义线程和main线程执行完毕,那jvm就会退出。如果有用户自定义线程存在的话,jvm就不会退出。
  • 用户自定义线程:应用程序里的线程,一般都是用户自定义线程;用户也可以在应用程序代码自定义守护线程,只需调用Thread类的设置方法即可。
  • 守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。
  • JVM中的垃圾回收线程就是典型的守护线程。

进程间通信方式:

  • 管道
  • FIFO
  • 信号量
  • 消息队列
  • 共享内存
  • 套接字

线程间通信方式:

  • 通过共享对象
  • 使用wait()、notify()、notifyAll()

创建线程有哪几种方式?

java提供了三种创建线程的方法:

  1. 通过实现Runnable接口;

    • 为了实现Runnable,一个类只需要执行一个方法调用run()。public void run()
    • 在创建实现Runnable接口的类之后,可以在类中实例化一个线程对象。Thread(Runnable threadOb, String threadName)
    • 新线程创建之后,调用它的start()方法运行
  2. 通过继承Thread类本身;

    • 创建一个新的类,该类继承Thread类,然后创建一个该类的实例。
    • 继承类必须重写run()方法,也必须调用start()方法才能执行。
    • 其本质上也是实现了Runnable接口的一个实例。
  3. 通过Callable和Future创建线程。

    • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,有返回值。
    • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()的返回值
    • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
    • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

4.线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池找那个等待下一个任务。

Runnable和Callable的区别?

  • 相同点:
    • 两者都是接口;
    • 两者都可以用来编写多线程程序;
    • 两者都需要调用Thread.start()启动线程
  • 不同点:
    • 实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果
    • Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
    • Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取到结果;当不调用此方法时,主线程不会阻塞。

线程有几个状态?状态间的转换

  • 新建、就绪、运行、阻塞、死亡
  • 线程start()执行后,处于 就绪 状态,等待 获得CPU资源 ,而后进行 运行 状态
  • 当线程处于 运行 状态下:
    • 当线程 失去CPU资源 或者 主动调用yield()方法 后进入 就绪 状态
    • 主动调用sleep()方法 ,进入 阻塞 状态,时间到了会进入 就绪 状态;
    • 主动调用suspend()方法 ,进入 阻塞 状态,主动调用resume方法,会进入 就绪 状态;
    • 调用了阻塞式IO方法 ,进入 阻塞 状态,调用完成后,会进入 就绪 状态;
    • 试图获取锁 ,进入 阻塞 状态,成功获取锁后,会进入 就绪 状态;
    • 线程在 等待某个通知 ,进入 阻塞 状态,其他线程发出通知后,进入 就绪 状态 。
  • 线程 运行完毕 ,或者 运行一半异常 ,或者 主动调用线程的stop()方法 ,进入 死亡 状态。

sleep() 和 wait() 有什么区别?

sleep()方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等待休眠时间结束后,线程进入就绪状态。
当一个synchronized块中调用了sleep()方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程已然无法访问这个对象。

wait()方法是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notiyAll方法来唤醒等待的线程。wait()方法必须在同步代码块或同步方法中调用。

  • 相同点:都会使线程进入阻塞状态
  • 不同点:
    1.声明的位置不同:sleep()方法是Thread类中的静态方法;wait()定义在Object类中。
    2.调用要求不同:sleep()方法可以在任何需要的场景调用;wait()方法只能在同步代码块或同步方法中调用。
    3.释放锁的机制不同:sleep()不会释放对象的锁;wait()会释放对象的锁,使得其他线程能够访问。

notify和notifyAll的区别?

  • 锁池:假设线程A已经拥有了某个对象的锁,而其他的线程想要调用这个对象的某个synchronized方法或synchronized块,由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

  • 等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中

  • 如果有对象调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

  • 当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或者notify()方法(只随机唤醒一个wait线程),被唤醒的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。

  • 优先级高的线程竞争到对象锁的概率大,假设某线程没有竞争到该对象锁,它还会留在锁池中,只有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

线程的 run() 和 start() 有什么区别?

  • 调用start()方法是用来启动线程的,轮到该线程执行时,会自动调用run();
  • 直接调用run()方法,无法达到启动多线程的目的,相当于主线程执行Thread对象的run()方法。
  • 一个线程对象的start()方法只能调用一次,多次调用会抛出java.lang.IllegalThreadStateException异常;
  • run()方法没有限制。

创建线程池有哪几种方式?

  • 定义:线程池就是创建若干个可执行的线程放入一个池(容器)中,有任务需要处理时,会提交到线程池中的任务队列,处理完之后线程并不会被销毁,而是仍然在线程池找那个等待下一个任务。
  • 目的:Java中创建一个线程,需要调用操作系统内核的API,操作系统要为线程分配一系列的资源,成本很高,所以线程是一个重量级对象,应该避免频繁创建和销毁。使用线程池就能很好地避免频繁创建和销毁。
  1. 使用工厂类Executors创建线程池

    • newFixedThreadPool:创建定长线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程数量不再变化,当线程发生错误结束时,线程池会补充一个新的线程。
    • newCachedThreadPool:创建可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,任务增加时可以自动添加新线程,线程池的容量不限制。
    • newScheduledThreadPool:创建定长线程池,可执行周期性的任务。
    • newSingleThreadExecutor:创建单线程的线程池,线程异常结束,会创建一个新的线程,能确保任务按提交顺序执行。
    • newSingleThreadScheduledExecutor:创建单线程可执行周期性任务的线程池。
    • newWorkStealingPool:任务可窃取线程池,不保证执行顺序,当有空闲线程时会从其他任务队列窃取任务执行,适合任务耗时差异较大。
  2. 使用new ThreadPoolExecutor自定义创建

Java中的ThreadPoolExecutor类

  • java.util.concurrent.ThreadPoolExecutor类是线程池中最核心的类。

在ThreadPoolExecutor类中提供了四个构造方法:

public class ThreadPoolExecutor extends AbstractExecutorService {
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue);
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory);
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler);
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler);
}

各个参数的含义如下:

  • corePoolSize:核心池的大小。默认情况下,在创建了线程池之后,线程池中线程数量为0,当有任务到来之后,就会创建一个线程去执行任务,当线程池中的线程数量达到corePoolSize之后,就会把到达的任务放到缓存队列中。

  • maximumPoolSize:线程池最大线程数。表示线程池最多能创建多少个线程。

  • keepAliveTime:表示线程没有执行任务时保持多久时间会终止。

  • unit:参数keepAliveTime的时间单位。

  • workQueue:阻塞队列,用来存储等待执行的任务。

  • threadFactory:线程工厂

  • handler:表示当拒绝处理任务时的策略,有以下四种取值:

    • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
  • ThreadPoolExecutor继承了AbstractExecutorService实现了ExecutorService继承了Executor

  • ThreadPoolExecutor类中几个比较重要的方法

execute()
submit()
shutdown()
shutdownNow()

线程池中submit()和execute()方法有什么区别?

  • execute()参数Runnable;submit()参数Runnable 或 Runnable和结果T 或 Callable
  • execute()没有返回值;而submit()有返回值,submit()的返回值Future调用get方法时,可以捕获处理异常

线程池实现原理

  1. 线程池状态
    • RUNNING 接受新任务并处理排队的任务
      • 线程池的初始化状态是RUNNING。线程池一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0.
    • SHUTDOWN 不接受新任务,但处理排队的任务
    • STOP 不接受新任务,不处理排队的任务
    • TIDYING 所有任务都已终止,workcount为零,转为TIDYING的任务将运行terminated方法
    • TERMINATED 已完成

RUNNING -> SHUTDOWN: 当调用shutdown()时
(RUNNING or SHUTDOWN) -> STOP: 当调用shutdownNow()时
SHUTDOWN -> TIDYING: 当队列和线程池都为空时
STOP -> TIDYING: 当线程池为空时
TIDYING -> TERMINATED: 当terminated()方法完成时

  • 因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理,可以通过重载terminated()方法来实现。
  1. 任务的执行

    • 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
    • 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
    • 如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
    • 如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
  2. 线程池中线程的初始化

    默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。

    在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:

    • prestartCoreThread():初始化一个核心线程;
    • prestartAllCoreThreads():初始化所有核心线程
  3. 任务缓存队列及排队策略

workQueue:一个任务缓存队列,用来存储等待执行的任务

常用的缓存队列:ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue

1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;

2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;

3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

  1. 任务拒绝策略

    当线程池的任务缓存队列已经满并别线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略

handler:表示拒绝处理任务时的策略,有四种取值:

  1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 这是线程池默认的拒绝策略,在任务不能再提交的时候,跑出异常,及时反馈程序运行状态。

  2. ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。使用此策略,可能会使我们无法发现系统的异常状态。

  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程),此拒绝策略,是一种喜新厌旧的拒绝策略。

  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 ,如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务

  5. 线程池的关闭

    ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:

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

    ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize(),

    • setCorePoolSize:设置核心池大小
    • setMaximumPoolSize:设置线程池最大能创建的线程数目大小

    当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。

    线程数=cpu可用核心数/(1-阻塞系数),其中阻塞系数的取值在[0,1]之间。计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近1。

    1、工作线程是不是越多越好?

    不是。

    a、服务器cpu核数有限,所以同时并发或者并行的线程数是有限的,所以1核cpu设置1000个线程是没有意义的。

    b、线程切换也是有开销的。频繁切换线程会使性能降低。

    如果是计算密集型,换句话说,线程绝大部分消耗都在CPU计算处理上,那么启动再多的线程也无济于事,所以线程数量=cpu合数比较合适。
    如果是IO密集型,线程很到部分消耗在等待上,所以可以启动更多的线程,但是线程数量不能超过线程池的上限,

常见的四种线程池

newFixedThreadPool:

public class FixPoolDemo {

    private static Runnable getThread(final int i){
        return new Runnable() {

            @Override
            public void run() {
                try{
                    Thread.sleep(500);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(i);
            }
        };
    }

    public static void main(String[] args) {
        ExecutorService fixPool = Executors.newFixedThreadPool(5);
        for(int i=0;i<10;i++){
            fixPool.execute(getThread(i));
        }
        fixPool.shutdown();
    }
}

newCachedThreadPool:

public class CachePoolDemo {

    private static Runnable getThread(final int i){
        return new Runnable(){

            @Override
            public void run() {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(i);
            }
        };
    }

    public static void main(String[] args) {
        ExecutorService cachePool = Executors.newCachedThreadPool();
        for(int i=0;i<10;i++){
            cachePool.execute(getThread(i));
        }
    }
}

newSingleThreadExecutor:

public class SinglePoolDemo {

    private static Runnable getThread(final int i){
        return new Runnable(){

            @Override
            public void run() {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(i);
            }
        };
    }

    public static void main(String[] args) {
        ExecutorService singlePool = Executors.newSingleThreadExecutor();
        for(int i=0;i<10;i++){
            singlePool.execute(getThread(i));
        }
        singlePool.shutdown();
    }
}

newScheduledThreadPool:

public class ScheduledPoolDemo {

    public static void main(String[] args) {
        ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(10);
        scheduledPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getId() + "执行了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 0, 2, TimeUnit.SECONDS);
    }
}

使用new ThreadPoolExecutor自定义创建

public class ThreadPoolDemo {

    private static Runnable getThread(final int i){
        return new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(i);
            }
        };
    }

    public static void main(String[] args) {
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 100, 200, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(5));
        for(int i=0;i<10;i++){
            threadPool.execute(getThread(i));
        }
        threadPool.shutdown();
    }
}

并发

java程序中如何保证多线程的运行安全?

  • 线程安全性问题体现在:

    • 原子性:一个或者多个操作在CPU执行的过程中不被中断的特性
    • 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到
    • 有序性:程序执行的顺序按照代码的先后顺序执行
  • 导致原因:

    • 缓存导致的可见性问题
    • 线程切换带来的原子性问题
    • 编译优化带来的有序性问题
  • 解决方法:

    • JDK Atomic开头的原子类、
    • Atomic包中的类的基本特性就是在多线程环境下,当有多个线程同时对单个变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以像自旋锁一样,继续尝试,一直等到执行成功。
    • synchronized、LOCK,可以解决原子性问题
  • synchronized、volatile、LOCK,可以解决可见性问题

    • Happens-Before规则可以解决有序性问题

    synchronized

    • 作用:

      • 确保线程互斥的访问同步代码
      • 保证共享变量的修改能够及时可见
      • 有效解决重排序问题
    • 用法:

      • 修饰普通方法
      • 修饰静态方法
      • 修饰代码块
    • 同步代码块是通过monitorenter和monitorexit指令获取线程的执行权

      • 代码块的同步是利用monitorenter和monitorexit这两个字节码指令。他们分别位于同步代码块的开始和结束为止,当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有的,就把锁的计数器加1,当执行monitorexit指令时,锁计数器-1,当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程就会进入阻塞状态,直到其他线程释放锁
    • 同步方法通过加ACC_SYNCHRONIZED标识实现线程的执行权的控制

      • 方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构中的ACC-SYNCHRONIZED访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的ACC-SYNCHRONIZED访问标志是否被设置,如果被设置了,执行线程将先持有monitor,然后再执行方法,最后再方法完成无论是正常完成还是非正常完成时释放monitor。

      lock他是Reentranlock类的,

      Reentranlock则需要用户手动释放锁,就有可能出现死锁现象;需要lock()和unlock()方法配合try/finally语句块来完成

      volatile

      volatile可以保证内存可见性,被volatile关键字修饰的变量在进行写操作转换成汇编语言时,会加上一个lock前缀指令,lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:

      • 1.重排序不能把后面的指令重排序到内存屏障之前的位置
      • 2.将当前cpu缓存的数据立刻回写到内存中;
      • 3.使在其他cpu缓存了该内存地址的数据无效

      如何使其他数据无效,主要是通过MESI协议,当cpu写数据时,如果发现操作的变量是共享变量,那么他会发出信号通知其他CPU将该变量的缓存设置为无效状态。当其他cpu使用这个变量时,首先会先去判断是否有对该变量更改的信号,当发现这个变量的缓存已经无效时,会重新从内存中读取变量。

  • Happens-Before规则如下:

    • 程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操作线性发生于书写在后面的操作
    • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作
    • volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
    • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
    • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测
    • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
    • 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始

多线程锁的升级原理是什么?

  • 锁的级别从低到高:无锁->偏向锁->轻量级锁->重量级锁

  • 锁分级别原因:

    • 没有优化以前,synchronized是重量级锁(悲观锁),使用wait和notify、notifyAll来切换线程状态非常消耗系统资源;
    • 线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。
    • 所以JVM对synchronized关键字进行了优化,把锁分为无锁、偏向锁、轻量级锁、重量级锁。
  1. 无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。

  2. 偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。

  • 偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;

  • 如果线程处于活动状态,升级为轻量级锁的状态。

  1. 轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程B所访问,此时偏向锁就会升级为轻量级锁,线程B会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
  • 当前只有一个等待线程,则该线程将通过自旋进行等待。当时当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;
  • 当一个线程已持有锁,另外一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
  1. 重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
  • 重量级锁通过对象内部的监视器实现,而其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

自旋锁

自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区。

自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。
所以当线程竞争不激烈,并且保持锁的时间段,适合使用自旋锁。

自旋锁可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。

可能引起的问题:
1.过多占据CPU时间:如果锁的当前持有者长时间不释放该锁,那么等待者将长时间的占据cpu时间片,导致CPU资源的浪费,因此可以设定一个时间,当锁持有者超过这个时间不释放锁时,等待者会放弃CPU时间片阻塞;
2.死锁问题:试想一下,有一个线程连续两次试图获得自旋锁(比如在递归程序中),第一次这个线程获得了该锁,当第二次试图加锁的时候,检测到锁已被占用(其实是被自己占用),那么这时,线程会一直等待自己释放该锁,而不能继续执行,这样就引起了死锁。因此递归程序使用自旋锁应该遵循以下原则:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。

阻塞锁

让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。。
JAVA中,能够进入\退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字(其中的重量锁),ReentrantLock,Object.wait()\notify()

可重入锁

可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁

显示锁和内置锁

显示锁用Lock来定义、内置锁用syschronized。
内置锁:每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁。线程进入同步代码块或方法的时候会自动获得该锁,在退出同步代码块或方法时会释放该锁。获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法。
内置锁是互斥锁。

什么是死锁?

  • 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞现象,若无外力作用,它们都将无法推进下去。

  • java死锁产生的四个必要条件:

    • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
    • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
    • 请求和保持,即当资源请求者在请求其他的资源的同时对原有资源的占有
    • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源,这样就形成了一个等待环路

死锁例子

public class Main{
    public static String obj1 = "obj1";
    public static String obj2 = "obj2";
    public static void main(String[] args) {
        Lock1 lock1 = new Lock1();
        Lock2 lock2 = new Lock2();
        Thread t1 = new Thread(lock1);
        Thread t2 = new Thread(lock2);
        t1.start();
        t2.start();
    }
}

class Lock1 implements Runnable{

    @Override
    public void run() {
        synchronized (Main.obj1){
            System.out.println("Lock1 lock obj1");
            try{
                Thread.sleep(500);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            synchronized (Main.obj2){
                System.out.println("Lock1 lock obj2");
            }
        }
    }
}

class Lock2 implements Runnable{

    @Override
    public void run() {
        synchronized (Main.obj2){
            System.out.println("Lock2 lock obj2");
            try{
                Thread.sleep(500);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            synchronized (Main.obj1){
                System.out.println("Lock2 lock obj1");
            }
        }
    }
}

怎么防止死锁?

A:死锁预防

  • 理解死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。只要打破四个必要条件之一就能有效预防死锁的发生。
  • 有序资源分配法、银行家算法

B:解决方法

  • 死锁预防

  • 1.破坏“互斥”条件:就是在系统里取消互斥。若资源不被一个进程独占使用,那么死锁是肯定不会发生的。但一般来说在所列的四个条件中,“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。

    2.破坏“占有并等待”条件:破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。

    方法一:创建进程时,要求它申请所需的全部资源,系统或满足其所有要求,或什么也不给它。这是所谓的 “ 一次性分配”方案。
    方法二:要求每个进程提出新的资源申请前,释放它所占有的资源。这样,一个进程在需要资源S时,须先把它先前占有的资源R释放掉,然后才能提出对S的申请,即使它可能很快又要用到资源R。

    3.破坏“不可抢占”条件:破坏“不可抢占”条件就是允许对资源实行抢夺。
    方法一:如果占有某些资源的一个进程进行进一步资源请求被拒绝,则该进程必须释放它最初占有的资源,如果有必要,可再次请求这些资源和另外的资源。
    方法二:如果一个进程请求当前被另一个进程占有的一个资源,则操作系统可以抢占另一个进程,要求它释放资源。只有在任意两个进程的优先级都不相同的条件下,方法二才能预防死锁。

    4.破坏“循环等待”条件:破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。

  • 死锁避免

  • 1、判断“系统安全状态”法

    在进行系统资源分配之前,先计算此次资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程; 否则,让进程等待。

    2、银行家算法

    1、申请的贷款额度不能超过银行现有的资金总额

    2、分批次向银行提款,但是贷款额度不能超过一开始最大需求量的总额

    3、暂时不能满足客户申请的资金额度时,在有限时间内给予贷款

    4、客户要在规定的时间内还款

  • 死锁检测和解除:先检测是否发生死锁,再采取适当措施将死锁清除,如系统重启、撤销进程剥夺资源、进程回退策略

ThreadLocal 是什么?有哪些使用场景?

  • ThreadLocal是线程本地存储,在每个线程中都创建了一个ThreadLocalMap对象,每个线程可以访问自己内部ThreadLocalMap对象内的value。

  • 经典的使用场景是为每个线程分配一个JDBC连接Connection。这样就可以保证每个线程都在各自的Connection上进行数据库的操作,不会出现A线程关了B线程正在使用的Connection;还有Session管理等问题。

  • ThreadLocal仅是一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在Thread里面。ThreadLoacalMap属于Thread。

在线程池中使用ThreadLocal为什么可能导致内存泄漏?

  • 在线程池中线程的存活时间太长,往往都是和程序同生共死的,这样Thread持有的ThreadLocalMap一直都不会被回收,再加上ThreadLocalMap中的Entry对ThreadLocal是弱引用,所以只要ThreadLocal结束了自己的生命周期是可以被回收掉的。Entry中的Value是被Entry强引用的,即便value的生命周期结束了,value也是无法被回收的,导致内存泄漏。

  • 线程池中正确使用ThreadLocal的方法为:在finally代码块中手动清理ThreadLocal中的value,调用ThreadLocal的remove()方法

内存分配并发问题如何解决?

  1. CAS+失败重试
    CAS是一种乐观锁的实现方式,每次不加锁假设没有冲突而去完成内存分配,当发生冲突时就重试,直到成功为止。
  2. TLAB
    为每个线程预先在Eden区分配一块内存,JVM在给对象分配内存时,首先在TLAB分配,当本地缓冲区用完了,再使用上述CAS+失败重试的方式。

说一下 synchronized 底层实现原理?

  • 作用:

    • 确保线程互斥的访问同步代码
    • 保证共享变量的修改能够及时可见
    • 有效解决重排序问题
  • 用法:

    • 修饰普通方法
    • 修饰静态方法
    • 修饰代码块
  • 原理:

    • 同步代码块是通过monitorenter和monitorexit指令获取线程的执行权
      • 代码块的同步是利用monitorenter和monitorexit这两个字节码指令。他们分别位于同步代码块的开始和结束为止,当jvm执行到monitorenter指令时,当前线程试图获取monitor对象的所有权,如果未加锁或者已经被当前线程所持有的,就把锁的计数器加1,当执行monitorexit指令时,锁计数器-1,当锁计数器为0时,该锁就被释放了。如果获取monitor对象失败,该线程就会进入阻塞状态,直到其他线程释放锁
    • 同步方法通过加ACC_SYNCHRONIZED标识实现线程的执行权的控制
      • 方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构中的ACC-SYNCHRONIZED访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的ACC-SYNCHRONIZED访问标志是否被设置,如果被设置了,执行线程将先持有monitor,然后再执行方法,最后再方法完成无论是正常完成还是非正常完成时释放monitor。

volatile

volatile可以保证内存可见性,被volatile关键字修饰的变量在进行写操作转换成汇编语言时,会加上一个lock前缀指令,lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:

  • 1.重排序不能把后面的指令重排序到内存屏障之前的位置
  • 2.将当前cpu缓存的数据立刻回写到内存中;
  • 3.使在其他cpu缓存了该内存地址的数据无效

如何使其他数据无效,主要是通过MESI协议,当cpu写数据时,如果发现操作的变量是共享变量,那么他会发出信号通知其他CPU将该变量的缓存设置为无效状态。当其他cpu使用这个变量时,首先会先去判断是否有对该变量更改的信号,当发现这个变量的缓存已经无效时,会重新从内存中读取变量。

在哪里使用到volatile,举两个例子

  • 1.状态量标记,这种对变量的读写操作,标记为volatile可以保证修改对线程立刻可见,比synchronized,lock有一定的效率提升。

  • 2.单例模式的实现,典型的双重检查锁定dcl,懒汉式的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排,非instance加上了volatile

volatile 能保证数据的原子性吗?

首先是不能保证原子性的,只是对单个volatile变量的读写具有原子性,但对类似于i++就不能保证原子性

假设线程A读取了i的值为10,这时候进入了阻塞状态,因为还没有对变量进行修改,触发不了volatile规则,线程B此时也读取I,主内存i的值依旧为10,做自增,然后立刻写回主存了,为11,

此时又轮到线程A执行,由于工作内存保存的是10,所以继续做自增,再写回内存,11又被写了一遍,所以虽然这两个线程执行了两次add。结果却只加了一次

这是因为线程A的读取操作已经做过了,只有在读取操作的时候,发现自己的缓存无效,才会去读主存的值,所以这里线程A只能继续做自增

synchronized 和 volatile 的区别是什么?

  • volatile只能作用于变量,使用范围较小。synchronized可以用在变量、方法、类、同步代码块等,使用范围比较广。
  • volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以保证。
  • volate不会造成线程阻塞。synchronized可能会造成线程阻塞。

synchronized 和 Lock 有什么区别?

  • 原始构成:

    • synchronized是关键字属于jvm层面
    • Lock是具体类是API层面的锁
  • 使用方法:

    • synchronized不需要用户手动释放锁,当synchronized代码执行完后自动让线程释放对锁的占用
    • Reentranlock则需要用户手动释放锁,就有可能出现死锁现象;需要lock()和unlock()方法配合try/finally语句块来完成
  • 等待是否可中断:

    • synchronized不可中断,除非抛出异常或者正常完成
    • Reentrantlock可中断
  • 加锁是否公平

    • synchronized非公平锁
    • Reentrantlock两者都可以,默认非公平锁
  • 锁绑定多个条件Condition

    • synchronized没有
    • ReentrantLock用来实现分组唤醒需要唤醒的线程,可以精确唤醒,而不是像synchronized要么随机唤醒一个要么唤醒全部线程

说一下 atomic 的原理?

Atomic包中的类的基本特性就是在多线程环境下,当有多个线程同时对单个变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以像自旋锁一样,继续尝试,一直等到执行成功。

说一下乐观锁和悲观锁

  • 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁

  • 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。

  • 乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

  • 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它自己拿到锁。

  • 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

(三)反射

什么是反射?

  • 主要是指程序可以访问、检测和修改它本身状态或行为的一种能力

  • JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

  • 当我们new student时,jvm会加载我们的student.class,jvm会去本地磁盘找student.class文件,并加载到jvm内存中,将.class文件读入内存,同时产生相对应的class对象,一个类只产生一个class对象。class对象中包含着student类的一些信息。反射的本质理解:就是得到class对象后,反向获取studengt对象的各种信息。

  • class类的实例表示正在运行的java应用程序中的类和接口,没有公共构造方法,class对象是在加载类时由java虚拟机以及调用类加载器中的defineClass方法自动构造的。

  • 获取class对象的三种方式

    1.getClass()

    2.任何数据类型都有一个静态的class属性

    3.通过class类的静态方法:forName

  • 调用方法

    1.获取构造方法:

    1).批量的方法:
    public Constructor[] getConstructors():所有"公有的"构造方法
    public Constructor[] getDeclaredConstructors():获取所有的构造方法(包括私有、受保护、默认、公有)

    2).获取单个的方法,并调用:
    public Constructor getConstructor(Class… parameterTypes):获取单个的"公有的"构造方法:
    public Constructor getDeclaredConstructor(Class… parameterTypes):获取"某个构造方法"可以是私有的,或受保护、默认、公有;

    调用构造方法:

    Constructor–>newInstance(Object… initargs)

    2、 newInstance是 Constructor类的方法(管理构造函数的类)

    api的解释为:

    ``newInstance(Object… initargs)
    使用此 Constructor 对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。

    它的返回值是T类型,所以newInstance是创建了一个构造方法的声明类的新实例对象。并为之调用

  • 获取成员变量及调用

    1.批量的*

    1.Field[] getFields():获取所有的"公有字段"*

    2).Field[] getDeclaredFields():获取所有字段,包括:私有、受保护、默认、公有;*

    2.获取单个的:*

    ** 1).public Field getField(String fieldName):获取某个"公有的"字段;*

    ** 2).public Field getDeclaredField(String fieldName):获取某个字段(可以是私有的)*

    ​ 设置字段的值:*

    ​ Field --> public void set(Object obj,Object value)😗

    ​ 参数说明:*

    ​ 1.obj:要设置的字段所在的对象;*

    ​ 2.value:要为字段设置的值;

    获取成员方法并调用:

    1.批量的:*

    ​ public Method[] getMethods():获取所有"公有方法";(包含了父类的方法也包含Object类)*

    ​ public Method[] getDeclaredMethods():获取所有的成员方法,包括私有的(不包括继承的)*

    2.获取单个的:*

    ​ public Method getMethod(String name,Class… parameterTypes)😗

    ​ 参数:*

    ​ name : 方法名;*

    ​ Class … : 形参的Class类型对象

    ​ public Method getDeclaredMethod(String name,Class… parameterTypes)*

    ​ 调用方法:*

    ​ Method --> public Object invoke(Object obj,Object… args)😗

    ​ 参数说明:*

    ​ obj : 要调用方法的对象;*

    ​ args:调用方式时所传递的实参;*

  • 通过反射越过泛型检查,泛型用在编译期,编译过后泛型擦除(消失掉)。所以是可以通过反射越过泛型检查的。

  • Java反射机制主要提供了以下功能:

    • 在运行时判断任意一个对象所属的类
    • 在运行时构造任意一个类的对象
    • 在运行时判断任意一个类所具有的成员变量和方法
    • 在运行时调用任意一个对象的方法

-补充代码

动态代理是什么?有哪些应用?

  • 动态代理:在运行时,创建目标类,可以调用和扩展目标类的方法
  • 应用场景:
    • 统计每个api的请求耗时
    • 统一的日志输出
    • 校验被调用的api是否已经登录和权限鉴定
    • Spring的AOP功能模块采用动态代理的机制来实现切面编程

怎么实现动态代理?

首先需要定义一个被代理的接口,还需要一个实现类去实现InvocationHandler调用处理程序接口,通过Proxy提供创建动态代理类和实例静态方法,将实现类的类加载器、被代理的接口、以及实现类本身作为参数传入到Proxy中的newProxyInstance,用来创建生成代理类,需要在invoke()方法中处理代理实例,并返回结果,动态代理的本质,就是使用反射机制实现。

//等我们会用这个类,自动生成代理类

public class ProxyInvocationHandler implements InvocationHandler {

//被代理的接口
private Rent rent;

public void setRent(Rent rent) {
    this.rent = rent;
}
//生成得到代理类

/**
 * this.getClass().getClassLoader()   这个类所在的位置
 * rent.getClass().getInterfaces()  被代理对象的接口
 * this  ProxyInvocationHandler implements InvocationHandler  处理方法
 * @return
 */
public Object getProxy(){
    return Proxy.newProxyInstance(this.getClass().getClassLoader(),
            rent.getClass().getInterfaces(),this);
}
//处理代理实例,并返回结果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //动态代理的本质,就是使用反射机制实现
    Object result = method.invoke(rent, args);
    return result;
}
}
  • JDK动态代理
  • CGLib动态代理
  • 使用Spring aop模块完成动态代理

4.Web组件

Servelt,Fliter,Listen

Servlet是用来处理客户端请求的动态资源,也就是当我们在浏览器中输入一个地址回车跳转后,请求就会被发送到对应的Servlet上进行处理。

Servlet的任务有:

1.接收请求数据:我们都知道客户端请求会被封装成HttpServletRequesr对象,里面包含了请求头、参数等各种信息。

2.处理请求:通常我们会在service,dopost,doget方法进行接收参数,并且调用业务层的方法来处理请求

3.完成响应“处理完请求后,我们一般会转发(forword)或者重定向(redirect)到某个页面,转发是HttpServletRequest中的方法,重定向是HttpServletResponse中的方法,两者是有很大区别的

Servlet的创建:Servlet可以在以第1次接收时被创建,也可以在在服务器启动时就被创建,这需要在web.xml的中添加一条配置信息,< load-on-startup>5< /load-on-startup>,当值为0或者大于0时,表示容器在应用启动时就加载这个servlet,当是一个负数时或者没有指定时,则指示容器在该servlet被请求时才加载。

servlet的生命周期方法

servlet的初始化,旨在servlet实例时候调用一次,Servlet是单例,整个服务器就只创建一个同类型Servlet

servlet的处理请求方法,在servlet被请求时,会被马上调用,每处理一次请求,就会被调用一次,

servlet销毁之前执行的方法,只执行一次,用于释放servlet占有的资源,通常servlet是没有什么可要释放的,所以该方法一般都是空

Fliter

filter与servlet在很多的方面极其相似,但是也有不同,例如filter和servlet一样都又三个生命周期方法,同时他们在web.xml中的配置文件也是差不多的、 但是servlet主要负责处理请求,而filter主要负责拦截请求,和放行。

filter四种拦截方式

1.REQUEST:直接访问目标资源时执行过滤器。包括:在地址栏中直接访问、表单提交、超链接、重定向,只要在地址栏中可以看到目标资源的路径,就是REQUEST

2.FORWOARD:转发访问执行过滤器。包括RequestDispatcher#forward()方法、< jsp:forward>标签都是转发访问;

3.INCLUDE:包含访问执行过滤器。包括RequestDispatcher#include()方法、< jsp:include>标签都是包含访问;

4.ERROR:当目标资源在web.xml中配置包括RequestDispatcher#include()方法、< jsp:include>标签都是包含访问;

url-mapping的写法
匹配规则有三种:

  • 精确匹配 —— 如/foo.htm,只会匹配foo.htm这个URL
  • 路径匹配 —— 如/foo/*,会匹配以foo为前缀的URL
  • 后缀匹配 —— 如.htm,会匹配所有以.htm为后缀的URL*
  • < url-pattern>的其他写法,如/foo/ ,/.htm ,/foo 都是不对的。

执行filter的顺序

如果有多个过滤器都匹配该请求,顺序决定于web.xml filter-mapping的顺序,在前面的先执行,后面的后执行

Listener

listener就是监听器,我们在JAVASE开发时,经常会给按钮加监听器,当点击这个按钮就会出发监听事件,调用onClick方法,本质是方法回掉。

在JavaWeb的Listener也是这么个原理,但是它监听的内容不同,它可以监听Application、Session、Request对象,当这些对象发生变化就会调用对应的监听方法。

应用域监听:
Ø ServletContext(监听Application)

¨ 生命周期监听:ServletContextListener,它有两个方法,一个在出生时调用,一个在死亡时调用;

¨ 属性监听:ServletContextAttributeListener,它有三个方法,一个在添加属性时调用,一个在替换属性时调用,最后一个是在移除属性时调用。

HttpSession(监听Session)

¨ 生命周期监听:HttpSessionListener,它有两个方法,一个在出生时调用,一个在死亡时调用;

¨ 属性监听:HttpSessioniAttributeListener,它有三个方法,一个在添加属性时调用,一个在替换属性时调用,最后一个是在移除属性时调用。

ServletRequest(监听Request)

¨ 生命周期监听:ServletRequestListener,它有两个方法,一个在出生时调用,一个在死亡时调用;

属性监听:ServletRequestAttributeListener,它有三个方法,一个在添加属性时调用,一个在替换属性时调用,最后一个是在移除属性时调用。

感知Session监听:
1:HttpSessionBindingListener监听
⑴在需要监听的实体类实现HttpSessionBindingListener接口
⑵重写valueBound()方法,这方法是在当该实体类被放到Session中时,触发该方法
⑶重写valueUnbound()方法,这方法是在当该实体类从Session中被移除时,触发该方法
2:HttpSessionActivationListener监听
⑴在需要监听的实体类实现HttpSessionActivationListener接口
⑵重写sessionWillPassivate()方法,这方法是在当该实体类被序列化时,触发该方法
⑶重写sessionDidActivate()方法,这方法是在当该实体类被反序列化时,触发该方法

(四)IO

java 中 IO 流分为几种?

  • 按照数据的流向:
    输入流、输出流
  • 按照数据的格式:
    字节流(stream)、字符流(reader、writer)
  • 按照数据的包装过程:
    节点流(低级流)、处理流(高级流)

Files的常用方法都有哪些?

  • Files.exists() 检测文件路径是否存在
  • Files.createFile()创建文件
  • Files.createDirectory()创建文件夹
  • Files.delete() 删除文件或者目录
  • Files.copy() 复制文件
  • Files.move() 移动文件
  • Files.size() 查看文件个数
  • Files.read() 读取文件
  • Files.write()写入文件

BIO、NIO、AIO 有什么区别?

IO是面向流的、阻塞的
NIO是面向块的、非阻塞的,NIO有三个重要组成部分:通道(Channel)、缓冲区(Buffer)、选择器(Selector)
选择器可以注册到多个通道上,结合多线程NIO,可以处理大量IO操作。

BIO同步阻塞IO:线程发起IO请求,不管内核是否准备好IO操作,从发起请求起,线程一直阻塞,直到操作完成
NIO同步非阻塞IO:线程发起IO请求,立即返回;内核在做好IO操作的准备之后,通过调用注册的回调函数通知线程做IO操作,线程开始阻塞,直到操作完成。
AIO异步非阻塞IO:线程发起IO请求,立即返回;内存在做好IO操作的准备之后,做IO操作,直到操作完成或者失败,通过调用注册的回调函数通知线程做IO操作完成或者失败。

epoll的边缘触发和水平触发

  • 水平触发:当数据缓冲区中还有数据时,每次epoll_wait都会返回事件
  • 边缘触发:有数据到来只触发一次,即使数据缓冲区还有数据也不会返回事件

(五)序列化

什么是 java 序列化?什么情况下需要序列化?

  • 序列化:将Java对象转换成字节流的过程。
  • 反序列化:将字节流转换成Java对象的过程。
  • 当Java对象需要在网络上传输或者持久化存储到文件中时,就需要对Java对象进行序列化处理。
  • 序列化的实现:类实现Serializable接口,这个接口没有需要实现的方法。实现Serializable接口是为了告诉jvm这个类的对象可以被序列化。
  • 意义:序列化机制允许将实现序列化的Java对象转换为字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
  • 使用场景:所有可在网络上传输的对象都必须是可序列化的;所有需要保存到磁盘的Java对象都必须是可序列化的。
  • 注意事项:
    • 某个类可以被序列化,则其子类也可以被序列化
    • 声明为static和transient的成员变量,不能被序列化。static成员变量是描述类级别的属性,transient表示临时数据
    • 反序列化读取序列化对象的顺序要保持一致
    • 通常建议:程序创建的每个JavaBean类都实现Serializeable接口

序列化实现的方式

  1. Serializeable
    1.1 普通序列化
    Serializeable接口是一个标记接口,不用实现任何方法。一旦实现了此接口,该类的对象就是可序列化的。
public class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

序列化步骤:
步骤一:创建一个ObjectOutputStream输出流;
步骤二:调用ObjectOutputStream对象的writeObject()输出可序列化对象。

public class writeObject {
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
            Person person = new Person("张三", 24);
            oos.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

反序列化步骤:
步骤一:创建一个ObjectInputStream输入流;
步骤二:调用ObjectInputStream对象的readObject()得到序列化对象。

public class readObject {
    public static void main(String[] args) {
        try {
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
            Person person = (Person) ois.readObject();
            System.out.println(person.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

反序列化并不会调用构造方法,反序列对象是由JVM自己生成的对象,不通过构造方法生成。

1.2 成员是引用的序列化
如果一个可序列化的类成员不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的;否则,会导致此类不能序列化。

1.3 同一对象序列化多次的机制
Java序列化同一对象,并不会将此对象序列化多次得到多个对象。

1.4 Java序列化算法潜在的问题
由于Java序列化算法不会重复序列化同一个对象,只会记录已序列化对象的编号。如果序列化一个可变对象(对象内容可变)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。

1.5 可选的自定义序列化
使用transient关键字选择不需要序列化的字段。
使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对引用类型,值是null;对基本类型,值是0;对boolean类型,值是false。

  • 可以通过重写writeObject与readObject方法来实现自定义序列化。

  • 可以通过重写writeReplace与readResolve方法来实现自定义序列化。

  • Java序列化算法

    1. 所有保存到磁盘的对象都有一个序列化编码号
    2. 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出
    3. 如果此对象已经序列化过,则直接输出编号即可
  1. Externalizable:强制自定义序列化

通过实现externalizable接口,必须实现writeExternal、readExternal方法。

public interface Externalizable extends java.io.Serializable {
     void writeExternal(ObjectOutput out) throws IOException;
     void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。

  1. 两种序列化对比
实现Serializable接口实现Externalizable接口
系统自动存储必要的信息程序员决定存储哪些信息
Java内建支持,易于实现,只需要实现该接口即可,无需任何代码支持必须实现接口内的两个方法
性能略差性能略好

序列化版本号serialVersionUID

  • java序列化提供了一个private static final long serialVersionUID的序列化版本号,只要版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。

  • 如果反序列化使用的class的版本号与序列化时使用的不一致,反序列化会报InvalidClassException异常。

  • 序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。

  • 如果只是修改了方法,反序列化不容影响,则无需修改版本号;

  • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号;

  • 如果修改了非瞬态变量,则可能导致反序列化失败。如果新类中实例变量的类型与序列化时类的类型不一致,则会反序列化失败,这时候需要更改serialVersionUID。如果只是新增了实例变量,则反序列化回来新增的是默认值;如果减少了实例变量,反序列化时会忽略掉减少的实例变量。

  • 所有需要网络传输的对象都需要实现序列化接口,通常建议所有的javaBean都实现Serializable接口。

  • 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。

  • 如果想让某个变量不被序列化,使用transient修饰。

  • 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。

  • 反序列化时必须有序列化对象的class文件。

  • 当通过文件、网络来读取序列化后的对象时,必须按照实际写入的顺序读取。

  • 单例类序列化,需要重写readResolve()方法;否则会破坏单例原则。

  • 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。

  • 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。

说一下你熟悉的设计模式?

  • Java中一般认为有23种设计模式,总体来说设计默认分为三大类:

  • 创建型模式,共五种:

    • 工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
  • 结构型模式,共七种:

    • 适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
  • 行为型模式,共十一种:

    • 策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式
  • 工厂模式:创建产品,根据产品是具体产品还是具体工厂可分为简单工厂模式和工厂方法模式,根据工厂的抽象程度可分为工厂方法模式和抽象工厂模式。
    简单工厂模式:该模式对对象的创建管理方式最为简单,因为其仅仅简单的对不同类的创建进行了一层薄薄的封装。该模式通过向工厂传递类型来指定要创建的对象。

  • 工厂方法模式:和简单工厂模式中工厂负责生产所有产品相比,工厂方法模式将生成具体产品的任务分发给具体的产品工厂。也就是定义一个抽象工厂,其定义了产品的生产接口,但不负责具体的产品,将生产任务交给不同的派生类工厂,这样就不用通过指定类型来创建对象了。

  • 抽象工厂模式:在工厂方法模式中通过增加新产品接口来实现产品的增加。

  • 单例模式:顾名思义只有一个实例,并且自己负责创建自己的对象,这个类提供了一种唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

    • 核心代码:构造方法私有化
    • 懒汉式、饿汉式、双检锁、静态内部类、枚举
public class Singleton{
    private volatile static Singleton instance = null;
    
    private Singleton(){
    }
    
    public static Singleton getInstance(){
        if(instance == null){
            synchronized(Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 适配器模式:将一个类的接口转换成客户期望的另一个接口,适配器让原本接口不兼容的类可以相互合作。

    • Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可是具体类。
    • Adapter(适配器类):适配器可以调用另一个接口,作为转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。
    • Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。
  • 命令模式:将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。

Web

Http

  1. http:是超文本传输协议,是以明文方式发送信息,如果黑客截取了Web浏览器和服务器之间的传输保温,就可以直接获得其中的信息。
  • 原理:
    1. 客户端的浏览器先要通过网络与服务器建立连接,该连接是通过tcp来完成的,一般tcp连接的端口号是80建立连接的,客户机发送一个请求给服务器,请求方式的格式为:统一资源标识符(url)、协议版本号、后面是mime信息包括请求修饰符、客户机信息和许可内容
    2. 服务器接收到请求后,给予相应的响应信息,其格式为一个状态行,包括信息的协议版本号、一个成功或者错误的代码,后面是mime信息包括服务器信息、实体信息和可能的内容 。
  1. http方法有哪些?
    1. get:用于请求访问已经被uri识别的资源,可以通过Url传参给服务器
    2. post:用于传输信息给服务器
    3. put:传输文件,报文主体中包含文件内容,保存到对应uri位置
    4. delete:删除文件,删除对应url位置的文件
    5. head:获得报文首部。只是不返回报文主体,一般用于验证uri是否有效
    6. options:查询相应uri支持的http方法
  2. get和put方法的区别
    1. get重点是从服务器上获取资源,post重点在向服务器发送数据;
    2. get传输数据是通过url请求,以field(字段)=value的形式,置于url后,并用?连接,多个请求数据间用&连接,这个过程用户是可见的;而post传输数据通过hhtp的post机制,将字段与对应值封存在请求实体中实体中发送给服务器,这个过程对用户是不可见的;
    3. get传输的数据量小,因为受url长度限制,但效率高;而post可以传输大量数据,所以上传文件时只能用post方式;
    4. get是不安全的,因为url是可见的,可能会泄露私密信息,密码等;post相比get安全性高;
    5. get方式只能支持ascii字符,向服务器传的中文字符可能会乱码; 而post支持标准版=字符集,可以正确传递中文字符。
  3. 常见的http相应的状态码

    — 200:请求被正常处理
    — 204:请求被受理但没有资源可以返回
    — 206:客户端只是请求资源的一部分,服务器只对请求的部分资源执行get方法,相应报文中通过conteng-range指定范围的资源。
    — 301:永久性重定向
    — 302:临时性重定向
    — 303:与302状态码有相似功能,只是希望客户端在请求一个Url的时候,能通过get方法重定向搭配另一个uri上
    — 304:发送附带条件的请求时,条件不满足时返回,与重定向无关
    — 307:临时重定向,只是强制要求使用post方法
    — 400:请求报文语法有误,服务器无法识别
    — 401:请求需要认证
    — 403:请求的对应资源禁止被访问
    — 404:服务器无法找到对应资源
    — 500:服务器内部错误
    — 503:服务器正忙

  4. http的长连接和短连接

    HTTP1.1规定了默认长度连接,数据传输完成了保持TCP连接不断开(不四次握手),等待在同域名下继续用这个通过传输数据;相反的就是短连接

  5. http1.0,1.1的区别
    • http1.0:规定浏览器和服务器保持短暂的连接,浏览器的每次请求都需要与服务器建立一个Tcp连接,服务器处理完成后立即断开tcp连接,服务器不跟踪每个客户端也不记录过去的请求(无状态)
      这种无状态可以借助cookie/session机制来做身份认证和状态记录,但是会出现两个问题
    1. 无连接的特征导致最大的性能缺陷就是无法复用连接。每次发送请求的时候,都需要进行一次tcp连接,而tcp连接释放过程又是比较费事。所以这种无连接的特性会使得网络的利用率非常低。
    2. 队头阻塞。由于http1.0规定下一个请求必须在前一个请求响应到达之前才能发送。假设前一个请求响应一直不到达,那么下一个请求就不发送,同样的后面的请求也给阻塞了。
    • 为了解决1.0的问题,1.1出现了,对于1.1不仅继承了1.0简单的特点,还解决了1.0性能上的问题。
    1. 首先是长连接,1.1增加了一个connection字段,通过设置keep-alive可以保持http连接不断开,避免了每次客户端与服务器都要重复建立tcp连接,提高了网络的利用率。如果客户端想关闭http连接,可以在请求头中携带connection,false来告知服务器关闭请求。
    2. 支持请求管道化。基于1.1的长连接,使得请求管道化成为可能。管道化使得请求能够并行传输。就比如说,响应的主体是一个html页面,页面中包含了很多Img,这个时候keep-alive就起了很大的作用,能够进行并行发送多个请求。 http管道化可以让我们把先进先出队列从客户端迁移到服务器。
    • 但存在的问题是,1.1还是无法解决队头阻塞问题。同样管道化也存在各种各样的问题,所以很多浏览器要么根本不支持,要么就默认直接关闭,而且虽然支持管道化,但是服务器也必须进行一个一个响应的送回,并行指的就是不同的tcp连接上的http请求和响应。
  • 此外1.1还加入了缓存处理新的字段如cache-control,支持断点传输,以及增加了Host字段,使得一个服务器能够用来创建读个web站点。 ‘

状态码

2XX:表示成功处理了请求的状态码

3XX:(重定向)表示要完成请求,需要进一步操作。通常,这些状态代码用来重定向

4XX:(请求错误)这些状态码表示请求可能出错,妨碍了服务器的处理(客户端出现的错误)

5XX:(服务器错误)这些状态码表示服务器在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求错误

重定向和转发的区别及应用场景分析

1.转发只能将请求转发给同一个WEB应用中的组件;而重定向不仅仅可以重定向到当前应用程序中的其他资源,还可以重定向到同一个站点上的其他应用程序中的资源,甚至是使用URL重定向到其他站点的资源

2.重定向访问过程后,浏览器的地址栏中显示的URL会发生改变,由初始的URL地址变成重定向的目标URL;请求转发结束后,浏览器地址栏的URL保持不变

3.重定向方法对浏览器的请求直接作出相应,相应的结果就是告诉浏览器去重新发出对另外一个URL的访问请求。

转发在服务器端内部将请求转发给另外一个资源,浏览器只知道发出了请求并得到了响应结果,并不知道在服务器程序内部发生了转发行为。

4.转发方法的调用者与被调用者之间共享相同的request对象和reponse对象,属于同一个访问请求和响应过程

重定向方法的调用者与被调用者使用各自的request对象和reponse对象,他们属于两个独立的访问请求和响应过程

5.无论是request.getRequestDispatcher().forward()方法,还是response.sendRedirect()方法,在调用它们之前,都不能有内容已经被实际输出到了客户端。如果缓冲区中已经有了一些内容,这些内容将被从缓冲区中。

1、转发使用的是getRequestDispatcher()方法;重定向使用的是sendRedirect();

2、转发:浏览器URL的地址栏不变。重定向:浏览器URL的地址栏改变;

3、转发是服务器行为,重定向是客户端行为;

4、转发是浏览器只做了一次访问请求。重定向是浏览器做了至少两次的访问请求

5、转发2次跳转之间传输的信息不会丢失,重定向2次跳转之间传输的信息会丢失(request范围)。

转发和重定向的选择
1、重定向的速度比转发慢,因为浏览器还得发出一个新的请求,如果在使用转发和重定向都无所谓的时候建议使用转发。

  **2、因为转发只能访问当前WEB的应用程序,所以不同WEB应用程序之间的访问,特别是要访问到另外一个WEB站点上的资源的情况,这个时候就只能使用重定向了。**

https

  • https:是以安全为目标的http通道,是http的安全版。https的安全基础是sll.ssl协议位于tcp/ip协议与各种应用协议之间,为数据通讯提供安全支持。
  • 设计目标:
    1. 数据保密性:保证数据内容在传输的过程中不会被第三方查看。
    2. 数据完整性:及时发现被第三方篡改的传输内容。
    3. 身份校验安全性:保证数据到大用户期望的目的地。

http和https区别

  1. 首先http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
  2. http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
  3. http的连接很简单,是无状态的。https协议是由ssl+http协议构建的可进行加密传输,比hht协议安全。其中无状态的意思就是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值