JAVA 面试题 合辑(一)

=====================JAVA 基础====================

==equals()方法的区别?为什么重写equals()方法就必须重写hashCode()方法?

1. ==在比较基本数据类型时比较的是值,比较两个对象时比较的是地址值。 equals()方法存在于Object类中,Object类中equals()方法底层依赖的是==操作,在所有没有重写equals()的类中,调用equals()其实和使用==的效果一样,也是比较的地址值。String重写了equals(),底层比较的是两个String对应位置的char字符是否==。

2. Object.hashCode()方法是一个本地native方法,返回的是对象引用中存储的对象的内存地址;

基于散列的集合(HashSet、HashMap和Hashtable)存放key时,调用该对象(存入对象)的hashCode()方法来得到该对象的hashCode值,然后根据该hashCode值决定该对象在HashSet中存储的位置;

所以如果equals方法返回true,那么两个对象的hasCode()返回值必须一样;

StringStringBuilderStringBuffer区别

不可变:String,底层是final char value[]

可变:StringBuffer、StringBuilder,底层是char[] value

线程安全:StringBuffer

线程不安全:StringBuilder

String类能被继承吗?为什么?

不能,String被final修饰,不可被继承。原因如下:

1. 字符串常量池的需要:字符串常量池是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象;

2. 允许String对象缓存HashCode:Java中String对象的哈希码被频繁地使用,字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存。这也是一种性能优化手段,意味着不必每次都去计算新的哈希码;

3. 安全性:String常被用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。

intInteger的区别?

Integer是int的包装类。Java 5 中,引入了自动装箱和自动拆箱功能(boxing/unboxing)。

装箱:将基本类型用它们对应的引用类型包装起来;

拆箱:将包装类型转换为基本数据类型;

在 Java 5 中新增了静态工厂方法 valueOf,在调用它的时候会利用一个缓存机制,带来了明显的性能改进。按照 Javadoc,这个值默认缓存是 -128 到 127 之间。

查看Integer类源码,发现里面有一个私有的静态内部类IntegerCache,而如果直接将一个基本数据类型的值赋给Integer对象,则会发生自动装箱,其原理就是通过调用Integer类的public static Integer valueOf(将int类型的值包装到一个对象中 ,就是在Integer类中有一个静态内部类IntegerCache,在IntegerCache类中有一个Integer数组,用以缓存当数值范围为-128~127时的Integer对象。

注意:可以通过下面这个参数设置最大值:-XX:AutoBoxCacheMax(JDK5)

属性是在使用Oracle/Sun JDK 6,在server模式下,使用:

-XX:AutoBoxCacheMax=NNN

参数即可将Integer的自动缓存区间设置为[-128,NNN]。(-128不可改)

实例化顺序

类加载器实例化时进行的操作步骤:加载–>连接->初始化。

父类静态代变量、父类静态代码块、

子类静态变量、子类静态代码块、

父类非静态变量(父类实例成员变量)、父类构造函数、

子类非静态变量(子类实例成员变量)、子类构造函数。

Object类常用的方法有哪些

1. equals()、hashCode()、getClass()、toString()--默认字符串:类名+哈希编码;

2. clone():实现对象的浅复制(当改变其中一个对象的引用类型属性实例的属性时,另一个对象相应的引用类型的属性实例中的属性也会发生变化),只有实现了Cloneable接口才可以调用该方法。否则抛出CloneNotSupportedException;深复制:引用类型属性也要实现clone()方法并显式调用;

3. finalize():用于JVM对象收集;

4. wait():使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断;

5. notify():唤醒在该对象上等待的某个线程;

6. notifyAll():唤醒在该对象上等待的所有线程;

深拷贝和浅拷贝区别

浅拷贝的问题就是两个对象并非独立的,浅拷贝复制引用。如果你修改了其中一个 Person 对象的 Name 对象,那么这次修改也会影响奥另外一个 Person 对象。

public class Person {

    private Name name;

    private Address address;

    public Person(Person originalPerson) {

         this.name = originalPerson.name;

         this.address = originalPerson.address;

    }

[…]

}

不同于浅拷贝,深拷贝是一个整个独立的对象拷贝。如果我们对整个 Person对象进行深拷贝,我们会对整个对象的结构都进行拷贝。

public class Person {

    private Name name;

    private Address address;

    public Person(Person otherPerson) {

         this.name    =  new Name(otherPerson.name);

         this.address =  new Address(otherPerson.address);

    }

[…]

}

抽象类和接口的区别

语法层次抽象类和接口分别给出了不同的语法定义。

设计层次

抽象层次不同,抽象类是对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。

跨域不同

抽象类所体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在”is-a"关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的,仅仅是实现了接口定义的契约而已,"like-a"的关系。

什么是泛型、为什么要使用以及泛型擦除

语法泛型,即“参数化类型”。

创建集合时就指定集合元素的类型,该集合只能保存其指定类型的元素,避免使用强制类型转换。

Java编译器生成的字节码是不包涵泛型信息的,泛型类型信息将在编译处理是被擦除,这个过程即类型擦除。泛型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。

类型擦除的主要过程如下:

1).将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。

