java基础面试题
文章目录
- 1. OSI 的网络模型都有哪些?
- 2. 关于String创建的对象
- 1.抽象和接口
- 2. java的内存区域
- 3. JMM中内存操作流程
- 3. Java中的volatile 变量是什么
- 3. volatile的内存屏障
- 3. 为什么加了volatile还是线程不安全的?
- 4. Java中什么是竞态条件? 举个例子说明。
- 5. 一个线程运行时发生异常会怎样
- 6. 为什么wait, notify 和 notifyAll这些方法不在thread类里面?
- 7. 什么是ThreadLocal变量?
- 8. 如何避免死锁?
- 9. Java中活锁和死锁有什么区别?
- 10. Vector、ArrayList、LinkedList
- 11. 三次握手和四次挥手
- 12. HashMap
- 13. java8 元空间
- 14. 单例模式-双检锁
- 15. 同步集合和并发集合
- 16. 线程池
- 17. synchronized的升级原理是什么?
- 19. http(超文本传输协议):
- 20. hashCode()和 equals():
- 21. 内存溢出和内存泄露
- 22. 公平锁和非公平锁(重入锁)
- 23 cpu密集型和IO密集型
- 24. 进程和线程的区别
- 24. Synchronized和Lock
- 25. CMS 和G1 的区别
- 27. GC的分代策略
- 30. 负载均衡策略
1. OSI 的网络模型都有哪些?
- 应用层:网络服务与最终用户的一个接口。常用的协议包括DNS,HTTP,FTP等。
- 表示层:数据的表示、安全、压缩。
- 会话层:建立、管理、终止会话。 SSL/TLS协议
- 传输层:定义传输数据的协议端口号,以及流控和差错校验。TCP协议和UDP协议。
- 网络层:进行逻辑地址寻址,实现不同网络之间的路径选择。
- 数据链路层:建立逻辑连接、进行硬件地址寻址、差错校验等功能。
- 物理层:建立、维护、断开物理连接。
原文链接:https://blog.csdn.net/qq_41701956/article/details/103253168
2. 关于String创建的对象
1. String s = new String("二哥");
创建了几个对象?
答: 两个对象; 分别位于堆和字符串常量池
使用 new 关键字创建一个字符串对象时,Java 虚拟机会先在字符串常量池中查找有没有‘二哥’这个字符串对象,
如果有,就不会在字符串常量池中创建‘二哥’这个对象了,直接在堆中创建一个‘二哥’的字符串对象,然后将堆中这个‘二哥’的对象地址返回赋值给变量 s,
如果没有,先在字符串常量池中创建一个‘二哥’的字符串对象,然后再在堆中创建一个‘二哥’的字符串对象,然后将堆中这个‘二哥’的字符串对象地址返回赋值给变量 s
2. String s = "三妹";
创建了几个对象?
答: 一个对象; 位于字符串常量池中
当执行 String s = "三妹"
时,Java 虚拟机会先在字符串常量池中查找有没有“三妹”这个字符串对象,
如果有,则不创建任何对象,直接将字符串常量池中这个“三妹”的对象地址返回,赋给变量 s;
如果没有,在字符串常量池中创建“三妹”这个对象,然后将其地址返回,赋给变量 s。
new 的方式始终会创建一个对象,不管字符串的内容是否已经存在,而双引号的方式会重复利用字符串常量池中已经存在的对象
1.抽象和接口
区别:
设计角度:
- 抽象是事物的对象,即对类抽象; 接口是对行为的抽象
- 抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象(继承只能是单继承嘛,所以是对类整体的抽象)
语法角度:
1)抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法(java8的默认方法可以写实现);
2)抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的;
3)抽象类可以有静态代码块和静态方法,而接口中不能含有静态代码块(java8可以写静态方法);
4)一个类只能继承一个抽象类,而一个类却可以实现多个接口。
https://www.cnblogs.com/dolphin0520/p/3811437.html
2. java的内存区域
3. JMM中内存操作流程
8 种基本操作,如图:
- lock 将对象变成线程独占的状态
- unlock 将线程独占状态的对象的锁释放出来
- read 从主内存读数据
- load 将从主内存读取的数据写入工作内存
- use 工作内存使用对象
- assign 对工作内存中的对象进行赋值
- store 将工作内存中的对象传送到主内存当中
- write 将对象写入主内存当中,并覆盖旧值
对于有些操作lock和unlock是没有的,比如volatile,毕竟加了这两个东西线程就安全了
JMM对8种内存交互操作制定的规则吧:
- 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
- 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
- 不允许线程将没有assign的数据从工作内存同步到主内存。
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
- 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
- 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。
为什么volatile也无法保证线程安全_IT农场-CSDN博客_volatile线程安全吗
Java内存模型原理,你真的理解吗? - 知乎 (zhihu.com)
3. Java中的volatile 变量是什么
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized
关键字更轻量级的同步机制。
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
volatile必须满足两个条件:
- 对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果准确性的;
- 该变量没有包含在具有其它变量的不变式中
java volatile关键字解惑 - 简书 (jianshu.com)
当一个变量定义为 volatile 之后,将具备两种特性:
保证此变量对所有的线程的可见性;
禁止指令重排序优化。
有volatile修饰的变量,赋值后多执行了一个
load addl $0x0, (%esp)
操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。
JSL(Java Language Specification, java语言规范)中表示,long和double分为高32位和低32位,在操作时不是原子性的,可能出现线程安全的问题,所以JSL推荐用volatile修饰一下
Java 并发编程:volatile的使用及其原理 - liuxiaopeng - 博客园 (cnblogs.com)
volatile变量提供了一把弱锁,比如用boolean类型做标识符
3. volatile的内存屏障
为什么会有内存屏障
- 每个CPU都会有自己的缓存(有的甚至L1,L2,L3),缓存的目的就是为了提高性能,避免每次都要向内存取。但是这样的弊端也很明显:不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。
- 用volatile关键字修饰变量可以解决上述问题,那么volatile是如何做到这一点的呢?那就是内存屏障,内存屏障是硬件层的概念,不同的硬件平台实现内存屏障的手段并不是一样,java通过屏蔽这些差异,统一由jvm来生成内存屏障的指令。Lock是软件指令。
硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
内存屏障有两个作用:
- 阻止屏障两侧的指令重排序;
- 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
其中第二点依赖与计算机的MESI协议
volatile写的内存屏障
在保守策略下,volatile写插入内存屏障后生成的指令序列示意图:
上图中StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存(不然两个写可能混乱,最终导致数据错误)。
这里比较有意思的是,volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面 是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方 法立即return或者再来一个volatile写)。
为了保证能正确 实现volatile的内存语义,JMM在采取了保守策略:在每个volatile写的后面,或者在每个volatile 读的前面插入一个StoreLoad屏障。从整 体执行效率的角度考虑,JMM最终选择了在每个 volatile写的后面插入一个StoreLoad 屏障。因为volatile写-读内存语义的常见使用模式是:一个写线程写volatile变量,多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM 在实现上的一个特点:首先确保正确性,然后再去追求执行效率。
volatile读的内存屏障
下图是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:
上图中LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore 屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
内存屏障的优化
上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不 改变 volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。
就是只要屏障用不着就可以不用, 也就是说 上述读/写屏障中的不一定会加上loadStore等屏障
比如: volatile读后面如果没有普通读,就不会加loadload屏障, 如果没有普通写,就不会加loadStore屏障
public class VolitileBarrierDemo {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readWrite() {
int i = v1; // 第一个volitile读 分两步 读volitile变量v1, 给i赋值 这两步是有序的
int j = v2; // 第二个volitile读 分两步 读volitile变量v2, 给j赋值 这两步是有序的
a = i + j; // 普通写 分两步 读 i 和 j的值
v1 = i + 1; // 第一个volitile写 两步 读i加1 给volitile v1赋值 volitile写
v2 = j * 2; // 第二个volitile写 两步 读j*2 给volitile v2赋值 volitile写
}
}
内存屏障如图:
注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见, 编译器通常会在这里插入一个StoreLoad屏障。
- LoadLoad 屏障:对于这样的语句Load1,LoadLoad,Load2。在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1, StoreStore, Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore 屏障:对于这样的语句Load1, LoadStore,Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad 屏障:对于这样的语句Store1, StoreLoad,Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
volatile 和 内存屏障 - 哈哈呵h - 博客园 (cnblogs.com)
3. 为什么加了volatile还是线程不安全的?
我们知道CPU的处理速度和主存的读写速度不是一个量级的,为了平衡这种巨大的差距,每个CPU都会有缓存。因此,共享变量会先放在主存中,每个线程都有属于自己的工作内存,并且会把位于主存中的共享变量拷贝到自己的工作内存,之后的读写操作均使用位于工作内存的变量副本,并在某个时刻将工作内存的变量副本写回到主存中去
Volatile的第一个语义就是保证此线程的可见性,一个线程对此变量的更改其他线程是立即可知的。也就是说 assign,store,write这三个操作是原子的,中间不会中断,会马上同步回主存,就好像直接操作主存一样,并通过缓存一致性通知其他缓存中的副本过期.
但是内存间操作还有load和use,这两步不是安全的,所以volatile不是安全的
https://www.jianshu.com/p/d52fea0d6ba5
为什么volatile也无法保证线程安全_IT农场-CSDN博客_volatile线程安全吗
4. Java中什么是竞态条件? 举个例子说明。
当某个计算正确性取决于多个线程的交替执行时序时, 就会发生静态条件,即争取的结果要取决于运气, 最常见的静态条件就是"先检查后执行",通过一个可能失效的观测结果来决定下一步的动作.
例如:
class Counter {
protected long count = 0;
public void add(long value) {
this.count = this.count + value;
}
}
观察线程A和B交错执行会发生什么,两个线程分别加了2和3到count变量上,两个线程执行结束后count变量的值应该等于5。然而由于两个线程是交叉执行的,两个线程从内存中读出的初始值都是0。然后各自加了2和3,并分别写回内存。最终的值并不是期望的5,而是最后写回内存的那个线程的值,上面例子中最后写回内存的是线程A,但实际中也可能是线程B。如果没有采用合适的同步机制,线程间的交叉执行情况就无法预料。add()方法就是一个临界区,它会产生竞态条件。
5. 一个线程运行时发生异常会怎样
所以这里存在两种情形:
- 如果该异常被捕获或抛出,则程序继续运行。
- 如果异常没有被捕获该线程将会停止执行
5.1 线程的状态
状态是五种还是六种内容大同小异,划分的依据是从操作系统和java代码两个层面来划分的。线程整体的流程是:新建—>可运行(就绪+运行)—>终结。
6. 为什么wait, notify 和 notifyAll这些方法不在thread类里面?
一个很明显的原因是JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
7. 什么是ThreadLocal变量?
ThreadLocal一般称为线程本地变量,它是一种特殊的线程绑定机制,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本。通过ThreadLocal可以将对象的可见范围限制在同一个线程内。
没有ThreadLocal的时候,一个线程在其声明周期内,可能穿过多个层级,多个方法,如果有个对象需要在此线程周期内多次调用,且是跨层级的(线程内共享),通常的做法是通过参数进行传递;而ThreadLocal将变量绑定在线程上,在一个线程周期内,无论“你身处何地”,只需通过其提供的get方法就可轻松获取到对象。极大地提高了对于“线程级变量”的访问便利性。
8. 如何避免死锁?
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。这是一个严重的问题,因为死锁会让你的程序挂起无法完成任务,死锁的发生必须满足以下四个条件:
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
避免死锁最简单的方法就是阻止循环等待条件,将系统中所有的资源设置标志位、排序,规定所有的进程申请资源必须以一定的顺序(升序或降序)做操作来避免死锁。
9. Java中活锁和死锁有什么区别?
指事物1可以使用资源,但它让其他事物先使用资源;事物2可以使用资源,但它也让其他事物先使用资源,于是两者一直谦让,都无法使用资源。
解决:
- 在锁让出的时候时候添加随机睡眠时间,
- 约定线程优先级
10. Vector、ArrayList、LinkedList
Vector、ArrayList、LinkedList均为线型的数据结构,但是从实现方式与应用场景中又存在差别。
-
底层实现方式
ArrayList内部用数组来实现;LinkedList内部采用双向链表实现;Vector内部用数组实现。 -
读写机制
ArrayList在执行插入元素是超过当前数组预定义的最大值时,数组需要扩容,扩容过程需要调用底层System.arraycopy()方法进行大量的数组复制操作;在删除元素时并不会减少数组的容量(如果需要缩小数组容量,可以调用trimToSize()方法);在查找元素时要遍历数组,对于非null的元素采取equals的方式寻找。
LinkedList在插入元素时,须创建一个新的Entry对象,并更新相应元素的前后元素的引用;在查找元素时,需遍历链表;在删除元素时,要遍历链表,找到要删除的元素,然后从链表上将此元素删除即可。
Vector与ArrayList仅在插入元素时容量扩充机制不一致。对于Vector,默认创建一个大小为10的Object数组,并将capacityIncrement设置为0;当插入元素数组大小不够时,如果capacityIncrement大于0,则将Object数组的大小扩大为现有size+capacityIncrement;如果capacityIncrement<=0,则将Object数组的大小扩大为现有大小的2倍。
- 读写效率
ArrayList对元素的增加和删除都会引起数组的内存分配空间动态发生变化。因此,对其进行插入和删除速度较慢,但检索速度很快。
LinkedList由于基于链表方式存放数据,增加和删除元素的速度较快,但是检索速度较慢。
- 线程安全性
ArrayList、LinkedList为非线程安全;Vector是基于synchronized实现的线程安全的ArrayList。
需要注意的是:单线程应尽量使用ArrayList,Vector因为同步会有性能损耗;即使在多线程环境下,我们可以利用Collections这个类中为我们提供的synchronizedList(List list)方法返回一个线程安全的同步列表对象
11. 三次握手和四次挥手
- 三次握手
- client发送消息给server: 我要和你发送消息了
- server做好数据接收准备并反馈消息给client: 我收到你的通知了,“我在你的基础上+1”
- client反馈消息给server:我收到你的反馈了,“我再加点标识符,证明是咱俩准备交互”
- 四次挥手
- client和server说:我没有新消息要给你了
- server收到后进行反馈:我先告诉你我收到你的通知了,但是可能我还没有接收完消息,你等会
- client接收后,进入等待状态,server处理完后告诉client:我处理完了,你可以关闭了
- client给server发消息:我收到你的通知了,我关了
- 四次挥手中最后一次,client给server发送最后确认消息后,client并不会马上关闭,进入TIME_WAIT状态,此状态下会等2MSL(两个最大报文段生存时间),因为怕网络不好,server收不到,所以等一会再关(如果server没收到还会发第三次挥手的信息); 在高并发的情况下,可能造成部分正常请求出现TIME_WAIT情况,这时可以适当减少TIME_WAIT的等待时间来处理
- 因为有三次握手/四次挥手的机制保证了TCP的通信可靠
MSL: 即Maximum Segment Lifetime,一个数据分片(报文)在网络中能够生存的最长时间, 即超过两分钟即认为这个报文已经在网络中被丢弃了, 现在一般是60s . Linux和Windows系统修改MSL的值_Han的小站-CSDN博客_linux msl值
更为复杂的TCP交互: TCP 连接状态详解_Han的小站-CSDN博客_tcp 连接状态
12. HashMap
HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体。 只是在JDK1.8中,链表长度大于等于8且数组长度大于64时的时候,链表会转成红黑树。
为什么用数组+链表+红黑树(jdk1.8)?
数组是用来确定桶的位置,利用元素的key的hash值对数组长度取模得到.用数据会比较快,且扩容比较好,
链表是用来解决hash冲突问题,当出现hash值一样的情形,就在数组上的对应位置形成一条链表。当链表元素超过8并且数组长度大于64时会转换成红黑树(元素少用链表,多则用红黑树是因为效率快),当元素减少到6时,会从树变成链表(中间7这个数值做缓冲)
这里的hash值并不是指hashcode,而是将hashcode高低十六位异或过的。
解决hash冲突的方式比较出名的有四种
-
开放定址法
-
链地址法 (HashMap)
-
再哈希法
-
公共溢出区域法
一般用什么作为HashMap的key?
一般用Integer、String这种不可变类当HashMap当key,而且String最为常用。因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算
来自:https://www.cnblogs.com/flyuz/p/11378491.html#1-hashmap%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86
在jdk1.7中,在多线程环境下,扩容时会造成环形链(由于头插法造成)或数据丢失。
在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
https://blog.csdn.net/qq9808/article/details/80850498
https://www.jianshu.com/p/e2f75c8cce01
13. java8 元空间
元空间是Java8开始才出现的内存区域,取代了之前的永久区
元空间中存储的是类的元数据信息(metadata),只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中,
原本永久区存放的static所引用的对象之类的存放在堆中
static的变量存放在方法区
https://segmentfault.com/q/1010000020746567
为什么要将永久代替换成Metaspace?
- 字符串存在永久代中,容易出现性能问题和内存溢出。
- 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。(1.8之前,永久代和老年代会一起被GC,无论哪个满了,都会触发Full GC)
- 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
- Oracle 可能会将HotSpot 与 JRockit 合二为一。
原文链接:https://blog.csdn.net/yjp198713/article/details/78759933/
链接:https://www.jianshu.com/p/93e35781eebc
14. 单例模式-双检锁
代码
public class Test {
private volatile static Test instance;
private Test() {
}
public static Test getInstance() {
if (instance == null) {
synchronized (Test.class) {
if (instance == null) {
instance = new Test();
}
}
}
return instance;
}
}
解析
- 第一个注意点:使用私有的构造函数,确保正常情况下该类不能被外部初始化(非正常情况比如通过反射初始化,一般使用反射之后单例模式也就失去效果了)。
- 第二个注意点:getInstance方法中第一个判空条件,逻辑上是可以去除的,去除之后并不影响单例的正确性,但是去除之后效率低。因为去掉之后,不管instance是否已经初始化,都会进行synchronized操作,而synchronized是一个重操作消耗性能。加上之后,如果已经初始化直接返回结果,不会进行synchronized操作。
- 第三个注意点:加上synchronized是为了防止多个线程同时调用getInstance方法时,各初始化instance一遍的并发问题。
- 第四个注意点:getInstance方法中的第二个判空条件是不可以去除,如果去除了,并且刚好有两个线程a和b都通过了第一个判空条件。此时假设a先获得锁,进入synchronized的代码块,初始化instance,a释放锁。接着b获得锁,进入synchronized的代码块,也直接初始化instance,instance被初始化多遍不符合单例模式的要求~。加上第二个判空条件之后,b获得锁进入synchronized的代码块,此时instance不为空,不执行初始化操作。
- 第五个注意点:instance的声明有一个voliate关键字,如果不用该关键字,有可能会出现异常。因为instance = new Test();并不是一个原子操作,会被编译成三条指令,如下所示。
- 1.给Test的实例分配内存
- 2.初始化Test的构造器
- 3.将instance对象指向分配的内存空间(注意 此时instance就不为空)
然后咧,java会指令重排序,JVM根据处理器的特性,充分利用多级缓存,多核等进行适当的指令重排序,使程序在保证业务运行的同时,充分利用CPU的执行特点,最大的发挥机器的性能!简单来说就是jvm执行上面三条指令的时候,不一定是1-2-3这样执行,有可能是1-3-2这样执行。
如果jvm是按照1-3-2来执行的话,当1-3执行完2还没执行的时候,如果另外一个线程调用getInstance(),因为3执行了此时instance不为空,直接返回instance。问题是2还没执行,此时instance相当于什么都没有,肯定是有问题的。然后咧,voliate有一个特性就是禁止指令重排序,上面的三条指令是按照1-2-3执行的,这样就没有问题了。
15. 同步集合和并发集合
同步集合:
- vector:就比arraylist多了个同步化机制(线程安全),因为效率较低,现在已经不太建议使用。在web应用中,特别是前台页面,往往效率(页面响应速度)是优先考虑的。
- statck:堆栈类,先进后出。
- hashtable:就比hashmap多了个线程安全。
- enumeration:枚举,相当于迭代器。
并发集合:
- ConcurrentHashMap:线程安全的HashMap的实现
- CopyOnWriteArrayList:线程安全且在读操作时无锁的ArrayList
- CopyOnWriteArraySet:基于CopyOnWriteArrayList,不添加重复元素
- ArrayBlockingQueue:基于数组、先进先出、线程安全,可实现指定时间的阻塞读写,并且容量可以限制
- LinkedBlockingQueue:基于链表实现,读写各用一把锁,在高并发读写操作都多的情况下,性能优于ArrayBlockingQueue
CopyOnWrite集合即写时复制的集合
通俗的理解是当我们往一个集合添加元素的时候,不直接往当前集合添加,而是先将当前集合进行Copy,复制出一个新的集合,然后新的集合里添加元素,添加完元素之后,再将原集合的引用指向新的集合。这样做的好处是我们可以对CopyOnWrite集合进行并发的读,而不需要加锁,因为当前集合不会添加任何元素。所以CopyOnWrite集合也是一种读写分离的思想,读和写不同的集合。 (所以这种集合适合读多写少的场景)
ConcurrentHashMap
使用分段锁来提高并发,对里面的数据进行分批加锁,而hashtable是一把大锁,直接把整个map锁住,有点像数据库中的表锁和行锁
原文链接:https://blog.csdn.net/yuruixin_china/article/details/82082195
原文链接:https://blog.csdn.net/qq_41701956/article/details/103253168
16. 线程池
16.1 创建线程池有哪几种方式?
- newFixedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。但它的队列可以长度是Integer.MAX_VALUE,有可能因为排队数造成OOM
- newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,线程池的规模不存在任何限制。但它会创建很多核心线程,最大数是Integer.MAX_VALUE,所以有可能因为线程数过多造成OOM
- newSingleThreadExecutor()
这是一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。和newFixedThreadPool一样,使用同一种队列类型-LinkedBlockingQueue
,可能会因排队数造成OOM
- newScheduledThreadPool(int corePoolSize)
创建了一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。和newSingleThreadExecutor
一样都是单线程执行,它的最大线程数是Integer.MAX_VALUE,可能因为线程数过多而OOM
- newWorkStealingPool()
(JDK1.8) 意为窃取线程池,抢占式执行,所以不保证执行的顺序,其底层用的是ForkJoinPool类,可指定并发量,默认是当前处理器的数量
16.2 执行逻辑
上述都是通过Executors类写好的,但它们都存在一定缺点,alibaba并不推荐使用它,它们底层都是调用了ThreadPoolExecutor方法(除了newWorkStealingPool),它有几个参数,构造函数参数说明:
- corePoolSize => 线程池核心线程数量
- maximumPoolSize => 线程池最大数量
- keepAliveTime => 空闲线程存活时间
- unit => 时间单位
- workQueue => 线程池所使用的缓冲队列
- threadFactory => 线程池创建线程使用的工厂
- handler => 线程池对拒绝任务的处理策略
- 丢弃任务并报错(默认策略)
- 丢弃任务
- 丢弃队列最前面的任务
- 由调用线程(提交任务的线程)处理任务
执行逻辑:
- 判断核心线程数是否已满,核心线程数大小和corePoolSize参数有关,未满则创建线程执行任务
- 若核心线程池已满,判断队列是否满,队列是否满和workQueue参数有关,若未满则加入队列中
- 若队列已满,判断线程池是否已满,线程池是否已满和maximumPoolSize参数有关,若未满创建(非核心)线程执行任务
- 若线程池已满,则采用拒绝策略处理无法执执行的任务,拒绝策略和handler参数有关
原文链接:https://blog.csdn.net/damokelisijian866/article/details/102982390
https://blog.csdn.net/qq_41701956/article/details/103253168
https://blog.csdn.net/tjbsl/article/details/98480843
https://blog.csdn.net/qq_31615049/article/details/80756781
16.3 线程池中线程是如何复用和回收的?
原理
线程池的优点就是提高对线程的管理,提高资源的利用率,控制线程的数量。
在线程池中,线程可以从阻塞队列 中不断 getTask() 新任务来执行,其核心原理在于线程池用Worker对Thread进行了封装,每调用一个 addWorker 就是等于新开一个线程,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去轮询,在这个轮询中,不停地检查是否还有任务等待被执行,如果有则直接去执行这个任务,也就是调用任务的 run() 方法,把 run() 方法当作和普通方法一样的地位去调用,相当于把每个任务的 run() 方法串联了起来,所以线程数量并不增加。
一. 线程如何复用?
ThreadPoolExecutor 在创建线程时,会将线程封装成工作线程 Worker ,并放入工作线程组中,然后这个 Worker 反复从阻塞队列中拿任务去执行。
- 通过取 Worker 的 firstTask 或者通过 getTask 方法从 workQueue 中获取待执行的任务。
- 直接调用 task 的 run 方法来执行具体的任务(而不是新建线程)
二. 线程如何回收?
- 获取不到任务时,回收自己
- 将worker移出线程池
- 线程池状态置为TERMINATED
线程池 | 线程如何复用?_小郭的博客-CSDN博客_线程池如何复用
12 线程池原理 · 深入浅出Java多线程 (redspider.group)
更多知识见 线程池.html
16.4 execute和submit的区别
- execute只能提交Runnable类型的任务,无返回值。submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。
- execute在执行任务时,如果遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常。
异常也是打印到控制台, 如果业务代码没有捕获, 异常相当于就丢失了(log不会收集控制台), 在Theard类中uncaughtExceptionHandler变量存储了线程异常时的处理, 默认情况下使用了ThreadGroup类的实现, ThreadGroup默认打印到控制台了
java.lang.ThreadGroup#uncaughtException public void uncaughtException(Thread t, Throwable e) { if (parent != null) { parent.uncaughtException(t, e); } else { Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler(); if (ueh != null) { ueh.uncaughtException(t, e); } else if (!(e instanceof ThreadDeath)) { System.err.print("Exception in thread \"" + t.getName() + "\" "); e.printStackTrace(System.err); } } }
17. synchronized的升级原理是什么?
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁 状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
锁分级别原因:
没有优化以前,synchronized是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对synchronized 关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。
无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。
偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销(因为大部分情况下还是只有一个线程在运行过来了)。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。 (重点是只有一个线程,如果有线程来争抢就会锁升级)
偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;
如果线程处于活动状态,升级为轻量级锁的状态。
轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
CAS是compare and swap的缩写,即比较后(比较内存中的旧值与预期值)交换(将旧值替换成预期值)
多次尝试CAS操作直至成功或失败,这个过程叫做自旋。
CAS利用cpu原语操作保证了线程安全,所以CAS保证锁升级时的线程安全,但它会存在ABA问题,当然有解决方案,用加版本号的方式解决(java已提供AtomicStampedReference对象处理),在JUC(java.util.concurrent)下用了很多这个,特别是atomic系列
自旋次数默认是10次,由
-Xx:PreBloackSpin
控制, 且有适应性自旋(很久没拿到锁的,等的时间短甚至阻塞,怕你死锁,反之可以等时间长点)详见:https://blog.csdn.net/aaa_bbb_ccc_123_456/article/details/103551391
https://blog.csdn.net/qq_43948583/article/details/104725206
原文链接:https://blog.csdn.net/meism5/article/details/90321826
https://blog.csdn.net/always_younger/article/details/79462684
19. http(超文本传输协议):
- 请求:
- 请求行:请求方法 请求资源 请求版本
- 头部: 属性: 属性值
- 主体(有请求主体时,必须添加实体首部 Content-Type,Content-Length,就是post请求时)
- 响应:
- 响应行:协议版本 响应码 响应信息(OK 或者 NOT FOUND等等)
- 头部: 属性: 属性值
- 主体
20. hashCode()和 equals():
在哈希表中判断两个元素是否重复要使用到 hashCode()和 equals()。
hashCode 决定数据在表中的存储位置,而 equals 判断是否存在相同数据。
当向集合 Set 中增加对象时,首先集合计算要增加对象的 hashCode 码,根据该值来得到一个位置用来存放当前对象,如在该位置没有一个对象存在的话,那么集合 Set 认为该对象在集合中不存在,直接增加进去。如果在该位置有一个对象存在的话,接着将准备增加到集合中的对象与该位置上的对象进行 equals 方法比较,如果该 equals 方法返回 false,那么集合认为集合中不存在该对象,在进行一次散列,将该对象放到散列后计算出的新地址里。如果 equals 方法返回 true,那么集合认为集合中已经存在该对象了,不会再将该对象增加到集合中了。
21. 内存溢出和内存泄露
两者都是基于jvm来说的
- 内存泄漏 memory leak
是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
对jvm来说,有些内存给了你,我无法回收了,那这些内存相当于泄露了,很多时候都是写的代码有问题, 比如说一个只在局部用到的变量但却定义成全局变量, 当方法执行完, 这个变量却不会回收, 因为全局变量会随着类才能释放
简单来说: 是内存无法回收导致的内存不足
- 内存溢出 out of memory :
指程序申请内存时,没有足够的内存供申请者使用,
对jvm来说,你问我要Long型的内存大小,但是我只能给了Int类型的内存大小了,这时你放进去,内存放不下就溢出了
简单来说: 是因为申请的内存太大或者可用内存不够
内存溢出原因:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
- 集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
- 代码中存在死循环或循环产生过多重复的对象实体;
- 使用的第三方软件中的BUG;
- 启动参数内存值设定的过小
内存溢出的解决方案:
第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其 它异常或错误。
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。
重点排查以下几点:
- 检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
- 检查代码中是否有死循环或递归调用。
- 检查是否有大循环重复产生新对象实体。
- 检查List、MAP等集合对象是否有使用完后,未清除的问题。List、Map等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
第四步,使用内存查看工具动态查看内存使用情况
原文链接:
22. 公平锁和非公平锁(重入锁)
重入锁: 同一个人,可以重复拥有这把锁(锁的次数就会增加),它是基于公平锁和非公平锁来实现的,默认是非公平锁
公平锁: 获得锁的几率大家都一样,通过 先进先出队列 控制
非公平锁: 获得锁的几率不一样,对某些线程来说就是不公平的,新来的线程可以插队,优先获得锁
它们两者的区别:
1、公平锁能保证:老的线程排队使用锁,新线程仍然排队使用锁。
2、非公平锁保证:老的线程排队使用锁;新线程抢占已经在排队的线程的锁。(基本可以这样理解)
公平锁的优缺点:
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大
非公平锁的优缺点:
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
公平锁获得锁之前,走"尝试加锁"函数,函数中需要判断队列是否有值,有值就等待,而非公平锁直接进行CAS,这样就有可能抢夺锁了,如果失败就走"尝试加锁"函数,此时不如需要判断队列(所以本质上的区别,两者去获得锁之前,会不会去判断队列)
java.util.concurrent.locks.ReentrantLock.NonfairSync
23 cpu密集型和IO密集型
cpu密集型,又称计算密集型,顾名思义就是应用需要非常多的CPU计算资源,对于计算密集型的应用,完全是靠CPU的核数来工作,所以为了让它的优势完全发挥出来,避免过多的线程上下文切换,比较理想方案是 线程数= CPU核数+1
或者在jdk1.8的forkjoin中线程数 = CPU内核线程数 * 2
比方说,如果系统的主要任务是计算 Hash 值,那么这时选用更高性能的 Hash 算法就可以大大提升系统的性能。发现这类问题的主要方式,是通过一些 Profile 工具来找到消耗 CPU 时间最多的方法或者模块,比如 Linux 的 perf、eBPF等
IO密集型: 就是对IO处理比较多,分为网络IO和磁盘IO. 一旦发生IO,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行,所以IO密集型更适合用多线程. 对于IO密集型应用:线程数= CPU核心数 / (1-阻塞系数)
, 系数一般在0.8~0.9
我们熟知的系统大部分都属于 IO 密集型,比如数据库系统、缓存系统、Web 系统
https://www.jianshu.com/p/f8b2e2869372
https://www.bilibili.com/video/BV1B7411L7tE?p=24
24. 进程和线程的区别
- 根本区别
进程:资源调度最小单位。
线程:CPU调度最小单位。
- 地址空间
进程:进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段。
线程:线程没有独立的地址空间,同一进程的线程共享本进程的地址空间。
- 内存和files共享
进程:当创建一个进程的时候,mm_struct会指向另外一块地址,使用copy-on-write进行复制。
线程:而创建一个线程的时候,mm_struct会指向父进程的同一块虚拟内存区域,所以会有资源冲突问题。
不论线程和进程,在linux中的创建都是很快速的。
- 块
进程:进程控制块PCB。一个进程用ProcessControlBlock上的一个entry记录其基本信息(pid,state,priority等),进程会被操作系统分配一个内存逻辑地址空间,即其拥有一段内存空间供使用。
线程:线程控制块TCB。线程是进程内负责执行一项任务的单元,这个单元用ThreadControlBlock上的一个entry记录其基本信息(tid,state,priority,counter,register info等),这个单元有着自己的stack来用于任务执行。
- 系统开销
进程:进程执行开销大。
线程:线程执行开销小。
- 切换速度 进程:切换相对慢。
线程:切换相对快。
24. Synchronized和Lock
类别 | synchronized | Lock |
---|---|---|
存在层次 | Java的关键字,在jvm层面上 | 是一个接口类 |
锁的释放 | 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁(自动释放锁) | 在finally中必须释放锁,不然容易造成线程死锁(手动释放) |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,具体下面会说道,大致就是可以尝试获得锁,线程可以不用一直等待 |
作用范围 | 代码块,变量,方法,类 | 写到代码中 |
锁类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) |
性能 | 少量同步(重量级锁性能差) | 大量同步(性能好) |
底层原理 | 底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁 | 底层是CAS乐观锁,依赖AbstractQueuedSynchronizer类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作。 |
来自: https://blog.csdn.net/u012403290/article/details/64910926
https://www.jianshu.com/p/b343a9637f95
25. CMS 和G1 的区别
区别一: 回收内存范围
-
CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
-
G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
G1 把老年代和新生代化成大小相等的独立区域, 消除了新/老代的物理隔离, 仍保留了逻辑隔离
区别二: STW的时间
- CMS收集器以最小的停顿时间为目标的收集器。
- G1收集器可预测垃圾回收的停顿时间(建立可预测的停顿时间模型)
区别三: 垃圾回收算法
- CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
- G1收集器使用的是“标记-复制”算法,进行了空间整合,降低了内存空间碎片。
区别四: 垃圾回收的过程不一样
jdk8提供了G1回收器, jdk9默认使用了G1回收器
CMS 和G1 的区别 - 简书 (jianshu.com)
新一代垃圾回收器ZGC的探索与实践 - 美团技术团队 (meituan.com)
27. GC的分代策略
针对HotSpot VM的实现,它里面的GC其实准确分类只有两大种:
- Partial GC:并不收集整个GC堆的模式
-
Young GC / Minor GC:只收集young gen的GC , 使用复制-回收算法
当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。
-
Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式, 使用 标记-清除算法
-
Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
- Full GC:收集整个堆,包括young gen(年轻代)、old gen(老年代)、perm gen(永久代)(如果存在的话)等所有部分的模式。
当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC (老年代/永久代 空间不够了)
G1的FullGC将是采用Serial收集器进行。这将会导致STW发生,这个时间直到收集完成为止。因此要注意G1的退化情况。调优的目的是尽量保证退化的情况不出现。
Major GC通常是跟full GC是等价的,收集整个GC堆。但这个词目前用的很混淆,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。
Major GC和Full GC的区别是什么?触发条件呢? - 知乎 (zhihu.com)
27-YongGC、MinorGC、 Major GC、FullGC傻傻分不清 - 知乎 (zhihu.com)
java8 各种GC的总结 - 简书 (jianshu.com)
30. 负载均衡策略
-
轮询策略
-
权重策略
-
随机策略
-
最小连接数策略 : 遍历服务提供者列表,选取连接数最小的⼀个服务实例。如果有相同的最小连接数,那么会调用轮询策略进行选取。
-
区域敏感策略: 根据服务所在区域(zone)的性能和服务的可用性来选择服务实例,在没有区域的环境下,该策略和轮询策略类似。
区域敏感策略 是Ribbon 独有, nacos和openFegin都内嵌了它
- ip_hash策略: 按照基于客户端IP的分配方式,这个方法确保了相同的客户端的请求一直发送到相同的服务器
ip策略是nginx独有
详解Nginx服务器之负载均衡策略(6种)_nginx 负载默认策略-CSDN博客
springcloud - Spring Cloud Ribbon 中的 7 种负载均衡策略 - Java中文社群 - SegmentFault 思否
原文地址:https://blog.csdn.net/Q176782/article/details/143927358
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!