Java基础

目录

面向对象基础

1.关于面向对象

2.什么是变量和常量?成员变量和局部变量?介绍一下静态变量和实例变量

3.八种基本数据类型

延伸问题:

4.常用字符串类 String、StringBuilder、StringBuffer

5.Java程序初始化的顺序是什么样子的? 

6.Java中值的传递方式?

7.在不知道某个类的类型的情况下,将类强制转化为另一个类,即:(type)entity,将entity强制转化为type类型,怎样才能保证entity本来就是type类型避免转化发生异常?

8.使用abstract修饰类、方法,有什么区别。

延伸问题

9.什么是内部类、匿名内部类、静态内部类、内部接口

10.强引用、软引用、弱引用、虚引用

集合

11.Java常见的集合

延伸问题:

List

Set

Map

Queue

Java的高级特性

泛型

13. IO和NIO

14.通过try-catch语法来抓取异常和处理异常

并发编程

15.锁

16.多线程

17.关于ThreadLocal,带着问题看这里

18.常见的JUC工具类

19.线程池


面向对象基础

1.关于面向对象

Java面向对象编程是一种思想,是现实生活中实际事务的一个抽象化的模型。这个模型包含了该事务的具体属性。

三大特性:封装(封装属性和方法)、继承(可以重写方法)、多态(存在继承、父类引用指向子类对象、发生了重写,调用时调用的重写方法)。(这里不具体介绍,网上很多相关的文章自行搜索)

延伸问题:

  • 不同的访问权限控制符表示的访问权限。private和public不用说,主要注意默认修饰符和protected,默认修饰符只能同包,protected可以是同包或子类。
  • super和this关键字的用法?父类和子类的:成员变量、方法、构造函数,注意:通过this和super调用构造函数时只能放在第一行,并且this和super调用构造函数不能同时使用(当时new一个子类对象实例时,也会调用父类的构造函数)
  • 介绍一下final、finally、finalize的区别和使用场景。关于finalize。finalize()是Object中的方法,当垃圾回收器将要回收对象所占内存之前被调用,即当一个对象被虚拟机宣告死亡时会先调用它finalize()方法,让此对象处理它生前的最后事情(这个对象可以趁这个时机挣脱死亡的命运)。但不保证方法里的任务会被执行完。
  • Object中equals和hashCode的作用分别是什么?
    equals用于对象比较,如果对象没有重写Object的equals方法,那么equals实际上是(this==obj)直接比较两个对象的引用地址是否相同,即:两个地址都指向同一个对象时才会返回true,而String重写了equals方法,通过比较具体的字符是否相同来比较两个字符串是否相等。
    Object中,haseCode是一个native本地方法,返回的是由对象存储地址转化的值。两个Object类型的对象equals为true,那么hashCode也是一样的,但是直接从代码角度,如果一个类重写了equals方法,没有重写hashCode方法,那么equals返回true时,两个对象的hashCode依然可能不一样。
  • Object中的克隆clone方法是什么克隆?浅克隆,浅克隆通俗的理解就是,复制了一个对象,但是对象中如果包含了其他对象或数组的引用,那么浅拷贝并不会复制引用指向的对象,比如,A对象中包含了LIst对象nodes的引用,那么在复制A的时候,复制对象B和A是不同的堆内存,但B中的List对象和A中的List对象是相同的,这就是浅克隆
  • 考虑为什么重写equals方法也必须同时重写hashCode方法?如果这两个方法只重写一种会出现什么问题?可以想一下HashMap和HashTable,它们在比较key是否相同时都会用到key的hashCode方法。如果一个类重写的equals方法,没有重写hashCode方法,有可能出现如:A a1 = new A(); A a2 = new new A();  a1.equals(a2)为true,但是我们将a1,a1作为key添加到HashMap中,会出现a1,a2都在map中,因为HashMap内部就会根据hashCode()方法来比较key是否相等。
  • 介绍一下override(重写)和overload(重载)区别?重载发生在同一个类中,重写发生在子类中
  • 为什么不能从返回值区分是否是重载方法
  • B继承A,并重写test方法,A a = new B(); a.test()是调用A的实现还是B的实现
    方法调用顺序级:父类构造函数(O) > 子类构造函数(O) > 子类重写的函数A > 父类函数A(子类没有重新函数A时)
  •  父类引用变量指向子类实例对象时,通过该引用变量只能调用父类中已经存在的方法或变量,否则只能通过强制转化为子类才能调用子类中独有的变量和方法
  • 是否可以继承多个类,是否可以实现多个接口?Java通过什么方式来实现达到多继承的目的
  • static修饰符常用:修饰内部类、修饰变量、修饰代码块、修饰方法

2.什么是变量和常量?成员变量和局部变量?介绍一下静态变量和实例变量

3.八种基本数据类型

     整型:byte、short、int、long,浮点型:float、double,字符型:char,布尔型:boolean

简单类型

boolean

byte

char

short

Int

long

float

double

二进制位数(空间)

1

8

16

16

32

64

32

64

封装器类

Boolean

Byte

Character

Short

Integer

Long

Float

Double

延伸问题:

  • 范围从小到大:byte->short->int->long->float->double
  • 介绍类型转换:自动转换(小转大)和强制转换(大转小),表达式类型的自动提升,二个整数变量相加时  默认是int类型相加  当二个浮点型类型相加时  默认是doblue类型相加
  • 强制转换的情况:范围大转小范围,父类转子类
  • 浮点类型是否可以直接用"=="比较,如果不能,应该怎么进行比较
  • char类型是否可以储存汉字
  • 对应的包装类型
  • 介绍一下自动装箱与拆箱
  • Integer和int在自动装箱拆箱时,使用"=="比较会出现的问题。包装类的缓存。
  • 为什么String对象比较相同时,使用equals方法,不用"=="。在Java中,除了值类型,另外还有一种引用类型,而不同的对象,引用值也是不相等的,即在内存中的不同的地址单元中。比如A a = new A()。 new A()产生的对象存放在堆内存空间,引用类型 a 存放在栈中,保存的是指向new A()对象在堆内存中的地址。而“==”就是比较引用类型a存放的地址,而我们通常在比较对象的时候是看对象的内容是否相等。equals方法是Object中定义的方法,我们通过重写equals方法就可以按照我们自己的想法来确定比较两个对象是否相等的方式。

4.常用字符串类 String、StringBuilder、StringBuffer

  • 执行速度方面,StringBuilder大于StringBuffer大于String。
    String最慢的原因:String为字符串常量,内部采用final修饰char数组,是定长的。因此当扩容的时候其实每次都是重新new了一个char[]数组,重新开辟了内存空间。
    StringBuilder和StringBuffer均为字符串变量,内部采用变量char[]数组,只有当目前char[]数组达到最大容量后才会进行扩容,因此达到char[]数组最大容量的时候才会重新new一个char[]数组,重新开辟内存空间,然后将原数组的值copy到新的数组里。
    简单点说,String修改每次都要new一个char数组,重新开辟内存空间。而StringBuilder和StringBuffer只有当容量满了之后才会new一个char数组。StringBuilder和StringBuffer默认初始数组容量为16。String直接就是赋值的字符串长度,new String()时数组容量是0.
  • 线程安全:StringBuilder是线程不安全的,而StringBuffer是线程安全的,StringBuffer中通过在方法上使用synchronized关键字(同步关键字)实现线程安全。