2).移除所有的类型参数。

Error Exception 的区别 CheckedExceptionRuntimeException 的区别

Throwable是所有异常的根,java.lang.Throwable

Error是错误,java.lang.Error;Error由Java虚拟机生成并抛出,包括动态链接失败,虚拟机错误等。程序对其不做处理。

Exception是异常,java.lang.Exception 。Exception一般分为Checked异常和Runtime异常,所有RuntimeException类及其子类的实例被称为Runtime异常,不属于该范畴的异常则被称为CheckedException

CheckedException,Java认为Checked异常都是可以被处理的异常,所以Java程序必须显示处理Checked异常。如果程序没有处理Checked异常,该程序在编译时就会发生错误无法编译。方法有两种

1 当前方法知道如何处理该异常,则用try...catch块来处理该异常。

2 当前方法不知道如何处理,则在定义该方法是声明抛出该异常。

RuntimeException,Runtime如除数是0和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。

System.arraycopy() Arrays.copyOf()两者之间的区别?

两者的区别在于,Arrays.copyOf()不仅仅只是拷贝数组中的元素,在拷贝元素时,会创建一个新的数组对象。而System.arrayCopy只拷贝已经存在数组元素。
如果我们看过Arrays.copyOf()的源码就会知道,该方法的底层还是调用了System.arrayCopyOf()方法。

一些关于Java优化的资料里也推荐使用System.arraycopy来批量处理数组,其本质就是让处理器利用一条指令处理一个数组中的多条记录,只需指定头指针然后就开始循环即可,执行一次指令,指针就后移一个位置。要操作多少个数据就循环多少次即可。

反射的原理

反射机制:所谓的反射机制就是java语言在运行时拥有一项自观的能力。通过这种能力可以彻底的了解自身的情况为下一步的动作做准备。

Java的反射机制的实现要借助于4个类:class,Constructor,Field,Method;其中class代表的时类对 象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象。通过这四个对象我们可以粗略的看到一个类的各个组成部分。

使用反射机制,导入java.lang.relfect 包,遵循三个步骤:
第一步是获得你想操作的类的 java.lang.Class 对象
第二步是调用诸如 getDeclaredMethods 的方法
第三步使用 反射API 来操作这些信息

=====================JAVA 集合====================

Java中的集合类及关系图

List和Set继承自Collection接口。

Set无序不允许元素重复,只能插入一个null值。HashSet和TreeSet是主要的实现类。

List有序且允许元素重复,可插入null。ArrayList/LinkedList/Vector是主要的实现类。

Map也属于集合系统,但和Collection接口没关系。Map是key对value的映射集合,其中key列就是一个集合。key不能重复,但是value可以重复。HashMap、TreeMap(TreeMap有序原因:实现了SortedMap接口)和Hashtable是三个主要的实现类。

SortedSet和SortedMap接口对元素按指定规则排序,SortedMap是对key列进行排序。

ArrayListVector区别

ArrayList和Vector都实现了List接口,都是通过数组实现的。

Vector是线程安全的,而ArrayList是非线程安全的。

List第一次创建的时候,会有一个初始大小,随着不断向List中增加元素,当List 认为容量不够的时候就会进行扩容。Vector缺省情况下自动增长原来一倍的数组长度,ArrayList增长原来的50%。

LinkedListget(i)方法是怎样实现的?

LinkedList,链表里边的get(i)方法里有一个很有意思的设计模式,二分查找法。有先判断index与size>>1的大小,如果小于size的2分之一,则从前面开始查找,如果大于size的2分之一,则从后面开始查找,减少了一半的时间。

ArrayListLinkedList区别及使用场景

区别

ArrayList底层是用数组实现的,可以认为ArrayList是一个可改变大小的数组。随着越来越多的元素被添加到ArrayList中,其规模是动态增加的。

ArrayList::初始大小10、扩容:原容量的0.5倍

