JAVA面试习题梳理-P2

1.JMM内存模型
JMM是JAVA的内存模型,是一种抽象模型并不真实存在


volatile可见性和不保证原子性.png

保证可见性:jvm在运行时会为线程创建对应的工作内存,区别于主内存即物理内存,而java内存模型中规定所有变量存储在主内存中,被所有线程共享。线程对变量的操作必须在工作内存中进行,所以线程需要将变量从主内存拷贝到自己的工作内存,然后对变量操作,操作完成后再将变量写回主内存,而不是直 接操作主内存的变量。各个线程中的工作内存存储着主内存的变量副本拷贝,不同线程之间不能访问对方的工作内存,线程间的通讯必须通过主内存完成。当线程A对变量M更改并写回主内存,其他线程第一时间同步主内存中共享变量M的最新值的机制,就叫做JMM的可见性(所有线程对共享变量或资源的操作都在主内存中 )。


volatile可见性验证.png

不保证原子性:以变量累加为例,多个线程取到主内存的变量A,各自在工作内存中累加后将变量写回主内存。这段操作在操作栈中分为多步,在最后一步写入主内存前,线程M1被线程M2抢占cup控制权,写入M2的变量值并同步给其他线程从而丢失了线程M1被挂起前的操作(写覆盖)。解决方案是使用具有原子性操作的包装类代替变量A的类型。
volatile不保证原子性验证及解决方案.png

禁止指令重排:没有数据依赖性的代码在jvm中的调用顺序默认经过优化后执行,从而导致多线程情况下对同一个变量A的操作结果出现不同的情况。

场景1:
image.png

问题的答案是不可以,因为有数据的依赖性。
场景2:
image.png

解决方案就是禁止指令重排,用volatile修饰变量A。


InstructionRearrangement.png

volatile解决单例模式下线程安全问题:
Singleton.png

2.CAS:比较并交换
通过比较,当一个变量在其所在的工作内存中的value和主内存中的value一致的情况下才会把工作内存中的value更新到主内存,否则保留主内存的value


cas.png

再度理解CAS,CAS是系统原语,功能是判断内存中每个位置的值是否为预期值,如果是则改为新值,这个过程是原子性的。
并发原语体现在JAVA语言中就是rt包中sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖硬件的功能,通过它实现原子操作,即在执行过程中不允许被打断。


cas底层原子性.png
compareAndSwapInt是本地方法,是CAS+unsafe保证线程安全的核心机制。
CAS的缺点:
1.高并发环境循环时间长,开销大(一直在dowhile自旋判断)

2.相比于sync修饰保证线程安全的方法,CAS只能保证数量是一的共享变量的原子操作
3.引发ABA问题:线程M1,M2都从主内存中取出变量A,并且线程M2进行了一些操作将变量A的值从最初的value1改成了value2并写回主内存,然后线程M2又将主内存变量A的值由value2改成了value1并写回主内存,这时候线程M1进行CAS操作时发现内存中仍然是value1,线程M1的CAS操作成功。虽然线程M1的操作是成功的但是并不代表这个过程是没有问题的。

除了AtomicInteger,也可以通过AtomicReference自定义原子类(类的原子包装):


atomicReference.png

ABA问题--重现


ABADemo.png

exposeABA.png

ABA问题--解决


solveABA.png

非线程安全集合


CollectionsNotSafeDemo.png

exposeArrayListNotSafe.png

safeArrayList.png

HashSet同样也是非线程安全的集合,原因同ArrayList,解决方案对应上面代码示例的2和3方案,即Collections.synchronizedSet(hashSet)和Set hashSet2 = new CopyOnWriteArraySet<>()
PS:1.CopyOnWriteArraySet的创建底层也是用的CopyOnWriteArrayList,由其构造器可知:
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList();
}
2.HashSet的底层使用的HashMap,由其构造器可知:
public HashSet() {
map = new HashMap<>();
},只不过set做add操作的时候,元素e作为map的key,Object类型的常量PRESENT作为map的value,代码可见:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
对于HashMap同样也是非线程安全的集合,解决方案:Collections.synchronizedMap(hashMap)和ConcurrentHashMap hashMap = new ConcurrentHashMap<>();
3.copyonwritearraylist适用于数据量不大的场景,不适用于数据量大的场景。由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc,适用于读多写少的场景,不适用于实时读的场景。
如果写操作未完成,那么直接读取原数组的数据;
如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

