你们在项目中高并发时你都用过哪些锁,它的原理是什么;你知道类加载的过程,双亲委派机制吗;你们有用到过线程池吗,它的工作过程是怎样的。本文重点对面试的问题进行介绍,祝愿每位程序员都能顺利上岸!!!
Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住.具体的使用场景可以参考: 还在为Synchronized烦恼吗?那么请移步Java并发编程数据安全之Synchronized篇

被Synchronized 关键字修饰的方法,代码块,在编译后的class 文件后,会通过monitorenter为对象上锁,通过monitorexit 释放锁,来实现排它锁。

monitor 数据结构中保存了当前获取所得线程,和等待去获取所得线程

monitor 实现了重量级锁,只有发生并发资源的抢占,就会当前未抢到锁的线程,放入到阻塞队列中,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。引入了偏向锁和轻量级锁 此时锁的实现是怎样的呢;

对象头的结构
线程1 进入,发现此时无锁,通过CAS 操作将线程1的id 设置到对象头中,并且将锁的标记设置为偏向锁;一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令。

线程2此时进入,进入后发现为偏向锁,则通过CAS操作 将线程2的id 设置到对象头中;CAS 成果则获取到锁,如果CAS 失败,则进行偏向锁的撤销,并将锁升级为轻量级锁;线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性。

线程2 则通过CAS 操作将对象头的Markword设置为指向该帧中保存的markword副本,如果设置成功,则说明成功获取锁,如果获取失败,则自旋等待获取锁(当自旋多次依旧没有获取到锁时。会将轻量级锁升级为重量级锁(设置对象的锁标记为重量级锁))底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
CAS的全称是:Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。因为没有加锁,所以线程不会陷入阻塞,效率较高;如果竞争激烈,重试频繁发生,效率会受影响。
在JUC(java.util.concurrent )包下实现的很多类都用到了CAS操作
- AbstractQueuedSynchronizer(AQS框架)
- A AtomicXXX类
一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功。
当线程A 已经把a的值修改为101 后,线程B 此时修改值为99 就会失败
此时重新获取a的值,然后在次进行比较和交换
底层通过调用Unsafe 类中被native 修饰的方法进行实现;
JMM(lava Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性。

你谈谈JMM
- JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存
使用volatile 关键字修饰的属性,可以打破内存屏障,使得一个线程对其进行修改后,保证其它线程的可见性。一旦一个共享变量(类的成员变量、类的静态成员变量)被volatie修饰之后,那么就具备了两层语义
- ① 保证线程间的可见性
- ② 禁止进行指令重排序
JIT 在将代码进行编译时,会对代码优化,从而导致代码执行的先后顺序发生改变,从而在特定条件下发生问题。此时就可以使用volatile 关键字。

volatile禁止指令重排序,用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阴止其他读写操作越过屏障,从而达到阳止重排序的效果.
发生指令重排序
使用volatile 禁止指令的重排序
你们使用volatile都遵从哪些规则
- 写变量让volatile修饰的变量的在代码最后位置
- 读变量让volatile修饰的变量的在代码最开始位置
全称是 AbstractQueuedSynchronizer,即抽象队列同步器。它是构建锁或者其他同步组件的基础框架
AQS与Synchronized的区别


我在项目中使用到了ReentrantLock 锁,来对资源并发修改情况下进行加锁,ReentrantLock 锁的使用和原理详解可以参考:JAVA并发编程–4.1理解ReentrantLock
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

ReentrantLock 的工作过程
- 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
- 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
- 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
- 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
语法层面:
synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现Lock 是接口,源码由jdk 提供,用java 语言实现使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
功能层面
二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能Lock 提供了许多 synchronized 不具备的功能,例如公平锁、可打断、可超时、多条件变量Lock有适合不同场景的实现,如 ReentrantLock,ReentrantReadWriteLock(读写锁)
性能层面
在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖在竞争激烈时,Lock 的实现通常会提供更好的性能
当一个线程需要获取多把锁时,就有可能发生死锁。
涉及死锁的线程都会处于等待状态,虽然系统可能还未完全停止工作,但由于死锁线程占用了部分资源(如CPU时间片、内存等),系统的整体性能会受到影响,表现为响应速度变慢、吞吐量下降等,请求结构一直等待数据返回。
我们通常使用jdk 自带的一些工具来进行排查:
本文对Java 中进程内经常使用的锁,以及原理面试题进行汇总。