LinkedList底层是通过双向链表实现的, LinkedList和ArrayList相比,增删的速度较快。但是查询和修改值的速度较慢。同时,LinkedList还实现了Queue接口,所以他还提供了offer(),peek(), poll()等方法。

使用场景:LinkedList更适合从中间插入或者删除(链表的特性)。ArrayList更适合检索和在末尾插入或删除(数组的特性)。

HashMapHashTable区别

  1. HashTable的方法前面都有synchronized来同步,是线程安全的;HashMap未经同步,是非线程安全的。
  2. HashTable不允许null值(key和value都不可以) ;HashMap允许nul l值(key和value都可以)。
  3. HashTable有一个contains(Object value)功能和containsValue(Object value)功能一样。
  4. HashTable使用Enumeration进行遍历;HashMap使用Iterator进行遍历
  5. HashTable中hash数组默认大小是11,增加的方式是old*2+1;HashMap中hash数组的默认大小是16,而且一定是2的指数。
  6. 哈希值的使用不同,HashTable直接使用对象的hashCode; HashMap重新计算hash值,而且用与代替求模。

CollectionCollections的区别

java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。
java.util.Collections 是一个包装类。它包含有各种有关集合操作的静态多态方法。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

HashMap实现原理

底层:HashMap底层实现还是数组,只是数组的一个元素可能是一个单链表(哈希冲突时才是链表)。

插入:put过程是先计算hash然后通过hash与table.length取摸计算index值,然后将key放到table[index]位置,当table[index]已存在其它元素时(哈希冲突),会在table[index]位置形成一个链表,将新添加的元素放在table[index],原来的元素通过Entry的next进行链接(新值链头,原值后移)--哈希冲突的解决方案;

获取:先根据key的hash值得到这个元素在数组中的位置,然后通过key的equals()在链表中找到key对应的Entry节点;

HashMap为什么用红黑树?用其它树不行么?

红黑树牺牲了一些查找性能 但其本身并不是完全平衡的二叉树。因此插入删除操作效率略高于AVL树;  AVL树用于自平衡的计算牺牲了插入删除性能,但是因为最多只有一层的高度差,查询效率会高一些。实际应用中,若搜索的次数远远大于插入和删除,那么选择AVL,如果搜索,插入删除次数几乎差不多,应该选择RB。

HashTable实现原理

和HashMap一样,Hashtable 也是一个散列表,它存储的内容是键值对(key-value)映射。Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序的。

Iterator

Iterator的API

关于Iterator主要有三个方法:hasNext()、next()、remove()

hasNext:没有指针下移操作,只是判断是否存在下一个元素

next:指针下移,返回该指针所指向的元素

remove:删除当前指针所指向的元素,一般和next方法一起用,这时候的作用就是删除next方法返回的元素

Java中的Iterator是一种fail-fast的设计。

当Iterator迭代一个容器的时候,如果此时有别的方法在更改Collection(容器)的内容,那么Iterator就会抛出ConcurrentModificationException 。

=====================多线程====================

进程和线程的区别

(1)进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元;

(2)同一个进程中可以包括多个线程,并且线程共享整个进程的资源;

(3)线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源;

多线程的实现方式?start()后线程立即启动么?

继承Thread类、实现Runnable接口、线程池(Cache、Fixed、Single、Schedule)、实现Callable接口通过FutureTask包装器来创建Thread线程;使用ExecutorService、Callable、Future实现有返回结果的多线程。

不是立刻启动:执行start()之后,线程等待CPU调度,当调度成功时,调用run()才启动线程;

如何停止一个线程?

java中有以下3种方法可以终止正在运行的线程:

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
  2. 使用stop方法强行终止,stop()方法停止线程则是非常暴力的,不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。
  3. 使用interrupt方法中断线程。interrupt()方法是在当前线程中打了一个停止标志,并不是真的停止线程。Thread.java类中提供了两种方法:this.interrupted(): 测试当前线程是否已经中断;this.isInterrupted(): 测试线程是否已经中断;

线程安全与非线程安全?如何保证线程安全?

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染;

线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据;

保证线程安全需要对非安全的代码进行加锁控制;

使用线程安全的类;

多线程并发情况下,线程共享的变量改为方法级的局部变量。

线程生命周期

新建(new Thread):当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。t = new Thread();

就绪(runnable):线程已经被启动,正在等待被分配给CPU时间片,也就是说此时线程正在就绪队列中排队等候得到CPU资源。t.start();

运行(running):线程获得CPU资源正在执行任务(run()方法);

死亡(dead):当线程执行完毕、发生异常或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态等待执行。

