掌握这14招,让面试官眼前一亮


Java基础14招,招招精通


1.进程和线程的区别?

面试中经常会提到 进程与线程的区别用来打开多线程的面试开关,当然就算你回答上此问题也是折磨你的开始。答不上来那么就出门左转吧。

进程:就是一个可执行程序在运行在系统中的展示。也就是说当你的系统要运行,是需要系统进行分配资源,当前所说的资源就是进程。他的作用就是:并发执行,提高资源利用率和吞吐率,比如:系统启动了两个程序即两个进程在运行,所谓的的并发(单核cpu)就是cpu来回切换进程

由于进程是资源分配和调度的基本单位,因为进程的创建、销毁、切换产生大量的时间和空间的开销,进程的数量不能太多

线程:是比进程更小的能独立运行的基本单位,他是进程的一个实体,可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。

2.你对synchronized关键字有了解吗

这里其实才是真正进入线程相关面试的开胃菜,那么废话不说了,跟我学习第二招吧。很重要的,他涉及的东西相当多,因为涉及 jvm,aqs,jdk版本迭代等相关知识的支撑,要说清楚他,我上面提的那几块知识是不可缺的。当然不要着急,由我一一给你拆分这个大招,带你一步一步掌握这个技能

synchronized是java提供的原子性内置锁,也称重量级锁,常和lock锁比较(这里就是synchronized专场,lock就不先提了,懂了synchronized,lock很简单,就是相对灵活点,以new 对象形式存在), synchronized 底层 就是对一个 monitor对象的监控,这种内置的并且使用者看不到的锁也被称为监视器锁,使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。他的作用主要就是实现原子性操作和解决共享变量的内存可见性问题。

重点拆分:
1.下面的这个介绍大家要记住,对你以后理解aqs很有帮助,aqs也就是多了公平和非公平以及自旋的情况处理

执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。

执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。

2.synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。
从内存语义来说,加锁的过程会清除工作内存中的共享变量(用户态),再从主内存读取(内核态),而释放锁的过程则是将工作内存(用户态)中的共享变量写回主内存(内核态)。【用户态和内核态的切换,极大损耗性能,当然一般来说是无法避免,可以解决的就是应用克隆技术,建立内存直接映射】

下面说的这个可以简单看下,了解,都是对aqs的概念展现

如果再深入到源码来说,synchronized实际上有两个队列waitSet和entryList。

1.当多个线程进入同步代码块时,首先进入entryList
2.有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
3.如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
4.如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null

大家试着去理解这张图片,加深印象
在这里插入图片描述

给大家10分钟休息休息,消化下上面说的东西,当然涉及到 synchronized 还没说完,刚上面只是原理流程。下面讲的才是加分项

加分项

synchronized 的优化机制

在讲synchronized 给大家普及几个简单的概念,对于理解sync的优化机制很有必要

自旋锁:由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态

自旋锁:自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。

锁消除:锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。

锁粗化:锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。

优化过程 无锁-偏向-轻量-重量

在分析这几种锁的区别时,我们先来思考一个问题
使用锁能够实现数据的安全性,但是会带来性能的下降。
不使用锁能够基于线程并行提升程序性能,但是却不能保
证线程安全性。这两者之间似乎是没有办法达到既能满足
性能也能满足安全性的要求。
没有锁竞争的时候就是偏向锁,有锁竞争就由偏向锁升级轻量锁
hotspot 虚拟机的作者经过调查发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程
多次获得。所以基于这样一个概率,是的 synchronized 在
JDK1.6 之后做了一些优化,为了减少获得锁和释放锁带来
的性能开销,引入了偏向锁、轻量级锁的概念。因此大家
会发现在 synchronized 中,锁存在四种状态
分别是:无锁、偏向锁、轻量级锁、重量级锁; 锁的状态
根据竞争激烈的程度从低到高不断升级。

写到这我觉得就先够了,真正了解这个升级过程,我觉得后面还是等我单独写一篇文章 synchronized 总结下篇幅足够了,当下如果把这都讲完就太费时间了,我怕大家看到第二招就不想看了,所以就看看下面我整理的图片,有个简单的了解吧
在这里插入图片描述
在这里插入图片描述

3.我看你介绍snychronized 说到了对象头,那么对象头都有哪些内容

代码如下在我们常用的Hotspot虚拟机中,对象在内存中布局实际包含3个部分:

