1.为什么会产生死锁?
线程1持有A资源,等待B资源,线程2持有B资源等待A资源,,线程1、2都持有各自的资源,不释放,这样就会陷入循环等待,就会产生死锁。解决答案是,线程1、2释放掉各自持有的资源。
教科书:
1.首先发生静态资源有限,即互斥
2.线程1、2各自持有静态资源,陷入循环等待
3.循环等待之后,如果1拿不到2的资源,那么释放,此时不会产生死锁,循环等待且不可剥夺,也不释放资源,所以叫请求保持条件。
2.wait和sleep的区别
区别:
从表面上来看:wait来自Object类,sleep来自Thread类
1.由于sleep针对的是线程,所以不可能是Object类的方法,理应在Thread类中,
2.wait在syn中,上的是sysn的锁,与锁相关,与对象头相关,所以理应属于对象
从深度原理上来说:
wait和锁相关,会释放锁,而sleep是线程本身,不涉及释放锁,
3.作用范围:sleep任何地方,wait只用在同步控制块或方法。
联系:
两者都会产生阻塞,
两者都会被中断唤醒,sleep中断唤醒(interupt方法)后会产生InterruptException异常,try、catch补货后可继续执行
3.进程和线程的区别
首先在任何一个操作系统中,必须执行程序的指令,指令是cpu识别的唯一东西,Cpu只知道指令,只会不断的执行指令,转移过去就需要保存指令,这个指令就叫做程序,保存程序有两种格式,linux中为ELF,windows中是exe文件,当我们执行程序的时候会把这些二进制的代码,还她所需要的数据,加载到内存中,开辟一个空间,这个空间就叫做进程的资源,就叫做一个进程。
在linux操作系统实现来说,进程和线程是没有区别的,进程和线程的控制块都是同一个,就是taskStrupt,如果两个进程共享了一个资源,我们就称两个进程为两个线程
比如A是进程,当A进程Fork出了一个线程B和他共享内存资源,那么A、B就成为线程。
4.Java线程的生命周期
线程的的生命周期只有三个,运行,阻塞,销毁。
由于Jvm采用了Linux线程来模拟了Java的线程,在java中创建的线程就是Thread对象,并不是操作系统线程,那么此时就需要先创建一个java的对象,再创建一个原生操作系统的线程,执行Thread中的run方法, 所以此时需要补充一个状态,就是new了一个java中的Thead对象但是还没有把原生操作系统线程分配给java对象的状态,叫做创建状态。
当创建好线程后,挂到操作系统上,等待调度,等待调度的阶段是阻塞的,阻塞在可调度的阶段,有别的线程在占用Cpu时间片,所以从阻塞状态切割出一个状态就叫就绪态,即为就绪等待执行状态。
如果在执行阶段调用了IO请求或者是去拿一把锁,此时拿不到锁,拿不到资源,就会发生阻塞状态。一种请求IO,一种拿到锁,此时Java为了辨别者两种情况,引入了两种状态一个阻塞,一个等待。
当运行完成之后,把原生操作系统资源还给了操作系统,但是Thread对象并没有回收,这时Thread对象为销毁也称终止状态。
5.程序开多少线程合适
开多少线程是和我们线程的性质完全相关的,看我们的业务是做什么的,
如果是完全压缩的这种,就属于CPU密集型,那么为了减少线程的上下文切换的时间,所以要求我们的CPU一直持有一个线程即单核CPU ,那么此时就无需进行线程切换,那么此时我们的线程数保持和CPU核心数一样即可。
如果是完全IO操作这种,就属于IO密集型,就看IO停顿多少时间,如果是1S两秒,那么就需要开辟较多的线程处理,假如资源无限大,那么需要开无限多的线程。如果又要节约系统资源,又要保证高性能,取折中方案,那么就线程数量开为CPU核心数的2倍。
6.notify和notifyAll的区别
面试官想问的是Sync底层的原理:
Object中,调用wai之后会放入到阻塞队列waitQueue中,这时候如果调用notify是随机唤醒一个,进入锁池即竞争队列entryList,如果是notifyAll的话,就会等待队列中的所有线程,放入竞争队列。
7.描述下Synchronized和lock区别
答:总-分-总
先说下Sync,java中最基本的互斥同步(保证共享数据在同一时刻只被一条线程访问)的手段就是Sync了。Sync经过编译后,会在同步代码块前后产生monitorEnter和monitorExist两个字节码指令,在只给你monitorEnter指令的时候,判断如果当前对象没有锁,或者当前线程已经持有锁(可重入),那么锁计时器加1,而在执行monitorExit指令时会减1,一旦锁计时器为0,锁立即被释放。如果获取锁失败那么当前线程就会被阻塞等待,直到对象释放锁为止。
因为阻塞和唤醒的操作,会让操作系统来帮忙,就会产生内核态到用户态的消耗,而虚拟机本身也会对其进行优化,比如在阻塞线程之前,加入一段时间的自旋等待CAS,以避免频繁的内核态到用户态的切换,所以出现了Lock。
reetrantlock是在JDK1.5的时候出现的,此时Sync还未进行优化,性能较差,连锁升级都没有,所以引入了Lock,历史问题。
而Sync还有个诟病,就是你只有一种条件,要不wait要不notify。
ReetrantLock有一些高级功能:
等待可中断:当前持有锁的线程,长期不释放锁的时候,正在等待的线程可以放弃等待,处理其他事情。等待可中断对于处理时间较长的同步块很有帮助。
支持公平锁:sync默认为非公平,必须按照申请时间来一次获取锁。ReetrantLock为默认为非公平锁,可以指定为公平锁,但是性能会有所下降
锁绑定多个条件变量:ReetrantLock中的条件变量,他可以切割出多个条件变量,多次调用newCondition即可。
8.ABA问题
线程1、2同时修改一个变量值A变为B,
线程1将A--》B,此时线程2因为某些原因阻塞了,
但是此时来个线程3将B的值又修改了为A,
此时线2获取到CPU时间片,发现变量值为预期A,所以又将A-》B
可以通过加版本号和时间戳A1-B1-A2
假如小明有个女朋友小红,后来他分了小红找了个男朋友小亮,但是最终小明又和小红在一起了。
这就是ABA问题。言简意赅
解决方案,加版本号,每次修改+1。
9.锁升级
轻量级锁:不存在线程竞争同一把锁,mark work中头标志位为00
重量级锁:当更新操作失败时,证明最少有一个其他线程在竞争抢锁,头部标志位为01
偏向锁:偏向于第一个线程,没有同步和CAS自旋操作。
10.什么是并发,什么是并行?会造成什么影响
并发就是,我们做核酸,每个人做2分钟,做完之后,下个人才可以做,时间有20分钟,这就是并发。当把时间缩短到每个学生考100ms的时候,就是CPU的原理。
并行就是,这个5分钟之内可以有多个学生考试。
并发安全,就是做核酸的时候,人很多,但是同一时间只能给一个人做,但是人们肯定不会遵守纪律,需要插队,所以需要一个管程,让人们排好队,不允许插队,这就是互斥同步。
11.CAS执行原理
一种是通过原语,lock cmpxchg(compare change)下的lock dec;lockincr;lock前缀将cpu总线锁住,保证同一时间只有一个线程可以执行。
一种是通过关中断(关闭时钟中断,不让CPU进行线程切换),保证lock dec和lock incr是原子操作
12.为什么要使用volitile
13.AQS面试
AQS字面意思是抽象的队列同步器,是用来构建锁或者其它同步器组件的基石。
通过内置的FIFO(先进先出)队列来完成线程获取锁的排队工作,通过一个state的int变量表示持有锁的状态。
AQS的流程
1.假设A,B,C三个线程 依次去抢一个同步锁,首先A先抢到锁把state置为1(3个状态:没占用是0,占用了是1,大于1是可重入锁),将自己设置为线程持有者ExclusiveOwnerThread
2.B线程进来后先尝试获取锁tryAcquire(),获取不到添加到队列中addWaiter(),如果队列为空,先创建傀儡节点,再把B添加到傀儡节点后边。
3.B线程调用acquireQueued(),将傀儡节点waitStatus设置为-1,最后调用LockSupport.part(this)方法将自己阻塞
4.C线程进来后也是先尝试获取不到锁,然后添加到B节点后,将B节点的waitStatus设置为-1,最后调用LockSupport.part(this)方法将自己阻塞
5.当A线程解锁后,将state置为0,将线程持有者置为null。最后将队列头节点waitStatus置为0,调用LockSupport的unpark方法,唤醒head节点下一个节点也就是B节点
6.B线程唤醒后,接着执行acquireQueued()方法,尝试获取锁成功,然后自己成为头节点,原来头结点指向null方便GC
7.B线程解锁后,C线程同上
14、ThreadLocal
1.ThreadLocal 是java中所提供的线程本地存储机制,可以利用该机制将数据(如对象)缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
2.ThreadLocal底层是通过ThreadLocalMap实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
3.如果线程池中使用ThreadLocal会造成内存泄漏,因为当使用完ThreadLocal对象后,理应当把设置的key、value,也就是Entry对象进行回收,但是线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏。解决方法,在一个线程使用完ThreadLocal后,手动调用ThreadLocal对象的remove方法移除Entry对象。
4.ThreadLocal经典应用场景是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享一个连接)