堵塞(blocked):由于某种原因导致正在运行的线程让出CPU并暂停自己的执行,即进入堵塞状态。用sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过去可进入就绪状态。

volatile关键字?

volatile关键字的作用是:使变量在多个线程间可见(可见性)

  1. 强制变量每次从内存中读取,而不会存储在寄存器中(强制刷新到内存中);
  2. volatile仅能使用在变量级别;
  3. volatile不会造成线程的阻塞;

synchronized关键字?

synchronized锁定当前变量,synchronized是Java中的关键字,是一种同步锁。只有当前线程可以访问该变量,其他线程被阻塞住;

synchronized则可以使用在变量、方法、和类级别的;它修饰的对象有以下几种:

1). 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;

2). 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

3). 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

4). 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

synchronized可能会造成线程的阻塞;

LockSynchronized区别?

a. Lock是一个接口,而synchronized是java的一个关键字

b. Lock有比synchronized更精确的线程语义和更好的性能。Lock的锁定是通过代码实现的,而synchronized是在JVM层面上实现的,synchronized会自动释放锁,而Lock一定要求程序员手工释放,并且必须在finally从句中释放。Lock还有更强大的功能,例如,它的tryLock方法可以非阻塞方式去拿锁。Lock锁的范围有局限性,块范围,而synchronized可以锁住块、对象、类。

多线程如何互相通信?

void notify() /void notifyAll() 唤醒在此对象监视器上等待的单个/所有线程。

void wait() /wait(long timeout) /wait(long timeout, int nanos) 

导致当前的线程等待,直到其他线程调用此对象的notify()方法或notifyAll()方法

/或者超过指定的时间量

/或者其他某个线程中断当前线程,或者已超过某个实际时间量。

sleepwait的区别(考察的方向是是否会释放锁)

wait()是Object类的方法,sleep()和yield()是Thread类的静态方法。

sleep和yield方法是用来让线程让出CPU时间,把CPU交给线程调度器,使得其他线程能获得CPU时间。

sleep()睡眠时,保持对象锁;而wait()睡眠时,释放对象锁。

产生死锁

四个必要条件:

一.互斥条件:所谓互斥就是进程在某一时间内独占资源。。
二.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
三.不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
四.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

死锁的预防

打破产生死锁的四个必要条件中的一个或几个,保证系统不会进入死锁状态。
一.打破互斥条件。即允许进程同时访问某些资源。但是,有的资源是不允许被同时访问的,像打印机等等,这是由资源本身的属性所决定的。所以,这种办法并无实用价值。
二.打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。就是说,当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。这就相当于该进程占有的资源被隐蔽地强占了。这种预防死锁的方法实现起来困难,会降低系统性能。
三.打破占有且申请条件。可以实行资源预先分配策略。即进程在运行前一次性地向系统申请它所需要的全部资源。如果某个进程所需的全部资源得不到满足,则不分配任何资源,此进程暂不运行。只有当系统能够满足当前进程的全部资源需求时,才一次性地将所申请的资源全部分配给该进程。由于运行的进程已占有了它所需的全部资源,所以不会发生占有资源又申请资源的现象,因此不会发生死锁。
四.打破循环等待条件,实行资源有序分配策略。采用这种策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按资源序号递增的顺序提出。进程占用了小号资源,才能申请大号资源,就不会产生环路,从而预防了死锁。

守护线程

守护线程是为其他线程的运行提供服务的线程。
setDaemon(boolean on)方法可以方便的设置线程的Daemon模式,true为守护模式,false为用户模式。

什么是阻塞?阻塞的情况?

阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu时间片,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu时间片转到运行(running)状态。

a. 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

b. 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

c. 其他阻塞:运行(running)的线程执行Thread.sleep(long ms),JVM会把该线程置为阻塞状态。当sleep()状态超时,线程重新转入可运行(runnable)状态。)

SemaphoreLockSupport的作用?

Semaphore:计数信号量。Semaphore管理一系列许可证。它的每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证。经常用于限制获取某种资源的线程数量。

LockSupport:它的park()和unPark()对应Object的wait()和notify()。

CAS

Compare And Set的简称。现代主流CPU都支持的一种硬件级别的原子操作, 比较并交换, 操作包含三个操作数:内存地址(V) 预期原值(A) 新值(B)

如果内存位置的值与预期原值相匹配, 那么处理器会自动将该位置值更新为新值,否则, 处理器不做任何操作.无论哪种情况, 它都会在CAS指令之前返回该位置的值.

Java中的ThreadPoolExecutor类构造器

