1.说一下你对java内存模型JVM的理解
java内存模型是一种抽象的模型,被定义出来屏蔽各种硬件和操作系统的访问差异。
- JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存 (Main Memory)中,每个线程都有
一个私有的本地内存 (Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
2说说你对原子性,可见性,有序性的理解
- 原子性是指一个操作不可分割,不可中断,要么全部执行,要么全不执行(synchronized)
- 可见性:是指一个线程修改了共享变量,其他线程能够立即知道这个修改
- 有序性:对于一个线程的执行代码,从前往后依次执行,单线程下可以认为是有顺序的,但是并发的时候有肯能会发生指令重排。
3. 什么是指令重排
在执行程序的时候,为了提高性能,编译器和处理器常常会对指令进行重排序。
- 编译器的优化重排序。编译器在不改变单线程程序的语义下可以重新安排语句的执行顺序。
- 指令级并行的重排序,如果不存在数据依赖性,处理器可以改变语句对应的机器执行顺序。
- 内存系统的重新排序,由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
4.指令重排的限制
指令重排有两个规则:happens-before和as-if-serial来约束
happens-before的定义
- 如果一个操作在happens-before的另一个操作之前,那么第一个操作的执行结果将对第二个操作可见,而且第一个的操作执行顺序在第二个之前
- 两个关系存在着happens-before关系,并不意味着java平台的具体实现就必须按照顺序来执行,只要执行结果保持一致,那么重排序不非法。
happens-before的规则 - 程序顺序规则
- 监视器锁过规则: 对于一个锁的解锁,happens-before于随后对这个锁进行加锁
- volatile变量规则:对一个volatiile域的写,happens-before于任意后续对这个域进行读
- 传递性:如果a后是b,b后是c,那么a happens-before c
- start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的 ThreadB.start()操作
happens-before于线程B中的任意操作。 - join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作 happens-before于线程A从
ThreadB.join()操作成功返回。
5.as-if-serial是什么,单线程一定是顺序的吗?
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果不能被改变。
编译器、runtime和处理器都必须遵守as-if-serial语义。
6.synchronized使用方式
- 修饰实例方法:作用于当前对象获取锁,进入同步代码前要获得当前对象实例的锁
- 修饰静态方法:也就是给当前类加锁,会作用于类的所有对象实例,进入同步代码前要获得当前 class 的 锁。
因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对
象,只有一份)。
如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静
态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized方法占用的锁是当前类的锁,
而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
synchronized void staic method() {
//业务代码
}
- 修饰代码块:指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进入同步代码库前要获得
给定对象的锁。 synchronized(类.class) 表示进入同步代码前要获得 当前 class的锁
7.synchronized的原理
1.synchronized修饰代码块的时候,jvm采用monitorenter和moninorexit指令来实现同步,monitorenter标记开始位置,monitorexit标记结束位置
2.synchronized修饰同步方法,jvm采用ACC_Synchronized标记来实现同步,这个标识指明了该方法是一个同步方法。
synchronized锁住的是什么呢?
monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor实现的。
实例对象结构里有对象头,对象头里面有一块结构叫Mark Word,Mark Word指针指向了monitor。
所谓的Monitor其实是一种同步工具,也可以说是一种同步机制。在Java虚拟机(HotSpot)中,Monitor是由
ObjectMonitor实现的,可以叫做内部锁,或者Monitor锁。
ObjectMonitor的工作原理:
ObjectMonitor有两个队列:WaitSet、EntryList,用来保存ObjectWaiter 对象列表。
_owner,获取 Monitor 对象的线程进入 _owner 区时, _count + 1。如果线程调用了wait() 方法,此时会释放
Monitor 对象, _owner 恢复为空, _count - 1。同时该等待线程进入 _WaitSet 中,等待被唤醒。
同步是锁住的什么东西:
monitorenter,在判断拥有同步标识 ACC_SYNCHRONIZED 抢先进入此方法的线程会优先拥有 Monitor 的
owner ,此时计数器+1。
monitorexit,当执行完退出后,计数器-1,归 0 后被其他进入的线程获得。
8.除了原子性,synchronized可见性,有序性,可重入性怎么实现?
可见性:
- 线程加锁前,将清空工作内存中的值,从而使共享变量需要从主内存中读取最新的值
- 线程加锁后,其他线程无法获得主内存中的共享变量
- 线程解锁前,必须把共享变量的最新值刷入到主内存中。
有序性:
- synchronized同步的代码块,具有排他性,所以synchronized能保证同一时刻代码是单线程执行的。
实现可重入性:
synchronized锁对象的时候有个计数器,会记录获取锁的次数,执行完代码就会减去一,清零就会释放锁。
9. 锁升级
锁升级顺序: 无锁-----》 偏向锁 -----》轻量锁 -----》重量级锁
10. synchronized做了哪些优化?
JDK1.6前synchronized是调用monit的enter和exit这种锁被叫做重量级锁,1.6之后就进行优化如增加适应性锁,锁消除,锁粗化,轻量级锁,偏向锁等策略
- 偏向锁:在无竞争的情况下,只是在Mark Word里存储当前线程指针,CAS操作都不做。
- 轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额外有CAS操作的开销。
- 自旋锁:减少不必要的CPU上下文切换。在轻量级锁升级为重量级锁时,就使用了自旋加锁的方式
- 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
- 锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除
10.说说synchronized和ReentrantLock的区别?
11.AQS了解多少
AQS就是AbstractQueuedSynchronized抽象同步队列,简称AQS。它是并发包中的基础,并发包中的锁就是基于AQS实现的。
- AQS是基于一个双向队列,其内部定义了一个节点类node,Node节点内部的SHARED用来标记该线程是获取共享资源时被阻碍挂起后放入AQS队列的,EXCLUSIVE用来标记线程是取独占资源时被挂起后放入AQS队列的。
- AQS 使用volatile修饰int的成员变量state来表示同步状态。
- 获取state分为共享式和独占式,一个线程使用独占式获取资源,其他线程就会在获取失败后被阻塞。一个线程使用共享式获取资源,另一个线程还可以使用CAS方式获取资源。
- 如果共享资源被占用,需要一定的阻塞等待来唤醒机制来保证锁的分配,AQS将会竞争共享资源失败的线程添加到一个变体的CLH中。
12.ReentrantLock实现原理
Reentrantlock是可重入的独占锁,
// 创建非公平锁
ReentrantLock lock = new ReentrantLock();
// 获取锁操作
lock.lock();
try {
// 执行代码逻辑
} catch (Exception ex) {
// ...
} finally {
// 解锁操作
lock.unlock();
}
new Reentrantlock默认的是创建非公平锁
公平锁:
- 公平锁是指多个线程按照申请锁的顺序来获取锁
- 公平锁的优点是等待锁的线程不会饿死,缺点是整体吞吐效率相对非公平锁要低
非公平锁: - 非公平锁是多个线程加锁的时候,直接尝试获取锁,获取不到才会到队尾等待,但如果此时锁刚好可以用,那么这个线程可以无阻碍的直接获取到锁。
- 非公平锁的优点是可以减少唤醒线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获取锁,CPU不必唤醒所有的线程,缺点是处于等待的线程可能会被饿死。
13.Reentrantlock怎么实现公平锁
Reentrantlock默认是非公平锁nonfairSync。
public ReentrantLock() {
sync = new NonfairSync();
}
同时也可以在构造函数中传递参数,实现公平 锁 fairSync
ReentrantLock lock = new ReentrantLock(true);--- ReentrantLock
// true 代表公平锁,false 代表非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
14.CAS了解多少
CAS 比较并交换 主要是包含三个参数 共享变量A的内存地址,预期值B,和共享变量C的值,只有当内存地址A中的值等于B时候才能更新为新值C。作为一条CPU指令,本身是能保证原子性的。
CAS三大问题:
- ABA问题:并发条件下如果A可能在修改中间变为b,b又变为A,此时A非一开始的a,这时去修改数据虽然会成功但是可能会存在问题,解决方案每次修改可以是加版本号
- 循环性能开销:自旋CAS如果一直不成功,一直循环,会给CPU带来非常大的开销。解决方案:在java中会设置一个自旋次数,超过一定次数,默认就会停止(默认10次)。
- 只能保证一个变量的原子操作:CAS保证的是一个变量执行原子操作,如果对多个变量无法直接保证原子性。解决方法:可以考虑用锁来保证原子的操作性,可以考虑合并多个变量,将多个变量封装到一个对象中。
15.Java有哪些保证原子性的方法?如何保证多线程下i++ 结果正确
- 使用原子类,如AtomicInteger,AtomicBoolean(内部是通过CAS实现)
- 使用juc中的锁
- 使用Synchronized
16.如果避免死锁
死锁是指两个或以上的线程争夺资源而造成的相互等待的现象,在无外力的作用下这些线程会一直等待而无法运行下去。
死锁的四个条件:互斥条件,请求并持有,不可剥夺条件,环路等待
如何避免呢:互斥是必须的,我们可以一次性请求所以数据(互斥条件),针对不可剥夺条件如果我们请求不到其他的资源,始终没有释放,可以将本身的资源进行释放。对于环路等待那么可以按照申请的资源编号来预防。
17 countDownLatch和CyclicBarrier
- CountDownLatch同步倒计数器。
- CyclicBarrier同步屏障
(两者可以看我多线程中文章)
18.CyclicBarrier和CountDownLatch有什么区别
19…Semaphore(信号量)
semaphore(信号量)用来控制同时访问特定资源线程数量。通过协调各个线程以保证合理使用公共资源。它可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接
ps:喜欢请点点关注,你的攒是我前进的动力。