公平锁:多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到
非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象

并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,缺省是非公平锁

公平锁和非公平锁的区别:
公平锁在并发环境中每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己
非公平锁上来就直接占有锁,如果尝试失败就再采用类似公平锁的方式

对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁
对于Synchronized而言,也是非公平锁。

非公平锁相对公平锁的优点在于吞吐量比较大

可重入锁(又名递归锁)
一个线程获取了外层的锁O,对于其加锁控制的代码中其他锁I不必重新索取,好比一个人有了大门的钥匙,对于房间内上的锁都有可进入的权限
ReentrantLock和Synchronized都是可重入锁
可重入锁的最大的作用是避免死锁


ReEnterLockDemo-phone.png

ReEnterLockDemo.png

自旋锁:尝试获取锁的方式不是立即阻塞,而是通过循环访问获取锁的方式,需要合理使用


SpinLockDemo.png

可重入读写锁(ReentrantReadWriteLock)
好处: 读读不能互斥,提升锁性能,减少线程竞争。
缺点是:当读锁过多时候,写锁少,存在锁饥饿现象。


ReentrantReadWriteLock-资源类.png

ReentrantReadWriteLock.png

JUC APIs
CountDownLatch:当某个线程M1阻塞直到另一些线程完成一系列操作后才被唤醒,CountDownLatch有两个方法,await阻塞调用的线程M1;countdown会将计数器减1,当计数器减到0时线程M1被唤醒继续执行


CountryEnum.png

CountDownLatchDemo.png

FeatureTask有类似CountDownLatch的效果:


image.png

线程通讯样例:
image.png

image.png

CyclicBarrier:集齐七颗龙珠才能召唤神龙


CyclicBarrierDemo.png

Semaphore:多个线程抢夺多个资源


SemaphoreDemo.png

阻塞队列
当阻塞队列是空时,从队列中获取元素的操作将会被阻塞
当阻塞队列是满时,往队列中添加元素的操作将会被阻塞
使用阻塞队列的好处:不需要关心什么时候阻塞线程,什么时候唤醒线程
架构:


BlockingQueue架构.png

重点阻塞队列:
ArrayBlockingQueue:数据结构组成的有界阻塞队列
LinkedBlockingQueue:链表结构组成的有界(但默认大小为Integer.MAX_VALUE)阻塞队列
SynchronousQueue:只存储单个元素的队列(需要消费一个才实时生产一个)


SynchronousQueueDemo.png

阻塞队列APIs:
BlockingQueueApis.png

Synchronized和Lock的区别
1.原始构成:Synchronized是关键字属于JVM层面,底层通过monitor对象来完成,wait和notify等方法依赖monitor对象,Lock是具体的类(java.util.concurrent.locks.Lock)是api层面的锁
2.使用方法:Synchronized不需要手动释放锁,当Synchronized代码执行完后系统会自动让线程释放对锁的占用,Lock需要手动释放锁并没有主动释放锁,有可能导致出现死锁的现象,需要lock()和unlock()方法配合try catch finally代码块完成
3.等待是否可以中断:Synchronized不可以中断,除非抛出异常或者正常运行结束推出,Lock可以中断:设置超时方法tryLock(long timeout,TimeUnit unit)或者lockInterruptibly()放代码块中,调用interrupt方法可以中断
4.加锁是否公平:Synchronized是非公平锁,Lock两者都可以,默认是非公平锁,构造方法传入boolean设置是否是公平锁
5.锁绑定多个条件Condition:Synchronized没有,Lock可以精确唤醒而不是像Synchronized唤醒一个或者全部
6.非静态方法的锁默认是this,静态方法的锁是对象对应的Class

线程通讯
1.生产者消费者


ProdConsumerShareData.png

ProdConsumerDemo.png

2.阻塞队列-生产者消费者