Java中的ThreadPoolExecutor类构造器中各个参数的含义:

  • corePoolSize:核心池的大小,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
  • maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程;
  • keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。例外如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
  • unit:参数keepAliveTime的时间单位。
  • workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,阻塞队列中ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
  • threadFactory:线程工厂,主要用来创建线程;
  • handler:表示当拒绝处理任务时的策略。可以自定义。

ThreadPoolExecutor线程池的使用

在ThreadPoolExecutor类中有几个非常重要的方法:

execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。

shutdown()和shutdownNow()是用来关闭线程池的。

public class Test {

     public static void main(String[] args) {   

         ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,

                 new ArrayBlockingQueue<Runnable>(5));

         for(int i=0;i<15;i++){

             MyTask myTask = new MyTask(i);

             executor.execute(myTask);

             System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+

             executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());

         }

         executor.shutdown();

     }

}

class MyTask implements Runnable {

……

}

常见线程池?

newSingleThreadExecutor返回以个包含单线程的Executor,将多个任务交给此Exector时,这个线程处理完一个任务后接着处理下一个任务,若该线程出现异常,将会有一个新的线程来替代。

newFixedThreadPool返回一个包含指定数目线程的线程池,如果任务数量多于线程数目,那么没有没有执行的任务必须等待,直到有任务完成为止。

newCachedThreadPool根据用户的任务数创建相应的线程来处理,该线程池不会对线程数目加以限制,完全依赖于JVM能创建线程的数量,可能引起内存不足。

创建多少线程数才合适?

一般需要根据任务的类型来配置线程池大小:

如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1;

如果是IO密集型任务,参考值可以设置为2*NCPU

如何拿到子线程的返回结果?

使用Future类的get()方法取得异步计算的结果对象,get 方法会阻塞线程直至真正的结果返回。

ConcurrentHashMap实现

要实现线程安全,就需要加锁, ConcurrentMap的做法简单来说, 就是把哈希表分成若干段, 对其中某一段操作时, 只锁住这一段, 其他段可以不受影响。

实现:

a. 整个ConcurrentMap由一个segment数组组成(即segments),数组中每一个segment是一张哈希表, 哈希表中存放的是一张hashentry链表。Segment继承ReentrantLock用来充当锁的角色。

b. 最终存储key,value时是对segment操作, 因此只要对需要插入键值对的segment上锁就可以保证线程安全。

JDK1.8之后的优化:

1. 取消segment字段,直接采用transient volatile HashEntry<K,V> table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率;transient修饰的变量其内容在序列化后无法获得访问;

2. 将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构;

可中断锁

可中断锁:顾名思义,就是可以相应中断的锁。

在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

可重入锁

如果锁具备可重入性,则称作为可重入锁。

像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。

举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

读写锁

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。

正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。可以通过readLock()获取读锁,通过writeLock()获取写锁。

Java并发容器

这些容器的关键方法大部分都实现了线程安全的功能,却不使用同步关键字 (synchronized)。值得注意的是 Queue 接口本身定义的几个常用方法的区别,

  1. add 方法和 offer 方法的区别在于超出容量限制时前者抛出异常,后者返回 false;
  2. remove 方法和 poll 方法都从队列中拿掉元素并返回,但是他们的区别在于空队列下操作前者抛出异常,而后者返回 null;
  3. element 方法和 peek 方法都返回队列顶端的元素,但是不把元素从队列中删掉,区别在于前者在空队列的时候抛出异常,后者返回 null。

阻塞队列 :

  • BlockingQueue.class,阻塞队列接口
  • BlockingDeque.class,双端阻塞队列接口
  • ArrayBlockingQueue.class,阻塞队列,数组实现
  • LinkedBlockingDeque.class,阻塞双端队列,链表实现
  • LinkedBlockingQueue.class,阻塞队列,链表实现
  • DelayQueue.class,阻塞队列,并且元素是 Delay 的子类,保证元素在达到一定时间后才可以取得到。
  • PriorityBlockingQueue.class,优先级阻塞队列
  • SynchronousQueue.class,同步队列,但是队列长度为 0,生产者放入队列的操作会被阻塞,直到消费者过来取,所以这个队列根本不需要空间存放元素;

fork-Join

