大家好,我是大彬~
今天给大家分享Java并发高频面试题(第二弹),助力春招上岸!
文章目录如下:
volatile是轻量级的同步机制,volatile保证变量对所有线程的可见性,不保证原子性。
volatile变量进行写操作的时候,JVM会向处理器发送一条LOCK前缀的指令,将该变量所在缓存行的数据写回系统内存。来看看缓存一致性协议是什么。 缓存一致性协议:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,就会从内存重新读取。
volatile关键字的两个作用:
指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。Java编译器会在生成指令系列时在适当的位置会插入
内存屏障指令来禁止处理器重排序。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个volatile字段进行写操作,Java内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。
原子性:确保线程互斥的访问同步代码;
可见性:保证共享变量的修改能够及时可见;
有序性:有效解决重排序问题。
synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权。
monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因
其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0 ,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
volatile只能使用在变量上;而synchronized可以在类,变量,方法和代码块上。volatile至保证可见性;synchronized保证原子性与可见性。volatile禁用指令重排序;synchronized不会。volatile不会造成阻塞;synchronized会。相同点:
InterruptedException不同点:
wait()是Object超类中的方法;而sleep()是线程Thread类中的方法wait()会释放锁,而sleep()并不释放锁wait()依靠notify或者notifyAll、中断、达到指定时间来唤醒;而sleep()到达指定时间被唤醒wait()需要先获取对象的锁,而Thread.sleep()不用call(),Runnable的方法是run();call()方法允许抛出异常;而Runnable接口run()方法不能继续上抛异常。守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。
volatile 使用共享内存实现线程间相互通信。多个线程同时监听一个变量,当这个变量被某一个线程修改的时候,其他线程可以感知到这个变化。
wait/notify为Object对象的方法,调用wait/notify需要先获得对象的锁。对象调用wait()之后线程释放锁,将线程放到对象的等待队列,当通知线程调用此对象的notify()方法后,等待线程并不会立即从wait()返回,需要等待通知线程释放锁(通知线程执行完同步代码块),等待队列里的线程获取锁,获取锁成功才能从wait()方法返回,即从wait()方法返回前提是线程获得锁。
当在一个线程调用另一个线程的join()方法时,当前线程阻塞等待被调用join方法的线程执行完毕才能继续执行。join()是基于等待通知机制实现的。
AQS,AbstractQueuedSynchronizer,抽象队列同步器,定义了一套多线程访问共享资源的同步器框架,许多并发工具的实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。
AQS使用一个volatile的int类型的成员变量state来表示同步状态,通过CAS修改同步状态的值。当线程调用 lock 方法时 ,如果 state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将 state加1。如果 state不为0,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态(独占或共享)构造成为一个节点(Node)并将其加入同步队列并进行自旋,当同步状态释放时,会把首节点中的后继节点对应的线程唤醒,使其再次尝试获取同步状态。

ReentrantLock内部自定义了同步器sync,在加锁的时候通过CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,会检查当前占有锁的线程和当前请求锁的线程是否一致,如果一致,同步状态加1,表示锁被当前线程获取了多次。
源码如下:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
按照线程访问顺序获取对象锁。synchronized是非公平锁,Lock默认是非公平锁,可以设置为公平锁,公平锁会影响性能。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
共享式与独占式的最主要区别在于:同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。
悲观锁,每次访问资源都会加锁,执行完同步代码释放锁,synchronized和ReentrantLock属于悲观锁。
乐观锁,不会锁定资源,所有的线程都能访问并修改同一个资源,如果没有冲突就修改成功并退出,否则就会继续循环尝试。乐观锁最常见的实现就是CAS。
适用场景:
乐观锁避免了悲观锁独占对象的问题,提高了并发性能,但它也有缺点:
CAS全称Compare And Swap,比较与交换,是乐观锁的主要实现方式。CAS在不使用锁的情况下实现多线程之间的变量同步。ReentrantLock内部的AQS和原子类内部都使用了CAS。
CAS算法涉及到三个操作数:
只有当V的值等于A时,才会使用原子方式用新值B来更新V的值,否则会继续重试直到成功更新值。
以AtomicInteger为例,AtomicInteger的getAndIncrement()方法底层就是CAS实现,关键代码是 compareAndSwapInt(obj, offset, expect, update),其含义就是,如果obj内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它为update,如果不相等,那就会继续重试直到成功更新值。
大彬,非科班出身,自学Java,校招斩获京东、携程、华为等offer。作为一名转码选手,深感这一路的不易。
希望我的分享能帮助到更多的小伙伴,我踩过的坑你们不要再踩。