MyResource.png

ProdConsumerDemo2.png

线程池
体系结构:


image.png

CallableDemo


CallableDemo.png

线程池的主要工作是控制运行的线程的数量,处理过程中将线程放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等待,等其他线程执行完毕后,再从队列中取出任务执行
主要特点,线程服用减少系统开支,控制最大并发数量,管理线程

第一降低资源消耗,通过重复利用已将创建的线程降低线程创建和销毁造成的系统开销
第二提高响应速度,当任务达到时,任务可以不需要等待线程创建就能立即执行
第三提高线程的可管理性,线程是稀缺资源,可以对线程统一分配调优和监控

线程池的创建都依赖ThreadPoolExecutor,如newCachedThreadPool的创建


ExecutorService创建.png

常用线程池


MyThreadPoolDemo.png

线程池参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
corePoolSize,线程池中常驻核心线程数
maximumPoolSize,线程池能够容纳同时执行的最大线程数,此值必须大于等于1
keepAliveTime,多余空闲线程的存活时间
当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
unit,keepAliveTime的单位
workQueue,任务队列,被提交但尚未被执行的任务
threadFactory,表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可
handler,拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数,对新的任务需求的处理方式

拒绝策略(4种):


image.png

如何合理配置线程池最大线程数量:
1.CPU密集型,即任务需要大量的运算而没有阻塞,CPU一直全速运行,一般公式:CPU核数+1个线程的线程池(Runtime.getRuntime().availableProcessors()获取处理器个数)
2.IO密集型,即线程并不是一直在执行任务,则应配置尽可能多的线程,
如CPU核数*2或者CPU/(1-阻塞系数),阻塞系数在0.8-0.9之间

死锁


DeadLockDemo.png

查看java程序进程编号命令:jps -l
查看死锁问题命令:jstack 进程号

注意:线程池不允许使用Excutors创建,而是通过ThreadPoolExector的方式,因为前者使用的阻塞队列的最大长度Integer.MAX_VALUE,可能会堆积大量请求造成系统OOM

JVM
垃圾回收的一种策略:对象可达性分析,即对象引用关系可以和GCRoot联通就是对象可达
可以作为GCRootd的对象:
1.虚拟机栈中引用的对象 2.方法区中的类静态属性引用对象
3.方法区中的常量引用对象 4.本地方法中的JNI(native方法)

JVM参数:
1.标准参数 如-version -help -showversion
2.x参数(了解) 如-Xint 执行模式 -Xcomp 编译模式 -Xmixed 先编译后执行
3.xx参数(重点)
jps查看java进程
jinfo查看某一个java进程是否启用某一个jvm参数,示例如下

image.png

jinfo也可以查看某一个java进程的某一个jvm参数的设置取值,示例如下
image.png

image.png

-Xms和-Xmx
image.png

查看jvm家底参数:
image.png

image.png

常用jvm参数


image.png

其中-Xmn默认堆内存的1/3
典型jvm参数设置


image.png

堆内存的分割
image.png

MinorGC的过程



-XX:+PrintGCDetails 打印程序运行过程发生的GC详情
image.png

image.png

image.png
在jdk8中. MaxTenuringThreshold范围为 0到15.
强引用:只要有强引用依然指向一个对象,就不会被垃圾回收,就算出现了OOM也不回收(默认)
image.png

软引用:内存够用的时候就保留,不够用就回收
image.png

弱引用:只要垃圾回收,不管jvm内存够不够都会被回收


image.png

软引用和弱引用的适用场景
image.png

WeakHashMap VS HashMap
image.png

虚引用
image.png

引用队列:弱引用和虚引用在gc之后,会存放到引用队列
image.png

GCroot与四种引用关系:一定被回收的是弱引用和虚引用,不会被回收的是强音用,看情况的是软引用
image.png

StackOverFlowError和Out'O'fMemoryError是异常还是错误?错误!
image.png

模拟java.lang.StackOverflowError
image.png

模拟java.lang.OutOfMemoryError: Java heap space
image.png

模拟java.lang.OutOfMemoryError: GC overhead limit exceeded
image.png