它把流程划分成 fork(分解)+join(合并)两个步骤(怎么那么像 MapReduce?),传统线程池来实现一个并行任务的时候,经常需要花费大量的时间去等待其他线程执行任务的完成,但是 fork-join 框架使用 work stealing 技术缓解了这个问题:

  1. 每个工作线程都有一个双端队列,当分给每个任务一个线程去执行的时候,这个任务会放到这个队列的头部;
  2. 当这个任务执行完毕,需要和另外一个任务的结果执行合并操作,可是那个任务却没有执行的时候,不会干等,而是把另一个任务放到队列的头部去,让它尽快执行;
  3. 当工作线程的队列为空,它会尝试从其他线程的队列尾部偷一个任务过来;
  4. 取得的任务可以被进一步分解。
  • ForkJoinPool.class,ForkJoin 框架的任务池,ExecutorService 的实现类
  • ForkJoinTask.class,Future 的子类,框架任务的抽象
  • ForkJoinWorkerThread.class,工作线程
  • RecursiveTask.class,ForkJoinTask 的实现类,compute 方法有返回值,下文中有例子
  • RecursiveAction.class,ForkJoinTask 的实现类,compute 方法无返回值,只需要覆写 compute 方法,对于可继续分解的子任务,调用 coInvoke 方法完成(参数是 RecursiveAction 子类对象的可变数组):

class SortTask extends RecursiveAction {

    final long[] array;

    final int lo;

    final int hi;

    private int THRESHOLD = 30;

    public SortTask(long[] array) {

        this.array = array;

        this.lo = 0;

        this.hi = array.length - 1;

    }

    public SortTask(long[] array, int lo, int hi) {

        this.array = array;

        this.lo = lo;

        this.hi = hi;

    }

    @Override

    protected void compute() {

        if (hi - lo < THRESHOLD)

            sequentiallySort(array, lo, hi);

        else {

            int pivot = partition(array, lo, hi);

            coInvoke(new SortTask(array, lo, pivot - 1), new SortTask(array,

                pivot + 1, hi));

        }

    }

    private int partition(long[] array, int lo, int hi) {

        long x = array[hi];

        int i = lo - 1;

        for (int j = lo; j < hi; j++) {

            if (array[j] <= x) {

                i++;

                swap(array, i, j);

            }

        }

        swap(array, i + 1, hi);

        return i + 1;

    }

    private void swap(long[] array, int i, int j) {

        if (i != j) {

            long temp = array[i];

            array[i] = array[j];

            array[j] = temp;

        }

    }

    private void sequentiallySort(long[] array, int lo, int hi) {

        Arrays.sort(array, lo, hi + 1);

    }

}

同步工具的使用

这些类大部分都是帮助做线程之间同步的。

  • CountDownLatch.class,一个线程调用 await 方法以后,会阻塞地等待计数器被调用 countDown 直到变成 0,功能上和下面的 CyclicBarrier 有点像。
  • CyclicBarrier.class,也是计数等待,只不过它是利用 await 方法本身来实现计数器“+1” 的操作,一旦计数器上显示的数字达到 Barrier 可以打破的界限,就会抛出 BrokenBarrierException,线程就可以继续往下执行。
  • Semaphore.class,功能上很简单,acquire() 和 release() 两个方法,一个尝试获取许可,一个释放许可,Semaphore 构造方法提供了传入一个表示该信号量所具备的许可数量。
  • Exchanger.class,这个类的实例就像是两列飞驰的火车(线程)之间开了一个神奇的小窗口,通过小窗口(exchange 方法)可以让两列火车安全地交换数据。

CAS含义?

使用循环CAS实现原子操作,JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

缺点  1)ABA问题,1 2 1 以为没有变化

2)循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。

3)只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

CAS的实现Atomic类库

这个包里面提供了一组原子变量类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)可以对基本数据、数组中的基本数据、对类中的基本数据进行操作。原子变量类相当于一种泛化的volatile变量,能够支持原子的和有条件的读-改-写操作。

java.util.concurrent.atomic中的类可以分成4组:

标量类(Scalar):AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference

数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

复合变量类:AtomicMarkableReference,AtomicStampedReference

ThreadLocal

一个ThreadLocal在一个线程中是共享的,在不同线程之间又是隔离的(每个线程都只能看到自己线程的值);线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

Synchronized锁是怎么实现的,放在字节码的什么位置

java虚拟机中的synchronized是基于进入和退出monitor对象实现的,同步分为显式同步和隐式同步,同步代码块代表着显式同步,指的是有明确的monitorenter和monitorexit指令。同步方法代表着隐式同步,同步方法是由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED标志来隐式实现的。

=====================JVM=================

Java虚拟机基本概念说明

1. 类加载子系统:负责从文件系统或者网络中加载class信息,加载的信息存放在一块称之为方法区的内存空间。