1.对象头
2.实例数据
3.对齐填充
而对象头包含两部分内容,Mark Word中的内容会随着锁标志位而发生变化,所以只说存储结构就好了。

对象自身运行时所需的数据,也被称为Mark Word,也就是用于轻量级锁和偏向锁的关键点。具体的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程ID、偏向锁时间戳。
存储类型指针,也就是指向类的元数据的指针,通过这个指针才能确定对象是属于哪个类的实例。
如果是数组的话,则还包含了数组的长度

在这里插入图片描述

4. ReentrantLock原理和synchronized有什么区别?

相比于synchronized,ReentrantLock需要显式的获取锁和释放锁,相对现在基本都是用JDK7和JDK8的版本,ReentrantLock的效率和synchronized区别基本可以持平了。他们的主要区别有以下几点:

1.等待可中断,当持有锁的线程长时间不释放锁的时候,等待中的线程可以选择放弃等待,转而处理其他的任务。
2.公平锁:synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以通过构造函数传参改变。只不过使用公平锁的话会导致性能急剧下降。
3.绑定多个条件:ReentrantLock可以同时绑定多个Condition条件对象。
ReentrantLock基于AQS(AbstractQueuedSynchronizer 抽象队列同步器)实现。别说了,我知道问题了,AQS我这里简单介绍下,以后单独出文章说明。

AQS内部维护一个state状态位,尝试加锁的时候通过CAS(CompareAndSwap)修改值,如果成功设置为1,并且把当前线程ID赋值,则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。
在这里插入图片描述

5 .那么CAS呢

看到这里,大家可以松口气了,简单了。so easy

CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:

1.变量内存地址,V表示
2.旧的预期值,A表示
3.准备设置的新值,B表示
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。

6 .cas的缺点:aba问题,自旋问题

你修改的如果是一个对象,不是一个基本属性值,则cas无法监控具体值,只会监控地址值
ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。

Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。

循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。

只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。

7. HashMap用过吧?

HashMap主要由数组和链表组成,他不是线程安全的。核心的点就是put插入数据的过程,get查询数据以及扩容的方式。JDK1.7和1.8的主要区别在于头插和尾插方式的修改,头插容易导致HashMap链表死循环(在执行resize()扩容的时候),并且1.8之后加入红黑树对性能有提升。
hashMap的算法复炸度O(1)

在这里插入图片描述
put插入数据流程

往map插入元素的时候首先通过对key hash然后与数组长度-1进行与运算((n-1)&hash),都是2的次幂所以等同于取模,但是位运算的效率更高。找到数组中的位置之后,如果数组中没有元素直接存入,反之则判断key是否相同,key相同就覆盖,否则就会插入到链表的尾部,如果链表的长度超过8,则会转换成红黑树,最后判断数组长度是否超过默认的长度*负载因子也就是12,超过则进行扩容。

get查询数据

查询数据相对来说就比较简单了,首先计算出hash值,然后去数组查询,是红黑树就去红黑树查,链表就遍历链表查询就可以了。

resize扩容过程

扩容的过程就是对key重新计算hash,然后把数据拷贝到新的数组。

8.既然hashMap不安全,那么如何解决呢

多线程环境可以使用Collections.synchronizedMap同步加锁的方式,还可以使用HashTable,但是同步的方式显然性能不达标,而ConurrentHashMap更适合高并发场景使用。

ConcurrentHashmap在JDK1.7和1.8的版本改动比较大,1.7使用Segment+HashEntry分段锁的方式实现,1.8则抛弃了Segment,改为使用CAS+synchronized+Node实现,同样也加入了红黑树,避免链表过长导致性能的问题。

1.7分段锁

从结构上说,1.7版本的ConcurrentHashMap采用分段锁机制,里面包含一个Segment数组,Segment继承与ReentrantLock,Segment则包含HashEntry的数组,HashEntry本身就是一个链表的结构,具有保存key、value的能力能指向下一个节点的指针。

实际上就是相当于每个Segment都是一个HashMap,默认的Segment长度是16,也就是支持16个线程的并发写,Segment之间相互不会受到影响。

在这里插入图片描述
put流程

其实发现整个流程和HashMap非常类似,只不过是先定位到具体的Segment,然后通过ReentrantLock去操作而已,后面的流程我就简化了,因为和HashMap基本上是一样的。