image.png

模拟java.lang.OutOfMemoryError: Direct buffer memory


image.png

image.png

image.png

模拟java.lang.OutOfMemoryError: unable to create native thread
image.png

image.png

调整最大线程数配置
image.png

模拟java.lang.OutOfMemoryError: Metaspace
image.png

image.png

垃圾回收器(新生代是GC 养老代是FullGC)
image.png

image.png

image.png

查看默认的垃圾回收器:java -XX:+PrintCommandLineFlags -version,查看
常用的垃圾垃圾处理器
image.png

image.png

========================
新生代:串行
image.png

image.png

新生代:并行


image.png

image.png

image.png

image.png

image.png

老年代:CMS


c78a5d45cda9035f6af977f32f639a4.png

5763e0d32c5c16d9706cdc9d75250fa.png

846bde7e228d301849925fca440d67f.png

66f4f9f105f40f4d0f39324f309a0a9.png

Serial Old


9fbe728bfb5d0b1bfe3680283a6815d.png

如何选择垃圾收集器
fece0f4ec5139b3960d2e10604b32bb.png

686a569a5c8090c7aa73c46500d85fd.png

G1
e10fd093442643ef71a3b6cfb17dccd.png

eb828e7c2b4f4eeb27d7d7f78175ef8.png

721befdf77cc823038572676af14247.png

a06b919707f51e51349ea70e608f9e4.png

集合springboot的jvm参数设置


0c232e223f94334224eec71a589cd8a.png

Linux命令
整机性能分析:top,主要看CPU和MEM以及LoadAverage(三个值分别代表1 5 15分钟系统的负载均衡,平均值超过60%代表系统压力过重),uptime是其简化版
CPU:vmstat -n 2 3 ,每个2秒采样一次,一共采样3次,结果集主要看procs和cpu,其中pros中的r表示运行和等待cup时间片的进程数,原则上整个系统的运行队列不能超过总核数的2倍,否则表示系统压力过大,pros中的b表示等待资源的进程数,比如正在等待磁盘IO 网络IO等;cpu中的us表示用户进程占用cpu的百分比,sy表示内核进程占用cpu的百分比,us和sy之和的参考值为80%,超过80%表示存在cpu不足或者需要优化用户进程,id表示cpu的空闲率
CPU:mpstat -P ALL 2 3 ,查看全部进程的cpu占用率,每2秒采样一次,采样3次
pidstat -u 1 -p 进程号,每一秒采样一次,查看某个进程的cpu的使用情况

内存:free -m或者pidstat -r 采样间隔时间 -p 进程号
硬盘:df -h
磁盘IO:iostat -xdk 2 3 或者pidstat -d 采样间隔时间 -p 进程号
网络IO:ifstat 1

cpu过高分析思路:
先用top命令找到cpu占比最高的情况
ps -ef或者jps进一步定位哪个进程导致 获取pid
通过ps -mp 获取的pid(如5101) -o THREAD,tid,time命令查看线程占用情况,获取占用cpu资源最大的线程id tid
将线程id转换为小写的16进制格式 比如5102换成13ee ,使用命令jstack 5101 | grep 13ee -A60
找到对应的业务代码后做相关代码层面的分析

github
常用词:watch:会持续收到该项目的动态
fork,复制某个项目到自己的github仓库中
star,可以理解为点赞
clone,将项目下载到本地
follow,关注你感兴趣的作者,会收到他们的动态

in:xxx关键词 in:name(,readme,description)
点赞超过5000的项目:xxx关键词 stars:>=5000
fork数超过500的项目:xxx关键词 forks:>500
fork数量在100到200之间 并且 点赞数量在80到100之间:xxx关键词 forks:100..200 stars:80..100
awesome xxx关键词:搜索学习内容
高亮显示一行或者多行:github代码地址+#L13(高亮13行代码)\github代码地址+#L13-L23(高亮13到23行代码)
项目内搜索:英文字母t
搜索某区域某语言下活跃的用户:location:beijing language:java

forkjoin框架:


image.png

image.png

-- END

你可能感兴趣的:(JAVA面试习题梳理-P2)