2. 方法区:就是存放类信息、常量信息、常量池信息、包括字符串字面量和数字常量等。

3. Java堆:在Java虚拟机启动的时候建立Java堆,它是Java程序最主要的内存工作区域,几乎所有的对象实例都存放到Java堆中,堆空间是所有线程共享的。

最为常见的就是将整个词Java堆分为新生代和老年代。其中新生代存新生的对象或者年龄不大的对象,老年代则存放老年对象。新生代分为eden区、s0区、s1区,s0和s1也被称为from和to区域,他们是两块大小相等井且可以互换角色的空间。

4. 直接内存:Java 的NIO库允许Java程序直接使用内存,从而提高性能,通常直接内存会优于Java堆,读写频繁的场合可能会考虑使用。

5. 每个虚拟机线程都有一个私有的栈:线程的Java栈在线程创建的时候被创建,java栈中保存着局部变量、方法参数、Java的方法调用、返回值等。

Java栈是一块线程私有的内存空间,一个栈,一般由三部分组成:局部变量表、操作数栈和帧数据区。

局部变量表:用于报错函数的参数及局部变量。

操作数栈:主要保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

帧数据区:除了局部变量表和操作数栈以外,栈还需要一些数据来支持常量池的解析,这里帧数据区保存着访问常量池的指针,方便程序访问常量池,另外,当函数返回或者出现异常时,虚拟机必须有一个异常处理表,方便发送异常的时候找到异常的代码,因此异常处理表也是数据区的一部分。

6. 本地方法栈和Java栈非常类似,最大不同为本地方法栈用于本地方法调用。Java虚拟机允许Java直接调用本地方法(通常使用c编写)

7. 垃圾收集系统是Java的核心,也是必不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理,我们稍后详细说明。

8. PC( Program Counter)寄存器也是每个线程私有的空间,java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个java线程总是在执行一个方法,这个方法被称为当前方法,如果当前方法不是本地方法,PC寄存器就会执行当前正在被执行的指令,如果是本地方法,则PC寄存器值为 undefined,寄存器存放如当前执行环境指针、程序计数器、操作栈指针、计算的变量指针等信息。 

9. 虚拟机最核心的组件就是执行引擎了,它负责执行虚拟机的字节码。一般户先进行编译成机器码后执行

jvm线上调优

-XX:+PrintGCDetails可以查看详细信息,包括各个区的情况

-Xms:设置Java程序启动时初始堆大小         -XmX:设置Java程序能获得的最大堆大小

在实际工作中,我们可以直接将初始的堆大小与最大堆大小设置相等,这样的好处是可以减少程序运行时的垃圾回收次数,从而提高性能. 

-XX:+ HeapDumpOnOutOfMemoryError,        -XX: HeapDumpPath

在默认情况下,-XX: Max Permsize为64MB,如果系统运行时生产大量的类,就需要一个相对合适的方法区,以免出现永久区内存溢出的问题。-XX: MaxTenuringThreshold,默认情况下为15。根据设置 MaxTenuringThreshold参数,可以指定新生代对象经过多少次回收后进入老年代。另外,大对象(新生代eden区无法装入时,也会直接进入老年代),JVM里有个参数可以设置对象的大小超过在指定的大小之后,直接晋升老年代.

-XX: PretenureSizeThreshold。使用 PretenureSizeThreshold可以进行指定进入老年代的对象大小,但是要注意TLAB区域优先分配空间。

JVM 内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存等

内存屏障:为了保障执行顺序和可见性的一条cpu指令

重排序:为了提高性能,编译器和处理器会对执行进行重拍

happen-before:操作间执行的顺序关系。有些操作先发生。

主内存:共享变量存储的区域即是主内存

工作内存:每个线程copy的本地内存,存储了该线程以读/写共享变量的副本

局部变量、实例变量的线程安全性?

局部变量是线程安全的。因为局部变量是声明在方法中的变量,方法是放在JVM内存中的方法区的,为方法分配内存的时候并不会为局部变量分配内存,只有在有线程在调用方法的时候才会为局部变量分配内存,并且局部变量是分配在栈上的,栈空间是线程私有的,所以,每当有线程执行方法,都会为其在栈上分配一个局部变量的空间。所以局部变量是属于线程的,是线程安全的。

实例变量的线程安全要分两种情况来说。对于单例模式下对象的实例变量,是非线程安全的。因为在单例模式下,所有线程操作的都是同一个对象,同一个实例变量。所以是不安全的。对于普通创建的对象下的实例变量,每个线程对应一个对象,对象与对象之间的实例变量的修改互不影响,所以是线程安全的。

