一. Java锁机制面试题
1. Synchronized和ReentranLock锁的区别
- 在底层框架上
- Synchronized是JVM层面的锁,是Java关键字,通过monitor对象来完成,使用 monitorenter 和 monitorexit 指令实现。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计 算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止 。
- ReentranLock是API层面的锁底层基于AQS实现。
- 是否需要手动释放锁
- synchronized不需要用户手动释放锁。
- ReentranLock可以手动释放锁,一般通过lock和unlock方法配合try/finally语句。
- 等待是否可中断
- synchronized是不可中断类型的锁
- Reentranlock则可以中断,可通过tryLock(long timeout,TimeUnit unit)设置超时方法
- 加锁是否公平
- synchronized为非公平锁
- ReetranLock默非公平锁,可以new ReentrantLock(true),true:公平锁 false:非公平锁
- 锁能否绑定Condition
- synchronized不能绑定,只能通过object类的wait()/notify()/notifyAll()方法要随机唤醒一个线程要么唤醒全部线程。
- eentrantLock通过绑定Condition结合await()/singal()方法实现线程的精确唤醒
2. volatile 的作用是什么
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
3. volatile 实现原理是什么
在 JVM 底层 volatile 是采用“内存屏障”来实现的
缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个 CPU 在写数据时,如果发现操作的变量是共享变量,则会通知其他 CPU 告知该变量的缓存行是无效的,因此其他 CPU 在读取该变量时,发现其无效会重新从主存中加载数据
4. volatile和synchronized的区别
volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
5. 说说悲观锁
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。
Java中的synchronized关键字和Lock都是悲观锁。
6. 说说乐观锁
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法
java中的原子类就是基于CAS算法实现的。
7. 什么是CAS算法?
- CAS:
Compare And Swap,即比较再交换。- 它是一条 CPU 并发原语。原语的执行必须是连续的,在执行过程中不允许中断,也就是说CAS是一条原子指令,不会造成所谓的数据不一致的问题。
- CAS算法涉及到三个操作数,需要读写的内存值 V,进行比较的值 E,要写入的新值 N,当线程要对V进行操作时,该线程会先去内存中读取V的值,保存到变量副本中,将该值和期望值进行比较,如果相等则进行更新,如果不相等,则该线程再次去读取内存中的值,在进行比较,直到相等为止。
8. CAS存在哪些问题
ABA问题
开销问题
循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
无法保证多个共享变量的原子操作
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
9. 说说 ‘ABA’ 问题
因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新。
比如说一个线程 t1 从内存位置 V 中取出 A,这时候另一个线程 t2也从内存中取出 A,并且 t1 进行了一些操作变成了 B,然后 t2 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线程 one 的 CAS 操作成功,但是不代表这个过程就是没有问题的
Java提供了两个类,AtomicStampedReference和AtomicMarkableReference来解决ABA问题。
10. 乐观锁和悲观锁的使用场景
- 乐观锁适用于读大于写的场景
- 悲观锁适用于写大于读的场景
11.说说什么是自旋锁
在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程状态,休眠和恢复线程的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。
而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
首先,我们来看自旋锁,它并不会放弃 CPU 时间片,而是通过自旋等待锁的释放,也就是说,它会不停地再次地尝试获取锁,如果失败就再次尝试,直到成功为止。
我们再来看下非自旋锁,非自旋锁和自旋锁是完全不一样的,如果它发现此时获取不到锁,它就把自己的线程切换状态,让线程休眠,然后 CPU 就可以在这段时间去做很多其他的事情,直到之前持有这把锁的线程释放了锁,于是 CPU 再把之前的线程恢复回来,让这个线程再去尝试获取这把锁。如果再次失败,就再次让线程休眠,如果成功,一样可以成功获取到同步资源的锁。
自旋锁的优点:
避免上下文切换等开销,提高了效率
自旋锁缺点
如果持有锁的线程一直没有释放锁,导致其他线程一直自旋获取锁,没有放弃cpu的时间片,这样就浪费了处理器的资源。自旋锁默认自旋次数为10次,如果没有成功获取锁,则线程休眠。让出cpu时间片。
12.说说公平锁和非公平锁
公平锁:
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞。
非公平锁
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
13.说所锁的升级过程
- 锁升级过程
无锁 ==> 偏向锁 ==> 轻量级锁 ==> 重量级锁
无锁
当一个对象被创建之后,还没有线程进入,这个时候对象处于无锁状态
偏向锁
当锁处于无锁状态时,有一个线程A访问同步块并获取锁时,会在对象头和栈帧中的锁记录记录线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来进行加锁和解锁,只需要简单的测试一下啊对象头中的线程ID和当前线程是否一致。
轻量级锁
在偏向锁的基础上,又有另外一个线程B进来,这时判断对象头中存储的线程A的ID和线程B不一致,就会使用CAS竞争锁,并且升级为轻量级锁。
重量级锁
当线程没有获得轻量级锁时,线程会CAS自旋来获取锁,当一个线程自旋10次之后,仍然未获得锁,那么就会升级成为重量级锁。
成为重量级锁之后,线程会进入阻塞队列(EntryList),线程不再自旋获取锁,而是由CPU进行调度,线程串行执行。
二.Java多线程面试题
1.实现多线程的几种方式
- 继承 Thread 类
- 实现 Runnable 接口
- 实现Callable接口,带有返回值。
- 通过线程池实现。
2. 线程的生命周期
- new(新建状态)
- runnable(就绪状态)
- running(运行状态)
- blocked(阻塞状态)
- dead(死亡)
3. 什么是死锁?如何避免死锁
- 什么是死锁:两个线程同时持有对方的锁资源,都在等待对方释放锁,就造成了死锁
- 如何解决:
- 指定获取锁的顺序,强制所有线程按照顺序去获取锁。
4. 说说 sleep() 方法和 wait() 方法区别和共同点?
- 两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁 。
- 两者都可以暂停线程的执行。
- Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
- wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。
5. JDK中自带的线程池有哪些?为什么不建议使用JDK自带线程池?
自带线程池:
newSingleThreadExecutor (只有一个线程的线程池)
newFixedThreadPool (返回固定线程数的线程池)
newCachedThreadPool (返回一个缓存线程池,线程可重用)
newScheduledThreadPool (创建一个可定期或延期执行的线程池)
自带线程池的缺点:
FixedThreadPool 和 SingleThreadExecutor :
允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool :
允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
6. ThreadPoolExecutor 七大参数
corePoolSize: 核心线程数线程数定义了最小可以同时运行的线程数量。maximumPoolSize: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。keepAliveTime:当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁;TimeUnit:keepAliveTime参数的时间单位。threadFactory:executor 创建新线程的时候会用到。handler:拒绝策略
7. run()方法和start()方法有和区别
调用start方法启动线程可使线程进入就绪状态,等待运行;run方法只是thread的一个普通方法调用,还是在主线程里执行。
8. 同步代码块和同步方法怎么选?
同步块是更好的选择,因为它不会锁着整个对象,当然你也可以然它锁住整个对象。同步方法会锁住整个对象,哪怕这个类中有不关联的同步块,这通常会导致停止继续执行,并等待获取这个对象锁。 同步块扩展性比较好,只需要锁住代码块里面相应的对象即可,可以避免死锁的产生。
原则:同步范围也小越好。
9. notify()和notityAll()的区别
- notify():唤醒一个处于等待状态的线程(无线等待或计时等待),如果多个线程在等待,并不能确切的唤醒一个线程,与JVM确定唤醒那个线程,与其优先级有关。
- notityAll():唤醒所有处于等待状态的线程,但是并不是将对象的锁给所有的线程,而是让它们去竞争,谁先获取到锁,谁先进入就绪状态。