5.Java程序初始化的顺序是什么样子的? 

一般遵循三个原则: 

  • 1.静态优先于非静态
  • 2.父类优先于子类
  • 3.字段优先于代码块,代码块优先于构造函数

所以调用顺序为:
父类静态成员变量=》父类静态代码块=》子类静态成员变量=》子类静态代码块=》父类成员变量=》父类代码块=》父类构造函数=》子类成员变量=》子类代码块=》子类构造函数

注意:在父类中调用了一个非静态重写方法的时候实际上是调用的子类的实现。非静态方法前面默认有个this,this在构造中表示正在创建的对象,也就是说:方法被子类重写了,调用非静态方法执行的就是子类的重写的代码。当然如果在父类中调用的是静态方法那么仍然是调用的父类的实现。

6.Java中值的传递方式?

传递基本类型时是直接拷贝值,而传递对象类型时,是直接传递对象的内存地址。

7.在不知道某个类的类型的情况下,将类强制转化为另一个类,即:(type)entity,将entity强制转化为type类型,怎样才能保证entity本来就是type类型避免转化发生异常?

    在强制转化前使用instanceof判断entity是否是type类型。用法:boolean result = object instanceof class

8.使用abstract修饰类、方法,有什么区别。

延伸问题

  • 接口和抽象类对比
    抽象类不能通过new和反射的方式实例化

9.什么是内部类、匿名内部类、静态内部类、内部接口

10.强引用、软引用、弱引用、虚引用

https://www.cnblogs.com/yw-ah/p/5830458.html

集合

11.Java常见的集合

    基于List接口的有:ArrayList、LinkedList、Vector(子类Stack)看这里

    基于Set接口的有:HashSet、LinkedHashSet、TreeSet

    基于Map的有:HashMap、LinkedHashMap、TreeMap、Hashtable(子类Properties经常使用)看这里

延伸问题:

List

  • ArrayList与LinkedList、Vector的特性与区别?
    ArrayList底层基于数组实现。而数组在内存会占用连续的内存空间。因此我们可以通过索引快速访问元素。但是当在数组中间插入或删除数据的时候,就需要对一大块连续的数据进行移动,才能保证通过索引的正确访问,这种情况下速度就不如链表了。
    此外,当数组长度不够时,会建立新的数组,并且将通过Arrays.copyOf将旧数组拷贝给新数组,此时需要开辟新的内存空间。因此使用ArrayList时设置一个比较适宜的初始容量,可以避免扩容带来的时间和内存的损耗。
    注意:需要注意的是Arrays.copyOf是浅拷贝,这句话需要从数组在堆中的存放考虑,一个比如有个一个String类型的数组A,并且A存储了3个字符串“1”,“2”,“3”,此时在内存中的表示如下图所示:

    因此我们常说的数组拷贝其实就是数组对象的拷贝,拷贝过后的数组内和原数组每个位置指向的对象时一样的,因此ArrayList数组扩容,并没有将ArrayList里面数组指向的对象重新复制一份,数组每个位置指向的对象仍然是之前的对象,只是数组这个对象扩容了,可以指向更多的对象了。具体扩容变化如下图所示:
  • LinkedList底层采用链表存储。添加元素时会生成新的Node节点,开辟新的内存空间,这种方式的好处是不会实现耗费内存,并且对随机插入删除操作友好,对一个节点的操作只会影响这个节点的父节点和子节点。
    可以用做队列使用
    Vector也是基于数组实现,不同的是Vector采用对方法加synchronized隐式锁的方式来保证线程安全,相当于对这个Vector对象加锁,这种方式并发效率不高。
    线程安全的List通常都使用CopyOnWriteArrayList,因为CopyOnWriteArrayList使用显示锁对写操作加锁,而读操作没有加锁,因此CopyOnWriteArrayList的并发读效率会更高。此外SynchronizedList采用在内部定义了一个Object对象metux,通过对该对象使用synchronized隐式锁来实现互斥锁操作,这种方式是读写互斥,因此并发效率不及CopyOnWriteArrayList。
  • 数组和集合的比较排序?
    集合中的元素可以使用工具类Collections.sort()方法进行比较:
    一种要求比较的元素实现了Comparable接口(Collections#sort(List<T> list))
    另一种是要求传入实现了Comparator接口的比较器类(Collections.sort(List<T> list, Comparator<? super T> c))
    而还可以直接调用父类中的sort(Comparator<? super E> c)方法
    数组可以采用Arrays.sort()方法进行排序,传入的参数同样,要么数组的元素实现了Comparable接口,要么传入一个实现了Comparator接口的比较器类。
  • 什么是快速失败fail-fast和安全失败fail-safe?怎么在遍历集合的时候删除元素?通过Iterator删除,普通的集合都是非线程安全的,如果一个线程直接调用删除,当多线程的情况下另外一个线程正在通过索引访问该集合,就可能出现索引处没有元素从而出现异常即快速失败fail-fast。而当集合调用Iterator()方法时,会生成一个Iterator对象,该对象会持有一个int类型的变量expectedModCount表示当前集合的修改次数,而集合本身也有一个变量modCount,正常情况下这两个值是一样的,但是当多线程访问修改集合时,就可能出现iterator对象持有的expectedModCount和modCount不一致,此时就说明集合被其他线程修改了,此时进行删除操作会主动抛出异常,这是安全失败fail-safe。不管怎样需要线程安全时要用线程安全的集合操作

Set

  • HashSet其实基于HashMap实现,LinkedHashSet是基于LinkedHashMap实现,TreeSet是基于TreeMap实现
    CopyOnWriteArraySet实际上是通过CopyOnWriteArrayList进行去重处理实现的,本质上还是CopyOnWriteArrayList

Map

  • HashMap、LinkedHashMap、TreeMap、Hashtable有什么特性与区别?
    HashMap:在JDK1.7采用数组+链表的结构,在JDK1.8采用数组+链表+红黑树的结构。
    LinkedHashMap:在HashMap的基础上,将每一个Entry节点用双向链表维护起来。
    TreeMap:基于红黑树实现
    HashTable:数组+链表的结构,并且线程安全。
  • HashMap 在JDK1.8和1.7有什么不同?
    1.数据结构变了
    JDK1.7,HashMap采用数组+链表,
    JDK1.8,HashMap采用数组+链表+红黑树,添加数据时,判断链表的长度有没有超过8,如果链表长度超过8则进入到treeifyBin的方法里,在这个方法里会先判断当前数组容量有没有达到64,如果数组容量没有达到64则调用resize进行扩容,如果数组容量达到64了则将链表转化为红黑树。
    2.插入数据的方式变了
    JDK1.7,插入数据采用头插法
    JDK1.8插入数据采用尾插法,在插入数据的时候头插法效率更高,但是在查询的时候,由于JDK1.8采用了数组+链表+红黑树的方式,避免了单链过长的问题,所以查询效率更高。
    3.扩容方式变了
    JDK1.7在添加数据时会判断比较当前size和threshold阈值,如果当前size大于阈值并且当前插入的位置数据不为空,则进行resize扩容,在resize方法内部会调用一个transfer方法,transfer方法通过循环从头遍历原来的链表并依次采用头插法构成新的链表,如果链表的节点重新计算hash后还是在同一个链表里,这时新的链表就是原链表结点倒序形成,这种方式在并发的时候就可能发生死循环,如A线程此时nodeA.next->nodeB ,而在A线程的操作未完成的时候,B线程先完成了transfer操作造成nodeB.next->nodeA,此时再回到A线程就形成了死循环
    JDK1.8因为加入了红黑树结构因此在resize的时候需要考虑到节点是否是红黑树节点,而对于链表结构,JDK1.8为了避免死循环的形成,去除了transfer方法,在resize方法内部通过两个临时节点loHead/loTail指向重hash后位置不变的链表头和尾,以及hiHead/hiTail指向重hash后位置改变的链表头和尾,这样在循环遍历链表的过程中就生成了新的链表,最后再将新的链表表头存放到数组中,从而避免了1.7链表倒序的问题,也就避免了并发时死循环的问题
  • 如果并发情况下使用HashMap会造成什么问题?
    死循环:1.7会出现,1.8解决了这个问题。1.7扩容的时候会出现。上面有说明
    数据丢失:两条线程同时向数组中添加数据时,都判断数组table[i]=null,此时两个线程一前一后向table[i]位置插入数据,这样存入会出现数据丢失,table[i]位置会存放后插入的数据。
    数据重复:如果两个线程都向链表中添加数据,两个线程同时判断链表中key不存在,而这两个线程的key实际是相同的,在后面向链表添加节点时,两个线程一先一后,这样就可能出现链表中存在key相同的两个节点,造成数据重复。
  • 线程安全的Map有ConcurrentHashMap,Collections.synchronizedMap()和SynchronizedMap,为什么要使用ConcurrentHashMap?因为ConcurrentHashMap的性能更好。
    ConcurrentHashMap在jdk1.7采用分段锁思想,将哈希表分段,分段存放在一个Segment[]数组里面,每一个Sement分别对每一个分段进行加锁处理。
    而在JDK1.8采用CAS+Synchronized来提高并发效率。
    如果通过计算hash后,得到数组的索引,而该位置的数据为null,那么就通过CAS乐观锁的方式进行添加节点,添加失败则进行自旋(然后通过一个无判断的for循环进行自旋操作)。添加成功则直接break。
    如果通过计算hash后,得到数组的索引,而该位置的数据不为null,而Synchronized是对数组存放的链表的表头进行加锁处理,因此,在添加的时候必须先获得表头的锁。
    Collection.synchronizedMap()就是生成SynchronizedMap,SynchronizedMap定义了一个Object类型的mutex,在Map内部通过synchronized(mutex)形成互斥锁操作。
  • HashMap和Hashtable的区别?
    HashMap线程不安全,Hashtable线程安全。
    HashMap允许键值为null,Hashtable键值为null会抛出异常。ConconrrentHashMap也不允许键值为null。
    哈希值的使用不同,HashTable直接使用对象的hashCode(ConconrrentHashMap也是)。而HashMap自己定义了一个计算hash值的方法重新计算key的hash值。
    Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。

Queue

  • 常见的Queue队列和Deque双端队列,队列先进后出,双端队列可以先进后出也可以先进先出。
    Deque接口继承了Queue接口,因此Deque也是Queue。
    常见的非线程安全非阻塞的队列和双端队列:
    ArrayQueue(基于数组实现,没有实现Queue接口),ArrayDeque也是基于数组,他们都用了int类型的head和tail表示队头和队尾的索引,不同的是ArrayDeque增加了从队尾出队的方法比如pollLast()。
    PriorityQueue内部也是使用了Object数组,PriorityQueue将添加元素和队列里的元素进行比较来实现优先对比,因此PriorityQueue在初始化的时候要么添加一个实现了Comparator接口的比较器类,要么添加的元素实现了Comparable接口。
    常见的线程安全的非阻塞队列和双端队列:
    ConcurrentLinkedQueue、ConcurrentLinkedDeque都是基于链表实现,在添加和出队元素的时候采用CAS乐观锁加自旋的方式实现线程安全。
    常见的线程安全的阻塞队列和双端队列
    ArrayBlockingQueue:基于数组实现,定义了三个final类型的变量
    final ReentrantLock lock;
    private final Condition notEmpty;
    private final Condition notFull;
    内部采用显示锁lock实现线程安全,put方法实现阻塞入队,当队列已满会调用notFull.await()阻塞当前线程,在调用remove、poll、take方法成功出队后会调用notFull.signal()方法唤醒添加元素的阻塞线程。take方法实现阻塞出队,当队列为空时使用notEmpty.await()方法阻塞当前线程,在调用add、offer、put方法成功添加元素后会调用notEmpty.signal()唤醒出队操作的阻塞线程。
    LinkedBlockQueue:基于单向链表实现,在new生成对象的时候可以指定容量,如果不指定(即默认情况)则使用Integer.MAX_VALUE。定义了四个final类型的变量
    private final AtomicInteger count = new AtomicInteger();
    private final ReentrantLock takeLock = new ReentrantLock();
    private final Condition notEmpty = takeLock.newCondition();
    private final ReentrantLock putLock = new ReentrantLock();
    private final Condition notFull = putLock.newCondition();
    
    可以看出相对于ArrayBlockingQueue入队和出队都使用同一个lock进行加锁,LinkedBlockingQueue将使用takeLock和putLock分别对出队和入队进行加锁,并且定义AtomicInteger变量count来表示当前链表元素个数,这样做的目的是为了提高并发效率,在LinkedBlockQueue中,调用put方法实现阻塞入队,当容量已满会调用notFull.await()阻塞当前线程,而当出队操作成功会唤醒put方法阻塞的线程,入队操作线程继续执行,入队操作完成后,在put方法内部会判断此时count+1是否小于最大容量,如果小于,会继续唤醒其他添加元素的阻塞线程。而调用take方法实现阻塞出队时,当count=0时会调用notEmpty.await()阻塞当前线程,而当入队操作成功唤醒take方法阻塞的线程后,出队操作线程继续执行,出队操作完成后,在take方法内部会判断count.getAndDecrement()是否小于1,如果不小于1表示,当前队列不止一个元素,因此可以继续唤醒其他被阻塞的出队操作线程。可以看出LindedBlockingQueue的入队和出队并发效率是要高于ArrayBlockingQueue的。
    为什么ArrayBlockingQueue不采用LinkedBlockingQueue一样的方式来提高并发效率呢?
    因为,ArrayBlockingQueue采用数组存储,在入队和出队的时候都是通过索引来进行操作的,并且会用到数组的length,他们都是线程不安全的,因此ArrayBlockingQueue入队和出队使用同一个锁来保证线程安全。
    PriorityBlockingQueue:基于数组实现,他的实现和ArrayBlockingQueue相似,不同的是在PriorityBlockingQueue进行入队操作的时候会将入队元素与队列中的元素进行比较以此来实现优先级排序。而我们在使用PriorityBlockingQueue的时候需要指定一个实现了Comparator接口的比较器类或者添加的元素是实现了Comparable接口了的。
    SynchronousQueue:内部没有数组或链表作为存放数据的容器,只用了一个链表来存放阻塞的操作线程。SynchronousQueue有公平模式和非公平模式,分别利用内部类TransferQueue和TranseferStack实现,他们都重写了继承了抽象类Transferer,重写了transfer方法.无论是调用put方法还是take方法,最终都会调用transfer方法
    transfer方法会根据传入的参数是否为空来设置布尔值isData,isData用来表示当前操作是put数据还是take数据。
    先说公平模式的transfer方法主要分为两步:
    1. 如果isData与当前链表的尾结点状态一致,那么将该节点添加到链表末尾。自旋一小会后睡眠等待。
    2. 如果isData与当前链表的头结点状态不一致,如果当前操作是put操作,则表明阻塞的是take操作,那么通过cas设置链表阻塞的第一个节点的值,然后让出队。如果当前操作是take操作,则表明阻塞的是put操作,则返回阻塞的第一个节点的值,并且让第一个节点出队。
    可以发现当等待队列中阻塞的操作与本次操作互补(take、put)时,并不会生成节点插入链表,并让链表中第一个阻塞的节点出队。如果本次操作与阻塞队列的操作相同的时候会在链表末尾插入节点。
    非公平模式的transfer:
    1. 状态一致,节点从链表表头插入
    2. 不一致也会从表头插入,但是会接连出栈两个节点。

Java的高级特性

泛型

12. Java泛型是什么?参数化类型这篇文章

  • Java是通过什么方式实现泛型的?类型擦除。什么是类型擦除? 在类信息通过类加载器加载到JVM数据区的时候,泛型参数被替换成Object类型,比如:可以利用反射调用泛型参数是Integer类型方法但输入String类型的参数。如果指定了上界会替换成上界类型。注意:编译的时候不会发生类型擦除。
  • Java中泛型有哪几种使用方式?泛型类、泛型接口、泛型方法,写一下他们各自怎么实现?
  • 什么是限定通配符和非限定通配符?使用“?”表示泛型,非限定通配符没有指定“?”表示的范围,限定通配符指定了“?”通配符的上界或者下界。说一下泛型中上下边界是什么意思换句话说List<? extends T>和List <? super T>之间有什么区别 ?看这里
  • 可以把List<String>传递给一个接受List<Object>参数的方法吗?编译通不过,类型擦除发生省Java类加载阶段
  • Java类的生命周期或者说Java类的运行过程?主要分为两个阶段:类加载阶段、运行阶段
    类加载的主要动作就是将.class文件以字节码的形式通过ClassLoader加载器加载到JVM运行时的数据区。
    类加载的详细步骤还分为:加载、连接(验证、准备、解析)、初始化三个部分
    准备是指:为类的静态变量分配内存,并将其赋默认值
    解析:将常量池中的符号引用替换为直接引用(内存地址)的过程
  • Java中List<Object>和原始类型List之间的区别?List<String>和原始类型List之间的区别?看这里

13. IO和NIO

  • IO流有哪些分类?根据流向分为输入流和输出流,根据类型分为字节流和字符流
  • 经常使用的输入流和输出流(字节流和字符流)有哪些
    字符流:
    BufferedWriter/BufferedReader,InputStreamWriter/OutputStreamReader,FileWriter/FileReader,PipedWriter/PipedReader,CharArrayWriter/CharArrayReader,StringWriter/StringReader,PrintWriter
    字节流:
    BufferedInputStream/BufferedOutputStream,FileInputStream/FileOutputStream,ByteArrayInputStream/ByteArrayOutputStream,PipedInputStream/PipedOutputStream,DataInputStream/DataOutputStream,ObjectInputStream/ObjectOutputStream
    注意:字节流和字符流向文件写数据,可以指定采用追加还是覆盖,覆盖会清空原来的数据,默认采用覆盖
  • 关闭流的方式:1. finally中调用close方法关闭,2. 对于实现了AutoCloseable接口的IO流可以使用try-with-resource语法实现自动关闭
  • BufferedWriter/BufferedReader、BufferedInputStream/BufferedOutputStream为什么更快,内部定义了char数组和byte数组,通过内部定义的数组来实现缓存的功能,实现数据的批量操作。
  • NIO和传统IO的区别
    传统IO:面向流、阻塞IO
    NIO:面向缓冲、非阻塞IO、利用Selector可以同时监测多个Channel
  • NIO的三个核心Channel、Buffer、Selector
    常见的Channel实现有:FileChannel对应文件IO、ServerSocketChannel/SocketChannel对应TCP的Server和client、DatagramChannel对应UDP
    常见的Buffer实现有:NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。当然NIO中还有MappedByteBuffer, HeapByteBuffer, DirectByteBuffer等
    Selector 一个组件,可以监测多个NIO channel,看看读或者写事件是否就绪。多个Channel以事件的方式可以注册到同一个Selector,从而达到用一个线程处理多个请求成为可能。
    Buffer: flip()方法,将Buffer从写模式切换到读模式,将position值重置为0,limit的值设置为之前position的值;
    Buffer: clear方法 vs compact方法:clear方法清空缓冲区;compact方法只会清空已读取的数据,而还未读取的数据继续保存在Buffer中;
  • NIO的分散与聚集:Scatter/Gather

14.通过try-catch语法来抓取异常和处理异常

  • 异常分为运行时异常和编译异常?常见的运行时异常有空指针异常,数组越界异常,非法参数异常等等,运行时异常根据具体程序运行时的情况,有可能会出现,有可能不会出现
  • 介绍finally关键字

并发编程

15.锁

  • Java主流锁分类 看图
    乐观锁/悲观锁
    公平锁/非公平锁
    可重入锁
    独享锁/共享锁
    互斥锁/读写锁
    分段锁
    偏向锁/轻量级锁/重量级锁
    自旋锁:循环+CAS
  • 简单说明synchronized实现原理
    synchronized实现原理,要先说明Java对象头和Monitor
    1. 在Hotspot虚拟机中,Java对象在内存中分为三部分:对象头、实例数据、对齐填充
        重点说明对象头由两部分组成:Mark Word(标记字段)、Klass Pointer(类型指针)
    2. Mark Work用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、偏向锁标志、偏向线      程ID、偏向时间戳等等,它是实现偏向锁和轻量级锁的关键
    3. synchronized实现同步有4种状态无锁、偏向锁、轻量级锁、重量级锁。偏向锁和轻量级锁是JDK1.6之后针对
        synchronized做出的升级实现
    偏向锁:利用Mark Work里面偏向锁标志为1,锁状态标志为01,偏向线程ID,来实现偏向锁,偏向锁可以提高带有同步但没有竞争的程序性能。
    轻量级锁:Mark Work里面锁状态标志为00,并且在Mark Work里保存了指向在线程栈中锁记录Lock Record的指针,线程栈中的锁记录用于存储对象目前的Mark Work的拷贝和指向该对象的指针
    重量级锁:Monitor是重量锁的实现基础:在HotSpot虚拟机中,Monitor是由C++编写的ObjectMonitor实现,每个对象在创建后都会与之绑定一个Monditor对象,在ObjectMonitor中有两个字段_EntrySet和_WaitSet分别用来表示同步队列和等待队列,就类似于显示锁中的同步队列和等待队列。
        在ObjectMonitor中有_owner和_count字段分别表示当前持有锁的线程和重入次数,就类似于显示锁中exclusiveOwnerThread和state
    锁膨胀(锁升级),此时锁状态为偏向锁,如果发生了线程竞争锁,那么偏向锁会升级为轻量级锁,如果竞争锁的线程大于两条,则轻量级锁会升级为重量级锁。锁升级
    注意:在使用synchronized加锁时,要注意Integer,String这一类不可变对象,这些对象实际都是包装类,每次赋予新值都会new一个新的对象,而基于synchronized重量级锁实现的机制,synchronized是没办法实现Integer,String这一类对象的安全的,显示锁Lock由于实现机制不同,可以实现对这些对象的加锁。
    synchronized加在方法上和加载代码块上时,底层的实现方式是不同的
    synchronized加在方法上后,相对于普通方法,synchronized修饰的方法在底层还有一个ACC_SYNCHRONIZED标识符,来表示执行该方法,执行线程必须先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。
    synchronized加在代码块中,在代码块中会加入指令monitorenter和monitorexit来完成,在底层执行指令的时候,执行到monitorenter指令,线程则需要先获取monitor。monitorexit是线程释放锁。
  • JVM针对synchronized隐式锁的升级除了上面说的偏向锁和轻量级锁之外,还有自旋锁、自适应自旋锁、锁消除
    自旋锁:在锁膨胀后,JVM会让当前线程进行循环尝试获取锁,在尝试若干次都没获取到锁后才将线程挂起,以避免线程挂起和唤醒的开销。由于自旋会占用CPU资源,因此也要视情况而定,即要设置一个自旋的结束点。对于synchronized底层的自旋可以通过PreBlockSpin参数配置,默认是10次。
    自适应自旋锁:在自旋锁的基础上,自适应表示自旋的次数和时间不在固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能成功,进而允许自旋等待更长的时间,比如100次循环。另外如果对于某个锁,自旋很少成功获得过,那么以后要获取这个锁将可能省略掉自旋,即线程没获取到锁直接被挂起。
    锁消除:JVM在编译时,会去除不可能存在共享资源竞争的锁。比如方法内的局部变量,在线程运行时,局部变量时存放在栈的局部变量表中的,而栈是线程独占的,因此不需要加锁,当然如果存在方法逃逸则仍然需要加锁。
     
  • 简单介绍显式锁的实现基础队列同步器AQS(AbstractQueuedSynchronizer)
    AQS,也就是队列同步器,是实现 Lock 的基础。AQS 有一个volatile修饰的int型的整数state标记位,表示有线程是否被占用,在AQS内部通过CAS修改state的值,保证了state修改的原子性(调用Unsafe类的本地方法实现),如果被占用其他线程需要进入到同步队列等待,然后自旋尝试获取锁。
    AQS内部有两个链表:
    一个是同步队列,一个双向链表,当线程获取锁失败后会插入到同步队列链表的末尾。
    一个是等待队列,也是一个双向链表,显式锁线程之间的通信主要是通过这个队列实现,当线程A在同步代码块中通过Condition调用await()后,会新生成持有当前线程的节点并添加到condition 的等待队列。等待队列可以有多个。另一个线程B通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法后,会使等待队列里的节点出队,使得线程A能够有机会移入到同步队列中,A线程会先尝试获取锁,获取锁失败会直接加入到同步队列中。
  • AQS中共享和独享(互斥)的实现原理?
    在AQS内部类Node中定义了多个final修饰的成员变量用于实现互斥锁和共享锁。
    Node类型的EXCLUSIVE用于构造阻塞队列的互斥锁节点,Node类型的SHARED用于构造阻塞队列的互斥锁节点,int类型的PROPAGATE表明当前状态为共享传播状态。当生成的节点Node中的nextWaiter为SHARED时,表示此时的节点是一个共享类型的节点,当这个节点的线程获取到锁后,会依次唤醒后面同样nextWaiter为SHARED的节点,直到遍历到nextWaiter为EXCLUSIVE的节点。
  • 显式锁ReentrantLock具体的实现原理是什么?
    volatile修饰的int类型的变量state来表示同步状态,通过CAS设置状态,AQS内部有同步队列和条件队列用于阻塞线程和线程之间的通信。,显示锁的可重入也是通过增加state的值实现的,同一个线程获取到锁state会加1,线程释放锁state会减1。看这里
  • 简单说明一下ReentrantLock内部的两种实现:公平锁和非公平锁(默认) 看这里
    FairSync公平锁会直接加入同步队列进行自旋判断。两种情况:情况一锁被占用state不等于0,如果线程就是当前占有锁的线程,则可重入,state加1,否则将线程构造成节点Node加入到同步队列。情况二 锁没有被占用state==0,先判断当前同步队列中是否存在正在等待获取锁的阻塞的线程,如果有,当前线程会加入到同步队列中。如果没有会通过CAS设置state,来尝试获取锁,获取失败仍然会加入到同步队列中。
    NonfairSync非公平锁会先尝试获取锁,获取失败后会进入公平锁的两种情况。
  • ReetrantReadWriteLock是如何实现的?ReadLock基于共享锁,WriteLock基于互斥锁
    在AQS内部类Node中定义了两个多个final修饰的成员变量用于实现互斥锁和共享锁。
    Node类型的EXCLUSIVE用于构造阻塞队列的互斥锁节点,Node类型的SHARED用于构造阻塞队列的互斥锁节点,int类型的PROPAGATE表明当前状态为共享传播状态。
    ReentrantLock、WriteLock都是互斥锁,阻塞队列中节点的nextWaiter设置为 EXCLUSIVE表明节点是互斥节点。
    Semaphore、CountDownLatch、ReadLock基于共享锁实现,都会用到SHARED节点。当线程获取不到信号量/读锁的时候,会将用当前线程生成一个nextWaiter为SHARED的Node节点加入到阻塞队列中,nextWaiter为SHARED表明该节点模式是共享模式。当共享锁中阻塞队列中一个nextWaiter为SHARED的节点获取到锁后,会唤醒后续同样nextWaiter为SHARE的节点也持有共享状态。
  • 获取锁的两种不同的方式共享模式和独占模式?看这里
    共享锁的主要特征在于当一个在等待队列中的共享节点成功获取到锁以后(它获取到的是共享锁),既然是共享,那它必须要依次唤醒后面所有可以跟它一起共享当前锁资源的节点,毫无疑问,这些节点必须也是在等待共享锁(这是大前提,如果等待的是独占锁,那前面已经有一个共享节点获取锁了,它肯定是获取不到的)。当共享锁被释放的时候,可以用读写锁为例进行思考,当一个读锁被释放,此时不论是读锁还是写锁都是可以竞争资源的。
  • 什么是乐观锁和悲观锁?CAS就是一种乐观锁的表示,synchronized就是悲观锁看这里
    在Java中CAS实现是通过Unsafe类调用本地方法实现。
    CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 
    CAS的原理简单的说就是:当前线程持有值A,这个值A在内存中对应的位置是V,每次使用的时候都会V获取值与当前线程持有的A对比是否一样,一样则设置V的值为新值B,并更新线程持有的值为B ,表示获取锁成功。
    CAS的缺点,CAS的三大问题
    1.CAS导致自旋消耗,CPU开销较大
    在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
    2.CAS只保证单变量的原子性,不能保证代码块的原子性
    CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了
    3.CAS还会带来ABA问题,解决办法是通过加时间戳和版本号,JDK8里AtomicStampedReference和AtomicMarkableReference就是通过加时间戳和加版本号标记实现的。
  • 什么是读写锁?读写锁适用于什么场景?读多,写少。读读不互斥的实现原理是什么?共享锁
  • ReentrantReadWriteLock 就是JDK提供的一种读写锁,他有那些特性:
    在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
    在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。
    一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁,通过调用writeLock()获取写锁,通过readLock()获取读锁,写锁直接调用readLock()就完成了降级为读锁;读锁不能“升级”为写锁
  • tryLock()和lock()的不同之处看这里
    tryLock:获取锁成功返回true,失败返回false,获取锁失败不会阻塞线程。
    lock:获取锁失败会阻塞线程,线程将加入到同步队列中。
  • 简要说明一下Condition起到的作用?阻塞线程,唤醒线程,线程通信,达到和Synchronized中通过Object的wait/notify/notifyAll方法同样的作用
  • 简单说明一下,当Condition变量调用await()方法和signal()/signalAll()方法时,条件队列的变化看这里
  • 关于同步synchronized关键字的用法?synchronized(obj)用于锁实例对象obj,加在静态方法前锁住整个类,加在普通方法前锁住该类的实例对象。
  • 提高锁性能的建议:
    1. 减小锁的持有时间,即只在必要的时候进行同步加锁,这样就能明显减少锁的持有时间
    2. 减小锁粒度,分段锁思想就是通过减小锁粒度来减小锁竞争的。
    3. 锁分离,针对不同的操作使用不同的锁,读写锁就是采用了这种思想
    4. 锁粗化:锁粗化表面上看和减小锁粒度是冲突的,事实上,他们并不是冲突的,他们关注的消耗锁性能的点不一样,减小锁粒度,是因为锁的范围太大,导致锁竞争频繁,而造成等待锁耗时。而锁粗化,是有连续的操作,但是因为锁的范围太小,导致频繁的获取锁释放锁而造成性能损耗。 

16.多线程

  • 进程和线程?
    进程是系统进行资源分配和调度的基本单位,进程是程序的基本执行实体。
    线程操作系统能够进行运算调度的最小单位,进程中往往存在很多任务,任务执行的基本实体就是线程。
  • 如何理解线程的同步与异步、并行与并发、阻塞与非阻塞?看这里
    同步与异步:同步是主动的去获取,异步是被动的被告知。同步和异步通常用来形容一次方法调用,同步方法一旦开始,调用者必须等到方法调用返回后,才能继续后续行为。异步方法调用更像一个消息传递,一旦开始,方法调用就会立马返回。
    并行与并发:都表示多个任务一起执行。并行表示真正的同时执行。而并发是感觉是同时执行的,实际上不是,实际上并发是交替执行的,比如一会儿执行A,一会儿执行B ,系统不停切换两个任务的执行,这使我们感觉他们好像在同时执行一样,实际上他们是串行执行的。
    阻塞和非阻塞强调访问一个线程临界资源会不会影响其他线程的运行。
    临界资源:表示线程之间共享的数据,但每次只能有一个线程使用,临界资源一旦被占用,其他线程想要使用这个资源,就必须等待。
  • 什么是线程中断?
    线程中断其实就是一种线程协作机制。线程中断并不是代表线程立即停止,而是给线程发送一个通知,告诉它,让它停止,至于线程会不会马上退出,完全是不可预知的结果。
    线程中断的3个方法:
    public void interrupt(){..}//中断线程,通知目标线程中断,设置中断标志位
    public boolean isInterrupted(){..}//判断线程是否被中断
    public boolean interrupted(){..}//判断是否被中断,并清除中断状态
  • 创建线程的三种方式?Thread、Runnable、Callable
  • 线程的生命周期或者叫线程的5种同步状态看这里
  • 线程由运行状态变为就绪状态的几种方式,几种方式有什么不同?sleep()、join()、wait()、await()
    1. 调用sleep、join方法可以引起阻塞,不会释放锁
    sleep方法是休眠时间过后,线程重新回到就绪状态
    join方法,比如在线程t1里调用线程t2.join(),那么当代码执行到t2.join()后,就相当于在线程t1里面调用了一个方法一样,必须要等到这个方法执行完线程t1才继续执行,也就是说当线程t1代码执行到t2.join()后,线程t2执行完后才会继续执行线程t1.因此在日常编码中可以通过join()方法来控制线程执行的先后顺序.线程顺序执行还可以通过显示锁的Condition来实现。
    2. 调用Thread.yield()方法,不会阻塞线程,而是让线程直接从执行状态转为就绪状态。但是当前线程让出资源会给优先级等于或高于当前线程的线程。因此有可能调用了yield后又继续执行。
    3. 因为方法、对象、代码块用了锁,造成线程执行的时候需要先获取锁而阻塞
    4. 因为线程当前占有锁,但是调用了wait(隐式锁里调用)/await(显式锁的Condition调用)引起释放了锁引起阻塞,
    需要通过在其他地方调用notify(notifyAll都是隐式锁使用)/signal(signalAll显式锁的Condition调用)来唤醒线程,当时唤醒线程过后,线程仍然是阻塞的,因为此时线程还需要获取到锁,当获取到锁后,线程才由阻塞状态变化为就绪状态
  • 什么是死锁?死锁的竞争条件和临界区?死锁的检测?
    死锁:通俗来讲就是两个以上的线程在获取资源的时候,该资源正在被其他线程所使用,而形成一种环路等待的情况。
    死锁的产生的4个条件:互斥、请求并持有、不被剥夺、循环等待
    死锁的解决办法:1. 一次性获取到需要的所有资源的锁 2. 给所有的资源进行编号,线程获取锁必须按照资源的编号顺序获取 3. 获取锁的时候设置占有锁的时间,超过时间后线程必须主动释放锁
  • volatile关键字修饰变量的特点看这里
    要说volatile,就需要先了解内存屏障
    1. 每个CPU都会有自己的缓存,缓存的目的就是为了提高性能,避免每次都要向内存取。但是这样会带来问题:不能实时的和内存发生信息交换,不同CPU执行的不同线程对同一个变量的缓存值可能不同。
    2. 内存屏障就可以解决这种问题,内存屏障有两个作用,第一禁止指令重排序,即屏障下面的代码不能跟屏障上面的代码交换执行顺序。第二在有内存屏障的地方,当线程修改了缓存的变量后,会确保修改过后的数据刷新到内存中,并让其他Cpu缓存中相关的数据失效,迫使Cpu从主内存中加载最新的数据到缓存中。
    3. Java中volatile关键字修饰的变量,翻译成汇编语言的时候,会有一个"lock:"前缀的指令,这个指令不是内存屏障,但是他通过对CPU总线和高速缓存加锁来实现内存屏障的作用。
    在具体的执行上,它先对总线和缓存加锁,然后执行后面的指令,在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。最后释放锁后会把高速缓存中的脏数据全部刷新回主内存,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效
    上述内容是volatile的原理,有了上诉原理,volatile修饰的变量,可以保证当发生并发读写时,写操作总是先于读操作。
  • 线程通信的几种?常见的有:synchronized+wait()/notify()/notifyAll(),显式锁+Condition,使用阻塞队列
  • Fork/Join机制,工作窃取算法看这里
  • 什么是CAS?CAS造成ABA问题?ABA问题。ABA问题的解决办法?加入版本号,比如用一个变量表示修改的次数,每次修改都令变量加一。
  • 如何实现一个生产者与消费者模型?(锁、信号量、线程通信、阻塞队列等)
  • wait()与sleep()有什么不同?释放锁和不释放锁
  • 保证线程安全的方法有哪些?主要从原子性、可见性、有序性考虑,做法有:使用原子类,使用volatile关键字,使用Lock和synchronized,使用ThreadLocal
  • 如何尽可能提高多线程的并发性能?在竞争代码区加锁,而不是直接锁住整个方法或整块代码,使用读写锁,读操作不加锁
  • 多线程能面试的点太多,经过上面的锻炼,自行搜索多线程面试题做一做

17.关于ThreadLocal,带着问题看这里

  • ThreadLocal是什么?
    泛型类,多线程局部变量
  • Thread的使用使用场景或者说有什么用解决什么问题?
    线程局部变量。使用场景,一个线程共享的对象,我们需要想这个对象中的某一个成员变量在线程间不共享,这个时候就可以在这个对象所属类中使用ThreadLocal来定义该成员变量。
  • ThreadLocal的实现原理?
    Thread内部有ThreadLocalMap成员变量,当我们在共享变量中初始化了一个ThreadLocal变量后,我们通过调用ThreadLocal的set(..)方法设置值的时候,实际上是通过Thread里的成员变量ThreadLocalMap存放的,key就是该ThreadLocal对象弱引用,key就是set()方法设置的value
  • ThreadLocalMap是什么?
    ThreadLocalMap是ThreadLocal的内部类,ThreaLocalMap的实现原理是Entry数组,Entry的key为ThreadLocal变量的弱引用,value是通过set方法设置的具体值
  • ThreadLocalMap是怎么避免内存泄露的?
    在ThreadLocalMap中key采用ThreadLocal类型的弱引用。具体怎么避免的分下面步理解:
    1. 比如我们通过ThreadLocal tLocal = new ThreadLocal<>();方式生成了一个ThreadLocal对象后,这时候tLocal是一个强引用指向刚刚生成的ThreadLocal对象,然后我们通过set(..)方法设置值,在set方法内部通过调用生成一个Entry,这个Entry的Key就是当前ThreadLocal对象的一个弱引用,value就是通过set方法设置的值。
    2. 由此我们可以发现在使用ThreadLocal的时候,我们会生成两个引用指向生成的ThreadLocal对象,一个是强引用tLocal,一个是ThreadLocalMap里面个弱引用Key。
    3. 现在我们再假设ThreadLocalMap里面指向ThreadLocal对象的引用为强引用,那么设想当我们设置tLocal=null后,则只有ThreadLocalMap里面的Key指向了ThreadLocal对象,而Key为强引用,此时只要线程不退出,则这个ThreadLocal对象将永远不会被回收,ThreadLocalMap里面的value也不会被回收。这样就存在内存泄漏的风险。而Key如果是弱引用就好办了,当设置tLocal=null后,就只剩一个ThreadLocalMap里面一个弱引用Key指向这个ThreadLocal对象了,根据弱引用的特点,这个ThreadLocal对象将很快被回收。
    4. ThreadLocalMap中Key引用的对象被回收了,那value引用的对象呢?ThreadLocalMap的设计本身已经有了这一问题的解决方案,那就是在每次get()/set()/remove()ThreadLocalMap中的值的时候,会自动清理key为null的value。
    以上就是ThreadLocalMap为什么将key设为弱引用的,其实就是为了避免内存泄漏。
  • 经过上一个问题我们知道ThreadLocalMap中的key是弱引用,当线程退出过后能够保证回收掉对应的ThreadLocal对象和value对应的对象,那么如果线程不会退出呢,那怎么办?比如我们使用了线程池,会出现什么问题?可以通过什么方式解决?
    1. 可能会出现,不同的任务使用ThreadLocal获取值是相同的,因为不同的任务放在线程池里执行时,可能多个任务都是被同一个线程执行的。而我们的本意当然是不同的任务在不同的线程中执行,他们对应ThreadLocal变量也是不同的。
    因此当使用线程池遇见ThreadLocal变量时,我们必须提供一个钩子(即回调函数),在任务执行完成后通过调用ThreadLocal的remove方法来将这个变量移出。
    2. 在平时开发中Serlvet采用多线程来处理多个请求同时访问,Tomcat容器也维护了一个线程池来服务请求。所以我们在开发web服务时,尤其要注意如果使用的ThreadLocal,一定要提供一个回调函数在请求结束时调用ThreadLocal的remove方法将这个变量移出。

18.常见的JUC工具类

  • 基础数据的原子类:
    AtomicLong,AtomicInteger,AtomicBoolean(通过unsafe类实现,基于CAS,真实的变量使用volatile修饰)
    LongAdder(基于Cell,分段锁思想,空间换实现,更适合高并发场景)
    DoubleAdder,LongAccumulator,DoubleAccumulate
  • 对象的原子性读写操作:
    AtomicReference、AtomicStampedReference、AtomicMarkableReference
    AtomicStampedReference、AtomicMarkableReference用来解决ABA问题,分别基于时间戳和标记位实现
  • 原子操作包装类:
    AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
  • 锁相关:
    ReentrantLock,ReentrantReadWriteLock,StampedLock(1.8改进的读写锁,采用乐观锁,防止写饥饿),LockSupport
  • 异步执行相关的类:
    Executors,ForkJoinPool(分治思想+工作窃取),Callable&&FutureTask,CompletableFuture(支持流式调用,多future组合,可以设置完成时间)
  • 常用的阻塞队列:
    LinkedBlockingDeque(双端队列)、ArrayBlockingQueue(单端队列)、PriorityBlockingQueue(优先级队列)、SynchronousQueue(单元素队列)
  • 控制多线程协助使用的类:
    CountDownLatch(倒计时器,多线程任务汇总)、CyclicBarrier(循环栅栏,多线程并发执行达到某一条件)、Semaphore(信号量,对共享资源的并发度控制)
    CountDownLatch的作用其实就是一个计数器,调用countDown()方法可以阻塞线程,并且计数减一,当阻塞的线程数达到CountDownLatch设置的临界值后,即CountDownLatch计数为0后,CountDownLatch将会唤醒通过countDown/await方法阻塞的线程
    并且当计数为0后,再调用countDown/await方法不会再阻塞线程。
    需要注意的是:CountDownLatch的countDown()方法才会计数减一并阻塞,await()方法只会阻塞,并不能使计数减一,因此await通常用于主任务,countDown用于子任务
    
    CyclicBarrier内部有一个计数器count,调用障碍器的await方法会使计数器count的值减一,当计数器count的值为0时,表明所有的子线程都调用了await方法,此时表明可以执行主线程任务了
    障碍器内部有一个ReentrantLock(显式锁),障碍器内部还有通过该显式锁获得的Condition变量,只要子线程里调用了障碍器的await方法,而await方法调用了dowait方法(dowait方法使用了显式锁)
    在执行dowait方法执行的时候会根据计算器count判断,如果count不等于0,将会调用Condition变量的await方法阻塞所有的进行到这里的线程,
    待所有子线程都执行了await方法后,障碍器内部的count值此时为0,然后会调用Condition变量的signalAll方法,唤醒所有阻塞的线程。
  • 常用的集合类:
    线程安全的Map:ConcurrentHashMap(无序)、ConcurrentSkipListMap(有序)
    线程安全的List:CopyOnWriteArrayList(适合读多写少、小数据量、并发量高)。CopyOnWirteArraySet内部实际是采用CopyOnWriteArrayList实现,每次新增都要判断List里面是否存在
    线程安全的Queue:ConcurrentLinkedQueue、ConcurrentLinkedDeque
  • 线程阻塞工具类:LockSupport,可以在线程任意位置让线程阻塞。

19.线程池

  • 什么是线程池?
    一种多线程使用方式,线程池中有几个核心的配置:核心线程数,最大线程数、空闲线程存活时间、任务队列、线程工厂、拒绝策略,当我我们往线程池中添加任务,线程池会启动线程来执行这些任务,任务执行完成后,线程不会关闭,而会等待执行下一个任务,这样就避免的创建线程,销毁线程的资源消耗,非常适合多任务,但每个任务又不会消耗太多时间的场景。
  • 怎么使用线程池?
    Java提供了Executors工具类可以用来创建内置好的线程池
    自定义线程池,通过使用ThreadPoolExecutor变量,指定核心线程数、最大线程数、超过核心线程数的空闲线程存活时间、阻塞队列、线程工厂、拒绝策略。
  • 为什么要用线程池?可以解决处理短时间任务时创建与销毁线程的代价
  • 有哪几种常见的线程池?各自的使用场景:
    Executors.newFixedThreadPool(2);固定线程池容量,核心线程数和最大线程数都为设置的值,空闲线程的存活时间为0S,使用LinkedBlockingQueue阻塞队列,没有设置LinkedBlockingQueue的最大容量,因此不会执行到拒绝策略
    Executors.newCachedThreadPool();动态扩容线程池,核心线程数为0,最大线程数为Integer.MAX_VALUE,使用SynchronousQueue阻塞队列,每次添加任务都会创建新的线程来执行任务,空闲线程的存活时间时60秒。不会执行拒绝策略
    Executors.newSingleThreadExecutor();单任务线程池,核心线程数和最大线程数都为1,空闲线程存活时间为0,使用LinkedBlockingQueueu阻塞队列,没有设置LinkedBlockingQueue的最大容量,因此不会执行到拒绝策略,其实就是特殊的FixedThreadPool。不会执行拒绝策略
    Executors.newScheduledThreadPool(3);定时任务线程池,最大线程数设置为Integer.MAX_VALUE,空闲线程的存活时间为0S,使用DelayedWorkQueue,可以延时执行任务。不会执行拒绝策略
  • 线程池有哪几种阻塞(任务)队列?他们的实现原理是什么?
    ArrayBlockingQueue基于数组有界的阻塞队列,需要指定容量
    LinkedBlockingQueue基于链表,可以指定链表长度
    PriorityBlockingQueue有序的阻塞队列,基于数组,也需要指定容量
    SynchronousQueue只能放一个元素的阻塞队列
  • 阻塞队列的常见操作:
    remove 移除并返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
    poll 移除并返问队列头部的元素 如果队列为空,则返回null
    take 移除并返回队列头部的元素,如果没有则阻塞
    add 增加一个元索 如果队列已满,ArrayBlockingQueue会抛出一个IllegalStateException("Queue full")异常,
    offer 添加一个元素并返回true 如果队列已满,则返回false
    put 添加一个元素 如果队列满,则阻塞
    element 返回队列头部的元素 如果队列为空,则抛出一个NoSuchElementException异常
    peek 返回队列头部的元素 如果队列为空,则返回null
  • 线程池的拒绝策略?
    AbortPolicy丢弃任务并抛出RejectedExecutionException异常。
    DiscardPolicy丢弃任务,但是不抛出异常。
    DiscardOldestPolicy丢弃队列最早添加的任务,然后重新提交被拒绝的任务
    CallerRunsPolicy由调用线程(提交任务的线程)处理该任务
  • 线程池的重要参数?
    corePoolSize、maximumPoolSize、keepAliveTime 超出核心线程数量的空闲线程的存活时间、BlockingQueue 存放任务的阻塞队列、ThreadFactory 创建线程的工厂类、RejectExecutionHandler 拒绝策略
  • execute方法和submit方法有什么不同?
    execute只能接受Runnable类型的任务
    submit可以接受Runnable和Callable两种类型的任务,submit在内部仍然是通过execute方法执行线程,只是submit在执行execute方法前会将Callable类型的任务包装成FutureTask添加到任务队列中,并且返回这个Future对象。
  • shutdown和shutdownNow有什么不同?
    shutdown当前正在执行的任务和阻塞队列里的任务继续执行完成,但立即停止接收新任务。
    shutdownNow当前正在执行的任务立即停止,并且停止接收新任务。
  • 线程池任务执行流程?依次判断 核心线程数是否已满、阻塞队列是否已满、是否达到最大线程数,看这里
    通过上面任务的执行流程,可以看出,在线程池中先加入的线程会先执行,如:像线程池中加入一个任务A,而此时,线程池核心线程已满,阻塞队列也已满,但还没有达到最大线程数,因此,线程池会新建一个线程T立即执行任务A。注意:任务A其实是在阻塞队列里的任务之后添加的,但是却先执行了。当A执行完毕后,线程T空闲,并且没有超出空闲时间,线程T会继续执行阻塞队列里的任务。
  • 线程池的几种状态,看图
    Running、shutdown方法->ShutDown、shutdownNow方法->Stop、没有任务执行的时候Tidying、Terminated
  • 如何估算一个线程池的大小
    CPU密集型:是CPU为N,则设置线程池大小为N+1
    IO密集型:一个估算线程池大小的经验公式:线程池大小=CPU数 * (1+等待时间/计算时间),通常设置2N+1
    Java中,可以通过:Runtime.getRuntime().availableProcessors()获取可用的CPU数量。

20.反射是什么?看这里

  • 反射可以用来做动态代理
  • 通过反射是否可以拿到私有属性和方法?可以,通过getDeclaredField和getDeclaredMethod,并且通过Field与Method的setAccessible()方法设置可否访问为true。
  • 通过反射获取对象和new 获取对象有什么区别?
    1. new只能用于编译期就能确定的类型, 而反射在运行时才确定类型并创建其对象,因此反射创建对象时可能发生ClassNotFoundException异常
    2. 反射的效率低(详细解释
        1)是method.invoke中每次都要进行参数数组包装,将参数包装为Object数组
        2)在method.invoke中要进行方法可见性检查
        3)  需要校验参数,反射时也必须检查每个实际参数与形式参数的类型匹配性。
        4)  由于反射涉及动态解析的类型,因此无法执行某些Java虚拟机优化。

21.最常见的设计模式,主要考察设计模式的具体实现和具体使用场景

  • 行为型:责任链模式、观察者模式、模板方法模式、策略模式、发布订阅模式
  • 创建型:单例模式、工厂模式、建造者模式
  • 结构型:适配器模式、装饰模式、代理模式
     
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值