静态变量是属于类的,存放在方法区,所有线程操作的是同一个,是非线程安全的。

jvm 中一次完整的 GC 流程是怎样的,对象如何晋升到老年代,说说你知道的几种主要的jvm 参数。

对象诞生即新生代->eden,在进行minor gc过程中,如果依旧存活,移动到from,变成Survivor,进行标记代数,如此检查一定次数后,晋升为老年代.

GC的一些机制,每种GC的区别

在cathalina.sh中配java_OPT

串行垃圾回收器 / 并行垃圾回收器 / G1回收器 (分区) / CMS回收器(主流):

CMS全称为:Concurrent Mark Sweep意为并发标记清除,他使用的是标记清除法,主要关注系统停顿时间。

使用-XX:+UseConcMarkSweepGC进行设置 即可以使用CMS

使用-XX:ConcGCThreads设置并发线程数量。

-XX:CMSInitiatingOccupancyFraction来指定,默认为68,也就是说当老年代的空间使用率达到68%的时候,会执行CMS回收,

之前我们在学习算法的时候说过,标记清除法有个缺点就是存在内存碎片的问题,那么CMS有个参数设置XX:+UseCMSCompactAtFullCollecion可以使CMS回收完成之后进行一次碎片整理,-XX:CMSFULLGCsBeforeCompaction参数可以设置进行多少次CMS回收之后,对内存进行一次压缩

g1 和 cms 区别,吞吐量优先和响应优先的垃圾收集器选择。

Cms是以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现。比较占用cpu资源,切易造成碎片。

G1是面向服务端的垃圾收集器,是jdk9默认的收集器,基于标记-整理算法实现。可利用多核、多cpu,保留分代,实现可预测停顿,可控。

ClassLoader加载机制?双亲委托加载模型的好处?

JVM在运行时会产生三个ClassLoader 

1.Bootstrap ClassLoader 

  c++编写的,启动JVM时调用的类加载器,主要用来加载JRE_HOME/lib当前目录下的核心jar,例如:rt.jar,jsse.jar等。 

2.Extension ClassLoader 

  用来加载JRE_HOME/lib/ext当前目录下核心扩展的jar,例如:dnsns.jar等 

3.AppClassLoader  

  AppClassLoader是加载Classpath下所有的jar和classes 

        这3种ClassLoader的优先级依次从高到低,使用所谓的“双亲委派模型”。双亲委托加载模型其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。

确切地说,如果一个类装载器被请求装载一个java.lang.Integer,它会首先把请求发送给上一级的类路径装载器,如果返回已装载,则该类装载器将不会装载这个java.lang.Integer,如果上一级的类路径装载器返回未装载,它才会装载java.lang.Integer。 

Java会出现内存泄漏吗?怎样预防和处理OOM

理论上Java因为有垃圾回收机制(GC)不会存在内存泄露问题(这也是Java被广泛使用于服务器端编程的一个重要原因);然而在实际开发中,可能会存在无用但可达的对象,这些对象不能被GC回收,因此也会导致内存泄露的发生。例如hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露

OOM for Perm:增加永久保存区域(这块内存主要是被JVM存放Class和Meta信息的)大小:-XX:MaxPermSize(默认64M)

OOM for GC:对象过多,导致内存溢出—>调整GC的策略,比如老生代存活率设置为80%;

垃圾回收算法

复制算法:其核心思想就是将内存空间分为两块,每次只使用其中一块,在垃级回收时,将正在使用的内存中的存留对象复制到未被使用的内存块中去,之后去清除之前正在使用的内存块中所有的对象,反复去交换俩个内存的角色,完成垃圾收集。(Java中新生代的from和to空间就是使用这个算法)

标记压缩法:标记压缩法在标记清除法基础之上做了优化,把存活的对象压缩到内存端,而后进行垃圾清理。(Java中老年代使用的就是标记压缩法)

还有引用计数法、标记清除法、分代算法、分区算法。

强引用,软饮用、弱引用、幻象引用​​​​​​​

引用包含4种不同的类型:强引用、软引用、弱引用、幻象引用;不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾回收的影响。

强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

软引用

软引用是用来描述一些有用但并不是必需的对象,在Java中用java.lang.ref.SoftReference类来表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。

弱引用

弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

幻象引用

和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与幻象引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。幻象引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有幻象引用,就会把这个幻象引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了幻象引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个幻象引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

猜你喜欢

转载自blog.csdn.net/haponchang/article/details/92741553