1.计算hash,定位到segment,segment如果是空就先初始化
2.使用ReentrantLock加锁,如果获取锁失败则尝试自旋,自旋超过次数就阻塞获取,保证一定获取锁成功
3.遍历HashEntry,就是和HashMap一样,数组中key和hash一样就直接替换,不存在就再插入链表,链表同样

在这里插入图片描述
get流程

get也很简单,key通过hash定位到segment,再遍历链表定位到具体的元素上,需要注意的是value是volatile的,所以get是不需要加锁的。

1.8CAS+synchronized

1.8抛弃分段锁,转为用CAS+synchronized来实现,同样HashEntry改为Node,也加入了红黑树的实现。主要还是看put的流程。

大致还是差不多的,主要是put过程大不一样
在这里插入图片描述
put流程

首先计算hash,遍历node数组,如果node是空的话,就通过CAS+自旋的方式初始化
如果当前数组位置是空则直接通过CAS自旋写入数据
如果hash==MOVED,说明需要扩容,执行扩容
如果都不满足,就使用synchronized写入数据,写入数据同样判断链表、红黑树,链表写入和HashMap的方式一样,key hash一样就覆盖,反之就尾插法,链表长度超过8就转换成红黑树

在这里插入图片描述
get查询

get很简单,通过key计算hash,如果key hash相同就返回,如果是红黑树按照红黑树获取,都不是就遍历链表获取。

9. volatile原理知道吗

相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,他没有上下文切换的额外开销成本。使用volatile声明的变量,可以确保值被更新的时候对其他线程立刻可见。volatile使用内存屏障来保证不会发生指令重排(happen-before规则会对volatile有限制),解决了内存可见性的问题。

10. 什么是 jmm(内存模型),可千万别说jvm内存结构

本身随着CPU和内存的发展速度差异的问题,导致CPU的速度远快于内存,所以现在的CPU加入了高速缓存,高速缓存一般可以分为L1、L2、L3三级缓存。基于上面的例子我们知道了这导致了缓存一致性的问题,所以加入了缓存一致性协议,同时导致了内存可见性的问题,而编译器和CPU的重排序导致了原子性和有序性的问题,JMM内存模型正是对多线程操作下的一系列规范约束,因为不可能让陈雇员的代码去兼容所有的CPU,通过JMM我们才屏蔽了不同硬件和操作系统内存的访问差异,这样保证了Java程序在不同的平台下达到一致的内存访问效果,同时也是保证在高效并发的时候程序能够正确执行。
在这里插入图片描述

11. 工作内存和主内存是什么

主内存可以认为就是物理内存,Java内存模型中实际就是虚拟机内存的一部分。而工作内存就是CPU缓存,他有可能是寄存器也有可能是L1\L2\L3缓存,都是有可能的。

12. ThreadLocal用过吗

ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。

ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。

弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。

但是这样还是会存在内存泄露的问题(回收并没有回收entry对象,导致栈值都没了,但是堆中存在值),假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。

但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的。

13. 引用类型有哪些:强软弱虚

记住我上面的口诀

1.强引用指的就是代码中普遍存在的赋值方式,比如A a = new A()这种。强引用关联的对象,永远不会被GC回收。
2.软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。
3.弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。
4.虚引用也被称作幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。

14. boss收尾: 线程池的原理

首先线程池有几个核心的参数概念:

1.最大线程数maximumPoolSize

2.核心线程数corePoolSize

3.活跃时间keepAliveTime(只控制核心线程数以外的线程)

4.阻塞队列workQueue

5.拒绝策略RejectedExecutionHandler

当提交一个新任务到线程池时,具体的执行流程如下:
在这里插入图片描述

1.当我们提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务
2.当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队
3.当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁
4.如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理

主要有4种拒绝策略:

1.AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
2.CallerRunsPolicy:只用调用者所在的线程来处理任务
3.DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务
4.DiscardPolicy:直接丢弃任务,也不抛出异常
傻瓜周末愉快,我是敖丙,你知道越多,你不知道越多,我们下期见。

写在最后

欢迎关注我的微信公众号【猿之村】来探讨Java技术
在这里插入图片描述

如有问题公众号或微信留言,必回
加我的微信进一步交流和学习
微信手动搜索【codeyuanzhicunup】添加即可
如有相关技术问题欢迎留言探讨,公众号主要用于技术分享,包括常见面试题剖析、以及源码解读、微服务框架、技术热点等。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值