0. &和&&的区别?
- &和&&都可以用作[逻辑与]的运算符,表示逻辑与(and),当运算符两边的表达式的结果都为true时,整个运算结果才为true,否则,只要有一方为false,则结果为false。
- &&还具有[短路]的功能,即如果第一个表达式为false,则不再计算第二个表达式,例如,对于if(str != null && !str.equals(“”))表达式,当str为null时,后面的表达式不会执行,所以不会出现NullPointerException如果将&&改为&,则会抛出NullPointerException异常。If(x==33 & ++y>0) y会增长,If(x==33 && ++y>0)不会增长
- &还可以用作[位运算符],当&操作符两边的表达式不是boolean类型时,&表示按位与操作,我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,例如,0x31 & 0x0f的结果为0x01。
0.1 equals() 和 "==" 有什么区别?
equals()方法用来比较的是两个对象的内容是否相等,由于所有的类都是继承自java.lang.Object类的,所以适用于所有对象,如果没有对该方法进行覆盖的话,调用的仍然是Object类中的方法,而Object中的equals方法返回的却是==的判断;
"==" 比较的是变量(栈)内存中存放的对象的(堆)内存地址,用来判断两个对象的地址是否相同,即是否是指相同一个对象。
1. object类里有哪些方法?
1.所有方法:
1. getClass()
2. hashCode()
3. equals()
hashCode()和equals()要同时重写原因:如果不重写equals,那么比较的将是对象的引用是否指向同一块内存地址,重写之后目的是为了比较两个对象的value值是否相等。
4. toString()
5. clone()
6. wait()...
7. notify()
8. notifyAll()
9. finalize():实例被垃圾回收器回收的时触发的操作
2. 各个方法作用:
protected Object clone() 创建并返回此对象的一个副本。
boolean equals(Object obj) 指示某个其他对象是否与此对象“相等”。
protected void finalize() 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
Class<? extendsObject> getClass() 返回一个对象的运行时类。
int hashCode() 返回该对象的哈希码值。
void notify() 唤醒在此对象监视器上等待的单个线程。
void notifyAll() 唤醒在此对象监视器上等待的所有线程。
String toString() 返回该对象的字符串表示。
void wait() 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
void wait(long timeout) 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。
void wait(long timeout, int nanos) 导致当前的线程等待,直到其他线程调用此对象的 notify()
2. hashcode是什么?
Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。即在散列集合包括HashSet、HashMap以及HashTable里,对每一个存储的桶元素都有一个唯一的"块编号",即它在集合里面的存储地址;当你调用contains方法的时候,它会根据hashcode找到块的存储地址从而定位到该桶元素。
3. HashMap1.7和1.8的区别
1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
2)扩容后数据存储位置的计算方式也不一样:1. 在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&(这里就是为什么扩容的时候为啥一定必须是2的多少次幂的原因所在,因为如果只有2的n次幂的情况时最后一位二进制数才一定是1,这样能最大程度减少hash碰撞)(hash值 & length-1),而在JDK1.8的时候直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。
3)JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)
4)JDK1.7的时候是先进行扩容后进行插入,而在JDK1.8的时候则是先插入后进行扩容。
4. 哈希表如何解决Hash冲突?
5. synchronized 和 Reentrantlock
Java为我们提供了内置锁(synchronized)和显式锁(ReentrantLock)两种同步方式。
内置锁使用起来非常方便,不需要显式的获取和释放,任何一个对象都能作为一把内置锁。使用内置锁能够解决大部分的同步场景。“任何一个对象都能作为一把内置锁”也意味着出现synchronized关键字的地方,都有一个对象与之关联,具体说来:
- 当synchronized作用于普通方法是,锁对象是this;
- 当synchronized作用于静态方法是,锁对象是当前类的Class对象;
- 当synchronized作用于代码块时,锁对象是synchronized(obj)中的这个obj。
- Reentrantlock性能好,不阻塞。lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
6. 怎么用无锁方案实现线程安全?
ThreadLocal:如果一个变量要被线程独享,可以通过java.lang.ThreadLocal类实现线程本地存储的功能。既然是本地存储的,那么就只有当前线程可以访问,自然是线程安全的。
1、Thread里面有个属性是一个类似于HashMap一样的东西,只是它的名字叫ThreadLocalMap,这个属性是default类型的,因此同一个package下面所有的类都可以引用到,因为是Thread的局部变量,所以每个线程都有一个自己单独的Map,相互之间是不冲突的,所以即使将ThreadLocal定义为static线程之间也不会冲突。
2、ThreadLocal和Thread是在同一个package下面,可以引用到这个类,可以对他做操作,此时ThreadLocal每定义一个,用this作为Key,你传入的值作为value,而this就是你定义的ThreadLocal,所以不同的ThreadLocal变量,都使用set,相互之间的数据不会冲突,因为他们的Key是不同的,当然同一个ThreadLocal做两次set操作后,会以最后一次为准。
3、综上所述,在线程之间并行,ThreadLocal可以像局部变量一样使用,且线程安全,且不同的ThreadLocal变量之间的数据毫无冲突。
ThreadLocal为什么会产生溢出?原博地址
ThreadLocal里面使用了一个存在弱引用的map, map的类型是
ThreadLocal.ThreadLocalMap
. Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。
但是,我们的value却不能回收,而这块value永远不会被访问到了,所以存在着内存泄露。因为存在一条从
current thread
连接过来的强引用。只有当前thread结束以后,current thread
就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。最好的做法是将调用threadlocal的remove方法,这也是等会后边要说的。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的
get(),set(),remove()
的时候都会清除线程ThreadLocalMap里所有key为null的value。
7. 线程池的核心参数、在线程池提交了以后的执行过程
public ThreadPoolExecutor(
int corePoolSize, //核心池的大小。
int maximumPoolSize, //池中允许的最大线程数,这个参数表示了线程池中最多能创建的线程数量
long keepAliveTime, //当线程数大于corePoolSize时,终止前多余的空闲线程等待新任务的最长时间
TimeUnit unit, //keepAliveTime时间单位
BlockingQueue<Runnable> workQueue, //存储还没来得及执行的任务
ThreadFactory threadFactory, //执行程序创建新线程时使用的工厂
RejectedExecutionHandler handler //由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序
)
corePoolSize与maximumPoolSize举例理解
1、池中线程数小于corePoolSize,新任务都不排队而是直接添加新线程。
2、池中线程数大于等于corePoolSize,workQueue未满,首选将新任务加入workQueue而不是添加新线程。
3、池中线程数大于等于corePoolSize,workQueue已满,但是线程数小于maximumPoolSize,添加新的线程来处理被添加的任务。
4、池中线程数大于大于corePoolSize,workQueue已满,并且线程数大于等于maximumPoolSize,新任务被拒绝,使用handler处理被拒绝的任务。
执行过程
首先,线程池会判断核心线程池里的线程(线程总数是30,则coreSize有可能是10)是否都在执行任务。如果没有比方说当前只有9个线程在工作,则从核心线程池中创建一个新的线程来执行任务。如果当前已经有10个线程在工作了,则进入下一步;
其次,线程池会判断工作队列是否已经满了,如果工作队列没有满,则将新提交的任务存储在这个工作队列里,如果工作队列已经满了,则进入下一个流程;
最后,线程池判断全部线程是否都在工作,如果没有,即30个线程只有25个在工作,则创建一个新的工作线程来执行任务,如果已经有30个线程来执行,没有任何空闲线程,则交给饱和策略来处理这个任务(默认的饱和策略为抛异常)。
8. 线程间的通信方式
Java中线程通信协作的最常见的两种方式:
- syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
- 2.ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
线程间直接的数据交换: - 通过管道进行线程间通信:1)字节流;2)字符流
9. 多线程实现方式
- 实现Runable接口
public class MyRunnable implements Runnable {
public void run() {
// 重写run方法
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
// 通过 Thread 调用 start() 方法来启动线程。
thread.start();
}
交替打印奇偶数:
public class PrintAlternately {
private static class Counter {
public int value = 1;
public boolean odd = true;
}
private static Counter counter = new Counter();
private static class PrintOdd implements Runnable {
@Override
public void run() {
while (counter.value <= 100) {
synchronized(counter) {
if (counter.odd) {
System.out.println(counter.value);
counter.value++;
counter.odd = !counter.odd;
//很重要,要去唤醒打印偶数的线程
counter.notify();
} else {
//交出锁,让打印偶数的线程执行
try {
counter.wait();
} catch (InterruptedException e) {}
}
}
}//while
}
}
private static class PrintEven implements Runnable {
@Override
public void run() {
while (counter.value <= 100) {
synchronized (counter) {
if (counter.odd) {
try {
counter.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(counter.value);
counter.value++;
counter.odd = !counter.odd;
counter.notify();
}
}//synchronized
}
}
}
public static void main(String[] args) {
new Thread(new PrintOdd()).start();
new Thread(new PrintEven()).start();
}
}
// 原文链接:https://blog.csdn.net/guohengcook/article/details/81638024
10. JVM划分区域
线程共享:方法区,堆
非线程共享:虚拟机栈,本地方法栈,程序计数器
静态变量在方法区内,线程间共享,不安全。
局部变量在栈内,线程间不共享,安全。
11.Java GC、新生代、老年代
堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
- 判断对象是否存活的算法
- 引用计数法
-当一个对象被创建时,就将该对象实例分配给一个变量,该变量计数设置为1。
-当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),
-当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。
优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。
缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。 - 可达性分析算法
程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点,无用的节点将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
a) 虚拟机栈中引用的对象(栈帧中的本地变量表);
b) 方法区中类静态属性引用的对象;
c) 方法区中常量引用的对象;
d) 本地方法栈中JNI(Native方法)引用的对象。
- 引用计数法
11. 常用垃圾回收器
- 新生代回收器:Serial、ParNew、Parallel Scavenge----复制算法
- 老年代回收器:Serial Old、Parallel Old--标记-整理 和 CMS--标记-清除
- 整堆回收器:G1--复制、标记-整理
参考链接
垃圾回收算法:- 标记-清除算法
标记-清除算法采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示。标记-清除算法不需要进行对象的移动,只需对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。 - 复制算法
复制算法的提出是为了克服句柄的开销和解决内存碎片的问题。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集合(GC Roots)中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。 - 标记-整理算法
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
- 标记-清除算法